View Javadoc
1   package de.dlr.shepard.data.spatialdata.endpoints;
2   
3   import de.dlr.shepard.auth.permission.io.PermissionsIO;
4   import de.dlr.shepard.auth.permission.model.Roles;
5   import de.dlr.shepard.common.util.Constants;
6   import de.dlr.shepard.common.util.QueryParamHelper;
7   import de.dlr.shepard.data.ContainerAttributes;
8   import de.dlr.shepard.data.spatialdata.io.SpatialDataContainerIO;
9   import de.dlr.shepard.data.spatialdata.io.SpatialDataPointIO;
10  import de.dlr.shepard.data.spatialdata.io.SpatialDataQueryParams;
11  import de.dlr.shepard.data.spatialdata.model.geometryFilter.AbstractGeometryFilter;
12  import de.dlr.shepard.data.spatialdata.services.SpatialDataContainerService;
13  import de.dlr.shepard.data.spatialdata.services.SpatialDataPointService;
14  import io.quarkus.resteasy.reactive.server.EndpointDisabled;
15  import jakarta.enterprise.context.RequestScoped;
16  import jakarta.inject.Inject;
17  import jakarta.transaction.Transactional;
18  import jakarta.validation.Valid;
19  import jakarta.validation.constraints.NotNull;
20  import jakarta.validation.constraints.PositiveOrZero;
21  import jakarta.ws.rs.Consumes;
22  import jakarta.ws.rs.DELETE;
23  import jakarta.ws.rs.GET;
24  import jakarta.ws.rs.POST;
25  import jakarta.ws.rs.PUT;
26  import jakarta.ws.rs.Path;
27  import jakarta.ws.rs.PathParam;
28  import jakarta.ws.rs.Produces;
29  import jakarta.ws.rs.QueryParam;
30  import jakarta.ws.rs.core.MediaType;
31  import jakarta.ws.rs.core.Response;
32  import jakarta.ws.rs.core.Response.Status;
33  import java.util.Collections;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Optional;
37  import org.eclipse.microprofile.openapi.annotations.Operation;
38  import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
39  import org.eclipse.microprofile.openapi.annotations.media.Content;
40  import org.eclipse.microprofile.openapi.annotations.media.ExampleObject;
41  import org.eclipse.microprofile.openapi.annotations.media.Schema;
42  import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
43  import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
44  import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
45  import org.eclipse.microprofile.openapi.annotations.tags.Tag;
46  
47  @EndpointDisabled(name = "shepard.spatial-data.enabled", stringValue = "false")
48  @Path(Constants.SPATIAL_DATA_CONTAINERS)
49  @Produces(MediaType.APPLICATION_JSON)
50  @Consumes(MediaType.APPLICATION_JSON)
51  @RequestScoped
52  public class SpatialDataPointRest {
53  
54    @Inject
55    SpatialDataPointService dataPointService;
56  
57    @Inject
58    SpatialDataContainerService containerService;
59  
60    //#region Container functions
61    @GET
62    @Tag(
63      name = Constants.SPATIAL_DATA_CONTAINER,
64      description = "Spatial data containers and their endpoints are experimental. The administrator has to activate this feature. Otherwise the endpoints will return 404."
65    )
66    @Operation(description = "Get spatial data containers.")
67    @APIResponse(
68      description = "ok",
69      responseCode = "200",
70      content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = SpatialDataContainerIO.class))
71    )
72    @APIResponse(responseCode = "400", description = "bad request")
73    @APIResponse(responseCode = "401", description = "not authorized")
74    @APIResponse(responseCode = "404", description = "not found")
75    @Parameter(name = Constants.QP_NAME)
76    @Parameter(name = Constants.QP_PAGE)
77    @Parameter(name = Constants.QP_SIZE)
78    @Parameter(name = Constants.QP_ORDER_BY_ATTRIBUTE)
79    @Parameter(name = Constants.QP_ORDER_DESC)
80    public Response getSpatialDataContainers(
81      @QueryParam(Constants.QP_NAME) String name,
82      @QueryParam(Constants.QP_PAGE) @PositiveOrZero Integer page,
83      @QueryParam(Constants.QP_SIZE) @PositiveOrZero Integer size,
84      @QueryParam(Constants.QP_ORDER_BY_ATTRIBUTE) ContainerAttributes orderBy,
85      @QueryParam(Constants.QP_ORDER_DESC) Boolean orderDesc
86    ) {
87      var params = new QueryParamHelper();
88      if (name != null) params = params.withName(name);
89      if (page != null && size != null) params = params.withPageAndSize(page, size);
90      if (orderBy != null) params = params.withOrderByAttribute(orderBy, orderDesc);
91  
92      var containers = containerService.getAllContainers(params);
93      var result = SpatialDataContainerIO.fromEntities(containers);
94      return Response.ok(result).build();
95    }
96  
97    @GET
98    @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}")
99    @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
100   @Operation(description = "Get spatial data container.")
101   @APIResponse(
102     description = "ok",
103     responseCode = "200",
104     content = @Content(schema = @Schema(implementation = SpatialDataContainerIO.class))
105   )
106   @APIResponse(responseCode = "400", description = "bad request")
107   @APIResponse(responseCode = "401", description = "not authorized")
108   @APIResponse(responseCode = "403", description = "forbidden")
109   @APIResponse(responseCode = "404", description = "not found")
110   @Parameter(name = Constants.SPATIAL_DATA_CONTAINER_ID)
111   public Response getSpatialDataContainer(
112     @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId
113   ) {
114     var container = containerService.getContainer(containerId);
115     return Response.ok(SpatialDataContainerIO.fromEntity(container)).build();
116   }
117 
118   @POST
119   @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
120   @Operation(description = "Create a new spatial data container.")
121   @APIResponse(
122     description = "created",
123     responseCode = "201",
124     content = @Content(schema = @Schema(implementation = SpatialDataContainerIO.class))
125   )
126   @Transactional
127   public Response createSpatialDataContainer(
128     @RequestBody(
129       required = true,
130       content = @Content(schema = @Schema(implementation = SpatialDataContainerIO.class))
131     ) @Valid SpatialDataContainerIO containerIo
132   ) {
133     var container = containerService.createContainer(containerIo);
134     return Response.ok(SpatialDataContainerIO.fromEntity(container)).status(Status.CREATED).build();
135   }
136 
137   @DELETE
138   @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}")
139   @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
140   @Operation(description = "Deletes spatial data container and related spatial data.")
141   @APIResponse(description = "ok", responseCode = "200")
142   @APIResponse(responseCode = "400", description = "bad request")
143   @APIResponse(responseCode = "401", description = "not authorized")
144   @APIResponse(responseCode = "403", description = "forbidden")
145   @APIResponse(responseCode = "404", description = "not found")
146   @Parameter(name = Constants.SPATIAL_DATA_CONTAINER_ID)
147   public Response deleteSpatialDataContainer(
148     @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId
149   ) {
150     containerService.deleteContainer(containerId);
151     return Response.status(Status.OK).build();
152   }
153 
154   //#endregion
155 
156   @GET
157   @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}/" + Constants.PAYLOAD)
158   @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
159   @Operation(description = "Get spatial data by container id")
160   @Parameter(
161     name = "metadataFilter",
162     required = false,
163     description = """
164     This filter should be a stringified list of JSON object for exact match in metadata. \n
165     Example : `{"track": 1, "layer": 4, "key": {"subKey": "some data"}}`
166     """
167   )
168   @Parameter(
169     name = "measurementsFilter",
170     required = false,
171     description = """
172     This filter should be a stringified list of JSON FilterConditions. \n
173     FilterCondition has this structure: `{\"key\": <KEY>, \"operator\": <OPERATOR>, \"value\": <VALUE>}`. \n
174     The key is a comma separated list of keys representing the path to the value. \n
175     The operator is one of `EQUALS`, `GREATER_THAN` or `LESS_THAN`. \n
176     The value needs to be a number. \n
177     Example: `[{\"key\":\"temperature,val\",\"operator\":\"EQUALS\",\"value\":20},{\"key\":\"temperature,val\",\"operator\":\"LESS_THAN\",\"value\":10}]`
178     """
179   )
180   @Parameter(
181     name = "geometryFilter",
182     required = false,
183     examples = {
184       @ExampleObject(
185         name = "K Nearest Neighbor",
186         value = """
187         {
188           "type": "K_NEAREST_NEIGHBOR",
189           "k": 5,
190           "x": 10,
191           "y": 20,
192           "z": 30
193         }"""
194       ),
195       @ExampleObject(
196         name = "Bounding Box",
197         value = """
198         {
199           "type": "AXIS_ALIGNED_BOUNDING_BOX",
200           "minX": 0,
201           "minY": 0,
202           "minZ": 0,
203           "maxX": 100,
204           "maxY": 100,
205           "maxZ": 100
206         }"""
207       ),
208       @ExampleObject(
209         name = "Bounding Sphere",
210         value = """
211         {
212           "type": "BOUNDING_SPHERE",
213           "radius": 50,
214           "centerX": 15,
215           "centerY": 25,
216           "centerZ": 20
217         }"""
218       ),
219     }
220   )
221   @Parameter(name = "startTime", required = false, description = "Start timestamp in nanoseconds, inclusive")
222   @Parameter(name = "endTime", required = false, description = "End timestamp in nanoseconds, inclusive")
223   @Parameter(name = "limit", required = false)
224   @Parameter(
225     name = "skip",
226     required = false,
227     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."
228   )
229   @APIResponse(
230     description = "OK",
231     responseCode = "200",
232     content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = SpatialDataPointIO.class))
233   )
234   @APIResponse(responseCode = "400", description = "bad request")
235   @APIResponse(responseCode = "401", description = "not authorized")
236   @APIResponse(responseCode = "403", description = "forbidden")
237   @APIResponse(responseCode = "404", description = "not found")
238   public Response getSpatialDataPoints(
239     @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId,
240     @QueryParam("geometryFilter") String geometryFilterParam,
241     @QueryParam("metadataFilter") String metadataFilterParam,
242     @QueryParam("measurementsFilter") String measurementsFilterParam,
243     @QueryParam("startTime") @PositiveOrZero Long startTime,
244     @QueryParam("endTime") @PositiveOrZero Long endTime,
245     @QueryParam("limit") @PositiveOrZero Integer limit,
246     @QueryParam("skip") @PositiveOrZero Integer skip
247   ) {
248     Optional<AbstractGeometryFilter> geometryFilter = SpatialDataParamParser.parseGeometryFilter(geometryFilterParam);
249     Optional<Map<String, Object>> metadata = SpatialDataParamParser.parseMetadata(metadataFilterParam);
250 
251     var measurementsFilter = SpatialDataParamParser.parseMeasurementsFilter(measurementsFilterParam);
252 
253     SpatialDataQueryParams spatialDataParams = new SpatialDataQueryParams(
254       geometryFilter.orElse(null),
255       metadata.orElse(Collections.emptyMap()),
256       measurementsFilter.orElse(Collections.emptyList()),
257       startTime,
258       endTime,
259       limit,
260       skip
261     );
262     var result = dataPointService.getSpatialDataPointIOs(containerId, spatialDataParams);
263     return Response.ok(result).build();
264   }
265 
266   @POST
267   @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}/" + Constants.PAYLOAD)
268   @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
269   @Operation(description = "Adds spatial data points to a spatial data container.")
270   @APIResponse(description = "OK", responseCode = "204")
271   @APIResponse(responseCode = "400", description = "bad request")
272   @APIResponse(responseCode = "401", description = "not authorized")
273   @APIResponse(responseCode = "403", description = "forbidden")
274   @APIResponse(responseCode = "404", description = "not found")
275   public Response createSpatialDataPoints(
276     @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId,
277     @RequestBody(
278       required = true,
279       content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = SpatialDataPointIO.class))
280     ) @Valid List<SpatialDataPointIO> dataPoints
281   ) {
282     dataPointService.createSpatialDataPoints(containerId, dataPoints);
283     return Response.noContent().build();
284   }
285 
286   @GET
287   @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}/" + Constants.PERMISSIONS)
288   @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
289   @Operation(description = "Get permissions")
290   @APIResponse(
291     description = "ok",
292     responseCode = "200",
293     content = @Content(schema = @Schema(implementation = PermissionsIO.class))
294   )
295   @APIResponse(responseCode = "400", description = "bad request")
296   @APIResponse(responseCode = "401", description = "not authorized")
297   @APIResponse(responseCode = "403", description = "forbidden")
298   @APIResponse(responseCode = "404", description = "not found")
299   @Parameter(name = Constants.SPATIAL_DATA_CONTAINER_ID)
300   public Response getSpatialDataPermissions(
301     @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId
302   ) {
303     var perms = containerService.getContainerPermissions(containerId);
304     return Response.ok(new PermissionsIO(perms)).build();
305   }
306 
307   @PUT
308   @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}/" + Constants.PERMISSIONS)
309   @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
310   @Operation(description = "Edit permissions")
311   @APIResponse(
312     description = "ok",
313     responseCode = "200",
314     content = @Content(schema = @Schema(implementation = PermissionsIO.class))
315   )
316   @APIResponse(responseCode = "400", description = "bad request")
317   @APIResponse(responseCode = "401", description = "not authorized")
318   @APIResponse(responseCode = "403", description = "forbidden")
319   @APIResponse(responseCode = "404", description = "not found")
320   @Parameter(name = Constants.SPATIAL_DATA_CONTAINER_ID)
321   public Response editSpatialDataPermissions(
322     @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId,
323     @RequestBody(
324       required = true,
325       content = @Content(schema = @Schema(implementation = PermissionsIO.class))
326     ) @Valid PermissionsIO permissions
327   ) {
328     var perms = containerService.updateContainerPermissions(permissions, containerId);
329     return Response.ok(new PermissionsIO(perms)).build();
330   }
331 
332   @GET
333   @Path("/{" + Constants.SPATIAL_DATA_CONTAINER_ID + "}/" + Constants.ROLES)
334   @Tag(name = Constants.SPATIAL_DATA_CONTAINER)
335   @Operation(description = "Get roles")
336   @APIResponse(
337     description = "ok",
338     responseCode = "200",
339     content = @Content(schema = @Schema(implementation = Roles.class))
340   )
341   @APIResponse(responseCode = "400", description = "bad request")
342   @APIResponse(responseCode = "401", description = "not authorized")
343   @APIResponse(responseCode = "403", description = "forbidden")
344   @APIResponse(responseCode = "404", description = "not found")
345   @Parameter(name = Constants.SPATIAL_DATA_CONTAINER_ID)
346   public Response getSpatialDataRoles(
347     @PathParam(Constants.SPATIAL_DATA_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId
348   ) {
349     var roles = containerService.getContainerRoles(containerId);
350     return Response.ok(roles).build();
351   }
352 }