diff --git a/build.gradle.kts b/build.gradle.kts
index 69fce64..18583ef 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -24,7 +24,7 @@ import java.net.URL
* The build gradle file for the Cyface Uploader.
*
* @author Armin Schnabel
- * @version 1.0.0
+ * @version 1.0.1
* @since 1.0.0
*/
buildscript {
@@ -62,7 +62,8 @@ version = "0.0.0" // Automatically overwritten by CI
// Versions of dependencies
extra["slf4jVersion"] = "2.0.7"
-extra["cyfaceSerializationVersion"] = "2.3.7"
+// TODO: Check if we can remove this after we port the new auth from android-backend to this library
+extra["cyfaceSerializationVersion"] = "3.2.0"
extra["googleApiClientVersion"] = "2.2.0" // transmission protocol
extra["gradleWrapperVersion"] = "7.6.1"
diff --git a/src/main/kotlin/de/cyface/uploader/Authenticator.kt b/src/main/kotlin/de/cyface/uploader/Authenticator.kt
deleted file mode 100644
index ca30763..0000000
--- a/src/main/kotlin/de/cyface/uploader/Authenticator.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2023 Cyface GmbH
- *
- * This file is part of the Cyface Uploader.
- *
- * The Cyface Uploader is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Cyface Uploader is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Cyface Uploader. If not, see .
- */
-package de.cyface.uploader
-
-import de.cyface.model.Activation
-import de.cyface.uploader.exception.LoginFailed
-import de.cyface.uploader.exception.RegistrationFailed
-import java.net.MalformedURLException
-import java.net.URL
-import kotlin.jvm.Throws
-
-/**
- * Interface for authenticating to a Cyface Data Collector.
- *
- * @author Armin Schnabel
- * @version 1.0.0
- * @since 1.0.0
- */
-interface Authenticator {
-
- /**
- * Authenticates with the Cyface data collector server available at the API endpoint.
- *
- * @param username The username of the user to authenticate
- * @param password The password of the user to authenticate
- * @throws LoginFailed when an expected error occurred, so that the UI can handle this.
- * @return The auth token as String. This token is only valid for some time. Just call this method before each
- * upload.
- */
- @Throws(LoginFailed::class)
- fun authenticate(username: String, password: String): String
-
- /**
- * Register a new user with the Cyface Data Collector server available at the API endpoint.
- *
- * @param email The email part of the credentials
- * @param password The password part of the credentials
- * @param captcha The captcha token
- * @param activation The template to use for the activation email.
- * @param group The database identifier of the group the user selected during registration
- * @throws RegistrationFailed when an expected error occurred, so that the UI can handle this.
- * @return [Result.UPLOAD_SUCCESSFUL] if successful.
- */
- @Throws(RegistrationFailed::class)
- fun register(email: String, password: String, captcha: String, activation: Activation, group: String): Result
-
- /**
- * @return the endpoint which will be used for authentication.
- * @throws MalformedURLException if the endpoint address provided is malformed.
- */
- @Throws(MalformedURLException::class)
- fun loginEndpoint(): URL
-
- /**
- * @return the endpoint which will be used for registration.
- * @throws MalformedURLException if the endpoint address provided is malformed.
- */
- @Throws(MalformedURLException::class)
- fun registrationEndpoint(): URL
-}
diff --git a/src/main/kotlin/de/cyface/uploader/DefaultAuthenticator.kt b/src/main/kotlin/de/cyface/uploader/DefaultAuthenticator.kt
deleted file mode 100644
index c03948d..0000000
--- a/src/main/kotlin/de/cyface/uploader/DefaultAuthenticator.kt
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright 2023 Cyface GmbH
- *
- * This file is part of the Cyface Uploader.
- *
- * The Cyface Uploader is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Cyface Uploader is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Cyface Uploader. If not, see .
- */
-package de.cyface.uploader
-
-import de.cyface.model.Activation
-import de.cyface.uploader.exception.AccountNotActivated
-import de.cyface.uploader.exception.BadRequestException
-import de.cyface.uploader.exception.ConflictException
-import de.cyface.uploader.exception.EntityNotParsableException
-import de.cyface.uploader.exception.ForbiddenException
-import de.cyface.uploader.exception.HostUnresolvable
-import de.cyface.uploader.exception.InternalServerErrorException
-import de.cyface.uploader.exception.LoginFailed
-import de.cyface.uploader.exception.NetworkUnavailableException
-import de.cyface.uploader.exception.RegistrationFailed
-import de.cyface.uploader.exception.ServerUnavailableException
-import de.cyface.uploader.exception.SynchronisationException
-import de.cyface.uploader.exception.TooManyRequestsException
-import de.cyface.uploader.exception.UnauthorizedException
-import de.cyface.uploader.exception.UnexpectedResponseCode
-import org.slf4j.LoggerFactory
-import java.net.HttpURLConnection
-import java.net.MalformedURLException
-import java.net.URL
-
-/**
- * Implementation of the [Authenticator].
- *
- * *Attention:* The authentication token is invalid after a few seconds.
- * Just call [DefaultAuthenticator.authenticate] again to get a new token.
- * Usually the token should be generated just before each [DefaultUploader.upload] call.
- *
- * @author Armin Schnabel
- * @version 1.0.1
- * @since 1.0.0
- * @property apiEndpoint An API endpoint running a Cyface data collector service, like `https://some.url/api/v3`
- */
-@Suppress("unused") // Part of the API
-class DefaultAuthenticator(private val apiEndpoint: String) : Authenticator {
-
- private val http: HttpConnection = HttpConnection()
-
- @Suppress("CyclomaticComplexMethod")
- override fun authenticate(username: String, password: String): String {
- var connection: HttpURLConnection? = null
- val authToken: String
- try {
- connection = http.open(loginEndpoint(), false)
-
- // Try to send the request and handle expected errors
- val loginResponse = http.login(connection, username, password, false)
- LOGGER.debug("Response $loginResponse")
-
- // Make sure the successful response contains an Authorization token
- authToken = connection.getHeaderField("Authorization")
- check(!(loginResponse == Result.LOGIN_SUCCESSFUL && authToken == null)) {
- "Login successful but response does not contain a token"
- }
-
- return authToken
- }
-
- // Soft catch "expected" errors in `LoginFailed` exception so that the UI can handle this.
- // As this API is not used by other parties, there is no external definition of expected errors.
- catch (e: SynchronisationException) {
- throw LoginFailed(e) // IOException while reading the response. Try again later.
- } catch (e: UnauthorizedException) {
- throw LoginFailed(e) // `HTTP_UNAUTHORIZED` (401). Handle in UI.
- } catch (e: ForbiddenException) {
- throw LoginFailed(e) // `HTTP_FORBIDDEN` (403). Seems to happen when server is unavailable. Handle in UI.
- } catch (e: NetworkUnavailableException) {
- throw LoginFailed(e) // Network disappeared. Try again later.
- } catch (e: TooManyRequestsException) {
- throw LoginFailed(e) // `HTTP_TOO_MANY_REQUESTS` (429). Try again later.
- } catch (e: HostUnresolvable) {
- throw LoginFailed(e) // Network without internet connection. Try again later.
- } catch (e: ServerUnavailableException) {
- throw LoginFailed(e) // Server not reachable. Try again later.
- } catch (e: AccountNotActivated) {
- throw LoginFailed(e) // User account not activated. Handle in UI.
- } catch (e: UnexpectedResponseCode) {
- // We currently show a UI error. Is this also reported to Sentry? Then it's ok not to throw this hard.
- throw LoginFailed(e) // server returns an unexpected response code
- }
-
- // Crash unexpected errors hard
- catch (e: BadRequestException) {
- error(e) // `HTTP_BAD_REQUEST` (400).
- } catch (e: ConflictException) {
- error(e) // `HTTP_CONFLICT` (409).
- } catch (e: EntityNotParsableException) {
- error(e) // `HTTP_ENTITY_NOT_PROCESSABLE` (422).
- } catch (e: InternalServerErrorException) {
- // If this actually happens, we might want to catch it softly, report to Sentry and show a UI error.
- error(e) // `HTTP_INTERNAL_ERROR` (500).
- } catch (e: MalformedURLException) {
- error(e) // The endpoint url is malformed.
- } finally {
- connection?.disconnect()
- }
- }
-
- @Suppress("CyclomaticComplexMethod")
- override fun register(
- email: String,
- password: String,
- captcha: String,
- activation: Activation,
- group: String
- ): Result {
- var connection: HttpURLConnection? = null
- try {
- connection = http.open(registrationEndpoint(), false)
-
- // Try to send the request and handle expected errors
- val response = http.register(connection, email, password, captcha, activation, group)
- LOGGER.debug("Response $response")
- return response
- }
-
- // Soft catch "expected" errors in `RegistrationFailed` exception so that the UI can handle this.
- // As this API is not used by other parties, there is no external definition of expected errors.
- catch (e: ConflictException) {
- throw RegistrationFailed(e) // `HTTP_CONFLICT` (409). Already registered. Handle in UI.
- } catch (e: SynchronisationException) {
- throw RegistrationFailed(e) // IOException while reading the response. Try again later.
- } catch (e: ForbiddenException) {
- // `HTTP_FORBIDDEN` (403). Seems to happen when server is unavailable. Handle in UI.
- throw RegistrationFailed(e)
- } catch (e: NetworkUnavailableException) {
- throw RegistrationFailed(e) // Network disappeared. Try again later.
- } catch (e: TooManyRequestsException) {
- throw RegistrationFailed(e) // `HTTP_TOO_MANY_REQUESTS` (429). Try again later.
- } catch (e: HostUnresolvable) {
- throw RegistrationFailed(e) // Network without internet connection. Try again later.
- } catch (e: ServerUnavailableException) {
- throw RegistrationFailed(e) // Server not reachable. Try again later.
- } catch (e: UnexpectedResponseCode) {
- // We currently show a UI error. Is this also reported to Sentry? Then it's ok not to throw this hard.
- throw RegistrationFailed(e) // server returns an unexpected response code
- }
-
- // Crash unexpected errors hard
- catch (e: BadRequestException) {
- error(e) // `HTTP_BAD_REQUEST` (400).
- } catch (e: UnauthorizedException) {
- error(e) // `HTTP_UNAUTHORIZED` (401).
- } catch (e: EntityNotParsableException) {
- error(e) // `HTTP_ENTITY_NOT_PROCESSABLE` (422).
- } catch (e: InternalServerErrorException) {
- error(e) // `HTTP_INTERNAL_ERROR` (500).
- } catch (e: AccountNotActivated) {
- error(e) // `PRECONDITION_REQUIRED` (428). Should not happen during registration.
- } catch (e: MalformedURLException) {
- error(e) // The endpoint url is malformed.
- } finally {
- connection?.disconnect()
- }
- }
-
- @Suppress("MemberVisibilityCanBePrivate") // Part of the API
- override fun loginEndpoint(): URL {
- return URL(returnUrlWithTrailingSlash(apiEndpoint) + "login")
- }
-
- @Suppress("MemberVisibilityCanBePrivate") // Part of the API
- override fun registrationEndpoint(): URL {
- return URL(returnUrlWithTrailingSlash(apiEndpoint) + "user")
- }
-
- companion object {
-
- /**
- * The logger used to log messages from this class. Configure it using src/main/resources/logback.xml.
- */
- private val LOGGER = LoggerFactory.getLogger(DefaultAuthenticator::class.java)
-
- /**
- * Adds a trailing slash to the server URL or leaves an existing trailing slash untouched.
- *
- * @param url The url to format.
- * @return The server URL with a trailing slash.
- */
- fun returnUrlWithTrailingSlash(url: String): String {
- return if (url.endsWith("/")) {
- url
- } else {
- "$url/"
- }
- }
- }
-}
diff --git a/src/main/kotlin/de/cyface/uploader/DefaultUploader.kt b/src/main/kotlin/de/cyface/uploader/DefaultUploader.kt
index 8fa88da..a612585 100644
--- a/src/main/kotlin/de/cyface/uploader/DefaultUploader.kt
+++ b/src/main/kotlin/de/cyface/uploader/DefaultUploader.kt
@@ -63,141 +63,186 @@ import javax.net.ssl.SSLException
/**
* Implementation of the [Uploader].
*
- * To use this interface just call [DefaultUploader.upload] with an authentication token, e.g. from
- * [DefaultAuthenticator.authenticate].
+ * To use this interface just call [DefaultUploader.uploadMeasurement] or [DefaultUploader.uploadAttachment].
*
* @author Armin Schnabel
- * @version 1.0.0
+ * @version 2.0.0
* @since 1.0.0
* @property apiEndpoint An API endpoint running a Cyface data collector service, like `https://some.url/api/v3`
*/
class DefaultUploader(private val apiEndpoint: String) : Uploader {
- @Suppress("unused", "CyclomaticComplexMethod", "LongMethod")
- override // Part of the API
- fun upload(
+ @Suppress("unused", "CyclomaticComplexMethod", "LongMethod") // Part of the API
+ override fun uploadMeasurement(
jwtToken: String,
metaData: RequestMetaData,
file: File,
progressListener: UploadProgressListener
+ ): Result {
+ val endpoint = measurementsEndpoint()
+ return uploadFile(jwtToken, metaData, file, endpoint, progressListener)
+ }
+
+ override fun uploadAttachment(
+ jwtToken: String,
+ metaData: RequestMetaData,
+ measurementId: Long,
+ file: File,
+ progressListener: UploadProgressListener
+ ): Result {
+ val endpoint = attachmentsEndpoint(measurementId)
+ return uploadFile(jwtToken, metaData, file, endpoint, progressListener)
+ }
+
+ override fun measurementsEndpoint(): URL {
+ return URL(returnUrlWithTrailingSlash(apiEndpoint) + "measurements")
+ }
+
+ override fun attachmentsEndpoint(measurementId: Long): URL {
+ return URL(returnUrlWithTrailingSlash(apiEndpoint) + "measurements/$measurementId/attachments")
+ }
+
+ @Throws(UploadFailed::class)
+ private fun uploadFile(
+ jwtToken: String,
+ metaData: RequestMetaData,
+ file: File,
+ endpoint: URL,
+ progressListener: UploadProgressListener
): Result {
return try {
- val jwtBearer = "Bearer $jwtToken"
-
- // Uploader
- val mediaContent =
- InputStreamContent("application/octet-stream", BufferedInputStream(FileInputStream(file)))
- mediaContent.length = file.length()
- LOGGER.debug("mediaContent.length: ${mediaContent.length}")
- val transport = NetHttpTransport() // Use Builder to modify behaviour
- val httpRequestInitializer = RequestInitializeHandler(metaData, jwtBearer)
- val uploader = MediaHttpUploader(mediaContent, transport, httpRequestInitializer)
-
- // We currently cannot merge multiple upload-chunk requests into one file on server side.
- // Thus, we prevent slicing the file into multiple files by increasing the chunk size.
- // If the file is larger sync would be successful but only the 1st chunk received DAT-730.
- // i.e. we throw an exception (which skips the upload) for too large measurements (44h+).
- uploader.chunkSize = MAX_CHUNK_SIZE
- if (file.length() > MAX_CHUNK_SIZE) {
- throw MeasurementTooLarge("Transfer file is too large: ${file.length()}")
- }
+ FileInputStream(file).use { fis ->
+ val uploader = initializeUploader(jwtToken, metaData, fis, file)
+
+ // We currently cannot merge multiple upload-chunk requests into one file on server side.
+ // Thus, we prevent slicing the file into multiple files by increasing the chunk size.
+ // If the file is larger sync would be successful but only the 1st chunk received DAT-730.
+ // i.e. we throw an exception (which skips the upload) for too large measurements (44h+).
+ uploader.chunkSize = MAX_CHUNK_SIZE
+ if (file.length() > MAX_CHUNK_SIZE) {
+ throw MeasurementTooLarge("Transfer file is too large: ${file.length()}")
+ }
- // Add meta data to PreRequest
- val jsonFactory = GsonFactory()
- val preRequestBody = preRequestBody(metaData)
- uploader.metadata = JsonHttpContent(jsonFactory, preRequestBody)
+ // Add meta data to PreRequest
+ val jsonFactory = GsonFactory()
+ val preRequestBody = preRequestBody(metaData)
+ uploader.metadata = JsonHttpContent(jsonFactory, preRequestBody)
- // Vert.X currently only supports compressing "down-stream" out of the box
- uploader.disableGZipContent = true
+ // Vert.X currently only supports compressing "down-stream" out of the box
+ uploader.disableGZipContent = true
- // Progress
- uploader.progressListener = ProgressHandler(progressListener)
+ // Progress
+ uploader.progressListener = ProgressHandler(progressListener)
- // Upload
- val requestUrl = GenericUrl(endpoint())
- val response = uploader.upload(requestUrl)
- try {
- readResponse(response, jsonFactory)
- } finally {
- response.disconnect()
+ // Upload
+ val requestUrl = GenericUrl(endpoint)
+ val response = uploader.upload(requestUrl)
+ try {
+ readResponse(response, jsonFactory)
+ } finally {
+ response.disconnect()
+ }
}
+ } catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
+ handleUploadException(e)
}
+ }
- // Soft catch errors in `UploadFailed` exception so that the caller can handle this without crashing.
- // This way the SDK's `SyncPerformer` can determine if the sync should be repeated.
- catch (e: SocketTimeoutException) {
- // Happened on emulator when endpoint is local network instead of 10.0.2.2 [DAT-727]
- // Server not reachable. Try again later.
- throw UploadFailed(ServerUnavailableException(e))
- } catch (e: SSLException) {
+ private fun initializeUploader(
+ jwtToken: String,
+ metaData: RequestMetaData,
+ fileInputStream: FileInputStream,
+ file: File
+ ): MediaHttpUploader {
+ val bufferedInputStream = BufferedInputStream(fileInputStream)
+ val mediaContent = InputStreamContent("application/octet-stream", bufferedInputStream)
+ mediaContent.length = file.length()
+ LOGGER.debug("mediaContent.length: ${mediaContent.length}")
+ val transport = NetHttpTransport() // Use Builder to modify behaviour
+ val jwtBearer = "Bearer $jwtToken"
+ val httpRequestInitializer = RequestInitializeHandler(metaData, jwtBearer)
+ return MediaHttpUploader(mediaContent, transport, httpRequestInitializer)
+ }
+
+ /**
+ * Handles exceptions thrown during upload.
+ *
+ * We wrap errors with [UploadFailed] so that the caller can handle this without crashing.
+ * This way the SDK's `SyncPerformer` can determine if the sync should be repeated.
+ */
+ @Suppress("ComplexMethod")
+ private fun handleUploadException(exception: Exception): Nothing {
+ fun handleIOException(e: IOException): Nothing {
+ LOGGER.warn("Caught IOException: ${e.message}")
+ // Unstable Wi-Fi connection [DAT-742]. transmission stream ended too early, likely because the sync
+ // thread was interrupted (sync canceled). Try again later.
+ if (e.message?.contains("unexpected end of stream") == true) {
+ throw SynchronizationInterruptedException("Upload interrupted", e)
+ }
+ // IOException while reading the response. Try again later.
+ throw UploadFailed(SynchronisationException(e))
+ }
+
+ fun handleSSLException(e: SSLException): Nothing {
LOGGER.warn("Caught SSLException: ${e.message}")
// Thrown by OkHttp when the network is no longer available [DAT-740]. Try again later.
- val message = e.message
- if (message != null && message.contains("I/O error during system call, Broken pipe")) {
+ if (e.message?.contains("I/O error during system call, Broken pipe") == true) {
throw UploadFailed(NetworkUnavailableException("Network became unavailable during upload."))
}
throw UploadFailed(SynchronisationException(e))
- } catch (e: InterruptedIOException) {
- LOGGER.warn("Caught InterruptedIOException: ${e.message}")
- val message = e.message
- if (message != null && message.contains("thread interrupted")) {
+ }
+
+ when (exception) {
+ // Crash unexpected errors hard
+ is MalformedURLException -> error(exception)
+
+ // Soft caught errors
+
+ // Happened on emulator when endpoint is local network instead of 10.0.2.2 [DAT-727]
+ // Server not reachable. Try again later.
+ is SocketTimeoutException -> throw UploadFailed(ServerUnavailableException(exception))
+ is SSLException -> handleSSLException(exception)
+ is InterruptedIOException -> {
+ LOGGER.warn("Caught InterruptedIOException: ${exception.message}")
// Request interrupted [DAT-741]. Try again later.
- throw UploadFailed(NetworkUnavailableException("Network interrupted during upload", e))
- }
- // InterruptedIOException while reading the response. Try again later.
- throw UploadFailed(SynchronisationException(e))
- } catch (e: IOException) {
- LOGGER.warn("Caught IOException: ${e.message}")
- val message = e.message
- if (message != null && message.contains("unexpected end of stream")) {
- // Unstable Wi-Fi connection [DAT-742]. transmission stream ended too early, likely because the sync
- // thread was interrupted (sync canceled). Try again later.
- throw SynchronizationInterruptedException("Upload interrupted", e)
+ if (exception.message?.contains("thread interrupted") == true) {
+ throw UploadFailed(NetworkUnavailableException("Network interrupted during upload", exception))
+ }
+ // InterruptedIOException while reading the response. Try again later.
+ throw UploadFailed(SynchronisationException(exception))
}
- // IOException while reading the response. Try again later.
- throw UploadFailed(SynchronisationException(e))
- } catch (e: MeasurementTooLarge) {
+ is IOException -> handleIOException(exception)
// File is too large to be uploaded. Handle in caller (e.g. skip the upload).
// The max size is currently static and set to 100 MB which should be about 44 hours of 100 Hz measurement.
- throw UploadFailed(e)
- } catch (e: BadRequestException) {
- throw UploadFailed(e) // `HTTP_BAD_REQUEST` (400).
- } catch (e: UnauthorizedException) {
- throw UploadFailed(e) // `HTTP_UNAUTHORIZED` (401).
- } catch (e: ForbiddenException) {
+ is MeasurementTooLarge -> throw UploadFailed(exception)
+ // `HTTP_BAD_REQUEST` (400).
+ is BadRequestException -> throw UploadFailed(exception)
+ // `HTTP_UNAUTHORIZED` (401).
+ is UnauthorizedException -> throw UploadFailed(exception)
// `HTTP_FORBIDDEN` (403). Seems to happen when server is unavailable. Handle in caller.
- throw UploadFailed(e)
- } catch (e: ConflictException) {
- throw UploadFailed(e) // `HTTP_CONFLICT` (409). Already uploaded. Handle in caller (e.g. mark as synced).
- } catch (e: EntityNotParsableException) {
- throw UploadFailed(e) // `HTTP_ENTITY_NOT_PROCESSABLE` (422).
- } catch (e: InternalServerErrorException) {
- throw UploadFailed(e) // `HTTP_INTERNAL_ERROR` (500).
- } catch (e: TooManyRequestsException) {
- throw UploadFailed(e) // `HTTP_TOO_MANY_REQUESTS` (429). Try again later.
- } catch (e: SynchronisationException) {
- throw UploadFailed(e) // IOException while reading the response. Try again later.
- } catch (e: UploadSessionExpired) {
- throw UploadFailed(e) // `HTTP_NOT_FOUND` (404). Try again.
- } catch (e: UnexpectedResponseCode) {
- throw UploadFailed(e) // Unexpected response code. Should be reported to the server admin.
- } catch (e: AccountNotActivated) {
+ is ForbiddenException -> throw UploadFailed(exception)
+ // `HTTP_CONFLICT` (409). Already uploaded. Handle in caller (e.g. mark as synced).
+ is ConflictException -> throw UploadFailed(exception)
+ // `HTTP_ENTITY_NOT_PROCESSABLE` (422).
+ is EntityNotParsableException -> throw UploadFailed(exception)
+ // `HTTP_INTERNAL_ERROR` (500).
+ is InternalServerErrorException -> throw UploadFailed(exception)
+ // `HTTP_TOO_MANY_REQUESTS` (429). Try again later.
+ is TooManyRequestsException -> throw UploadFailed(exception)
+ // IOException while reading the response. Try again later.
+ is SynchronisationException -> throw UploadFailed(exception)
+ // `HTTP_NOT_FOUND` (404). Try again.
+ is UploadSessionExpired -> throw UploadFailed(exception)
+ // Unexpected response code. Should be reported to the server admin.
+ is UnexpectedResponseCode -> throw UploadFailed(exception)
// `PRECONDITION_REQUIRED` (428). Shouldn't happen during upload, report to server admin.
- throw UploadFailed(e)
- }
+ is AccountNotActivated -> throw UploadFailed(exception)
+ // This is not yet thrown as a specific exception.
+ // Network without internet connection. Try again later.
+ // is HostUnresolvable -> throw LoginFailed(e)
- // Crash unexpected errors hard
- catch (e: MalformedURLException) {
- error(e) // The endpoint url is malformed.
+ else -> throw UploadFailed(SynchronisationException(exception))
}
- // This is not yet thrown as a specific exception.
- /*catch (e: HostUnresolvable) {
- throw LoginFailed(e) // Network without internet connection. Try again later.
- }*/
- }
-
- override fun endpoint(): URL {
- return URL(DefaultAuthenticator.returnUrlWithTrailingSlash(apiEndpoint) + "measurements")
}
@Throws(
@@ -344,6 +389,20 @@ class DefaultUploader(private val apiEndpoint: String) : Uploader {
*/
private const val PAYLOAD_TOO_LARGE = 413
+ /**
+ * Adds a trailing slash to the server URL or leaves an existing trailing slash untouched.
+ *
+ * @param url The url to format.
+ * @return The server URL with a trailing slash.
+ */
+ fun returnUrlWithTrailingSlash(url: String): String {
+ return if (url.endsWith("/")) {
+ url
+ } else {
+ "$url/"
+ }
+ }
+
/**
* Assembles a `HttpContent` object which contains the metadata.
*
@@ -354,15 +413,15 @@ class DefaultUploader(private val apiEndpoint: String) : Uploader {
val attributes: MutableMap = HashMap()
// Location meta data
- if (metaData.startLocation != null) {
- attributes["startLocLat"] = metaData.startLocation.latitude.toString()
- attributes["startLocLon"] = metaData.startLocation.longitude.toString()
- attributes["startLocTS"] = metaData.startLocation.timestamp.toString()
+ metaData.startLocation?.let { startLocation ->
+ attributes["startLocLat"] = startLocation.latitude.toString()
+ attributes["startLocLon"] = startLocation.longitude.toString()
+ attributes["startLocTS"] = startLocation.timestamp.toString()
}
- if (metaData.endLocation != null) {
- attributes["endLocLat"] = metaData.endLocation.latitude.toString()
- attributes["endLocLon"] = metaData.endLocation.longitude.toString()
- attributes["endLocTS"] = metaData.endLocation.timestamp.toString()
+ metaData.endLocation?.let { endLocation ->
+ attributes["endLocLat"] = endLocation.latitude.toString()
+ attributes["endLocLon"] = endLocation.longitude.toString()
+ attributes["endLocTS"] = endLocation.timestamp.toString()
}
attributes["locationCount"] = metaData.locationCount.toString()
@@ -373,8 +432,12 @@ class DefaultUploader(private val apiEndpoint: String) : Uploader {
attributes["osVersion"] = metaData.operatingSystemVersion
attributes["appVersion"] = metaData.applicationVersion
attributes["length"] = metaData.length.toString()
- attributes["modality"] = metaData.modality.toString()
+ attributes["modality"] = metaData.modality
attributes["formatVersion"] = metaData.formatVersion.toString()
+ attributes["logCount"] = metaData.logCount.toString()
+ attributes["imageCount"] = metaData.imageCount.toString()
+ attributes["videoCount"] = metaData.videoCount.toString()
+ attributes["filesSize"] = metaData.filesSize.toString()
return attributes
}
diff --git a/src/main/kotlin/de/cyface/uploader/Http.kt b/src/main/kotlin/de/cyface/uploader/Http.kt
index d8b34fa..516c3b8 100644
--- a/src/main/kotlin/de/cyface/uploader/Http.kt
+++ b/src/main/kotlin/de/cyface/uploader/Http.kt
@@ -18,20 +18,7 @@
*/
package de.cyface.uploader
-import de.cyface.model.Activation
-import de.cyface.uploader.exception.AccountNotActivated
-import de.cyface.uploader.exception.BadRequestException
-import de.cyface.uploader.exception.ConflictException
-import de.cyface.uploader.exception.EntityNotParsableException
-import de.cyface.uploader.exception.ForbiddenException
-import de.cyface.uploader.exception.HostUnresolvable
-import de.cyface.uploader.exception.InternalServerErrorException
-import de.cyface.uploader.exception.NetworkUnavailableException
-import de.cyface.uploader.exception.ServerUnavailableException
import de.cyface.uploader.exception.SynchronisationException
-import de.cyface.uploader.exception.TooManyRequestsException
-import de.cyface.uploader.exception.UnauthorizedException
-import de.cyface.uploader.exception.UnexpectedResponseCode
import java.net.HttpURLConnection
import java.net.URL
@@ -40,7 +27,7 @@ import java.net.URL
*
* @author Klemens Muthmann
* @author Armin Schnabel
- * @version 12.0.0
+ * @version 13.0.0
* @since 1.0.0
*/
interface Http {
@@ -54,91 +41,4 @@ interface Http {
*/
@Throws(SynchronisationException::class)
fun open(url: URL, hasBinaryContent: Boolean): HttpURLConnection
-
- /**
- * The post request which authenticates a user at the server.
- *
- * @param connection The `HttpURLConnection` to be used for the request.
- * @param username The username part of the credentials
- * @param password The password part of the credentials
- * @param compress True if the {@param payload} should get compressed
- * @throws SynchronisationException If an IOException occurred while reading the response code.
- * @throws BadRequestException When server returns `HttpURLConnection#HTTP_BAD_REQUEST`
- * @throws UnauthorizedException When the server returns `HttpURLConnection#HTTP_UNAUTHORIZED`
- * @throws ForbiddenException When the server returns `HttpURLConnection#HTTP_FORBIDDEN`
- * @throws ConflictException When the server returns `HttpURLConnection#HTTP_CONFLICT`
- * @throws EntityNotParsableException When the server returns [DefaultUploader.HTTP_ENTITY_NOT_PROCESSABLE]
- * @throws InternalServerErrorException When the server returns `HttpURLConnection#HTTP_INTERNAL_ERROR`
- * @throws NetworkUnavailableException When the network used for transmission becomes unavailable.
- * @throws TooManyRequestsException When the server returns [DefaultUploader.HTTP_TOO_MANY_REQUESTS]
- * @throws HostUnresolvable e.g. when the phone is connected to a network which is not connected to the internet
- * @throws ServerUnavailableException When no connection could be established with the server
- * @throws UnexpectedResponseCode When the server returns an unexpected response code
- * @throws AccountNotActivated When the user account is not activated
- * @return [Result.LOGIN_SUCCESSFUL] if successful or else an `Exception`.
- */
- @Throws(
- SynchronisationException::class,
- UnauthorizedException::class,
- BadRequestException::class,
- InternalServerErrorException::class,
- ForbiddenException::class,
- EntityNotParsableException::class,
- ConflictException::class,
- NetworkUnavailableException::class,
- TooManyRequestsException::class,
- HostUnresolvable::class,
- ServerUnavailableException::class,
- UnexpectedResponseCode::class,
- AccountNotActivated::class
- )
- fun login(connection: HttpURLConnection, username: String, password: String, compress: Boolean): Result
-
- /**
- * The post request which registers a new user at the server.
- *
- * @param connection The `HttpURLConnection` to be used for the request.
- * @param email The email part of the credentials
- * @param password The password part of the credentials
- * @param captcha The captcha token
- * @param activation The template to use for the activation email.
- * @param group The database identifier of the group the user selected during registration
- * @throws SynchronisationException If an IOException occurred while reading the response code.
- * @throws BadRequestException When server returns `HttpURLConnection#HTTP_BAD_REQUEST`
- * @throws UnauthorizedException When the server returns `HttpURLConnection#HTTP_UNAUTHORIZED`
- * @throws ForbiddenException When the server returns `HttpURLConnection#HTTP_FORBIDDEN`
- * @throws ConflictException When the server returns `HttpURLConnection#HTTP_CONFLICT`
- * @throws EntityNotParsableException When the server returns [DefaultUploader.HTTP_ENTITY_NOT_PROCESSABLE]
- * @throws InternalServerErrorException When the server returns `HttpURLConnection#HTTP_INTERNAL_ERROR`
- * @throws NetworkUnavailableException When the network used for transmission becomes unavailable.
- * @throws TooManyRequestsException When the server returns [DefaultUploader.HTTP_TOO_MANY_REQUESTS]
- * @throws HostUnresolvable e.g. when the phone is connected to a network which is not connected to the internet
- * @throws ServerUnavailableException When no connection could be established with the server
- * @throws UnexpectedResponseCode When the server returns an unexpected response code
- * @throws AccountNotActivated When the user account is not activated
- * @return [Result.UPLOAD_SUCCESSFUL] if successful or else an `Exception`.
- */
- @Throws(
- SynchronisationException::class,
- UnauthorizedException::class,
- BadRequestException::class,
- InternalServerErrorException::class,
- ForbiddenException::class,
- EntityNotParsableException::class,
- ConflictException::class,
- NetworkUnavailableException::class,
- TooManyRequestsException::class,
- HostUnresolvable::class,
- ServerUnavailableException::class,
- UnexpectedResponseCode::class,
- AccountNotActivated::class
- )
- fun register(
- connection: HttpURLConnection,
- email: String,
- password: String,
- captcha: String,
- activation: Activation,
- group: String
- ): Result
}
diff --git a/src/main/kotlin/de/cyface/uploader/HttpConnection.kt b/src/main/kotlin/de/cyface/uploader/HttpConnection.kt
index 3f665a8..3842c1f 100644
--- a/src/main/kotlin/de/cyface/uploader/HttpConnection.kt
+++ b/src/main/kotlin/de/cyface/uploader/HttpConnection.kt
@@ -18,34 +18,14 @@
*/
package de.cyface.uploader
-import de.cyface.model.Activation
import de.cyface.uploader.DefaultUploader.Companion.DEFAULT_CHARSET
-import de.cyface.uploader.exception.AccountNotActivated
-import de.cyface.uploader.exception.BadRequestException
-import de.cyface.uploader.exception.ConflictException
-import de.cyface.uploader.exception.EntityNotParsableException
-import de.cyface.uploader.exception.ForbiddenException
-import de.cyface.uploader.exception.HostUnresolvable
-import de.cyface.uploader.exception.InternalServerErrorException
-import de.cyface.uploader.exception.NetworkUnavailableException
-import de.cyface.uploader.exception.ServerUnavailableException
import de.cyface.uploader.exception.SynchronisationException
-import de.cyface.uploader.exception.TooManyRequestsException
-import de.cyface.uploader.exception.UnauthorizedException
-import de.cyface.uploader.exception.UnexpectedResponseCode
-import de.cyface.uploader.exception.UploadSessionExpired
-import org.slf4j.LoggerFactory
-import java.io.BufferedOutputStream
-import java.io.ByteArrayOutputStream
import java.io.IOException
-import java.io.InterruptedIOException
import java.net.HttpURLConnection
import java.net.ProtocolException
import java.net.URL
-import java.util.zip.GZIPOutputStream
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
-import javax.net.ssl.SSLException
import javax.net.ssl.SSLSession
/**
@@ -53,7 +33,7 @@ import javax.net.ssl.SSLSession
*
* @author Klemens Muthmann
* @author Armin Schnabel
- * @version 13.0.0
+ * @version 14.0.0
* @since 2.0.0
*/
class HttpConnection : Http {
@@ -83,227 +63,4 @@ class HttpConnection : Http {
connection.setRequestProperty("User-Agent", System.getProperty("http.agent"))
return connection
}
-
- override fun login(
- connection: HttpURLConnection,
- username: String,
- password: String,
- compress: Boolean
- ): Result {
- // For performance reasons (documentation) set either fixedLength (known length) or chunked streaming mode
- // we currently don't use fixedLengthStreamingMode as we only use this request for small login requests
- connection.setChunkedStreamingMode(0)
- val credentials = credentials(username, password)
- val outputStream = initOutputStream(connection)
- try {
- LOGGER.debug("Transmitting with compression $compress.")
- if (compress) {
- connection.setRequestProperty("Content-Encoding", "gzip")
- outputStream.write(gzip(credentials.toByteArray(DEFAULT_CHARSET)))
- } else {
- outputStream.write(credentials.toByteArray(DEFAULT_CHARSET))
- }
- outputStream.flush()
- outputStream.close()
- } catch (e: SSLException) {
- // This exception is thrown by OkHttp when the network is no longer available
- val message = e.message
- if (message != null && message.contains("I/O error during system call, Broken pipe")) {
- LOGGER.warn("Caught SSLException: ${e.message}")
- throw NetworkUnavailableException("Network became unavailable during transmission.", e)
- } else {
- error(e) // SSLException with unknown cause
- }
- } catch (e: InterruptedIOException) {
- // This exception is thrown when the login request is interrupted, e.g. see MOV-761
- throw NetworkUnavailableException("Network interrupted during login", e)
- } catch (e: IOException) {
- error(e)
- }
- return try {
- readResponse(connection)
- } catch (e: UploadSessionExpired) {
- error(e) // unexpected for login
- }
- }
-
- override fun register(
- connection: HttpURLConnection,
- email: String,
- password: String,
- captcha: String,
- activation: Activation,
- group: String
- ): Result {
- // For performance reasons (documentation) set either fixedLength (known length) or chunked streaming mode
- // we currently don't use fixedLengthStreamingMode as we only use this request for small login requests
- connection.setChunkedStreamingMode(0)
- val payload = registrationPayload(email, password, captcha, activation, group)
- val outputStream = initOutputStream(connection)
- try {
- outputStream.write(payload.toByteArray(DEFAULT_CHARSET))
- outputStream.flush()
- outputStream.close()
- } catch (e: SSLException) {
- // This exception is thrown by OkHttp when the network is no longer available
- val message = e.message
- if (message != null && message.contains("I/O error during system call, Broken pipe")) {
- LOGGER.warn("Caught SSLException: ${e.message}")
- throw NetworkUnavailableException(
- "Network became unavailable during transmission.",
- e
- )
- } else {
- error(e) // SSLException with unknown cause
- }
- } catch (e: InterruptedIOException) {
- // This exception is thrown when the request is interrupted, e.g. see MOV-761
- throw NetworkUnavailableException("Network interrupted during login", e)
- } catch (e: IOException) {
- error(e)
- }
- return try {
- readResponse(connection)
- } catch (e: UploadSessionExpired) {
- error(e)
- }
- }
-
- fun credentials(username: String, password: String): String {
- return "{\"username\":\"$username\",\"password\":\"$password\"}"
- }
-
- private fun registrationPayload(
- email: String,
- password: String,
- captcha: String,
- template: Activation,
- group: String
- ): String {
- return "{\"email\":\"$email\",\"password\":\"$password\",\"captcha\":\"$captcha\",\"template\":\"" +
- "${template.name}\",\"group\":\"$group\"}"
- }
-
- private fun gzip(input: ByteArray): ByteArray {
- return try {
- var gzipOutputStream: GZIPOutputStream? = null
- try {
- val byteArrayOutputStream = ByteArrayOutputStream()
- gzipOutputStream = GZIPOutputStream(byteArrayOutputStream)
- try {
- gzipOutputStream.write(input)
- gzipOutputStream.flush()
- } finally {
- gzipOutputStream.close()
- }
- gzipOutputStream = null
- byteArrayOutputStream.toByteArray()
- } finally {
- gzipOutputStream?.close()
- }
- } catch (@Suppress("SwallowedException") e: IOException) {
- error("Failed to gzip.")
- }
- }
-
- /**
- * Initializes a `BufferedOutputStream` for the provided connection.
- *
- * @param connection the `HttpURLConnection` to create the stream for.
- * @return the `BufferedOutputStream` created.
- * @throws ServerUnavailableException When no connection could be established with the server
- * @throws HostUnresolvable e.g. when the phone is connected to a network which is not connected to the internet.
- */
- @Throws(ServerUnavailableException::class, HostUnresolvable::class)
- private fun initOutputStream(connection: HttpURLConnection): BufferedOutputStream {
- connection.doOutput = true // To upload data to the server
- return try {
- // Wrapping this in a Buffered steam for performance reasons
- BufferedOutputStream(connection.outputStream)
- } catch (e: IOException) {
- val message = e.message
- if (message != null && message.contains("Unable to resolve host")) {
- throw HostUnresolvable(e)
- }
- throw ServerUnavailableException(e)
- }
- }
-
- /**
- * Reads the [HttpResponse] from the [HttpURLConnection] and identifies known errors.
- *
- * @param connection The connection that received the response.
- * @return The [HttpResponse].
- * @throws SynchronisationException If an IOException occurred while reading the response code.
- * @throws BadRequestException When server returns `HttpURLConnection#HTTP_BAD_REQUEST`
- * @throws UnauthorizedException When the server returns `HttpURLConnection#HTTP_UNAUTHORIZED`
- * @throws ForbiddenException When the server returns `HttpURLConnection#HTTP_FORBIDDEN`
- * @throws ConflictException When the server returns `HttpURLConnection#HTTP_CONFLICT`
- * @throws EntityNotParsableException When the server returns [.HTTP_ENTITY_NOT_PROCESSABLE]
- * @throws InternalServerErrorException When the server returns `HttpURLConnection#HTTP_INTERNAL_ERROR`
- * @throws TooManyRequestsException When the server returns [.HTTP_TOO_MANY_REQUESTS]
- * @throws UploadSessionExpired When the server returns [HttpURLConnection.HTTP_NOT_FOUND]
- * @throws UnexpectedResponseCode When the server returns an unexpected response code
- * @throws AccountNotActivated When the user account is not activated
- */
- @Throws(
- SynchronisationException::class,
- BadRequestException::class,
- UnauthorizedException::class,
- ForbiddenException::class,
- ConflictException::class,
- EntityNotParsableException::class,
- InternalServerErrorException::class,
- TooManyRequestsException::class,
- UploadSessionExpired::class,
- UnexpectedResponseCode::class,
- AccountNotActivated::class
- )
- private fun readResponse(connection: HttpURLConnection): Result {
- val responseCode: Int
- val responseMessage: String
- return try {
- responseCode = connection.responseCode
- responseMessage = connection.responseMessage
- val responseBody = readResponseBody(connection)
- if (responseCode in SUCCESS_CODE_START..SUCCESS_CODE_END) {
- DefaultUploader.handleSuccess(HttpResponse(responseCode, responseBody, responseMessage))
- } else {
- DefaultUploader.handleError(HttpResponse(responseCode, responseBody, responseMessage))
- }
- } catch (e: IOException) {
- throw SynchronisationException(e)
- }
- }
-
- /**
- * Reads the body from the [HttpURLConnection]. This contains either the error or the success message.
- *
- * @param connection the [HttpURLConnection] to read the response from
- * @return the [HttpResponse] body
- */
- private fun readResponseBody(connection: HttpURLConnection): String {
- // First try to read and return a success response body
- return try {
- DefaultUploader.readInputStream(connection.inputStream)
- } catch (@Suppress("SwallowedException") e: IOException) {
- // When reading the InputStream fails, we check if there is an ErrorStream to read from
- // (For details see https://developer.android.com/reference/java/net/HttpURLConnection)
- val errorStream = connection.errorStream ?: return ""
-
- // Return empty string if there were no errors, connection is not connected or server sent no useful data.
- // This occurred e.g. on Xiaomi Mi A1 after disabling Wi-Fi instantly after sync start
- DefaultUploader.readInputStream(errorStream)
- }
- }
-
- companion object {
- /**
- * The logger used to log messages from this class. Configure it using src/main/resources/logback.xml.
- */
- private val LOGGER = LoggerFactory.getLogger(HttpConnection::class.java)
-
- private const val SUCCESS_CODE_START = 200
- private const val SUCCESS_CODE_END = 299
- }
}
diff --git a/src/main/kotlin/de/cyface/uploader/RequestInitializeHandler.kt b/src/main/kotlin/de/cyface/uploader/RequestInitializeHandler.kt
index d6b45f6..9863e98 100644
--- a/src/main/kotlin/de/cyface/uploader/RequestInitializeHandler.kt
+++ b/src/main/kotlin/de/cyface/uploader/RequestInitializeHandler.kt
@@ -51,15 +51,15 @@ class RequestInitializeHandler(
private fun addMetaData(metaData: RequestMetaData, headers: HttpHeaders) {
// Location meta data
- if (metaData.startLocation != null) {
- headers["startLocLat"] = metaData.startLocation.latitude.toString()
- headers["startLocLon"] = metaData.startLocation.longitude.toString()
- headers["startLocTS"] = metaData.startLocation.timestamp.toString()
+ metaData.startLocation?.let { startLocation ->
+ headers["startLocLat"] = startLocation.latitude.toString()
+ headers["startLocLon"] = startLocation.longitude.toString()
+ headers["startLocTS"] = startLocation.timestamp.toString()
}
- if (metaData.endLocation != null) {
- headers["endLocLat"] = metaData.endLocation.latitude.toString()
- headers["endLocLon"] = metaData.endLocation.longitude.toString()
- headers["endLocTS"] = metaData.endLocation.timestamp.toString()
+ metaData.endLocation?.let { endLocation ->
+ headers["endLocLat"] = endLocation.latitude.toString()
+ headers["endLocLon"] = endLocation.longitude.toString()
+ headers["endLocTS"] = endLocation.timestamp.toString()
}
headers["locationCount"] = metaData.locationCount.toString()
@@ -70,7 +70,11 @@ class RequestInitializeHandler(
headers["osVersion"] = metaData.operatingSystemVersion
headers["appVersion"] = metaData.applicationVersion
headers["length"] = metaData.length.toString()
- headers["modality"] = metaData.modality.toString()
+ headers["modality"] = metaData.modality
headers["formatVersion"] = metaData.formatVersion.toString()
+ headers["logCount"] = metaData.logCount
+ headers["imageCount"] = metaData.imageCount
+ headers["videoCount"] = metaData.videoCount
+ headers["filesSize"] = metaData.filesSize
}
}
diff --git a/src/main/kotlin/de/cyface/uploader/Uploader.kt b/src/main/kotlin/de/cyface/uploader/Uploader.kt
index 9a6cc62..addf74c 100644
--- a/src/main/kotlin/de/cyface/uploader/Uploader.kt
+++ b/src/main/kotlin/de/cyface/uploader/Uploader.kt
@@ -28,16 +28,16 @@ import java.net.URL
* Interface for uploading files to a Cyface Data Collector.
*
* @author Armin Schnabel
- * @version 1.0.0
+ * @version 2.0.0
* @since 1.0.0
*/
interface Uploader {
/**
- * Uploads the provided file to the server available at the [endpoint].
+ * Uploads the provided measurement file to the server.
*
* @param jwtToken A String in the format "eyXyz123***".
- * @param metaData The [RequestMetaData] required for the Multipart request.
+ * @param metaData The [RequestMetaData] required for the upload request.
* @param file The data file to upload via this post request.
* @param progressListener The [UploadProgressListener] to be informed about the upload progress.
* @throws UploadFailed when an error occurred.
@@ -45,8 +45,7 @@ interface Uploader {
* not interested in the data.
*/
@Throws(UploadFailed::class)
- @Suppress("unused") // Part of the API
- fun upload(
+ fun uploadMeasurement(
jwtToken: String,
metaData: RequestMetaData,
file: File,
@@ -54,9 +53,40 @@ interface Uploader {
): Result
/**
- * @return the endpoint which will be used for the upload.
- * @throws MalformedURLException if the endpoint address provided is malformed.
+ * Uploads the provided attachment file to the server, associated with a specific measurement.
+ *
+ * @param jwtToken A String in the format "eyXyz123***".
+ * @param metaData The [RequestMetaData] required for the upload request.
+ * @param measurementId The id of the measurement the file is attached to.
+ * @param file The attachment file to upload via this post request.
+ * @param progressListener The [UploadProgressListener] to be informed about the upload progress.
+ * @throws UploadFailed when an error occurred.
+ * @return [Result.UPLOAD_SUCCESSFUL] when successful and [Result.UPLOAD_SKIPPED] when the server is
+ * not interested in the data.
+ */
+ @Throws(UploadFailed::class)
+ fun uploadAttachment(
+ jwtToken: String,
+ metaData: RequestMetaData,
+ measurementId: Long,
+ file: File,
+ progressListener: UploadProgressListener
+ ): Result
+
+ /**
+ * @return The URL endpoint used for uploading measurement files.
+ * @throws MalformedURLException if the endpoint address is malformed.
+ */
+ @Throws(MalformedURLException::class)
+ fun measurementsEndpoint(): URL
+
+ /**
+ * Determines the URL endpoint for uploading attachment files associated with a specific measurement.
+ *
+ * @param measurementId The ID of the measurement the files are attached to.
+ * @return The URL endpoint used for uploading attachment files.
+ * @throws MalformedURLException if the endpoint address is malformed.
*/
@Throws(MalformedURLException::class)
- fun endpoint(): URL
+ fun attachmentsEndpoint(measurementId: Long): URL
}
diff --git a/src/main/kotlin/de/cyface/uploader/exception/LoginFailed.kt b/src/main/kotlin/de/cyface/uploader/exception/LoginFailed.kt
deleted file mode 100644
index 9c0c62e..0000000
--- a/src/main/kotlin/de/cyface/uploader/exception/LoginFailed.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2023 Cyface GmbH
- *
- * This file is part of the Cyface Uploader.
- *
- * The Cyface Uploader is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Cyface Uploader is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Cyface Uploader. If not, see .
- */
-package de.cyface.uploader.exception
-
-/**
- * An `Exception` thrown when there is an expected error with the login request so the UI can handle this.
- *
- * @author Armin Schnabel
- * @version 1.0.0
- * @since 7.7.0
- */
-class LoginFailed : Exception {
- /**
- * @param detailedMessage A more detailed message explaining the context for this `Exception`.
- */
- @Suppress("unused") // Part of the API
- constructor(detailedMessage: String?) : super(detailedMessage)
-
- /**
- * @param detailedMessage A more detailed message explaining the context for this `Exception`.
- * @param cause The `Exception` that caused this one.
- */
- @Suppress("unused") // Part of the API
- constructor(detailedMessage: String?, cause: Exception?) : super(detailedMessage, cause)
-
- /**
- * @param cause The `Exception` that caused this one.
- */
- @Suppress("unused") // Part of the API
- constructor(cause: Exception?) : super(cause)
-}
diff --git a/src/main/kotlin/de/cyface/uploader/exception/RegistrationFailed.kt b/src/main/kotlin/de/cyface/uploader/exception/RegistrationFailed.kt
deleted file mode 100644
index 8f9751f..0000000
--- a/src/main/kotlin/de/cyface/uploader/exception/RegistrationFailed.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2023 Cyface GmbH
- *
- * This file is part of the Cyface Uploader.
- *
- * The Cyface Uploader is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Cyface Uploader is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Cyface Uploader. If not, see .
- */
-package de.cyface.uploader.exception
-
-/**
- * An `Exception` thrown when there is an expected error with the registration request so the UI can handle this.
- *
- * @author Armin Schnabel
- * @version 1.0.0
- * @since 7.7.0
- */
-class RegistrationFailed : Exception {
- /**
- * @param detailedMessage A more detailed message explaining the context for this `Exception`.
- */
- @Suppress("unused") // Part of the API
- constructor(detailedMessage: String?) : super(detailedMessage)
-
- /**
- * @param detailedMessage A more detailed message explaining the context for this `Exception`.
- * @param cause The `Exception` that caused this one.
- */
- @Suppress("unused") // Part of the API
- constructor(detailedMessage: String?, cause: Exception?) : super(detailedMessage, cause)
-
- /**
- * @param cause The `Exception` that caused this one.
- */
- @Suppress("unused") // Part of the API
- constructor(cause: Exception?) : super(cause)
-}
diff --git a/src/test/kotlin/de/cyface/uploader/DefaultUploaderTest.kt b/src/test/kotlin/de/cyface/uploader/DefaultUploaderTest.kt
index 16568d8..516911f 100644
--- a/src/test/kotlin/de/cyface/uploader/DefaultUploaderTest.kt
+++ b/src/test/kotlin/de/cyface/uploader/DefaultUploaderTest.kt
@@ -53,7 +53,11 @@ class DefaultUploaderTest {
startLocation,
endLocation,
Modality.BICYCLE.databaseIdentifier,
- 3
+ 3,
+ 0,
+ 0,
+ 0,
+ 0
)
// Act
@@ -76,6 +80,10 @@ class DefaultUploaderTest {
expected["locationCount"] = "5"
expected["modality"] = "BICYCLE"
expected["formatVersion"] = "3"
+ expected["logCount"] = "0"
+ expected["imageCount"] = "0"
+ expected["videoCount"] = "0"
+ expected["filesSize"] = "0"
assertThat(result, equalTo(expected))
}
}
diff --git a/src/test/kotlin/de/cyface/uploader/HttpConnectionTest.kt b/src/test/kotlin/de/cyface/uploader/HttpConnectionTest.kt
deleted file mode 100644
index 69d4577..0000000
--- a/src/test/kotlin/de/cyface/uploader/HttpConnectionTest.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2023 Cyface GmbH
- *
- * This file is part of the Cyface Uploader.
- *
- * The Cyface Uploader is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Cyface Uploader is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Cyface Uploader. If not, see .
- */
-package de.cyface.uploader
-
-import com.natpryce.hamkrest.assertion.assertThat
-import com.natpryce.hamkrest.equalTo
-import org.junit.jupiter.api.Test
-
-/**
- * @author Armin Schnabel
- * @version 1.0.0
- * @since 1.0.0
- */
-class HttpConnectionTest {
- @Test
- fun testCredentials() {
- // Arrange
- val http = HttpConnection()
-
- // Act
- val credentials = http.credentials("test@cyface.de", "secret")
-
- // Assert
- assertThat(credentials, equalTo("{\"username\":\"test@cyface.de\",\"password\":\"secret\"}"))
- }
-}