SpatialDataPointRest.java

package de.dlr.shepard.data.spatialdata.endpoints;

import de.dlr.shepard.auth.permission.io.PermissionsIO;
import de.dlr.shepard.auth.permission.model.Roles;
import de.dlr.shepard.common.util.Constants;
import de.dlr.shepard.common.util.QueryParamHelper;
import de.dlr.shepard.data.ContainerAttributes;
import de.dlr.shepard.data.spatialdata.io.SpatialDataContainerIO;
import de.dlr.shepard.data.spatialdata.io.SpatialDataPointIO;
import de.dlr.shepard.data.spatialdata.io.SpatialDataQueryParams;
import de.dlr.shepard.data.spatialdata.model.geometryFilter.AbstractGeometryFilter;
import de.dlr.shepard.data.spatialdata.services.SpatialDataContainerService;
import de.dlr.shepard.data.spatialdata.services.SpatialDataPointService;
import io.quarkus.resteasy.reactive.server.EndpointDisabled;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.ExampleObject;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

@EndpointDisabled(name = "shepard.spatial-data.enabled", stringValue = "false")
@Path(Constants.SPATIAL_DATA_CONTAINERS)
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RequestScoped
public class SpatialDataPointRest {

  @Inject
  SpatialDataPointService dataPointService;

  @Inject
  SpatialDataContainerService containerService;

