
pro.taskana.workbasket.rest.WorkbasketDefinitionController Maven / Gradle / Ivy
package pro.taskana.workbasket.rest;
import static pro.taskana.common.internal.util.CheckedFunction.wrap;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import pro.taskana.common.api.exceptions.ConcurrencyException;
import pro.taskana.common.api.exceptions.DomainNotFoundException;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.NotAuthorizedException;
import pro.taskana.common.rest.RestEndpoints;
import pro.taskana.workbasket.api.WorkbasketQuery;
import pro.taskana.workbasket.api.WorkbasketService;
import pro.taskana.workbasket.api.exceptions.NotAuthorizedOnWorkbasketException;
import pro.taskana.workbasket.api.exceptions.WorkbasketAccessItemAlreadyExistException;
import pro.taskana.workbasket.api.exceptions.WorkbasketAlreadyExistException;
import pro.taskana.workbasket.api.exceptions.WorkbasketNotFoundException;
import pro.taskana.workbasket.api.models.Workbasket;
import pro.taskana.workbasket.api.models.WorkbasketAccessItem;
import pro.taskana.workbasket.api.models.WorkbasketSummary;
import pro.taskana.workbasket.internal.models.WorkbasketImpl;
import pro.taskana.workbasket.rest.assembler.WorkbasketAccessItemRepresentationModelAssembler;
import pro.taskana.workbasket.rest.assembler.WorkbasketDefinitionRepresentationModelAssembler;
import pro.taskana.workbasket.rest.assembler.WorkbasketRepresentationModelAssembler;
import pro.taskana.workbasket.rest.models.WorkbasketAccessItemRepresentationModel;
import pro.taskana.workbasket.rest.models.WorkbasketDefinitionCollectionRepresentationModel;
import pro.taskana.workbasket.rest.models.WorkbasketDefinitionRepresentationModel;
import pro.taskana.workbasket.rest.models.WorkbasketRepresentationModel;
/** Controller for all {@link WorkbasketDefinitionRepresentationModel} related endpoints. */
@RestController
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class WorkbasketDefinitionController {
private final WorkbasketService workbasketService;
private final WorkbasketDefinitionRepresentationModelAssembler workbasketDefinitionAssembler;
private final WorkbasketRepresentationModelAssembler workbasketAssembler;
private final WorkbasketAccessItemRepresentationModelAssembler accessItemAssembler;
private final ObjectMapper mapper;
@Autowired
WorkbasketDefinitionController(
WorkbasketService workbasketService,
WorkbasketDefinitionRepresentationModelAssembler workbasketDefinitionAssembler,
WorkbasketRepresentationModelAssembler workbasketAssembler,
WorkbasketAccessItemRepresentationModelAssembler accessItemAssembler,
ObjectMapper mapper) {
this.workbasketService = workbasketService;
this.workbasketDefinitionAssembler = workbasketDefinitionAssembler;
this.workbasketAssembler = workbasketAssembler;
this.accessItemAssembler = accessItemAssembler;
this.mapper = mapper;
}
/**
* This endpoint exports all Workbaskets with the corresponding Workbasket Access Items and
* Distribution Targets. We call this data structure Workbasket Definition.
*
* @title Export Workbaskets
* @param domain Filter the export for a specific domain.
* @return all workbaskets.
*/
@Operation(
summary = "Export Workbaskets",
description =
"This endpoint exports all Workbaskets with the corresponding Workbasket Access Items "
+ "and Distribution Targets. We call this data structure Workbasket Definition.",
parameters = {
@Parameter(name = "domain", description = "Filter the export for a specific domain.")
},
responses = {
@ApiResponse(
responseCode = "200",
description = "all workbaskets.",
content = {
@Content(
mediaType = MediaTypes.HAL_JSON_VALUE,
schema =
@Schema(
implementation = WorkbasketDefinitionCollectionRepresentationModel.class))
})
})
@GetMapping(path = RestEndpoints.URL_WORKBASKET_DEFINITIONS)
@Transactional(readOnly = true, rollbackFor = Exception.class)
public ResponseEntity exportWorkbaskets(
@RequestParam(value = "domain", required = false) String[] domain) {
WorkbasketQuery query = workbasketService.createWorkbasketQuery();
Optional.ofNullable(domain).ifPresent(query::domainIn);
List workbasketSummaryList = query.list();
WorkbasketDefinitionCollectionRepresentationModel pageModel =
workbasketSummaryList.stream()
.map(WorkbasketSummary::getId)
.map(wrap(workbasketService::getWorkbasket))
.collect(
Collectors.collectingAndThen(
Collectors.toList(), workbasketDefinitionAssembler::toTaskanaCollectionModel));
return ResponseEntity.ok(pageModel);
}
/**
* This endpoint imports a list of Workbasket Definitions.
*
* This does not exactly match the REST norm, but we want to have an option to import all
* settings at once. When a logical equal (key and domain are equal) Workbasket already exists an
* update will be executed. Otherwise a new Workbasket will be created.
*
* @title Import Workbaskets
* @param file the list of Workbasket Definitions which will be imported to the current system.
* @return no content
* @throws IOException if multipart file cannot be parsed.
* @throws NotAuthorizedException if the user is not authorized.
* @throws DomainNotFoundException if domain information is incorrect.
* @throws WorkbasketAlreadyExistException if any Workbasket already exists when trying to create
* a new one.
* @throws WorkbasketNotFoundException if do not exists a {@linkplain Workbasket} in the system
* with the used id.
* @throws InvalidArgumentException if any Workbasket has invalid information or authorization
* information in {@linkplain Workbasket}s' definitions is incorrect.
* @throws WorkbasketAccessItemAlreadyExistException if a WorkbasketAccessItem for the same
* Workbasket and access id already exists.
* @throws ConcurrencyException if Workbasket was updated by an other user
* @throws NotAuthorizedOnWorkbasketException if the current user has not correct permissions
*/
@Operation(
summary = "Import Workbaskets",
description =
"This endpoint imports a list of Workbasket Definitions.
This does not exactly match "
+ "the REST norm, but we want to have an option to import all settings at once. When"
+ " a logical equal (key and domain are equal) Workbasket already exists an update "
+ "will be executed. Otherwise a new Workbasket will be created.",
requestBody =
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description =
"the list of Workbasket Definitions which will be imported to the current system."
+ " To get an example file containing Workbasket Definitions, go to the "
+ "[TASKANA UI](http://localhost:8080/taskana/index.html) and export the"
+ "Workbaskets",
required = true,
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)),
responses = {
@ApiResponse(
responseCode = "204",
content = {@Content(schema = @Schema())})
})
@PostMapping(
path = RestEndpoints.URL_WORKBASKET_DEFINITIONS,
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Transactional(rollbackFor = Exception.class)
public ResponseEntity importWorkbaskets(@RequestParam("file") MultipartFile file)
throws IOException,
DomainNotFoundException,
InvalidArgumentException,
WorkbasketAlreadyExistException,
WorkbasketNotFoundException,
WorkbasketAccessItemAlreadyExistException,
ConcurrencyException,
NotAuthorizedOnWorkbasketException,
NotAuthorizedException {
WorkbasketDefinitionCollectionRepresentationModel definitions =
mapper.readValue(
file.getInputStream(),
new TypeReference() {});
// key: logical ID
// value: system ID (in database)
Map systemIds =
workbasketService.createWorkbasketQuery().list().stream()
.collect(Collectors.toMap(this::logicalId, WorkbasketSummary::getId));
checkForDuplicates(definitions.getContent());
// key: old system ID
// value: system ID
Map idConversion = new HashMap<>();
// STEP 1: update or create workbaskets from the import
for (WorkbasketDefinitionRepresentationModel definition : definitions.getContent()) {
Workbasket importedWb = workbasketAssembler.toEntityModel(definition.getWorkbasket());
String newId;
WorkbasketImpl wbWithoutId = (WorkbasketImpl) removeId(importedWb);
if (systemIds.containsKey(logicalId(importedWb))) {
Workbasket modifiedWb =
workbasketService.getWorkbasket(importedWb.getKey(), importedWb.getDomain());
wbWithoutId.setModified(modifiedWb.getModified());
workbasketService.updateWorkbasket(wbWithoutId);
newId = systemIds.get(logicalId(importedWb));
} else {
newId = workbasketService.createWorkbasket(wbWithoutId).getId();
}
// Since we would have a n² runtime when doing a lookup and updating the access items we
// decided to
// simply delete all existing accessItems and create new ones.
boolean authenticated =
definition.getAuthorizations().stream()
.anyMatch(
access ->
(access.getWorkbasketId().equals(importedWb.getId()))
&& (access.getWorkbasketKey().equals(importedWb.getKey())));
if (!authenticated && !definition.getAuthorizations().isEmpty()) {
throw new InvalidArgumentException(
"The given Authentications for Workbasket "
+ importedWb.getId()
+ " don't match in WorkbasketId and WorkbasketKey. "
+ "Please provide consistent WorkbasketDefinitions");
}
for (WorkbasketAccessItem accessItem : workbasketService.getWorkbasketAccessItems(newId)) {
workbasketService.deleteWorkbasketAccessItem(accessItem.getId());
}
for (WorkbasketAccessItemRepresentationModel authorization : definition.getAuthorizations()) {
authorization.setWorkbasketId(newId);
workbasketService.createWorkbasketAccessItem(
accessItemAssembler.toEntityModel(authorization));
}
idConversion.put(importedWb.getId(), newId);
}
// STEP 2: update distribution targets
// This can not be done in step 1 because the system IDs are only known after step 1
for (WorkbasketDefinitionRepresentationModel definition : definitions.getContent()) {
List distributionTargets = new ArrayList<>();
for (String oldId : definition.getDistributionTargets()) {
if (idConversion.containsKey(oldId)) {
distributionTargets.add(idConversion.get(oldId));
} else if (systemIds.containsValue(oldId)) {
distributionTargets.add(oldId);
} else {
throw new InvalidArgumentException(
String.format(
"invalid import state: Workbasket '%s' does not exist in the given import list",
oldId));
}
}
workbasketService.setDistributionTargets(
// no verification necessary since the workbasket was already imported in step 1.
idConversion.get(definition.getWorkbasket().getWorkbasketId()), distributionTargets);
}
return ResponseEntity.noContent().build();
}
private Workbasket removeId(Workbasket importedWb) {
WorkbasketRepresentationModel wbRes = workbasketAssembler.toModel(importedWb);
wbRes.setWorkbasketId(null);
return workbasketAssembler.toEntityModel(wbRes);
}
private void checkForDuplicates(Collection definitions)
throws WorkbasketAlreadyExistException {
Set identifiers = new HashSet<>();
for (WorkbasketDefinitionRepresentationModel definition : definitions) {
String identifier = logicalId(workbasketAssembler.toEntityModel(definition.getWorkbasket()));
if (identifiers.contains(identifier)) {
throw new WorkbasketAlreadyExistException(
definition.getWorkbasket().getKey(), definition.getWorkbasket().getDomain());
}
identifiers.add(identifier);
}
}
private String logicalId(WorkbasketSummary workbasket) {
return logicalId(workbasket.getKey(), workbasket.getDomain());
}
private String logicalId(String key, String domain) {
return key + "|" + domain;
}
}