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\"}")) - } -}