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
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
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 }