Skip to content
Draft
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 @@ -16,7 +16,11 @@

package com.ritense.valtimo.contract.authentication

import java.util.UUID

interface Team {
val key: String
val title: String
val adHocCaseDocumentId: UUID? get() = null
val adHoc: Boolean get() = adHocCaseDocumentId != null
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.ritense.valtimo.contract.authentication

import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import java.util.UUID

interface TeamManagementService {

Expand All @@ -44,4 +45,20 @@ interface TeamManagementService {
fun addUserToTeam(username: String, teamKey: String): String

fun removeUserFromTeam(username: String, teamKey: String)

fun createAdHocTeam(adHocCaseDocumentId: UUID, title: String? = null): Team {
throw UnsupportedOperationException("Ad hoc teams are not supported by this implementation")
}

fun findAllByAdHocCaseDocumentId(
adHocCaseDocumentId: UUID,
titleContains: String? = null,
pageable: Pageable = Pageable.unpaged()
): Page<Team> {
throw UnsupportedOperationException("Ad hoc teams are not supported by this implementation")
}

fun deleteAllByAdHocCaseDocumentId(adHocCaseDocumentId: UUID) {
throw UnsupportedOperationException("Ad hoc teams are not supported by this implementation")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">

<include file="20260403-backfill-plugin-definition-key-on-fixed-refs.xml" relativeToChangelogFile="true"/>
<include file="20260430-add-ad-hoc-case-document-id-to-team.xml" relativeToChangelogFile="true"/>

</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2015-2026 Ritense BV, the Netherlands.
~
~ Licensed under EUPL, Version 1.2 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
~
~ 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.
-->

<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">

<changeSet id="1" author="Ritense">
<addColumn tableName="team">
<column name="ad_hoc_case_document_id" type="${uuidType}">
<constraints nullable="true"/>
</column>
</addColumn>

<createIndex tableName="team" indexName="idx_team_ad_hoc_case_document_id">
<column name="ad_hoc_case_document_id"/>
</createIndex>
</changeSet>

</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import com.ritense.authorization.AuthorizationService
import com.ritense.team.authorization.TeamSpecificationFactory
import com.ritense.team.exporter.TeamExporter
import com.ritense.team.importer.TeamImporter
import com.ritense.team.listener.DocumentDeletedTeamCleanupListener
import com.ritense.team.repository.TeamRepository
import com.ritense.team.repository.TeamUserRepository
import com.ritense.team.security.config.TeamHttpSecurityConfigurer
import com.ritense.team.service.TeamActionProvider
import com.ritense.team.service.TeamManagementServiceImpl
import com.ritense.team.web.rest.AdHocTeamResource
import com.ritense.team.web.rest.TeamResource
import com.ritense.valtimo.contract.authentication.TeamManagementService
import com.ritense.valtimo.contract.authentication.UserManagementService
Expand Down Expand Up @@ -103,6 +105,22 @@ class TeamAutoConfiguration {
return TeamImporter(objectMapper, teamManagementService)
}

@Bean
@ConditionalOnMissingBean(AdHocTeamResource::class)
fun adHocTeamResource(
teamManagementService: TeamManagementService,
): AdHocTeamResource {
return AdHocTeamResource(teamManagementService)
}

@Bean
@ConditionalOnMissingBean(DocumentDeletedTeamCleanupListener::class)
fun documentDeletedTeamCleanupListener(
teamManagementService: TeamManagementService,
): DocumentDeletedTeamCleanupListener {
return DocumentDeletedTeamCleanupListener(teamManagementService)
}

@Bean
@ConditionalOnMissingBean(TeamSpecificationFactory::class)
fun teamSpecificationFactory(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import jakarta.persistence.FetchType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.Table
import java.util.UUID
import com.ritense.valtimo.contract.authentication.Team as TeamInterface

@Entity
Expand All @@ -39,5 +40,8 @@ data class Team(
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "team_user", joinColumns = [JoinColumn(name = "team_key")])
@Column(name = "username")
var users: List<String> = emptyList()
var users: List<String> = emptyList(),

@Column(name = "ad_hoc_case_document_id")
override val adHocCaseDocumentId: UUID? = null
) : TeamInterface
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* 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.
*/

package com.ritense.team.listener

import com.ritense.valtimo.contract.annotation.SkipComponentScan
import com.ritense.valtimo.contract.authentication.TeamManagementService
import com.ritense.valtimo.contract.event.DocumentDeletedEvent
import org.springframework.context.event.EventListener

@SkipComponentScan
class DocumentDeletedTeamCleanupListener(
private val teamManagementService: TeamManagementService,
) {

@EventListener
fun handle(event: DocumentDeletedEvent) {
teamManagementService.deleteAllByAdHocCaseDocumentId(event.caseDocumentId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
import org.springframework.data.jpa.repository.Query
import java.util.Optional
import java.util.UUID

interface TeamRepository : JpaRepository<Team, String>, JpaSpecificationExecutor<Team> {
fun findByTitleContainingIgnoreCase(title: String): List<Team>

@Query("SELECT t FROM Team t LEFT JOIN FETCH t.users WHERE t.key = :key")
fun findByKeyWithUsers(key: String): Optional<Team>

fun deleteByAdHocCaseDocumentId(adHocCaseDocumentId: UUID)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ package com.ritense.team.repository
import com.ritense.team.domain.Team
import jakarta.persistence.criteria.JoinType
import org.springframework.data.jpa.domain.Specification
import java.util.UUID

class TeamRepositoryConfigSpecificationHelper {

companion object {

const val TITLE: String = "title"
const val AD_HOC_CASE_DOCUMENT_ID: String = "adHocCaseDocumentId"

@JvmStatic
fun byTitleContains(titlePart: String) = Specification<Team> { root, _, cb ->
Expand All @@ -38,5 +40,15 @@ class TeamRepositoryConfigSpecificationHelper {
}
null
}

@JvmStatic
fun byAdHocCaseDocumentIdIsNull() = Specification<Team> { root, _, cb ->
cb.isNull(root.get<UUID>(AD_HOC_CASE_DOCUMENT_ID))
}

@JvmStatic
fun byAdHocCaseDocumentId(adHocCaseDocumentId: UUID) = Specification<Team> { root, _, cb ->
cb.equal(root.get<UUID>(AD_HOC_CASE_DOCUMENT_ID), adHocCaseDocumentId)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class TeamHttpSecurityConfigurer : HttpSecurityConfigurer {
.authenticated()
.requestMatchers(antMatcher(HttpMethod.GET, "/api/v1/team/{teamKey}/candidate-user"))
.authenticated()
.requestMatchers(antMatcher(HttpMethod.GET, "/api/v1/case/{caseId}/team")).authenticated()
.requestMatchers(antMatcher(HttpMethod.POST, "/api/v1/case/{caseId}/team")).authenticated()
.requestMatchers(antMatcher(HttpMethod.DELETE, "/api/v1/case/{caseId}/team/{teamKey}"))
.authenticated()
}
} catch (e: Exception) {
throw HttpConfigurerConfigurationException(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.domain.Specification
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.UUID
import com.ritense.valtimo.contract.authentication.Team as TeamInterface

@Service
Expand All @@ -60,6 +61,8 @@ class TeamManagementServiceImpl(
)
)

specification = specification.and(TeamRepositoryConfigSpecificationHelper.byAdHocCaseDocumentIdIsNull())

if (titleContains != null) {
specification = specification.and(TeamRepositoryConfigSpecificationHelper.byTitleContains(titleContains))
}
Expand Down Expand Up @@ -148,6 +151,44 @@ class TeamManagementServiceImpl(
return findAll(titleContains = null, pageable = pageable)
}

override fun createAdHocTeam(adHocCaseDocumentId: UUID, title: String?): TeamInterface {
val generatedKey = "adhoc-${UUID.randomUUID()}"
val generatedTitle = title ?: "Ad hoc team"
val team = Team(
key = generatedKey,
title = generatedTitle,
adHocCaseDocumentId = adHocCaseDocumentId
)
requirePermission(team, TeamActionProvider.CREATE)
return teamRepository.save(team)
}

@Transactional(readOnly = true)
override fun findAllByAdHocCaseDocumentId(
adHocCaseDocumentId: UUID,
titleContains: String?,
pageable: Pageable
): Page<TeamInterface> {
var specification: Specification<Team> = authorizationService.getAuthorizationSpecification(
EntityAuthorizationRequest(
Team::class.java,
TeamActionProvider.VIEW_LIST
)
)

specification = specification.and(TeamRepositoryConfigSpecificationHelper.byAdHocCaseDocumentId(adHocCaseDocumentId))

if (titleContains != null) {
specification = specification.and(TeamRepositoryConfigSpecificationHelper.byTitleContains(titleContains))
}
specification = specification.and(TeamRepositoryConfigSpecificationHelper.fetchUsers())
return teamRepository.findAll(specification, pageable).map { it as TeamInterface }
}

override fun deleteAllByAdHocCaseDocumentId(adHocCaseDocumentId: UUID) {
teamRepository.deleteByAdHocCaseDocumentId(adHocCaseDocumentId)
}

private fun requirePermission(team: Team, action: Action<Team>) {
authorizationService.requirePermission(
EntityAuthorizationRequest(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* 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.
*/

package com.ritense.team.web.rest

import com.ritense.team.web.rest.dto.AdHocTeamCreateRequestDto
import com.ritense.team.web.rest.dto.AdHocTeamResponseDto
import com.ritense.valtimo.contract.annotation.SkipComponentScan
import com.ritense.valtimo.contract.authentication.TeamManagementService
import jakarta.validation.Valid
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.web.SortDefault
import org.springframework.data.web.SortDefault.SortDefaults
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import java.util.UUID

@RestController
@SkipComponentScan
@RequestMapping("/api/v1/case/{caseId}/team")
class AdHocTeamResource(
private val teamManagementService: TeamManagementService,
) {

@GetMapping
fun getAdHocTeams(
@PathVariable caseId: UUID,
@RequestParam(required = false) titleContains: String?,
@SortDefaults(SortDefault(sort = ["title"])) pageable: Pageable,
): Page<AdHocTeamResponseDto> {
return teamManagementService.findAllByAdHocCaseDocumentId(caseId, titleContains, pageable)
.map { AdHocTeamResponseDto.from(it) }
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createAdHocTeam(
@PathVariable caseId: UUID,
@Valid @RequestBody(required = false) request: AdHocTeamCreateRequestDto?,
): AdHocTeamResponseDto {
val team = teamManagementService.createAdHocTeam(caseId, request?.title)
return AdHocTeamResponseDto.from(team)
}

@DeleteMapping("/{teamKey}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteAdHocTeam(
@PathVariable caseId: UUID,
@PathVariable teamKey: String,
) {
val team = teamManagementService.findByKey(teamKey)
?: throw IllegalArgumentException("Team not found")
require(team.adHocCaseDocumentId == caseId) { "Team does not belong to this case" }
teamManagementService.delete(teamKey)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2015-2026 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* 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.
*/

package com.ritense.team.web.rest.dto

import jakarta.validation.constraints.Size

data class AdHocTeamCreateRequestDto(
@field:Size(max = 255)
val title: String? = null
)
Loading
Loading