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.List;
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     List<TimeseriesIO> timeseriesList = timeseriesService
264       .findByQuintupleInContainer(timeseriesContainerId, measurement, device, location, symbolicName, field)
265       .map(TimeseriesIO::new)
266       .toList();
267     return Response.ok(timeseriesList).build();
268   }
269 
270   @GET
271   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.TIMESERIES + "/{" + Constants.TIMESERIES_ID + "}")
272   @Tag(name = Constants.TIMESERIES_CONTAINER)
273   @Operation(description = "Get the 5-tuple describing a timeseries by its id.")
274   @APIResponse(
275     description = "ok",
276     responseCode = "200",
277     content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TimeseriesIO.class))
278   )
279   @APIResponse(responseCode = "400", description = "bad request")
280   @APIResponse(responseCode = "401", description = "not authorized")
281   @APIResponse(responseCode = "403", description = "forbidden")
282   @APIResponse(responseCode = "404", description = "not found")
283   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
284   public Response getTimeseriesById(
285     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long containerId,
286     @PathParam(Constants.TIMESERIES_ID) @NotNull @PositiveOrZero Long timeseriesId
287   ) {
288     var timeseries = timeseriesService.getTimeseriesById(timeseriesId);
289     return Response.ok(new TimeseriesIO(timeseries)).build();
290   }
291 
292   @GET
293   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PAYLOAD)
294   @Tag(name = Constants.TIMESERIES_CONTAINER)
295   @Operation(description = "Get timeseries payload")
296   @APIResponse(
297     description = "ok",
298     responseCode = "200",
299     content = @Content(schema = @Schema(implementation = TimeseriesWithDataPoints.class))
300   )
301   @APIResponse(responseCode = "400", description = "bad request")
302   @APIResponse(responseCode = "401", description = "not authorized")
303   @APIResponse(responseCode = "403", description = "forbidden")
304   @APIResponse(responseCode = "404", description = "not found")
305   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
306   @Parameter(name = Constants.MEASUREMENT, required = true)
307   @Parameter(name = Constants.LOCATION, required = true)
308   @Parameter(name = Constants.DEVICE, required = true)
309   @Parameter(name = Constants.SYMBOLICNAME, required = true)
310   @Parameter(name = Constants.FIELD, required = true)
311   @Parameter(name = Constants.START, required = true)
312   @Parameter(name = Constants.END, required = true)
313   @Parameter(name = Constants.FUNCTION)
314   @Parameter(name = Constants.GROUP_BY)
315   @Parameter(name = Constants.FILLOPTION)
316   public Response getTimeseries(
317     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId,
318     @QueryParam(Constants.MEASUREMENT) @NotBlank String measurement,
319     @QueryParam(Constants.LOCATION) @NotBlank String location,
320     @QueryParam(Constants.DEVICE) @NotBlank String device,
321     @QueryParam(Constants.SYMBOLICNAME) @NotBlank String symbolicName,
322     @QueryParam(Constants.FIELD) @NotBlank String field,
323     @QueryParam(Constants.START) @NotNull @PositiveOrZero Long start,
324     @QueryParam(Constants.END) @NotNull @PositiveOrZero Long end,
325     @QueryParam(Constants.FUNCTION) AggregateFunction function,
326     @QueryParam(Constants.GROUP_BY) Long groupBy,
327     @QueryParam(Constants.FILLOPTION) FillOption fillOption
328   ) throws Exception {
329     var timeseries = new TimeseriesTuple(measurement, device, location, symbolicName, field);
330     TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
331       start,
332       end,
333       groupBy,
334       fillOption,
335       function
336     );
337     var timeseriesData = timeseriesService.getDataPointsByTimeseries(timeseriesContainerId, timeseries, queryParams);
338     TimeseriesWithDataPoints timeseriesWithData = new TimeseriesWithDataPoints(timeseries, timeseriesData);
339     return Response.ok(timeseriesWithData).build();
340   }
341 
342   @GET
343   @Produces({ MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON })
344   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.EXPORT)
345   @Tag(name = Constants.TIMESERIES_CONTAINER)
346   @Operation(description = "Export timeseries payload")
347   @APIResponse(
348     description = "ok",
349     responseCode = "200",
350     content = @Content(
351       mediaType = MediaType.APPLICATION_OCTET_STREAM,
352       schema = @Schema(type = SchemaType.STRING, format = "binary")
353     )
354   )
355   @APIResponse(responseCode = "400", description = "bad request")
356   @APIResponse(responseCode = "401", description = "not authorized")
357   @APIResponse(responseCode = "403", description = "forbidden")
358   @APIResponse(responseCode = "404", description = "not found")
359   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
360   @Parameter(name = Constants.MEASUREMENT, required = true)
361   @Parameter(name = Constants.LOCATION, required = true)
362   @Parameter(name = Constants.DEVICE, required = true)
363   @Parameter(name = Constants.SYMBOLICNAME, required = true)
364   @Parameter(name = Constants.FIELD, required = true)
365   @Parameter(name = Constants.START, required = true)
366   @Parameter(name = Constants.END, required = true)
367   @Parameter(name = Constants.FUNCTION)
368   @Parameter(name = Constants.GROUP_BY)
369   @Parameter(name = Constants.FILLOPTION)
370   @Parameter(name = Constants.CSVFORMAT)
371   public Response exportTimeseries(
372     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId,
373     @QueryParam(Constants.MEASUREMENT) @NotBlank String measurement,
374     @QueryParam(Constants.LOCATION) @NotBlank String location,
375     @QueryParam(Constants.DEVICE) @NotBlank String device,
376     @QueryParam(Constants.SYMBOLICNAME) @NotBlank String symbolicName,
377     @QueryParam(Constants.FIELD) @NotBlank String field,
378     @QueryParam(Constants.START) @NotNull @PositiveOrZero Long start,
379     @QueryParam(Constants.END) @NotNull @PositiveOrZero Long end,
380     @QueryParam(Constants.FUNCTION) AggregateFunction function,
381     @QueryParam(Constants.GROUP_BY) Long groupBy,
382     @QueryParam(Constants.FILLOPTION) FillOption fillOption,
383     @QueryParam(Constants.CSVFORMAT) @DefaultValue(value = "ROW") CsvFormat csvFormat
384   ) throws IOException {
385     var timeseries = new TimeseriesTuple(measurement, device, location, symbolicName, field);
386     TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
387       start,
388       end,
389       groupBy,
390       fillOption,
391       function
392     );
393     var inputStream = timeseriesCsvService.exportTimeseriesDataToCsv(
394       timeseriesContainerId,
395       timeseries,
396       queryParams,
397       csvFormat
398     );
399 
400     return Response.ok(inputStream, MediaType.APPLICATION_OCTET_STREAM)
401       .header("Content-Disposition", "attachment; filename=\"timeseries-export.csv\"")
402       .build();
403   }
404 
405   @POST
406   @Consumes(MediaType.MULTIPART_FORM_DATA)
407   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.IMPORT)
408   @Tag(name = Constants.TIMESERIES_CONTAINER)
409   @Operation(description = "Import timeseries payload")
410   @APIResponse(description = "ok", responseCode = "200")
411   @APIResponse(responseCode = "400", description = "bad request")
412   @APIResponse(responseCode = "401", description = "not authorized")
413   @APIResponse(responseCode = "403", description = "forbidden")
414   @APIResponse(responseCode = "404", description = "not found")
415   @Subscribable
416   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
417   public Response importTimeseries(
418     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId,
419     MultipartBodyFileUpload body
420   ) throws IOException {
421     String filePath = body.fileUpload != null ? body.fileUpload.uploadedFile().toString() : null;
422 
423     if (filePath == null) {
424       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
425     }
426 
427     timeseriesCsvService.importTimeseriesFromCsv(timeseriesContainerId, filePath);
428     return Response.ok().build();
429   }
430 
431   @GET
432   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PERMISSIONS)
433   @Tag(name = Constants.TIMESERIES_CONTAINER)
434   @Operation(description = "Get permissions")
435   @APIResponse(
436     description = "ok",
437     responseCode = "200",
438     content = @Content(schema = @Schema(implementation = PermissionsIO.class))
439   )
440   @APIResponse(responseCode = "400", description = "bad request")
441   @APIResponse(responseCode = "401", description = "not authorized")
442   @APIResponse(responseCode = "403", description = "forbidden")
443   @APIResponse(responseCode = "404", description = "not found")
444   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
445   public PermissionsIO getTimeseriesPermissions(
446     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId
447   ) {
448     var permissions = permissionsService.getPermissionsOfEntity(timeseriesContainerId);
449     return new PermissionsIO(permissions);
450   }
451 
452   @PUT
453   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PERMISSIONS)
454   @Tag(name = Constants.TIMESERIES_CONTAINER)
455   @Operation(description = "Edit permissions")
456   @APIResponse(
457     description = "ok",
458     responseCode = "200",
459     content = @Content(schema = @Schema(implementation = PermissionsIO.class))
460   )
461   @APIResponse(responseCode = "400", description = "bad request")
462   @APIResponse(responseCode = "401", description = "not authorized")
463   @APIResponse(responseCode = "403", description = "forbidden")
464   @APIResponse(responseCode = "404", description = "not found")
465   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
466   public PermissionsIO editTimeseriesPermissions(
467     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId,
468     @RequestBody(
469       content = @Content(schema = @Schema(implementation = PermissionsIO.class))
470     ) @Valid PermissionsIO permissions
471   ) {
472     var updatedPermissions = permissionsService.updatePermissionsByNeo4jId(permissions, timeseriesContainerId);
473     if (updatedPermissions == null) throw new NotFoundException();
474     return new PermissionsIO(updatedPermissions);
475   }
476 
477   @GET
478   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.ROLES)
479   @Tag(name = Constants.TIMESERIES_CONTAINER)
480   @Operation(description = "Get roles")
481   @APIResponse(
482     description = "ok",
483     responseCode = "200",
484     content = @Content(schema = @Schema(implementation = Roles.class))
485   )
486   @APIResponse(responseCode = "400", description = "bad request")
487   @APIResponse(responseCode = "401", description = "not authorized")
488   @APIResponse(responseCode = "403", description = "forbidden")
489   @APIResponse(responseCode = "404", description = "not found")
490   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
491   public Roles getTimeseriesRoles(
492     @PathParam(Constants.TIMESERIES_CONTAINER_ID) @NotNull @PositiveOrZero Long timeseriesContainerId
493   ) {
494     var roles = permissionsService.getUserRolesOnEntity(
495       timeseriesContainerId,
496       securityContext.getUserPrincipal().getName()
497     );
498     if (roles == null) throw new NotFoundException();
499     return roles;
500   }
501 
502   @Schema(type = SchemaType.STRING, format = "binary", description = "Timeseries as CSV")
503   public interface UploadItemSchema {}
504 
505   public static class UploadFormSchema {
506 
507     @Schema(required = true)
508     public UploadItemSchema file;
509   }
510 
511   @Schema(implementation = UploadFormSchema.class)
512   public static class MultipartBodyFileUpload {
513 
514     @RestForm(Constants.FILE)
515     public FileUpload fileUpload;
516   }
517 }