diff --git a/kotlin-http-client/kotlin-http-client-atlas/src/main/kotlin/com/linkedplanet/kotlinhttpclient/atlas/AtlasHttpClient.kt b/kotlin-http-client/kotlin-http-client-atlas/src/main/kotlin/com/linkedplanet/kotlinhttpclient/atlas/AtlasHttpClient.kt index ec6d0406..149e89db 100644 --- a/kotlin-http-client/kotlin-http-client-atlas/src/main/kotlin/com/linkedplanet/kotlinhttpclient/atlas/AtlasHttpClient.kt +++ b/kotlin-http-client/kotlin-http-client-atlas/src/main/kotlin/com/linkedplanet/kotlinhttpclient/atlas/AtlasHttpClient.kt @@ -115,7 +115,11 @@ class AtlasHttpClient(private val appLink: ApplicationLink) : BaseHttpClient() { val file = tempFileWithData(filename, inputStream) val filePart = RequestFilePart(mimeType, filename, file, "file") request.setFiles(listOf(filePart)) - + request.apply { + setHeader("X-Atlassian-Token", "no-check") + setHeader("Connection", "keep-alive") + setHeader("Cache-Control", "no-cache") + } request.execute(object : ApplicationLinkResponseHandler>> { override fun credentialsRequired(response: Response) = null diff --git a/kotlin-http-client/kotlin-http-client-ktor/src/main/kotlin/com/linkedplanet/kotlinhttpclient/ktor/KtorHttpClient.kt b/kotlin-http-client/kotlin-http-client-ktor/src/main/kotlin/com/linkedplanet/kotlinhttpclient/ktor/KtorHttpClient.kt index 3de72cbd..b93fbfdd 100644 --- a/kotlin-http-client/kotlin-http-client-ktor/src/main/kotlin/com/linkedplanet/kotlinhttpclient/ktor/KtorHttpClient.kt +++ b/kotlin-http-client/kotlin-http-client-ktor/src/main/kotlin/com/linkedplanet/kotlinhttpclient/ktor/KtorHttpClient.kt @@ -141,6 +141,7 @@ class KtorHttpClient( } ) { url("$baseUrl$url") + header("X-Atlassian-Token", "no-check") header("Connection", "keep-alive") header("Cache-Control", "no-cache") } diff --git a/kotlin-insight-client/kotlin-insight-client-api/src/main/kotlin/com/linkedplanet/kotlininsightclient/api/error/InsightClientError.kt b/kotlin-insight-client/kotlin-insight-client-api/src/main/kotlin/com/linkedplanet/kotlininsightclient/api/error/InsightClientError.kt index f4c92fee..90ba846c 100644 --- a/kotlin-insight-client/kotlin-insight-client-api/src/main/kotlin/com/linkedplanet/kotlininsightclient/api/error/InsightClientError.kt +++ b/kotlin-insight-client/kotlin-insight-client-api/src/main/kotlin/com/linkedplanet/kotlininsightclient/api/error/InsightClientError.kt @@ -52,12 +52,12 @@ sealed class InsightClientError( ) : AtlassianClientError(error, message, stacktrace, statusCode) { companion object { - private const val internalErrorString = "Jira/Insight hat ein internes Problem festgestellt" + private const val internalErrorString = "Jira/Assets hat ein internes Problem festgestellt" fun fromException(e: Throwable): InsightClientError = - ExceptionInsightClientError("Insight-Fehler", e.message ?: internalErrorString, e.stackTraceToString()) + ExceptionInsightClientError("Assets-Fehler", e.message ?: internalErrorString, e.stackTraceToString()) fun internalError(message: String): Either = - InternalInsightClientError("Interner Insight-Fehler", message).asEither() + InternalInsightClientError("Interner Assets-Fehler", message).asEither() } } @@ -75,16 +75,16 @@ class ObjectNotFoundError(val objectId: InsightObjectId): class ObjectTypeNotFoundError(val rootObjectTypeId: InsightObjectTypeId) : InsightClientError("Insight Objekttyp unbekannt", "Der Objekttyp mit der angegebenen InsightObjectTypeId=$rootObjectTypeId wurde nicht gefunden.") -class OtherNotFoundError(message: String) : InsightClientError("Nicht gefunden.", message) +class OtherNotFoundError(message: String) : InsightClientError("Nicht gefunden.", message, statusCode = 404) open class OtherInsightClientError(error: String, message: String) : InsightClientError(error, message) /** * Somewhere inside an HTTP connection failed. */ -class HttpInsightClientError(statusCode: Int, error: String, message: String) : +class HttpInsightClientError(statusCode: Int, error: String = "Assets-Fehler", message: String) : InsightClientError( error = error, - message = "$message StatusCode:$statusCode", + message = "$message (StatusCode:$statusCode)", statusCode = statusCode ) diff --git a/kotlin-insight-client/kotlin-insight-client-http/src/main/kotlin/com/linkedplanet/kotlininsightclient/http/HttpInsightAttachmentOperator.kt b/kotlin-insight-client/kotlin-insight-client-http/src/main/kotlin/com/linkedplanet/kotlininsightclient/http/HttpInsightAttachmentOperator.kt index e4d2c5d0..a7d69384 100644 --- a/kotlin-insight-client/kotlin-insight-client-http/src/main/kotlin/com/linkedplanet/kotlininsightclient/http/HttpInsightAttachmentOperator.kt +++ b/kotlin-insight-client/kotlin-insight-client-http/src/main/kotlin/com/linkedplanet/kotlininsightclient/http/HttpInsightAttachmentOperator.kt @@ -102,10 +102,10 @@ class HttpInsightAttachmentOperator(private val context: HttpInsightClientContex override suspend fun downloadAttachmentZip(objectId: InsightObjectId): Either = either { val attachments = getAttachments(objectId).bind() - val fileMap: Map = attachments.map { attachment -> + val fileMap: Map = attachments.associate { attachment -> val attachmentContent = downloadAttachment(attachment.url).bind() attachment.filename to attachmentContent - }.toMap() + } zipInputStreamForMultipleInputStreams(fileMap).bind() } diff --git a/kotlin-insight-client/kotlin-insight-client-sdk/pom.xml b/kotlin-insight-client/kotlin-insight-client-sdk/pom.xml index 4c4d6569..b0ac342a 100644 --- a/kotlin-insight-client/kotlin-insight-client-sdk/pom.xml +++ b/kotlin-insight-client/kotlin-insight-client-sdk/pom.xml @@ -20,17 +20,10 @@ ${project.version} - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - - com.atlassian.jira jira-api - ${jira.version} provided @@ -75,31 +68,5 @@ - - com.atlassian.servicedesk - insight-core-model - ${insight.version} - provided - - - com.atlassian.servicedesk - insight-core-persistence - ${insight.version} - provided - - - com.atlassian.plugin - atlassian-spring-scanner-annotation - ${atlassian.spring.scanner.version} - provided - - - - - javax.inject - javax.inject - 1 - provided - diff --git a/kotlin-insight-client/kotlin-insight-client-sdk/src/main/kotlin/com/linkedplanet/kotlininsightclient/sdk/SdkInsightAttachmentOperator.kt b/kotlin-insight-client/kotlin-insight-client-sdk/src/main/kotlin/com/linkedplanet/kotlininsightclient/sdk/SdkInsightAttachmentOperator.kt index 0672ecc8..b339cc89 100644 --- a/kotlin-insight-client/kotlin-insight-client-sdk/src/main/kotlin/com/linkedplanet/kotlininsightclient/sdk/SdkInsightAttachmentOperator.kt +++ b/kotlin-insight-client/kotlin-insight-client-sdk/src/main/kotlin/com/linkedplanet/kotlininsightclient/sdk/SdkInsightAttachmentOperator.kt @@ -21,19 +21,25 @@ package com.linkedplanet.kotlininsightclient.sdk import arrow.core.Either import arrow.core.raise.either +import com.atlassian.jira.security.JiraAuthenticationContext +import com.atlassian.sal.api.net.Request +import com.atlassian.sal.api.net.Response +import com.atlassian.sal.api.net.TrustedRequest +import com.atlassian.sal.api.net.TrustedRequestFactory +import com.linkedplanet.kotlininsightclient.api.error.HttpInsightClientError import com.linkedplanet.kotlininsightclient.api.error.InsightClientError -import com.linkedplanet.kotlininsightclient.api.error.OtherNotFoundError import com.linkedplanet.kotlininsightclient.api.interfaces.InsightAttachmentOperator import com.linkedplanet.kotlininsightclient.api.model.AttachmentId import com.linkedplanet.kotlininsightclient.api.model.InsightAttachment import com.linkedplanet.kotlininsightclient.api.model.InsightObjectId import com.linkedplanet.kotlininsightclient.sdk.services.ReverseEngineeredAttachmentUrlResolver -import com.linkedplanet.kotlininsightclient.sdk.services.ReverseEngineeredFileManager import com.linkedplanet.kotlininsightclient.sdk.util.catchAsInsightClientError +import com.linkedplanet.kotlininsightclient.sdk.util.getComponent import com.linkedplanet.kotlininsightclient.sdk.util.getOSGiComponent import com.linkedplanet.kotlininsightclient.sdk.util.toISOString import com.riadalabs.jira.plugins.insight.channel.external.api.facade.ObjectFacade import com.riadalabs.jira.plugins.insight.services.model.AttachmentBean +import org.apache.http.client.utils.URIBuilder import java.io.InputStream import java.io.PipedInputStream import java.io.PipedOutputStream @@ -47,13 +53,12 @@ import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import kotlin.io.path.createTempFile - object SdkInsightAttachmentOperator : InsightAttachmentOperator { private val objectFacade: ObjectFacade by getOSGiComponent() - - private val fileManager = ReverseEngineeredFileManager() + private val jiraAuthenticationContext: JiraAuthenticationContext by getComponent() private val attachmentUrlResolver = ReverseEngineeredAttachmentUrlResolver() + private val trustedRequestFactory: TrustedRequestFactory<*> by getOSGiComponent() override suspend fun getAttachments(objectId: InsightObjectId): Either> = catchAsInsightClientError { @@ -63,27 +68,44 @@ object SdkInsightAttachmentOperator : InsightAttachmentOperator { } override suspend fun downloadAttachment(url: String): Either = - catchAsInsightClientError { - val attachmentId = attachmentUrlResolver.parseAttachmentIdFromPathInformation(url) - val attachmentBean = objectFacade.loadAttachmentBeanById(attachmentId) - fileManager.getObjectAttachmentContent(attachmentBean.objectId, attachmentBean.nameInFileSystem) - }.mapLeft { OtherNotFoundError("Attachment download failed for url:$url") } + either { + val request = trustedGetRequestForCurrentUser(url).bind() + val response = Either.catch { request.executeAndReturn { it } as Response } + .mapLeft { downloadFailed(500, url) }.bind() + if (!response.isSuccessful) { + raise(downloadFailed(response.statusCode, url)) + } + response.responseBodyAsStream + } + + private fun downloadFailed(statusCode: Int, url: String, ) = HttpInsightClientError( + statusCode = statusCode, + message = "Anhang Download fehlgeschlagen für URL: $url" + ) + + private fun trustedGetRequestForCurrentUser(url: String): Either = + Either.catch { + trustedRequestFactory.createTrustedRequest(Request.MethodType.GET, url).apply { + addTrustedTokenAuthentication(URIBuilder(url).host, jiraAuthenticationContext.loggedInUser.username) + setHeader("Content-Type", "application/json") + } + }.mapLeft { + HttpInsightClientError( + statusCode = 500, + message = "Es konnte keine Verbindung zu Assets erzeugt werden." + ) + } override suspend fun downloadAttachmentZip(objectId: InsightObjectId): Either = either { - val fileMap = allAttachmentStreamsForInsightObject(objectId).bind() + val attachments = getAttachments(objectId).bind() + val fileMap: Map = attachments.associate { attachment -> + val attachmentContent = downloadAttachment(attachment.url).bind() + attachment.filename to attachmentContent + } zipInputStreamForMultipleInputStreams(fileMap).bind() } - private fun allAttachmentStreamsForInsightObject(objectId: InsightObjectId) = - catchAsInsightClientError { - val attachmentBeans = objectFacade.findAttachmentBeans(objectId.raw) - attachmentBeans.map { bean -> - val attachmentContent = fileManager.getObjectAttachmentContent(bean.objectId, bean.nameInFileSystem) - bean.filename to attachmentContent - }.toMap() - } - private fun zipInputStreamForMultipleInputStreams( fileMap: Map ): Either = @@ -112,8 +134,7 @@ object SdkInsightAttachmentOperator : InsightAttachmentOperator { Files.copy(inputStream, tempFilePath, StandardCopyOption.REPLACE_EXISTING) val mimeType = URLConnection.guessContentTypeFromName(filename) val bean = objectFacade.addAttachmentBean(objectId.raw, tempFilePath.toFile(), filename, mimeType, null) - val insightAttachment = beanToInsightAttachment(bean) - insightAttachment + beanToInsightAttachment(bean) } override suspend fun deleteAttachment(attachmentId: AttachmentId): Either = diff --git a/kotlin-insight-client/kotlin-insight-client-sdk/src/main/kotlin/com/linkedplanet/kotlininsightclient/sdk/services/ReverseEngineeredFileManager.kt b/kotlin-insight-client/kotlin-insight-client-sdk/src/main/kotlin/com/linkedplanet/kotlininsightclient/sdk/services/ReverseEngineeredFileManager.kt deleted file mode 100644 index 58467fb2..00000000 --- a/kotlin-insight-client/kotlin-insight-client-sdk/src/main/kotlin/com/linkedplanet/kotlininsightclient/sdk/services/ReverseEngineeredFileManager.kt +++ /dev/null @@ -1,75 +0,0 @@ -/*- - * #%L - * kotlin-insight-client-sdk - * %% - * Copyright (C) 2022 - 2023 linked-planet GmbH - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ -package com.linkedplanet.kotlininsightclient.sdk.services - -import com.atlassian.jira.config.util.AttachmentPathManager -import com.atlassian.jira.issue.AttachmentManager -import com.linkedplanet.kotlininsightclient.sdk.util.getComponent -import java.io.File -import java.io.FileInputStream -import java.io.FileNotFoundException -import java.io.InputStream - -/** - * Partial Kotlin Version of io.riada.insight.persistence.FileManager - * - * The public API offers ways to upload an attachment File, but there is no way to download the File. - * The original File is not accessible at all by our projects class loader. - */ -internal class ReverseEngineeredFileManager{ - - private val attachmentManager: AttachmentManager by getComponent() - private val attachmentPathManager: AttachmentPathManager by getComponent() - - @Throws(FileNotFoundException::class) - fun getObjectAttachmentContent(objectId: Int, attachmentFileName: String): InputStream { - return getInputStream(this.getObjectAttachmentDirectory(objectId), attachmentFileName) - } - - @Throws(FileNotFoundException::class) - private fun getInputStream(directory: File, fileName: String): InputStream { - return FileInputStream(File(directory, fileName)) - } - - private fun getObjectAttachmentDirectory(objectId: Int): File { - return this.getDirectoryName("object/$objectId") - } - - private fun getDirectoryName(objectDirectoryName: String): File { - val applicationDirectory = File(getAttachmentPath(), "insight") - return this.getDirectoryOrCreateIfNotExist(applicationDirectory, objectDirectoryName) - } - - private fun getDirectoryOrCreateIfNotExist(applicationDirectory: File, objectDirectoryName: String): File { - val returnDir = File(applicationDirectory, objectDirectoryName) - if (!returnDir.exists()) { - returnDir.mkdirs() - } - return returnDir - } - - private fun getAttachmentPath(): String = - if (attachmentManager.attachmentsEnabled()) { - attachmentPathManager.attachmentPath - } else { - attachmentPathManager.defaultAttachmentPath - } - -} \ No newline at end of file diff --git a/kotlin-insight-client/kotlin-insight-client-test-base/pom.xml b/kotlin-insight-client/kotlin-insight-client-test-base/pom.xml index 35992369..dcbedb4b 100644 --- a/kotlin-insight-client/kotlin-insight-client-test-base/pom.xml +++ b/kotlin-insight-client/kotlin-insight-client-test-base/pom.xml @@ -38,6 +38,11 @@ provided + + org.slf4j + slf4j-api + provided + diff --git a/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/AuthenticatedJiraHttpClientFactory.kt b/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/AuthenticatedJiraHttpClientFactory.kt index 70a21cc9..80db9ee1 100644 --- a/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/AuthenticatedJiraHttpClientFactory.kt +++ b/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/AuthenticatedJiraHttpClientFactory.kt @@ -20,11 +20,10 @@ package com.linkedplanet.kotlininsightclient import arrow.core.Either -import arrow.core.left import arrow.core.right import com.google.gson.Gson -import com.linkedplanet.kotlininsightclient.api.error.AuthenticationError import com.linkedplanet.kotlininsightclient.api.error.InsightClientError +import org.slf4j.LoggerFactory import org.http4k.client.Java8HttpClient import org.http4k.core.Body import org.http4k.core.HttpHandler @@ -36,6 +35,7 @@ import org.http4k.core.then import org.http4k.filter.ClientFilters import org.http4k.filter.cookie.BasicCookieStorage import org.http4k.filter.cookie.CookieStorage +import java.util.* /** * Provides access to Jira itself via a logged in AuthenticatedHttpHandler @@ -46,31 +46,35 @@ class AuthenticatedJiraHttpClientFactory( companion object { data class Credentials(val username: String, val password: String) } + private val log = LoggerFactory.getLogger(this::class.java) private val storage: CookieStorage = BasicCookieStorage() // this is just a HashMap private val httpHandler: HttpHandler = ClientFilters.Cookies(storage = storage).then(Java8HttpClient()) private val gson = Gson() - fun login(credentials: Credentials) : Either { + fun login(credentials: Credentials): Either { + val username = credentials.username + val password = credentials.password val body = gson.toJson(credentials) val request = Request(Method.POST, "$jiraOrigin/rest/auth/1/session") .header("content-type", "application/json") .body(Body(body)) - val loginResponse = httpHandler(request) if (loginResponse.status != Status.OK) { - return AuthenticationError( - "Login failed with HTTP StatusCode:${loginResponse.status.code}" - ).left() - } else { - val privateHandler = object : AuthenticatedHttpHandler, HttpHandler by httpHandler { - override fun getWithRelativePath(path: String): Response { - val absolutePath = "$jiraOrigin$path" - return this(Request(Method.GET, absolutePath)) - } + log.debug("Continue despite 'session' login failing with HTTP StatusCode:${loginResponse.status.code}") + } + val privateHandler = object : AuthenticatedHttpHandler, HttpHandler by httpHandler { + override fun getWithRelativePath(path: String): Response { + val absolutePath = "$jiraOrigin$path" + val getRequest = Request(Method.GET, absolutePath) + .header( + "Authorization", + "Basic ${Base64.getEncoder().encodeToString("$username:$password".toByteArray())}" + ) + return this(getRequest) } - return privateHandler.right() } + return privateHandler.right() } } diff --git a/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/InsightAttachmentOperatorTest.kt b/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/InsightAttachmentOperatorTest.kt index be6e2137..f9359b4e 100644 --- a/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/InsightAttachmentOperatorTest.kt +++ b/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/InsightAttachmentOperatorTest.kt @@ -31,6 +31,7 @@ import com.linkedplanet.kotlininsightclient.api.model.InsightObjectId import kotlinx.coroutines.runBlocking import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.greaterThanOrEqualTo import org.junit.Test import java.security.MessageDigest import java.util.* @@ -103,7 +104,7 @@ interface InsightAttachmentOperatorTest { @Test fun attachmentTestGetAttachmentsForNotExistingObject() = runBlocking { val responseError = insightAttachmentOperator.getAttachments(InsightObjectId.notPersistedObjectId).asError() - assertThat(responseError.message, containsString("-1")) + assertThat(responseError.error.length, greaterThanOrEqualTo(1)) } @Test @@ -167,7 +168,7 @@ interface InsightAttachmentOperatorTest { @Test fun attachmentTestDownloadZipForNotExistingObject() = runBlocking { val responseError = insightAttachmentOperator.downloadAttachmentZip(InsightObjectId.notPersistedObjectId).asError() - assertThat(responseError.message, containsString("-1")) + assertThat(responseError.error.length, greaterThanOrEqualTo(1)) } } diff --git a/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/InsightObjectOperatorTest.kt b/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/InsightObjectOperatorTest.kt index 1b3d5663..f02f5dd8 100644 --- a/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/InsightObjectOperatorTest.kt +++ b/kotlin-insight-client/kotlin-insight-client-test-base/src/main/kotlin/com/linkedplanet/kotlininsightclient/InsightObjectOperatorTest.kt @@ -266,10 +266,10 @@ interface InsightObjectOperatorTest { name = "testObjectWithAllDefaultTypes", testBoolean = false, testInteger = 72, - testFloat = 3.12345678901234, // only double precision does survive this roundtrip + testFloat = 3.123456789012, // A longer number does not survice the official REST calls testDate = LocalDate.parse("1984-04-01"), testDateTime = ZonedDateTime.parse("1983-12-07T14:55:24Z"), - testUrl = setOf("http://localhost", "http://127.0.0.1"), + testUrl = setOf("http://127.0.0.1", "http://localhost"), testEmail = "awesome@linked-planet.com", testTextArea = "text area text", testSelect = listOf("Test Option 2"), diff --git a/kotlin-insight-client/kotlin-insight-client-test-sdk/pom.xml b/kotlin-insight-client/kotlin-insight-client-test-sdk/pom.xml index ba877021..7e0e5f11 100644 --- a/kotlin-insight-client/kotlin-insight-client-test-sdk/pom.xml +++ b/kotlin-insight-client/kotlin-insight-client-test-sdk/pom.xml @@ -37,12 +37,6 @@ com.atlassian.jira jira-api provided - - - jta - jta - - @@ -65,12 +59,6 @@ com.atlassian.jira jira-rest-plugin provided - - - jta - jta - - diff --git a/kotlin-insight-client/pom.xml b/kotlin-insight-client/pom.xml index 7690c1d0..8e4de5a9 100644 --- a/kotlin-insight-client/pom.xml +++ b/kotlin-insight-client/pom.xml @@ -19,6 +19,10 @@ kotlin-insight-client-sdk + + ${project.groupId}.${project.artifactId} + + @@ -26,13 +30,6 @@ com.atlassian.jira jira-api ${jira.version} - provided - - - jta - jta - - com.atlassian.jira @@ -71,7 +68,6 @@ - org.jetbrains.kotlinx diff --git a/kotlin-jira-client/kotlin-jira-client-sdk/pom.xml b/kotlin-jira-client/kotlin-jira-client-sdk/pom.xml index 58247703..97841693 100644 --- a/kotlin-jira-client/kotlin-jira-client-sdk/pom.xml +++ b/kotlin-jira-client/kotlin-jira-client-sdk/pom.xml @@ -26,19 +26,23 @@ provided + + com.fasterxml.jackson.core + jackson-databind + ${fasterxml.jackson.version} + + + com.fasterxml.jackson.module + jackson-module-kotlin + ${fasterxml.jackson.version} + + com.atlassian.jira jira-api provided - - - jta - jta - - - com.atlassian.jira jira-core @@ -54,19 +58,6 @@ - - - com.atlassian.jira - jira-rest-plugin - provided - - - jta - jta - - - - com.atlassian.jira.plugins insight @@ -94,5 +85,10 @@ + + com.atlassian.jira + jira-rest-plugin + provided + diff --git a/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/field/SdkJiraField.kt b/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/field/SdkJiraField.kt index d98c3f8f..df3446be 100644 --- a/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/field/SdkJiraField.kt +++ b/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/field/SdkJiraField.kt @@ -190,9 +190,12 @@ private fun IssueInputParameters.setIssueType(field: JiraIssueTypeNameField) { private fun JiraCustomField.customField(): CustomField { val fields = customFieldManager.getCustomFieldObjectsByName(customFieldName) when { - fields.isEmpty() -> throw IllegalArgumentException("Field name is unknown") - fields.size > 1 -> throw IllegalArgumentException("Field name is not unique") - else -> return fields.firstOrNull()!! + fields.isEmpty() -> + throw IllegalArgumentException("Field '$customFieldName' is unknown") + fields.size > 1 -> + throw IllegalArgumentException("Field '$customFieldName' is not unique") + else -> + return fields.firstOrNull()!! } } diff --git a/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/EitherExtension.kt b/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/EitherExtension.kt index 117ee652..7c64d321 100644 --- a/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/EitherExtension.kt +++ b/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/EitherExtension.kt @@ -79,7 +79,7 @@ inline fun Either.Companion.catchJiraClientError( ): Either = catch(f).mapLeft { JiraClientError( error = error ?: "Jira-Fehler", - message = message ?: it.message ?: "-", + message = message ?: it.localizedMessage ?: it.message ?: "-", stacktrace = it.stackTraceToString(), 500 ) diff --git a/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/IssueJsonConverter.kt b/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/IssueJsonConverter.kt index 6c13f063..069ed994 100644 --- a/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/IssueJsonConverter.kt +++ b/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/IssueJsonConverter.kt @@ -29,12 +29,26 @@ import com.atlassian.jira.issue.fields.rest.json.beans.JiraBaseUrls import com.atlassian.jira.rest.v2.issue.IncludedFields import com.atlassian.jira.rest.v2.issue.IssueBean import com.atlassian.jira.rest.v2.issue.builder.BeanBuilderFactory -import com.google.gson.* +import com.fasterxml.jackson.annotation.JsonAutoDetect +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.PropertyAccessor +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.introspect.Annotated +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector +import com.fasterxml.jackson.databind.ser.PropertyWriter +import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.google.gson.JsonObject +import com.google.gson.JsonParser as GsonJsonParser import com.linkedplanet.kotlinjiraclient.api.model.IssueQueryParams import com.linkedplanet.kotlinjiraclient.sdk.field.FieldAccessorImpl import org.slf4j.LoggerFactory import javax.ws.rs.core.UriBuilder -import javax.xml.bind.annotation.XmlTransient /** * Converts a Jira Issue to Json. @@ -65,8 +79,41 @@ class IssueJsonConverter { } private val jiraBaseUrls: JiraBaseUrls = ComponentAccessor.getComponent(JiraBaseUrls::class.java) private val uriBuilder: UriBuilder = UriBuilder.fromPath(jiraBaseUrls.restApi2BaseUrl()) - private val gson = setupGson() + private val jacksonObjectMapper: ObjectMapper = jacksonObjectMapper().apply { + setFilterProvider( + SimpleFilterProvider() + .addFilter("lp-only-json-property-filter", OnlyAnnotatedFieldsFilter) + .setFailOnUnknownId(false) + ) + setAnnotationIntrospector(object : JacksonAnnotationIntrospector() { + override fun findFilterId(a: Annotated): Any = "lp-only-json-property-filter" + }) + setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + } + + object OnlyAnnotatedFieldsFilter : SimpleBeanPropertyFilter() { + override fun serializeAsField( + pojo: Any?, + jgen: JsonGenerator, + provider: SerializerProvider, + writer: PropertyWriter + ) { + val annotations = writer.member?.allAnnotations?.annotations() + ?.mapNotNull { it.annotationClass.qualifiedName } + ?: emptyList() + val hasAnnotation = annotations.any { + it == JsonProperty::class.qualifiedName || + it == JsonInclude::class.qualifiedName || + it.endsWith("XmlElement") || + it.endsWith("XmlAttribute") + } + + if (hasAnnotation) { + writer.serializeAsField(pojo, jgen, provider) + } + } + } @Throws(FieldException::class) fun createJsonIssue( @@ -80,7 +127,10 @@ class IssueJsonConverter { this.addOrderableFieldsToBean(issueBean, issue) this.addAvailableNavigableFieldsToBean(issueBean, issue) - return gson.toJsonTree(issueBean).asJsonObject + // Jackson is the official way now to serialize Beans. GSON will fail due to infinite loops in the model. + val jacksonJson: JsonNode = jacksonObjectMapper.valueToTree(issueBean) + + return GsonJsonParser.parseString(jacksonJson.toString()).asJsonObject // expose as GSON for API compatibility } @Throws(FieldException::class) @@ -111,17 +161,4 @@ class IssueJsonConverter { bean.addField(this, json, false) } } - - private fun setupGson() = - GsonBuilder() - .setExclusionStrategies(object : ExclusionStrategy { - override fun shouldSkipField(f: FieldAttributes?): Boolean { - return f?.getAnnotation(XmlTransient::class.java) != null - } - - override fun shouldSkipClass(clazz: Class<*>?): Boolean { - return false - } - }) - .create() } diff --git a/kotlin-jira-client/kotlin-jira-client-test-applink/pom.xml b/kotlin-jira-client/kotlin-jira-client-test-applink/pom.xml index d70f263c..4e275167 100644 --- a/kotlin-jira-client/kotlin-jira-client-test-applink/pom.xml +++ b/kotlin-jira-client/kotlin-jira-client-test-applink/pom.xml @@ -15,8 +15,6 @@ ${project.groupId}.${project.artifactId} - 2.2.1 - 1990 diff --git a/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/JiraIssueOperatorTest.kt b/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/JiraIssueOperatorTest.kt index 668c560e..926f6496 100644 --- a/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/JiraIssueOperatorTest.kt +++ b/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/JiraIssueOperatorTest.kt @@ -39,13 +39,13 @@ interface JiraIssueOperatorTest : BaseTestConfigProvider Either.Right(jsonObject.getAsJsonPrimitive("key").asString) } existingIssueIds.orFail().forEach { - issueOperator.deleteIssue(it) + issueOperator.deleteIssue(it).orFail() } (1..10).forEach { searchedKeyIndex -> @@ -62,8 +62,7 @@ interface JiraIssueOperatorTest : BaseTestConfigProvider Either.orFail(): R { this.mapLeft { - TestCase.fail("Unexpected JiraClientError: ${it.error} - ${it.message}") + val msg = "Unexpected JiraClientError: ${it.error} - ${it.message}" + TestCase.fail(msg) } return this.getOrNull()!! } internal fun Either.orFail() { this.mapLeft { - TestCase.fail("Unexpected JiraClientError: ${it.error} - ${it.message}") + val msg = "Unexpected JiraClientError: ${it.error} - ${it.message}" + TestCase.fail(msg) } } diff --git a/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/util/JiraIssueTestHelper.kt b/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/util/JiraIssueTestHelper.kt index 90741058..5e272063 100644 --- a/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/util/JiraIssueTestHelper.kt +++ b/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/util/JiraIssueTestHelper.kt @@ -87,7 +87,7 @@ fun issueParser(jsonObject: JsonObject, map: Map): Either): Either): Either null + else -> jsonElement + } +} fun JsonArray.parseInsightFieldKey(): String = firstOrNull()?.asString?.parseInsightFieldKey() ?: "" diff --git a/kotlin-jira-client/kotlin-jira-client-test-sdk/pom.xml b/kotlin-jira-client/kotlin-jira-client-test-sdk/pom.xml index 9acccb80..67ad9665 100644 --- a/kotlin-jira-client/kotlin-jira-client-test-sdk/pom.xml +++ b/kotlin-jira-client/kotlin-jira-client-test-sdk/pom.xml @@ -15,8 +15,6 @@ ${project.groupId}.${project.artifactId} - 2.2.1 - 2990 @@ -39,12 +37,6 @@ com.atlassian.jira jira-api provided - - - jta - jta - - @@ -67,12 +59,6 @@ com.atlassian.jira jira-rest-plugin provided - - - jta - jta - - @@ -204,6 +190,9 @@ com.google.code.gson:gson + com.fasterxml.jackson.core:jackson-core + com.fasterxml.jackson.core:jackson-databind + com.fasterxml.jackson.core:jackson-annotations ${jira.version} ${jira.version} diff --git a/kotlin-jira-client/pom.xml b/kotlin-jira-client/pom.xml index 63547667..7c18e8bf 100644 --- a/kotlin-jira-client/pom.xml +++ b/kotlin-jira-client/pom.xml @@ -22,7 +22,6 @@ ${project.groupId}.${project.artifactId} - 2.2.1 diff --git a/pom.xml b/pom.xml index 4aaa65e3..65c86f17 100644 --- a/pom.xml +++ b/pom.xml @@ -28,19 +28,20 @@ 3.3.1 17 official - 2.1.20 + 2.0.20 1.6.4 - 1.2.1 - 1.6.2 + 1.2.4 ${project.groupId}.${project.artifactId} - 9.12.10 - 5.12.10 + 10.3.7 + 10.3.7 2990 3.0.4 9.3.3 10.12.1-QR-0157 9.2.3 2.0.9 + + 2.16.2 @@ -58,6 +59,13 @@ + + org.jetbrains.kotlin + kotlin-bom + ${kotlin.version} + pom + import + org.jetbrains.kotlinx kotlinx-coroutines-core @@ -290,7 +298,7 @@ org.jetbrains.dokka dokka-maven-plugin - 1.7.20 + 2.0.0 org.apache.maven.plugins