  //#region Container functions
  @GET
  @Tag(
    name = Constants.SPATIAL_DATA_CONTAINER,
    description = "Spatial data containers and their endpoints are experimental. The administrator has to activate this feature. Otherwise the endpoints will return 404."
  )
  @Operation(description = "Get spatial data containers.")
  @APIResponse(
    description = "ok",
    responseCode = "200",
    content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = SpatialDataContainerIO.class))
  )
  @APIResponse(responseCode = "400", description = "bad request")
  @APIResponse(responseCode = "401", description = "not authorized")
  @APIResponse(responseCode = "404", description = "not found")
  @Parameter(name = Constants.QP_NAME)
  @Parameter(name = Constants.QP_PAGE)
  @Parameter(name = Constants.QP_SIZE)
  @Parameter(name = Constants.QP_ORDER_BY_ATTRIBUTE)
  @Parameter(name = Constants.QP_ORDER_DESC)
  public Response getSpatialDataContainers(
    @QueryParam(Constants.QP_NAME) String name,
    @QueryParam(Constants.QP_PAGE) @PositiveOrZero Integer page,
    @QueryParam(Constants.QP_SIZE) @PositiveOrZero Integer size,
    @QueryParam(Constants.QP_ORDER_BY_ATTRIBUTE) ContainerAttributes orderBy,
    @QueryParam(Constants.QP_ORDER_DESC) Boolean orderDesc
  ) {
    var params = new QueryParamHelper();
    if (name != null) params = params.withName(name);
    if (page != null && size != null) params = params.withPageAndSize(page, size);
    if (orderBy != null) params = params.withOrderByAttribute(orderBy, orderDesc);

    var containers = containerService.getAllContainers(params);
    var result = SpatialDataContainerIO.fromEntities(containers);
    return Response.ok(result).build();
  }

  @GET
  @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}")
  @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
  @Operation(description = "Get spatial data container.")
  @APIResponse(
    description = "ok",
    responseCode = "200",
    content = @Content(schema = @Schema(implementation = SpatialDataContainerIO.class))
  )
  @APIResponse(responseCode = "400", description = "bad request")
  @APIResponse(responseCode = "401", description = "not authorized")
  @APIResponse(responseCode = "403", description = "forbidden")
  @APIResponse(responseCode = "404", description = "not found")
  @Parameter(name = Constants.SPATIAL_DATA_CONTAINER_ID)
  public Response getSpatialDataContainer(
    @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId
  ) {
    var container = containerService.getContainer(containerId);
    return Response.ok(SpatialDataContainerIO.fromEntity(container)).build();
  }

  @POST
  @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
  @Operation(description = "Create a new spatial data container.")
  @APIResponse(
    description = "created",
    responseCode = "201",
    content = @Content(schema = @Schema(implementation = SpatialDataContainerIO.class))
  )
  @Transactional
  public Response createSpatialDataContainer(
    @RequestBody(
      required = true,
      content = @Content(schema = @Schema(implementation = SpatialDataContainerIO.class))
    ) @Valid SpatialDataContainerIO containerIo
  ) {
    var container = containerService.createContainer(containerIo);
    return Response.ok(SpatialDataContainerIO.fromEntity(container)).status(Status.CREATED).build();
  }

  @DELETE
  @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}")
  @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
  @Operation(description = "Deletes spatial data container and related spatial data.")
  @APIResponse(description = "ok", responseCode = "200")
  @APIResponse(responseCode = "400", description = "bad request")
  @APIResponse(responseCode = "401", description = "not authorized")
  @APIResponse(responseCode = "403", description = "forbidden")
  @APIResponse(responseCode = "404", description = "not found")
  @Parameter(name = Constants.SPATIAL_DATA_CONTAINER_ID)
  public Response deleteSpatialDataContainer(
    @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId
  ) {
    containerService.deleteContainer(containerId);
    return Response.status(Status.OK).build();
  }

  //#endregion

  @GET
  @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}/" + Constants.PAYLOAD)
  @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
  @Operation(description = "Get spatial data by container id")
  @Parameter(
    name = "metadataFilter",
    required = false,
    description = """
    This filter should be a stringified list of JSON object for exact match in metadata. \n
    Example : `{"track": 1, "layer": 4, "key": {"subKey": "some data"}}`
    """
  )
  @Parameter(
    name = "measurementsFilter",
    required = false,
    description = """
    This filter should be a stringified list of JSON FilterConditions. \n
    FilterCondition has this structure: `{\"key\": <KEY>, \"operator\": <OPERATOR>, \"value\": <VALUE>}`. \n
    The key is a comma separated list of keys representing the path to the value. \n
    The operator is one of `EQUALS`, `GREATER_THAN` or `LESS_THAN`. \n
    The value needs to be a number. \n
    Example: `[{\"key\":\"temperature,val\",\"operator\":\"EQUALS\",\"value\":20},{\"key\":\"temperature,val\",\"operator\":\"LESS_THAN\",\"value\":10}]`
    """
  )
  @Parameter(
    name = "geometryFilter",
    required = false,
    examples = {
      @ExampleObject(
        name = "K Nearest Neighbor",
        value = """
        {
          "type": "K_NEAREST_NEIGHBOR",
          "k": 5,
          "x": 10,
          "y": 20,
          "z": 30
        }"""
      ),
      @ExampleObject(
        name = "Bounding Box",
        value = """
        {
          "type": "AXIS_ALIGNED_BOUNDING_BOX",
          "minX": 0,
          "minY": 0,
          "minZ": 0,
          "maxX": 100,
          "maxY": 100,
          "maxZ": 100
        }"""
      ),
      @ExampleObject(
        name = "Bounding Sphere",
        value = """
        {
          "type": "BOUNDING_SPHERE",
          "radius": 50,
          "centerX": 15,
          "centerY": 25,
          "centerZ": 20
        }"""
      ),
    }
  )
  @Parameter(name = "startTime", required = false, description = "Start timestamp in nanoseconds, inclusive")
  @Parameter(name = "endTime", required = false, description = "End timestamp in nanoseconds, inclusive")
  @Parameter(name = "limit", required = false)
  @Parameter(
    name = "skip",
    required = false,
    description = "Returns every nth data point from the container. We use the modulo operator on the point ids, therefore an even distribution cannot be guaranteed."
  )
  @APIResponse(
    description = "OK",
    responseCode = "200",
    content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = SpatialDataPointIO.class))
  )
  @APIResponse(responseCode = "400", description = "bad request")
  @APIResponse(responseCode = "401", description = "not authorized")
  @APIResponse(responseCode = "403", description = "forbidden")
  @APIResponse(responseCode = "404", description = "not found")
  public Response getSpatialDataPoints(
    @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId,
    @QueryParam("geometryFilter") String geometryFilterParam,
    @QueryParam("metadataFilter") String metadataFilterParam,
    @QueryParam("measurementsFilter") String measurementsFilterParam,
    @QueryParam("startTime") @PositiveOrZero Long startTime,
    @QueryParam("endTime") @PositiveOrZero Long endTime,
    @QueryParam("limit") @PositiveOrZero Integer limit,
    @QueryParam("skip") @PositiveOrZero Integer skip
  ) {
    Optional<AbstractGeometryFilter> geometryFilter = SpatialDataParamParser.parseGeometryFilter(geometryFilterParam);
    Optional<Map<String, Object>> metadata = SpatialDataParamParser.parseMetadata(metadataFilterParam);

    var measurementsFilter = SpatialDataParamParser.parseMeasurementsFilter(measurementsFilterParam);

    SpatialDataQueryParams spatialDataParams = new SpatialDataQueryParams(
      geometryFilter.orElse(null),
      metadata.orElse(Collections.emptyMap()),
      measurementsFilter.orElse(Collections.emptyList()),
      startTime,
      endTime,
      limit,
      skip
    );
    var result = dataPointService.getSpatialDataPointIOs(containerId, spatialDataParams);
    return Response.ok(result).build();
  }

  @POST
  @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}/" + Constants.PAYLOAD)
  @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
  @Operation(description = "Adds spatial data points to a spatial data container.")
  @APIResponse(description = "OK", responseCode = "204")
  @APIResponse(responseCode = "400", description = "bad request")
  @APIResponse(responseCode = "401", description = "not authorized")
  @APIResponse(responseCode = "403", description = "forbidden")
  @APIResponse(responseCode = "404", description = "not found")
  public Response createSpatialDataPoints(
    @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId,
    @RequestBody(
      required = true,
      content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = SpatialDataPointIO.class))
    ) @Valid List<SpatialDataPointIO> dataPoints
  ) {
    dataPointService.createSpatialDataPoints(containerId, dataPoints);
    return Response.noContent().build();
  }

  @GET
  @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}/" + Constants.PERMISSIONS)
  @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
  @Operation(description = "Get permissions")
  @APIResponse(
    description = "ok",
    responseCode = "200",
    content = @Content(schema = @Schema(implementation = PermissionsIO.class))
  )
  @APIResponse(responseCode = "400", description = "bad request")
  @APIResponse(responseCode = "401", description = "not authorized")
  @APIResponse(responseCode = "403", description = "forbidden")
  @APIResponse(responseCode = "404", description = "not found")
  @Parameter(name = Constants.SPATIAL_DATA_CONTAINER_ID)
  public Response getSpatialDataPermissions(
    @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId
  ) {
    var perms = containerService.getContainerPermissions(containerId);
    return Response.ok(new PermissionsIO(perms)).build();
  }

  @PUT
  @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}/" + Constants.PERMISSIONS)
  @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
  @Operation(description = "Edit permissions")
  @APIResponse(
    description = "ok",
    responseCode = "200",
    content = @Content(schema = @Schema(implementation = PermissionsIO.class))
  )
  @APIResponse(responseCode = "400", description = "bad request")
  @APIResponse(responseCode = "401", description = "not authorized")
  @APIResponse(responseCode = "403", description = "forbidden")
  @APIResponse(responseCode = "404", description = "not found")
  @Parameter(name = Constants.SPATIAL_DATA_CONTAINER_ID)
  public Response editSpatialDataPermissions(
    @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId,
    @RequestBody(
      required = true,
      content = @Content(schema = @Schema(implementation = PermissionsIO.class))
    ) @Valid PermissionsIO permissions
  ) {
    var perms = containerService.updateContainerPermissions(permissions, containerId);
    return Response.ok(new PermissionsIO(perms)).build();
  }

  @GET
  @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}/" + Constants.ROLES)
  @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
  @Operation(description = "Get roles")
  @APIResponse(
    description = "ok",
    responseCode = "200",
    content = @Content(schema = @Schema(implementation = Roles.class))
  )
  @APIResponse(responseCode = "400", description = "bad request")
  @APIResponse(responseCode = "401", description = "not authorized")
  @APIResponse(responseCode = "403", description = "forbidden")
  @APIResponse(responseCode = "404", description = "not found")
  @Parameter(name = Constants.SPATIAL_DATA_CONTAINER_ID)
  public Response getSpatialDataRoles(
    @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId
  ) {
    var roles = containerService.getContainerRoles(containerId);
    return Response.ok(roles).build();
  }
}