Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.google.gson.JsonObject
import com.linkedplanet.kotlinatlassianclientcore.common.api.Page
import com.linkedplanet.kotlinjiraclient.api.error.JiraClientError
import com.linkedplanet.kotlinjiraclient.api.model.JiraIssue
import com.linkedplanet.kotlinjiraclient.api.model.IssueQueryParams

/**
* Provides methods for working with Jira issues, including retrieving issues by JQL query, issue type, or key; creating and updating issues; and deleting issues.
Expand All @@ -40,6 +41,7 @@ interface JiraIssueOperator<JiraFieldType> {
*/
suspend fun <T> getIssuesByJQL(
jql: String,
queryParams: IssueQueryParams = IssueQueryParams(),
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, List<T>>

Expand All @@ -55,6 +57,7 @@ interface JiraIssueOperator<JiraFieldType> {
jql: String,
pageIndex: Int = 0,
pageSize: Int = RESULTS_PER_PAGE,
queryParams: IssueQueryParams = IssueQueryParams(),
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, Page<T>>

Expand All @@ -66,6 +69,7 @@ interface JiraIssueOperator<JiraFieldType> {
*/
suspend fun <T> getIssueByJQL(
jql: String,
queryParams: IssueQueryParams = IssueQueryParams(),
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, T?>

Expand All @@ -79,6 +83,7 @@ interface JiraIssueOperator<JiraFieldType> {
suspend fun <T> getIssuesByIssueType(
projectId: Long,
issueTypeId: Int,
queryParams: IssueQueryParams = IssueQueryParams(),
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, List<T>>

Expand All @@ -96,6 +101,7 @@ interface JiraIssueOperator<JiraFieldType> {
issueTypeId: Int,
pageIndex: Int = 0,
pageSize: Int = RESULTS_PER_PAGE,
queryParams: IssueQueryParams = IssueQueryParams(),
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, Page<T>>

Expand All @@ -108,6 +114,7 @@ interface JiraIssueOperator<JiraFieldType> {
*/
suspend fun <T> getIssueByKey(
key: String,
queryParams: IssueQueryParams = IssueQueryParams(),
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, T?>

Expand All @@ -119,6 +126,7 @@ interface JiraIssueOperator<JiraFieldType> {
*/
suspend fun <T> getIssueById(
id: Int,
queryParams: IssueQueryParams = IssueQueryParams(),
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, T?>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*-
* #%L
* kotlin-jira-client-api
* %%
* Copyright (C) 2022 - 2025 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.kotlinjiraclient.api.model

enum class ExpandedOption(val key: String) {
VERSIONED_REPRESENTATIONS("versionedRepresentations"),
RENDERED_FIELDS("renderedFields"),
NAMES("names"),
SCHEMA("schema"),
TRANSITIONS("transitions"),
OPERATIONS("operations"),
EDITMETA("editmeta"),
CHANGELOG("changelog");

companion object {
fun fromKey(key: String): ExpandedOption? {
return values().find { it.key == key }
}
}

override fun toString(): String {
return this.key
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*-
* #%L
* kotlin-jira-client-api
* %%
* Copyright (C) 2022 - 2025 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.kotlinjiraclient.api.model

import com.linkedplanet.kotlinjiraclient.api.model.ExpandedOption.NAMES
import com.linkedplanet.kotlinjiraclient.api.model.ExpandedOption.TRANSITIONS

/**
* Controls expansion and other parameters of the returned Jira Issue.
*
* The following parameters are planed for the future: fields, fieldsByKeys, properties
*/
data class IssueQueryParams(
val expanded: List<ExpandedOption> = listOf(NAMES, TRANSITIONS)
)
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.linkedplanet.kotlinjiraclient.api.error.JiraClientError
import com.linkedplanet.kotlinjiraclient.api.interfaces.JiraIssueOperator
import com.linkedplanet.kotlinjiraclient.api.model.JiraIssue
import com.linkedplanet.kotlinatlassianclientcore.common.api.Page
import com.linkedplanet.kotlinjiraclient.api.model.IssueQueryParams
import com.linkedplanet.kotlinjiraclient.http.field.HttpJiraField
import com.linkedplanet.kotlinjiraclient.http.model.HttpMappingField
import com.linkedplanet.kotlinjiraclient.http.util.fromHttpDomainError
Expand All @@ -41,6 +42,7 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs

override suspend fun <T> getIssuesByJQL(
jql: String,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, List<T>> = either {
recursiveRestCallPaginatedRaw { index, maxSize ->
Expand All @@ -51,7 +53,7 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs
"jql" to jql,
"startAt" to index.toString(),
"maxResults" to maxSize.toString(),
"expand" to "names,transitions",
"expand" to queryParams.expanded.joinToString(","),
),
null,
"application/json",
Expand All @@ -75,6 +77,7 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs
jql: String,
pageIndex: Int,
pageSize: Int,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, Page<T>> = either {
val page = context.httpClient.executeGet<HttpJiraIssuePage>(
Expand All @@ -83,7 +86,7 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs
"jql" to jql,
"startAt" to (pageIndex * pageSize).toString(),
"maxResults" to pageSize.toString(),
"expand" to "names,transitions",
"expand" to queryParams.expanded.joinToString(","),
),
object : TypeToken<HttpJiraIssuePage>() {}.type
)
Expand Down Expand Up @@ -111,37 +114,43 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs

override suspend fun <T> getIssueByJQL(
jql: String,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, T?> = either {
getIssuesByJQLPaginated(jql, 0, 1, parser).bind().items.firstOrNull()
getIssuesByJQLPaginated(jql, 0, 1, queryParams, parser).bind().items.firstOrNull()
}

override suspend fun <T> getIssuesByIssueType(
projectId: Long,
issueTypeId: Int,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, List<T>> = either {
getIssuesByJQL("project=$projectId AND issueType=$issueTypeId", parser).bind()
val jql = "project=$projectId AND issueType=$issueTypeId"
getIssuesByJQL(jql, queryParams, parser).bind()
}

override suspend fun <T> getIssuesByTypePaginated(
projectId: Long,
issueTypeId: Int,
pageIndex: Int,
pageSize: Int,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, Page<T>> = either {
getIssuesByJQLPaginated("project=$projectId AND issueType=$issueTypeId", pageIndex, pageSize, parser).bind()
val jql = "project=$projectId AND issueType=$issueTypeId"
getIssuesByJQLPaginated(jql, pageIndex, pageSize, queryParams, parser).bind()
}

override suspend fun <T> getIssueByKey(
key: String,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, T?> = either {
val successResponse = context.httpClient.executeGetCall(
"/rest/api/2/issue/${key}",
mapOf(
"expand" to "names,transitions"
"expand" to queryParams.expanded.joinToString(","),
),
)
.map { it.body }
Expand All @@ -167,8 +176,9 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs

override suspend fun <T> getIssueById(
id: Int,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, T?> = getIssueByKey(id.toString(), parser)
): Either<JiraClientError, T?> = getIssueByKey(id.toString(), queryParams, parser)

override suspend fun createIssue(
projectId: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import com.linkedplanet.kotlinatlassianclientcore.common.error.asEither
import com.linkedplanet.kotlinjiraclient.api.error.JiraClientError
import com.linkedplanet.kotlinjiraclient.api.interfaces.JiraIssueOperator
import com.linkedplanet.kotlinjiraclient.api.model.JiraIssue
import com.linkedplanet.kotlinjiraclient.api.model.IssueQueryParams
import com.linkedplanet.kotlinjiraclient.sdk.field.SdkJiraField
import com.linkedplanet.kotlinjiraclient.sdk.util.IssueJsonConverter
import com.linkedplanet.kotlinjiraclient.sdk.util.catchJiraClientError
Expand Down Expand Up @@ -134,15 +135,17 @@ object SdkJiraIssueOperator : JiraIssueOperator<SdkJiraField> {

override suspend fun <T> getIssueById(
id: Int,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, T?> =
getIssueByKey(id.toString(), parser)
getIssueByKey(id.toString(), queryParams, parser)

override suspend fun <T> getIssueByJQL(
jql: String,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, T?> = either {
val potentiallyMultipleIssues = getIssuesByJQLPaginated(jql, 0, 1, parser).bind()
val potentiallyMultipleIssues = getIssuesByJQLPaginated(jql, 0, 1, queryParams, parser).bind()
if (potentiallyMultipleIssues.totalItems < 1) {
JiraClientError("Issue not found", "No issue was found.").asEither<JiraClientError, T?>().bind()
}
Expand All @@ -152,21 +155,28 @@ object SdkJiraIssueOperator : JiraIssueOperator<SdkJiraField> {
override suspend fun <T> getIssuesByIssueType(
projectId: Long,
issueTypeId: Int,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, List<T>> =
getIssuesByJQL("project=$projectId AND issueType=$issueTypeId", parser)
): Either<JiraClientError, List<T>> {
val jql = "project=$projectId AND issueType=$issueTypeId"
return getIssuesByJQL(jql, queryParams, parser)
}

override suspend fun <T> getIssuesByTypePaginated(
projectId: Long,
issueTypeId: Int,
pageIndex: Int,
pageSize: Int,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, Page<T>> =
getIssuesByJQLPaginated("project=$projectId AND issueType=$issueTypeId", pageIndex, pageSize, parser)
): Either<JiraClientError, Page<T>> {
val jql = "project=$projectId AND issueType=$issueTypeId"
return getIssuesByJQLPaginated(jql, pageIndex, pageSize, queryParams, parser)
}

override suspend fun <T> getIssueByKey(
key: String,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, T?> = either {
Either.catchJiraClientError {
Expand All @@ -176,45 +186,51 @@ object SdkJiraIssueOperator : JiraIssueOperator<SdkJiraField> {
}
val issue = issueResult.toEither().bind().issue
?: return@catchJiraClientError null
issueToConcreteType(issue, parser).bind()
issueToConcreteType(issue, queryParams, parser).bind()
}.bind()
}

override suspend fun <T> getIssuesByJQL(
jql: String,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, List<T>> = either {
val issuePage = getIssuesByJqlWithPagerFilter(jql, PagerFilter.getUnlimitedFilter(), parser).bind()
val issuePage = getIssuesByJqlWithPagerFilter(jql, PagerFilter.getUnlimitedFilter(), queryParams, parser).bind()
issuePage.items
}

override suspend fun <T> getIssuesByJQLPaginated(
jql: String,
pageIndex: Int,
pageSize: Int,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, Page<T>> =
getIssuesByJqlWithPagerFilter(jql, PagerFilter.newPageAlignedFilter(pageIndex * pageSize, pageSize), parser)
): Either<JiraClientError, Page<T>> {
val pagerFilter = PagerFilter.newPageAlignedFilter(pageIndex * pageSize, pageSize)
return getIssuesByJqlWithPagerFilter(jql, pagerFilter, queryParams, parser)
}

private suspend fun <T> issueToConcreteType(
issue: Issue,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, T> = Either.catchJiraClientError {
val jsonIssue: JsonObject = issueJsonConverter.createJsonIssue(issue)
val jsonIssue: JsonObject = issueJsonConverter.createJsonIssue(issue, queryParams)
val customFieldMap = customFieldManager.getCustomFieldObjects(issue).associate { it.name to it.id }
return parser(jsonIssue, customFieldMap)
}

private suspend fun <T> getIssuesByJqlWithPagerFilter(
jql: String,
pagerFilter: PagerFilter<*>?,
queryParams: IssueQueryParams,
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
): Either<JiraClientError, Page<T>> = either {
val user = userOrError().bind()
val query = Either.catchJiraClientError { jqlParser.parseQuery(jql) }.bind()
val search = Either.catchJiraClientError { searchService.search(user, query, pagerFilter) }.bind()
val issues = search.results
.map { issue -> issueToConcreteType(issue, parser) }
.map { issue -> issueToConcreteType(issue, queryParams, parser) }
.bindAll()
val totalItems = search.total
val pageSize = pagerFilter?.pageSize ?: 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ 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.linkedplanet.kotlinjiraclient.api.model.IssueQueryParams
import com.linkedplanet.kotlinjiraclient.sdk.field.FieldAccessorImpl
import org.slf4j.LoggerFactory
import javax.ws.rs.core.UriBuilder
Expand Down Expand Up @@ -66,10 +67,13 @@ class IssueJsonConverter {


@Throws(FieldException::class)
fun createJsonIssue(issue: Issue): JsonObject {
val expand = "names,transitions"
fun createJsonIssue(
issue: Issue,
queryParams: IssueQueryParams,
): JsonObject {
val expanded = queryParams.expanded.joinToString(",")
val issueBean: IssueBean = beanBuilderFactory
.newIssueBeanBuilder2(IncludedFields.includeNavigableByDefault(null), expand, uriBuilder)
.newIssueBeanBuilder2(IncludedFields.includeNavigableByDefault(null), expanded, uriBuilder)
.build(issue)
this.addOrderableFieldsToBean(issueBean, issue)
this.addAvailableNavigableFieldsToBean(issueBean, issue)
Expand Down
Loading
Loading