diff --git a/src/main/kotlin/app/api/Api.kt b/src/main/kotlin/app/api/Api.kt index 5b58a2e5..a2a5658e 100644 --- a/src/main/kotlin/app/api/Api.kt +++ b/src/main/kotlin/app/api/Api.kt @@ -9,11 +9,15 @@ import app.model.Repo import app.model.User interface Api { - fun authorize() - fun getUser(): User - fun getRepo(repoRehash: String): Repo - fun postRepo(repo: Repo) - fun postCommits(commitsList: List) - fun deleteCommits(commitsList: List) - fun postFacts(factsList: List) + companion object { + val OUT_OF_DATE = 1 + } + + fun authorize(): Result + fun getUser(): Result + fun getRepo(repoRehash: String): Result + fun postRepo(repo: Repo): Result + fun postCommits(commitsList: List): Result + fun deleteCommits(commitsList: List): Result + fun postFacts(factsList: List): Result } diff --git a/src/main/kotlin/app/api/ApiError.kt b/src/main/kotlin/app/api/ApiError.kt new file mode 100644 index 00000000..981f1d5b --- /dev/null +++ b/src/main/kotlin/app/api/ApiError.kt @@ -0,0 +1,75 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.api + +import app.Logger +import app.model.Error +import app.model.Errors +import com.github.kittinunf.fuel.core.FuelError +import com.google.protobuf.InvalidProtocolBufferException +import java.nio.charset.Charset +import java.security.InvalidParameterException + +class ApiError(exception: Exception) : Exception(exception.message) { + companion object { + private val AUTH_ERROR_CODES = listOf(401, 403) + } + + // Response content. + var httpStatusCode: Int = 0 + var httpResponseMessage: String = "" + var httpBodyMessage: String = "" + + // Server errors from response. + var serverErrors = listOf() + + // Type of errors. + var isParseError = false + var isAuthError: Boolean = false + get() = AUTH_ERROR_CODES.contains(httpStatusCode) + + constructor(fuelError: FuelError) : this(fuelError as Exception) { + httpStatusCode = fuelError.response.httpStatusCode + httpResponseMessage = fuelError.response.httpResponseMessage + if (fuelError.response.httpResponseHeaders["Content-Type"] + ?.contains("application/octet-stream") == true) { + try { + serverErrors = Errors(fuelError.response.data).errors + } catch (e: Exception) { + Logger.error(e, "Error while parsing errors from server") + } + } else { + httpBodyMessage = fuelError.response.data + .toString(Charset.defaultCharset()) + } + } + + constructor(parseException: InvalidProtocolBufferException) : + this(parseException as Exception) { + isParseError = true + } + + constructor(parseException: InvalidParameterException) : + this(parseException as Exception) { + isParseError = true + } + + fun isWithServerCode(serverErrorCode: Int): Boolean { + return serverErrors.find { error -> + error.code == serverErrorCode } != null + } +} + +fun ApiError?.ifNotNullThrow() { + if (this != null) { + throw this + } +} + +fun ApiError?.isWithServerCode(serverErrorCode: Int): Boolean { + if (this != null) { + return this.isWithServerCode(serverErrorCode) + } + return false +} diff --git a/src/main/kotlin/app/api/MockApi.kt b/src/main/kotlin/app/api/MockApi.kt index fd4bb8cb..c08688ec 100644 --- a/src/main/kotlin/app/api/MockApi.kt +++ b/src/main/kotlin/app/api/MockApi.kt @@ -21,40 +21,45 @@ class MockApi( // GET requests. // DELETE requests. var receivedDeletedCommits: MutableList = mutableListOf() - override fun authorize() { + override fun authorize(): Result { Logger.debug { "MockApi: authorize request" } + return Result() } - override fun getUser(): User { + override fun getUser(): Result { Logger.debug { "MockApi: getUser request" } - return mockUser + return Result(mockUser) } - override fun getRepo(repoRehash: String): Repo { + override fun getRepo(repoRehash: String): Result { Logger.debug { "MockApi: getRepo request" } - return mockRepo + return Result(mockRepo) } - override fun postRepo(repo: Repo) { + override fun postRepo(repo: Repo): Result { Logger.debug { "MockApi: postRepo request ($repo)" } receivedRepos.add(repo) + return Result() } - override fun postCommits(commitsList: List) { + override fun postCommits(commitsList: List): Result { Logger.debug { "MockApi: postCommits request (${commitsList.size} commits)" } receivedAddedCommits.addAll(commitsList) + return Result() } - override fun deleteCommits(commitsList: List) { + override fun deleteCommits(commitsList: List): Result { Logger.debug { "MockApi: deleteCommits request (${commitsList.size} commits)" } receivedDeletedCommits.addAll(commitsList) + return Result() } - override fun postFacts(factsList: List) { + override fun postFacts(factsList: List): Result { Logger.debug { "MockApi: postStats request (${factsList.size} stats)" } receivedFacts.addAll(factsList) + return Result() } } diff --git a/src/main/kotlin/app/api/Result.kt b/src/main/kotlin/app/api/Result.kt new file mode 100644 index 00000000..9a56a26a --- /dev/null +++ b/src/main/kotlin/app/api/Result.kt @@ -0,0 +1,19 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.api + +data class Result (val data: T? = null, val error: ApiError? = null) { + fun getOrThrow(): T { + if (error == null) { + return data!! + } + throw error + } + + fun onErrorThrow() { + if (error != null) { + throw error + } + } +} diff --git a/src/main/kotlin/app/api/ServerApi.kt b/src/main/kotlin/app/api/ServerApi.kt index 7053d3b3..f475c6a4 100644 --- a/src/main/kotlin/app/api/ServerApi.kt +++ b/src/main/kotlin/app/api/ServerApi.kt @@ -12,7 +12,6 @@ import app.model.Fact import app.model.FactGroup import app.model.Repo import app.model.User -import app.utils.RequestException import com.github.kittinunf.fuel.core.FuelManager import com.github.kittinunf.fuel.core.Method import com.github.kittinunf.fuel.core.Request @@ -108,22 +107,27 @@ class ServerApi (private val configurator: Configurator) : Api { private fun makeRequest(request: Request, requestName: String, - parser: (ByteArray) -> T): T { + parser: (ByteArray) -> T): Result { + var error: ApiError? = null + var data: T? = null + try { Logger.debug { "Request $requestName initialized" } val (_, res, result) = request.responseString() val (_, e) = result if (e == null) { Logger.debug { "Request $requestName success" } - return parser(res.data) + data = parser(res.data) } else { - throw RequestException(e) + error = ApiError(e) } } catch (e: InvalidProtocolBufferException) { - throw RequestException(e) + error = ApiError(e) } catch (e: InvalidParameterException) { - throw RequestException(e) + error = ApiError(e) } + + return Result(data, error) } private fun getVersionCodeHeader(): Pair { @@ -134,16 +138,16 @@ class ServerApi (private val configurator: Configurator) : Api { return Pair(HEADER_CONTENT_TYPE, HEADER_CONTENT_TYPE_PROTO) } - override fun authorize() { + override fun authorize(): Result { return makeRequest(createRequestGetToken(), "getToken", {}) } - override fun getUser(): User { + override fun getUser(): Result { return makeRequest(createRequestGetUser(), "getUser", { body -> User(body) }) } - override fun getRepo(repoRehash: String): Repo { + override fun getRepo(repoRehash: String): Result { if (repoRehash.isBlank()) { throw IllegalArgumentException() } @@ -152,25 +156,25 @@ class ServerApi (private val configurator: Configurator) : Api { { body -> Repo(body) }) } - override fun postRepo(repo: Repo) { - makeRequest(createRequestPostRepo(repo), - "postRepo", {}) + override fun postRepo(repo: Repo): Result { + return makeRequest(createRequestPostRepo(repo), + "postRepo", {}) } - override fun postCommits(commitsList: List) { + override fun postCommits(commitsList: List): Result { val commits = CommitGroup(commitsList) - makeRequest(createRequestPostCommits(commits), - "postCommits", {}) + return makeRequest(createRequestPostCommits(commits), + "postCommits", {}) } - override fun deleteCommits(commitsList: List) { + override fun deleteCommits(commitsList: List): Result { val commits = CommitGroup(commitsList) - makeRequest(createRequestDeleteCommits(commits), - "deleteCommits", {}) + return makeRequest(createRequestDeleteCommits(commits), + "deleteCommits", {}) } - override fun postFacts(factsList: List) { + override fun postFacts(factsList: List): Result { val facts = FactGroup(factsList) - makeRequest(createRequestPostFacts(facts), "postFacts", {}) + return makeRequest(createRequestPostFacts(facts), "postFacts", {}) } } diff --git a/src/main/kotlin/app/hashers/CodeLongevity.kt b/src/main/kotlin/app/hashers/CodeLongevity.kt index bddfe087..e28c7166 100644 --- a/src/main/kotlin/app/hashers/CodeLongevity.kt +++ b/src/main/kotlin/app/hashers/CodeLongevity.kt @@ -22,7 +22,6 @@ import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.treewalk.TreeWalk import org.eclipse.jgit.util.io.DisabledOutputStream -import java.io.InputStream import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException @@ -211,7 +210,7 @@ class CodeLongevity(private val serverRepo: Repo, } if (stats.size > 0) { - api.postFacts(stats) + api.postFacts(stats).onErrorThrow() Logger.info { "Sent ${stats.size} facts to server" } } } diff --git a/src/main/kotlin/app/hashers/CommitHasher.kt b/src/main/kotlin/app/hashers/CommitHasher.kt index dd40535b..aae20917 100644 --- a/src/main/kotlin/app/hashers/CommitHasher.kt +++ b/src/main/kotlin/app/hashers/CommitHasher.kt @@ -53,7 +53,10 @@ class CommitHasher(private val serverRepo: Repo = Repo(), commit } - .buffer(20, TimeUnit.SECONDS) // Group ready commits by time. + // Group ready commits by time and count. Max payload 10 mb, + // one commit with stats takes around 1 kb, so pack by max 1000 + // with 10x margin of safety. + .buffer(20, TimeUnit.SECONDS, 1000) .subscribe({ commitsBundle -> // OnNext. postCommitsToServer(commitsBundle) // Send ready commits. }, onError) @@ -70,14 +73,14 @@ class CommitHasher(private val serverRepo: Repo = Repo(), private fun postCommitsToServer(commits: List) { if (commits.isNotEmpty()) { - api.postCommits(commits) + api.postCommits(commits).onErrorThrow() Logger.info { "Sent ${commits.size} added commits to server" } } } private fun deleteCommitsOnServer(commits: List) { if (commits.isNotEmpty()) { - api.deleteCommits(commits) + api.deleteCommits(commits).onErrorThrow() Logger.info { "Sent ${commits.size} deleted commits to server" } } } diff --git a/src/main/kotlin/app/hashers/FactHasher.kt b/src/main/kotlin/app/hashers/FactHasher.kt index 4d9a3c58..41332ac0 100644 --- a/src/main/kotlin/app/hashers/FactHasher.kt +++ b/src/main/kotlin/app/hashers/FactHasher.kt @@ -82,7 +82,7 @@ class FactHasher(private val serverRepo: Repo = Repo(), fsRepoDateStart[email] = timestamp // RepoDateEnd. - if ((fsRepoDateEnd[email] ?: -1) == -1L) { + if (fsRepoDateEnd[email]!! == -1L) { fsRepoDateEnd[email] = timestamp } @@ -137,7 +137,7 @@ class FactHasher(private val serverRepo: Repo = Repo(), private fun postFactsToServer(facts: List) { if (facts.isNotEmpty()) { - api.postFacts(facts) + api.postFacts(facts).onErrorThrow() Logger.info { "Sent ${facts.size} facts to server" } } } diff --git a/src/main/kotlin/app/hashers/RepoHasher.kt b/src/main/kotlin/app/hashers/RepoHasher.kt index 7197e297..069a6aa1 100644 --- a/src/main/kotlin/app/hashers/RepoHasher.kt +++ b/src/main/kotlin/app/hashers/RepoHasher.kt @@ -88,9 +88,9 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, throw HashingException(errors) } - println("Hashing $localRepo successfully finished.") + println("Hashing $localRepo complete finished.") Logger.info(Logger.Events.HASHING_REPO_SUCCESS) - { "Hashing repo succesfully" } + { "Hashing repo completed" } } finally { closeGit(git) @@ -116,7 +116,7 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, } private fun getRepoFromServer() { - val repo = api.getRepo(serverRepo.rehash) + val repo = api.getRepo(serverRepo.rehash).getOrThrow() serverRepo.commits = repo.commits Logger.info{ "Received repo from server with ${serverRepo.commits.size} commits" @@ -126,7 +126,7 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, private fun postRepoToServer() { serverRepo.commits = listOf() - api.postRepo(serverRepo) + api.postRepo(serverRepo).onErrorThrow() Logger.debug { serverRepo.toString() } } diff --git a/src/main/kotlin/app/model/DiffContent.kt b/src/main/kotlin/app/model/DiffContent.kt index 82cb7d1e..212eb978 100644 --- a/src/main/kotlin/app/model/DiffContent.kt +++ b/src/main/kotlin/app/model/DiffContent.kt @@ -1,3 +1,6 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + package app.model class DiffContent( diff --git a/src/main/kotlin/app/model/Error.kt b/src/main/kotlin/app/model/Error.kt new file mode 100644 index 00000000..0baf8997 --- /dev/null +++ b/src/main/kotlin/app/model/Error.kt @@ -0,0 +1,35 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.model + +import app.Protos +import com.google.protobuf.InvalidProtocolBufferException +import java.security.InvalidParameterException + +data class Error( + var code: Int = 0, + var message: String = "" +) { + @Throws(InvalidParameterException::class) + constructor(proto: Protos.Error) : this() { + code = proto.code + message = proto.message + } + + @Throws(InvalidProtocolBufferException::class) + constructor(bytes: ByteArray) : this(Protos.Error.parseFrom(bytes)) + + constructor(serialized: String) : this(serialized.toByteArray()) + + fun getProto(): Protos.Error { + return Protos.Error.newBuilder() + .setCode(code) + .setMessage(message) + .build() + } + + fun serialize(): ByteArray { + return getProto().toByteArray() + } +} diff --git a/src/main/kotlin/app/model/Errors.kt b/src/main/kotlin/app/model/Errors.kt new file mode 100644 index 00000000..59b9625a --- /dev/null +++ b/src/main/kotlin/app/model/Errors.kt @@ -0,0 +1,32 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.model + +import app.Protos +import com.google.protobuf.InvalidProtocolBufferException +import java.security.InvalidParameterException + +data class Errors ( + var errors: List = listOf() +) { + @Throws(InvalidParameterException::class) + constructor(proto: Protos.Errors) : this() { + errors = proto.errorsList.map { error -> Error(error) } + } + + @Throws(InvalidProtocolBufferException::class) + constructor(bytes: ByteArray) : this(Protos.Errors.parseFrom(bytes)) + + constructor(serialized: String) : this(serialized.toByteArray()) + + fun getProto(): Protos.Errors { + return Protos.Errors.newBuilder() + .addAllErrors(errors.map { error -> error.getProto() }) + .build() + } + + fun serialize(): ByteArray { + return getProto().toByteArray() + } +} diff --git a/src/main/kotlin/app/model/Fact.kt b/src/main/kotlin/app/model/Fact.kt index 91daf5a8..e137f1d9 100644 --- a/src/main/kotlin/app/model/Fact.kt +++ b/src/main/kotlin/app/model/Fact.kt @@ -1,3 +1,6 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + package app.model import app.Protos diff --git a/src/main/kotlin/app/model/FactGroup.kt b/src/main/kotlin/app/model/FactGroup.kt index 9c00871f..681196d1 100644 --- a/src/main/kotlin/app/model/FactGroup.kt +++ b/src/main/kotlin/app/model/FactGroup.kt @@ -1,3 +1,6 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + package app.model import app.Protos diff --git a/src/main/kotlin/app/ui/AuthState.kt b/src/main/kotlin/app/ui/AuthState.kt index 11446e6e..66948e9b 100644 --- a/src/main/kotlin/app/ui/AuthState.kt +++ b/src/main/kotlin/app/ui/AuthState.kt @@ -3,13 +3,14 @@ package app.ui -import app.Analytics import app.BuildConfig import app.Logger import app.api.Api import app.config.Configurator import app.utils.PasswordHelper -import app.utils.RequestException +import app.api.ApiError +import app.api.ifNotNullThrow +import app.api.isWithServerCode /** * Authorization console UI state. @@ -20,7 +21,8 @@ class AuthState constructor(private val context: Context, : ConsoleState { var username = "" var password = "" - var connectionError = false + var retry = true + var authorized = false override fun doAction() { if (!configurator.isValidCredentials()) { @@ -28,13 +30,15 @@ class AuthState constructor(private val context: Context, getPassword() } - while (!tryAuth() && !connectionError) { + authorized = tryAuth() + while (!authorized && retry) { getPassword() + authorized = tryAuth() } } override fun next() { - if (!connectionError) { + if (authorized) { context.changeState(ListRepoState(context, api, configurator)) } else { context.changeState(CloseState()) @@ -68,9 +72,17 @@ class AuthState constructor(private val context: Context, fun tryAuth(): Boolean { try { println("Authenticating...") - api.authorize() + val (_, error) = api.authorize() + if (error.isWithServerCode(Api.OUT_OF_DATE)) { + println("App is out of date. Please get new version at " + + "https://sourcerer.io") + retry = false + return false + } + // Other request errors should be processed by try/catch. + error.ifNotNullThrow() - val user = api.getUser() + val user = api.getUser().getOrThrow() configurator.setRepos(user.repos) println("You are successfully authenticated. Your profile page is " @@ -81,7 +93,7 @@ class AuthState constructor(private val context: Context, Logger.info(Logger.Events.AUTH) { "Auth success" } return true - } catch (e: RequestException) { + } catch (e: ApiError) { if (e.isAuthError) { if(e.httpBodyMessage.isNotBlank()) { println(e.httpBodyMessage) @@ -89,10 +101,11 @@ class AuthState constructor(private val context: Context, println("Authentication error. Try again.") } } else { - connectionError = true println("Connection problems. Try again later.") + retry = false } } + return false } } diff --git a/src/main/proto/sourcerer.proto b/src/main/proto/sourcerer.proto index efa1ca90..f30a9c9a 100644 --- a/src/main/proto/sourcerer.proto +++ b/src/main/proto/sourcerer.proto @@ -98,3 +98,13 @@ message Repo { // starts after last commit from the overlap. repeated Commit commits = 5; } + +// Errors from server. +message Error { + uint32 code = 1; + string message = 2; +} + +message Errors { + repeated Error errors = 1; +} diff --git a/src/test/kotlin/test/tests/hashers/FactHasherTest.kt b/src/test/kotlin/test/tests/hashers/FactHasherTest.kt index 33f51e42..f6302188 100644 --- a/src/test/kotlin/test/tests/hashers/FactHasherTest.kt +++ b/src/test/kotlin/test/tests/hashers/FactHasherTest.kt @@ -193,7 +193,6 @@ class FactHasherTest : Spek({ "2"))) } - afterGroup { testRepo.destroy() }