DrawingController.java
/*
* Copyright (C) 2025 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import jakarta.validation.Valid;
import java.io.Serializable;
import java.lang.invoke.MethodHandles;
import java.util.Set;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.server.ResponseStatusException;
import org.tailormap.api.annotation.AppRestController;
import org.tailormap.api.drawing.DrawingService;
import org.tailormap.api.persistence.Application;
import org.tailormap.api.viewer.model.Drawing;
/**
* Controller for drawing operations. Note that the following endpoints are secured/require authentication:
*
* <ul>
* <li>PUT /{viewerKind}/{viewerName}/drawing (@see DrawingController#createOrUpdateDrawing)
* <li>DELETE /drawing/{drawingId} (@see DrawingController#deleteDrawing)
* </ul>
*
* The following endpoints do not require authentication (but may return different results based on the user's
* role/authorization):
*
* <ul>
* <li>GET /drawing/list (@see DrawingController#listDrawings)
* <li>GET /{viewerKind}/{viewerName}/drawing/{drawingId} (@see DrawingController#getDrawing)
* </ul>
*/
@AppRestController
@Validated
public class DrawingController {
private static final Logger logger =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final DrawingService drawingService;
public DrawingController(DrawingService drawingService) {
this.drawingService = drawingService;
}
/**
* Create or update a drawing. Requires authentication.
*
* @param drawing the drawing to create or update
* @param application the application that this drawing is created or updated in (used to determine the SRID)
* @return the created or updated drawing
*/
@PutMapping(
path = {"${tailormap-api.base-path}/{viewerKind}/{viewerName}/drawing"},
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed(value = "create_or_update_drawing", description = "time spent to create or update a drawing")
@Counted(value = "create_or_update_drawing", description = "number of created or updated drawings")
@Valid public ResponseEntity<Serializable> createOrUpdateDrawing(
@NonNull @RequestBody Drawing drawing, @ModelAttribute Application application)
throws JsonProcessingException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authentication required");
}
drawing = drawing.srid(getApplicationSrid(application));
HttpStatus httpStatus;
if (drawing.getId() == null) {
logger.trace("create new drawing {}", drawing);
drawing = drawingService.createDrawing(drawing, authentication);
httpStatus = HttpStatus.CREATED;
} else {
logger.trace("update existing drawing {}", drawing);
drawing = drawingService.updateDrawing(drawing, authentication);
httpStatus = HttpStatus.OK;
}
return ResponseEntity.status(httpStatus).body(drawing);
}
/**
* Get a drawing by id. Does not require authentication per-se, but authorizations are checked at the drawing level.
*
* @param drawingId the id of the drawing to retrieve
* @param application the application that this drawing is created or updated in (used to determine the SRID)
* @return the drawing, if found and accessible
* @throws ResponseStatusException if the drawing is not found or not accessible
* @see DrawingService#getDrawing(UUID, Authentication, boolean, int)
*/
@GetMapping(
path = {"${tailormap-api.base-path}/{viewerKind}/{viewerName}/drawing/{drawingId}"},
produces = MediaType.APPLICATION_JSON_VALUE)
@Counted(value = "get_drawing", description = "number of drawings retrieved")
@Timed(value = "get_drawing", description = "time spent to retrieve a drawing")
@Valid public ResponseEntity<Serializable> getDrawing(
@NonNull @PathVariable UUID drawingId, @ModelAttribute Application application)
throws ResponseStatusException {
final Drawing drawing = drawingService
.getDrawing(
drawingId,
SecurityContextHolder.getContext().getAuthentication(),
true,
getApplicationSrid(application))
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Drawing not found"));
return ResponseEntity.status(HttpStatus.OK).body(drawing);
}
/**
* List drawings. Does not require authentication per-se, but authorizations are checked at the drawing level.
*
* @return a, possibly empty, set of drawings
* @see DrawingService#getDrawingsForUser(Authentication)
*/
@GetMapping(path = "${tailormap-api.base-path}/drawing/list", produces = MediaType.APPLICATION_JSON_VALUE)
@Counted(value = "list_drawings", description = "number of drawings listed")
@Timed(value = "list_drawings", description = "time spent to list drawings")
public Set<Drawing> listDrawings() {
return drawingService.getDrawingsForUser(
SecurityContextHolder.getContext().getAuthentication());
}
/**
* Delete a drawing. Requires authentication.
*
* @param drawingId the id of the drawing to delete
* @return a response entity with status NO_CONTENT
* @see DrawingService#deleteDrawing(UUID, Authentication)
*/
@DeleteMapping(path = "${tailormap-api.base-path}/drawing/{drawingId}")
@Timed(value = "delete_drawing", description = "time spent to delete a drawing")
@Counted(value = "delete_drawing", description = "number of drawings deleted")
public ResponseEntity<Serializable> deleteDrawing(@NonNull @PathVariable UUID drawingId) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authentication required");
}
drawingService.deleteDrawing(drawingId, authentication);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
private Integer getApplicationSrid(Application application) {
if (application.getCrs().contains(":")) {
return Integer.valueOf(
application.getCrs().substring(application.getCrs().lastIndexOf(":") + 1));
} else {
return Integer.valueOf(application.getCrs());
}
}
}