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