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 251e5d1..9085a1d 100644 --- a/src/main/kotlin/de/cyface/uploader/DefaultUploader.kt +++ b/src/main/kotlin/de/cyface/uploader/DefaultUploader.kt @@ -63,8 +63,7 @@ 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.upload] with an authentication token. * * @author Armin Schnabel * @version 1.0.0 @@ -197,7 +196,7 @@ class DefaultUploader(private val apiEndpoint: String) : Uploader { } override fun endpoint(): URL { - return URL(DefaultAuthenticator.returnUrlWithTrailingSlash(apiEndpoint) + "measurements") + return URL(returnUrlWithTrailingSlash(apiEndpoint) + "measurements") } @Throws( @@ -344,6 +343,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. + */ + private fun returnUrlWithTrailingSlash(url: String): String { + return if (url.endsWith("/")) { + url + } else { + "$url/" + } + } + /** * Assembles a `HttpContent` object which contains the metadata. * @@ -355,14 +368,14 @@ class DefaultUploader(private val apiEndpoint: String) : Uploader { // 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() + attributes["startLocLat"] = metaData.startLocation!!.latitude.toString() + attributes["startLocLon"] = metaData.startLocation!!.longitude.toString() + attributes["startLocTS"] = metaData.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() + attributes["endLocLat"] = metaData.endLocation!!.latitude.toString() + attributes["endLocLon"] = metaData.endLocation!!.longitude.toString() + attributes["endLocTS"] = metaData.endLocation!!.timestamp.toString() } attributes["locationCount"] = metaData.locationCount.toString() @@ -373,7 +386,7 @@ 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() 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..e3e9f38 100644 --- a/src/main/kotlin/de/cyface/uploader/HttpConnection.kt +++ b/src/main/kotlin/de/cyface/uploader/HttpConnection.kt @@ -18,7 +18,6 @@ */ 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 @@ -53,7 +52,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 +82,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 a60fafe..12fea6f 100644 --- a/src/main/kotlin/de/cyface/uploader/RequestInitializeHandler.kt +++ b/src/main/kotlin/de/cyface/uploader/RequestInitializeHandler.kt @@ -52,14 +52,14 @@ 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() + headers["startLocLat"] = metaData.startLocation!!.latitude.toString() + headers["startLocLon"] = metaData.startLocation!!.longitude.toString() + headers["startLocTS"] = metaData.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() + headers["endLocLat"] = metaData.endLocation!!.latitude.toString() + headers["endLocLon"] = metaData.endLocation!!.longitude.toString() + headers["endLocTS"] = metaData.endLocation!!.timestamp.toString() } headers["locationCount"] = metaData.locationCount.toString() @@ -70,7 +70,7 @@ 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