View Javadoc
1   package de.dlr.shepard.data.timeseries.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.auth.permission.services.PermissionsService;
6   import de.dlr.shepard.common.exceptions.InvalidAuthException;
7   import de.dlr.shepard.common.filters.Subscribable;
8   import de.dlr.shepard.common.util.Constants;
9   import de.dlr.shepard.common.util.QueryParamHelper;
10  import de.dlr.shepard.data.ContainerAttributes;
11  import de.dlr.shepard.data.timeseries.io.TimeseriesContainerIO;
12  import de.dlr.shepard.data.timeseries.io.TimeseriesContainerIOMapper;
13  import de.dlr.shepard.data.timeseries.io.TimeseriesIO;
14  import de.dlr.shepard.data.timeseries.io.TimeseriesWithDataPoints;
15  import de.dlr.shepard.data.timeseries.model.Timeseries;
16  import de.dlr.shepard.data.timeseries.model.TimeseriesDataPointsQueryParams;
17  import de.dlr.shepard.data.timeseries.model.TimeseriesTuple;
18  import de.dlr.shepard.data.timeseries.model.enums.AggregateFunction;
19  import de.dlr.shepard.data.timeseries.model.enums.CsvFormat;
20  import de.dlr.shepard.data.timeseries.model.enums.FillOption;
21  import de.dlr.shepard.data.timeseries.services.TimeseriesContainerService;
22  import de.dlr.shepard.data.timeseries.services.TimeseriesCsvService;
23  import de.dlr.shepard.data.timeseries.services.TimeseriesService;
24  import jakarta.enterprise.context.RequestScoped;
25  import jakarta.inject.Inject;
26  import jakarta.transaction.Transactional;
27  import jakarta.validation.Valid;
28  import jakarta.validation.constraints.NotBlank;
29  import jakarta.validation.constraints.NotNull;
30  import jakarta.validation.constraints.PositiveOrZero;
31  import jakarta.ws.rs.Consumes;
32  import jakarta.ws.rs.DELETE;
33  import jakarta.ws.rs.DefaultValue;
34  import jakarta.ws.rs.GET;
35  import jakarta.ws.rs.NotFoundException;
36  import jakarta.ws.rs.POST;
37  import jakarta.ws.rs.PUT;
38  import jakarta.ws.rs.Path;
39  import jakarta.ws.rs.PathParam;
40  import jakarta.ws.rs.Produces;
41  import jakarta.ws.rs.QueryParam;
42  import jakarta.ws.rs.WebApplicationException;
43  import jakarta.ws.rs.core.Context;
44  import jakarta.ws.rs.core.MediaType;
45  import jakarta.ws.rs.core.Response;
46  import jakarta.ws.rs.core.Response.Status;
47  import jakarta.ws.rs.core.SecurityContext;
48  import java.io.IOException;
49  import java.nio.file.InvalidPathException;
50  import java.util.Collections;
51  import java.util.NoSuchElementException;
52  import org.eclipse.microprofile.openapi.annotations.Operation;
53  import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
54  import org.eclipse.microprofile.openapi.annotations.media.Content;
55  import org.eclipse.microprofile.openapi.annotations.media.Schema;
56  import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
57  import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
58  import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
59  import org.eclipse.microprofile.openapi.annotations.tags.Tag;
60  import org.jboss.resteasy.reactive.RestForm;
61  import org.jboss.resteasy.reactive.multipart.FileUpload;
62  
63  @Consumes(MediaType.APPLICATION_JSON)
64  @Produces(MediaType.APPLICATION_JSON)
65  @Path(Constants.TIMESERIES_CONTAINERS)
66  @RequestScoped
67  public class TimeseriesRest {
68  
69    @Inject
70    TimeseriesService timeseriesService;
71  
72    @Inject
73    TimeseriesCsvService timeseriesCsvService;
74  
75    @Inject
76    TimeseriesContainerService timeseriesContainerService;
77  
78    @Inject
79    PermissionsService permissionsService;
80  
81    @Context
82    private SecurityContext securityContext;
83  
84    @GET
85    @Tag(name = Constants.TIMESERIES_CONTAINER)
86    @Operation(description = "Get all timeseries containers")
87    @APIResponse(
88      description = "ok",
89      responseCode = "200",
90      content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TimeseriesContainerIO.class))
91    )
92    @APIResponse(responseCode = "400", description = "bad request")
93    @APIResponse(responseCode = "401", description = "not authorized")
94    @APIResponse(responseCode = "403", description = "forbidden")
95    @APIResponse(responseCode = "404", description = "not found")
96    @Parameter(name = Constants.QP_NAME)
97    @Parameter(name = Constants.QP_PAGE)
98    @Parameter(name = Constants.QP_SIZE)
99    @Parameter(name = Constants.QP_ORDER_BY_ATTRIBUTE)
100   @Parameter(name = Constants.QP_ORDER_DESC)
101   public Response getAllTimeseriesContainers(
102     @QueryParam(Constants.QP_NAME) String name,
103     @QueryParam(Constants.QP_PAGE) @PositiveOrZero Integer page,
104     @QueryParam(Constants.QP_SIZE) @PositiveOrZero Integer size,
105     @QueryParam(Constants.QP_ORDER_BY_ATTRIBUTE) ContainerAttributes orderBy,
106     @QueryParam(Constants.QP_ORDER_DESC) Boolean orderDesc
107   ) {
108     var params = new QueryParamHelper();
109     if (name != null) params = params.withName(name);
110     if (page != null && size != null) params = params.withPageAndSize(page, size);
111     if (orderBy != null) params = params.withOrderByAttribute(orderBy, orderDesc);
112     var containers = timeseriesContainerService.getAllContainers(params);
113     var result = TimeseriesContainerIOMapper.map(containers);
114 
115     return Response.ok(result).build();
116   }
117 
118   @GET
119   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}")
120   @Tag(name = Constants.TIMESERIES_CONTAINER)
121   @Operation(description = "Get timeseries container")
122   @APIResponse(
123     description = "ok",
124     responseCode = "200",
125     content = @Content(schema = @Schema(implementation = TimeseriesContainerIO.class))
126   )
127   @APIResponse(responseCode = "400", description = "bad request")
128   @APIResponse(responseCode = "401", description = "not authorized")
129   @APIResponse(responseCode = "403", description = "forbidden")
130   @APIResponse(responseCode = "404", description = "not found")
131   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
132   public Response getTimeseriesContainer(
133     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId
134   ) {
135     var container = timeseriesContainerService.getContainer(timeseriesContainerId);
136     return Response.ok(TimeseriesContainerIOMapper.map(container)).build();
137   }
138 
139   @POST
140   @Tag(name = Constants.TIMESERIES_CONTAINER)
141   @Operation(description = "Create a new timeseries container")
142   @APIResponse(
143     description = "created",
144     responseCode = "201",
145     content = @Content(schema = @Schema(implementation = TimeseriesContainerIO.class))
146   )
147   @APIResponse(responseCode = "400", description = "bad request")
148   @APIResponse(responseCode = "401", description = "not authorized")
149   @APIResponse(responseCode = "403", description = "forbidden")
150   @APIResponse(responseCode = "404", description = "not found")
151   @Transactional
152   public Response createTimeseriesContainer(
153     @RequestBody(
154       content = @Content(schema = @Schema(implementation = TimeseriesContainerIO.class))
155     ) @Valid TimeseriesContainerIO timeseriesContainer
156   ) {
157     var container = timeseriesContainerService.createContainer(timeseriesContainer);
158     return Response.ok(TimeseriesContainerIOMapper.map(container)).status(Status.CREATED).build();
159   }
160 
161   @DELETE
162   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}")
163   @Subscribable
164   @Tag(name = Constants.TIMESERIES_CONTAINER)
165   @Operation(description = "Delete timeseries container")
166   @APIResponse(description = "deleted", responseCode = "204")
167   @APIResponse(responseCode = "400", description = "bad request")
168   @APIResponse(responseCode = "401", description = "not authorized")
169   @APIResponse(responseCode = "403", description = "forbidden")
170   @APIResponse(responseCode = "404", description = "not found")
171   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
172   public Response deleteTimeseriesContainer(
173     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId
174   ) {
175     timeseriesContainerService.deleteContainer(timeseriesContainerId);
176     return Response.status(Status.NO_CONTENT).build();
177   }
178 
179   @POST
180   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PAYLOAD)
181   @Subscribable
182   @Tag(name = Constants.TIMESERIES_CONTAINER)
183   @Operation(description = "Upload timeseries to container")
184   @APIResponse(
185     description = "created",
186     responseCode = "201",
187     content = @Content(schema = @Schema(implementation = TimeseriesTuple.class))
188   )
189   @APIResponse(responseCode = "400", description = "bad request")
190   @APIResponse(responseCode = "401", description = "not authorized")
191   @APIResponse(responseCode = "403", description = "forbidden")
192   @APIResponse(responseCode = "404", description = "not found")
193   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
194   public Response createTimeseries(
195     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId,
196     @RequestBody(
197       content = @Content(schema = @Schema(implementation = TimeseriesWithDataPoints.class))
198     ) @Valid TimeseriesWithDataPoints payload
199   ) {
200     Timeseries timeseries = timeseriesService.saveDataPoints(containerId, payload.getTimeseries(), payload.getPoints());
201 
202     return Response.ok(timeseries.getTimeseriesTuple()).status(Status.CREATED).build();
203   }
204 
205   @Deprecated(forRemoval = true)
206   @GET
207   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.AVAILABLE)
208   @Tag(name = Constants.TIMESERIES_CONTAINER)
209   @Operation(
210     description = "Get timeseries available. Deprecated, use /timeseriesContainers/{containerId}/timeseries instead."
211   )
212   @APIResponse(
213     description = "ok",
214     responseCode = "200",
215     content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TimeseriesTuple.class))
216   )
217   @APIResponse(responseCode = "400", description = "bad request")
218   @APIResponse(responseCode = "401", description = "not authorized")
219   @APIResponse(responseCode = "403", description = "forbidden")
220   @APIResponse(responseCode = "404", description = "not found")
221   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
222   public Response getTimeseriesAvailable(
223     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId
224   ) {
225     try {
226       var timeseriesListWithoutId = timeseriesService
227         .getTimeseriesAvailable(timeseriesContainerId)
228         .map(Timeseries::getTimeseriesTuple)
229         .toList();
230       return Response.ok(timeseriesListWithoutId).build();
231     } catch (InvalidPathException | InvalidAuthException e) {
232       return Response.ok(Collections.emptyList()).build();
233     }
234   }
235 
236   @GET
237   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.TIMESERIES)
238   @Tag(name = Constants.TIMESERIES_CONTAINER)
239   @Operation(description = "Get all available timeseries for that container.")
240   @APIResponse(
241     description = "ok",
242     responseCode = "200",
243     content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TimeseriesIO.class))
244   )
245   @APIResponse(responseCode = "400", description = "bad request")
246   @APIResponse(responseCode = "401", description = "not authorized")
247   @APIResponse(responseCode = "403", description = "forbidden")
248   @APIResponse(responseCode = "404", description = "not found")
249   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
250   @Parameter(name = Constants.MEASUREMENT)
251   @Parameter(name = Constants.DEVICE)
252   @Parameter(name = Constants.LOCATION)
253   @Parameter(name = Constants.SYMBOLICNAME)
254   @Parameter(name = Constants.FIELD)
255   public Response getTimeseriesOfContainer(
256     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId,
257     @QueryParam(Constants.MEASUREMENT) String measurement,
258     @QueryParam(Constants.DEVICE) String device,
259     @QueryParam(Constants.LOCATION) String location,
260     @QueryParam(Constants.SYMBOLICNAME) String symbolicName,
261     @QueryParam(Constants.FIELD) String field
262   ) {
263     var timeseriesList = timeseriesService
264       .getTimeseriesAvailable(timeseriesContainerId)
265       .map(TimeseriesIO::new)
266       .filter(
267         entity ->
268           (measurement == null || measurement.isEmpty() || entity.getMeasurement().equals(measurement)) &&
269           (device == null || device.isEmpty() || entity.getDevice().equals(device)) &&
270           (location == null || location.isEmpty() || entity.getLocation().equals(location)) &&
271           (symbolicName == null || symbolicName.isEmpty() || entity.getSymbolicName().equals(symbolicName)) &&
272           (field == null || field.isEmpty() || entity.getField().equals(field))
273       )
274       .toList();
275     return Response.ok(timeseriesList).build();
276   }
277 
278   @GET
279   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.TIMESERIES + "/{" + Constants.TIMESERIES_ID + "}")
280   @Tag(name = Constants.TIMESERIES_CONTAINER)
281   @Operation(description = "Get the 5-tuple describing a timeseries by its id.")
282   @APIResponse(
283     description = "ok",
284     responseCode = "200",
285     content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TimeseriesIO.class))
286   )
287   @APIResponse(responseCode = "400", description = "bad request")
288   @APIResponse(responseCode = "401", description = "not authorized")
289   @APIResponse(responseCode = "403", description = "forbidden")
290   @APIResponse(responseCode = "404", description = "not found")
291   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
292   public Response getTimeseriesById(
293     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId,
294     @PathParam(Constants.TIMESERIES_ID) @NotNull @PositiveOrZero Long timeseriesId
295   ) {
296     var timeseries = timeseriesService.getTimeseriesById(timeseriesId);
297     return Response.ok(new TimeseriesIO(timeseries)).build();
298   }
299 
300   @GET
301   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PAYLOAD)
302   @Tag(name = Constants.TIMESERIES_CONTAINER)
303   @Operation(description = "Get timeseries payload")
304   @APIResponse(
305     description = "ok",
306     responseCode = "200",
307     content = @Content(schema = @Schema(implementation = TimeseriesWithDataPoints.class))
308   )
309   @APIResponse(responseCode = "400", description = "bad request")
310   @APIResponse(responseCode = "401", description = "not authorized")
311   @APIResponse(responseCode = "403", description = "forbidden")
312   @APIResponse(responseCode = "404", description = "not found")
313   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
314   @Parameter(name = Constants.MEASUREMENT, required = true)
315   @Parameter(name = Constants.LOCATION, required = true)
316   @Parameter(name = Constants.DEVICE, required = true)
317   @Parameter(name = Constants.SYMBOLICNAME, required = true)
318   @Parameter(name = Constants.FIELD, required = true)
319   @Parameter(name = Constants.START, required = true)
320   @Parameter(name = Constants.END, required = true)
321   @Parameter(name = Constants.FUNCTION)
322   @Parameter(name = Constants.GROUP_BY)
323   @Parameter(name = Constants.FILLOPTION)
324   public Response getTimeseries(
325     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId,
326     @QueryParam(Constants.MEASUREMENT) @NotBlank String measurement,
327     @QueryParam(Constants.LOCATION) @NotBlank String location,
328     @QueryParam(Constants.DEVICE) @NotBlank String device,
329     @QueryParam(Constants.SYMBOLICNAME) @NotBlank String symbolicName,
330     @QueryParam(Constants.FIELD) @NotBlank String field,
331     @QueryParam(Constants.START) @NotNull @PositiveOrZero Long start,
332     @QueryParam(Constants.END) @NotNull @PositiveOrZero Long end,
333     @QueryParam(Constants.FUNCTION) AggregateFunction function,
334     @QueryParam(Constants.GROUP_BY) Long groupBy,
335     @QueryParam(Constants.FILLOPTION) FillOption fillOption
336   ) throws Exception {
337     var timeseries = new TimeseriesTuple(measurement, device, location, symbolicName, field);
338     TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
339       start,
340       end,
341       groupBy,
342       fillOption,
343       function
344     );
345     var timeseriesData = timeseriesService.getDataPointsByTimeseries(timeseriesContainerId, timeseries, queryParams);
346     TimeseriesWithDataPoints timeseriesWithData = new TimeseriesWithDataPoints(timeseries, timeseriesData);
347     return Response.ok(timeseriesWithData).build();
348   }
349 
350   @GET
351   @Produces({ MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON })
352   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.EXPORT)
353   @Tag(name = Constants.TIMESERIES_CONTAINER)
354   @Operation(description = "Export timeseries payload")
355   @APIResponse(
356     description = "ok",
357     responseCode = "200",
358     content = @Content(
359       mediaType = MediaType.APPLICATION_OCTET_STREAM,
360       schema = @Schema(type = SchemaType.STRING, format = "binary")
361     )
362   )
363   @APIResponse(responseCode = "400", description = "bad request")
364   @APIResponse(responseCode = "401", description = "not authorized")
365   @APIResponse(responseCode = "403", description = "forbidden")
366   @APIResponse(responseCode = "404", description = "not found")
367   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
368   @Parameter(name = Constants.MEASUREMENT, required = true)
369   @Parameter(name = Constants.LOCATION, required = true)
370   @Parameter(name = Constants.DEVICE, required = true)
371   @Parameter(name = Constants.SYMBOLICNAME, required = true)
372   @Parameter(name = Constants.FIELD, required = true)
373   @Parameter(name = Constants.START, required = true)
374   @Parameter(name = Constants.END, required = true)
375   @Parameter(name = Constants.FUNCTION)
376   @Parameter(name = Constants.GROUP_BY)
377   @Parameter(name = Constants.FILLOPTION)
378   @Parameter(name = Constants.CSVFORMAT)
379   public Response exportTimeseries(
380     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId,
381     @QueryParam(Constants.MEASUREMENT) @NotBlank String measurement,
382     @QueryParam(Constants.LOCATION) @NotBlank String location,
383     @QueryParam(Constants.DEVICE) @NotBlank String device,
384     @QueryParam(Constants.SYMBOLICNAME) @NotBlank String symbolicName,
385     @QueryParam(Constants.FIELD) @NotBlank String field,
386     @QueryParam(Constants.START) @NotNull @PositiveOrZero Long start,
387     @QueryParam(Constants.END) @NotNull @PositiveOrZero Long end,
388     @QueryParam(Constants.FUNCTION) AggregateFunction function,
389     @QueryParam(Constants.GROUP_BY) Long groupBy,
390     @QueryParam(Constants.FILLOPTION) FillOption fillOption,
391     @QueryParam(Constants.CSVFORMAT) @DefaultValue(value = "ROW") CsvFormat csvFormat
392   ) throws IOException {
393     var timeseries = new TimeseriesTuple(measurement, device, location, symbolicName, field);
394     TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
395       start,
396       end,
397       groupBy,
398       fillOption,
399       function
400     );
401     var inputStream = timeseriesCsvService.exportTimeseriesDataToCsv(
402       timeseriesContainerId,
403       timeseries,
404       queryParams,
405       csvFormat
406     );
407 
408     return Response.ok(inputStream, MediaType.APPLICATION_OCTET_STREAM)
409       .header("Content-Disposition", "attachment; filename=\"timeseries-export.csv\"")
410       .build();
411   }
412 
413   @POST
414   @Consumes(MediaType.MULTIPART_FORM_DATA)
415   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.IMPORT)
416   @Tag(name = Constants.TIMESERIES_CONTAINER)
417   @Operation(description = "Import timeseries payload")
418   @APIResponse(description = "ok", responseCode = "200")
419   @APIResponse(responseCode = "400", description = "bad request")
420   @APIResponse(responseCode = "401", description = "not authorized")
421   @APIResponse(responseCode = "403", description = "forbidden")
422   @APIResponse(responseCode = "404", description = "not found")
423   @Subscribable
424   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
425   public Response importTimeseries(
426     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId,
427     MultipartBodyFileUpload body
428   ) throws IOException {
429     String filePath = body.fileUpload != null ? body.fileUpload.uploadedFile().toString() : null;
430 
431     if (filePath == null) {
432       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
433     }
434 
435     timeseriesCsvService.importTimeseriesFromCsv(timeseriesContainerId, filePath);
436     return Response.ok().build();
437   }
438 
439   @GET
440   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PERMISSIONS)
441   @Tag(name = Constants.TIMESERIES_CONTAINER)
442   @Operation(description = "Get permissions")
443   @APIResponse(
444     description = "ok",
445     responseCode = "200",
446     content = @Content(schema = @Schema(implementation = PermissionsIO.class))
447   )
448   @APIResponse(responseCode = "400", description = "bad request")
449   @APIResponse(responseCode = "401", description = "not authorized")
450   @APIResponse(responseCode = "403", description = "forbidden")
451   @APIResponse(responseCode = "404", description = "not found")
452   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
453   public PermissionsIO getTimeseriesPermissions(
454     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId
455   ) {
456     var permissions = permissionsService.getPermissionsOfEntity(timeseriesContainerId);
457     return new PermissionsIO(permissions);
458   }
459 
460   @PUT
461   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PERMISSIONS)
462   @Tag(name = Constants.TIMESERIES_CONTAINER)
463   @Operation(description = "Edit permissions")
464   @APIResponse(
465     description = "ok",
466     responseCode = "200",
467     content = @Content(schema = @Schema(implementation = PermissionsIO.class))
468   )
469   @APIResponse(responseCode = "400", description = "bad request")
470   @APIResponse(responseCode = "401", description = "not authorized")
471   @APIResponse(responseCode = "403", description = "forbidden")
472   @APIResponse(responseCode = "404", description = "not found")
473   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
474   public PermissionsIO editTimeseriesPermissions(
475     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId,
476     @RequestBody(
477       content = @Content(schema = @Schema(implementation = PermissionsIO.class))
478     ) @Valid PermissionsIO permissions
479   ) {
480     var updatedPermissions = permissionsService.updatePermissionsByNeo4jId(permissions, timeseriesContainerId);
481     if (updatedPermissions == null) throw new NotFoundException();
482     return new PermissionsIO(updatedPermissions);
483   }
484 
485   @GET
486   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.ROLES)
487   @Tag(name = Constants.TIMESERIES_CONTAINER)
488   @Operation(description = "Get roles")
489   @APIResponse(
490     description = "ok",
491     responseCode = "200",
492     content = @Content(schema = @Schema(implementation = Roles.class))
493   )
494   @APIResponse(responseCode = "400", description = "bad request")
495   @APIResponse(responseCode = "401", description = "not authorized")
496   @APIResponse(responseCode = "403", description = "forbidden")
497   @APIResponse(responseCode = "404", description = "not found")
498   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
499   public Roles getTimeseriesRoles(
500     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId
501   ) {
502     var roles = permissionsService.getUserRolesOnEntity(
503       timeseriesContainerId,
504       securityContext.getUserPrincipal().getName()
505     );
506     if (roles == null) throw new NotFoundException();
507     return roles;
508   }
509 
510   @Schema(type = SchemaType.STRING, format = "binary", description = "Timeseries as CSV")
511   public interface UploadItemSchema {}
512 
513   public static class UploadFormSchema {
514 
515     @Schema(required = true)
516     public UploadItemSchema file;
517   }
518 
519   @Schema(implementation = UploadFormSchema.class)
520   public static class MultipartBodyFileUpload {
521 
522     @RestForm(Constants.FILE)
523     public FileUpload fileUpload;
524   }
525 }