ExperimentalTimeseriesRest.java

  1. package de.dlr.shepard.timeseries.endpoints;

  2. import de.dlr.shepard.configuration.feature.toggles.ExperimentalTimeseriesFeatureToggle;
  3. import de.dlr.shepard.exceptions.InvalidBodyException;
  4. import de.dlr.shepard.exceptions.InvalidRequestException;
  5. import de.dlr.shepard.filters.Subscribable;
  6. import de.dlr.shepard.neo4Core.entities.TimeseriesContainer;
  7. import de.dlr.shepard.neo4Core.io.PermissionsIO;
  8. import de.dlr.shepard.neo4Core.io.RolesIO;
  9. import de.dlr.shepard.neo4Core.io.TimeseriesContainerIO;
  10. import de.dlr.shepard.neo4Core.orderBy.ContainerAttributes;
  11. import de.dlr.shepard.neo4Core.services.PermissionsService;
  12. import de.dlr.shepard.security.PermissionsUtil;
  13. import de.dlr.shepard.timeseries.io.ExperimentalTimeseriesWithDataPoints;
  14. import de.dlr.shepard.timeseries.io.TimeseriesContainerIOMapper;
  15. import de.dlr.shepard.timeseries.model.ExperimentalTimeseries;
  16. import de.dlr.shepard.timeseries.model.ExperimentalTimeseriesDataPointsQueryParams;
  17. import de.dlr.shepard.timeseries.model.ExperimentalTimeseriesEntity;
  18. import de.dlr.shepard.timeseries.model.enums.AggregateFunction;
  19. import de.dlr.shepard.timeseries.model.enums.FillOption;
  20. import de.dlr.shepard.timeseries.services.ExperimentalTimeseriesContainerService;
  21. import de.dlr.shepard.timeseries.services.ExperimentalTimeseriesCsvService;
  22. import de.dlr.shepard.timeseries.services.ExperimentalTimeseriesService;
  23. import de.dlr.shepard.util.Constants;
  24. import de.dlr.shepard.util.QueryParamHelper;
  25. import io.quarkus.arc.properties.IfBuildProperty;
  26. import jakarta.enterprise.context.RequestScoped;
  27. import jakarta.inject.Inject;
  28. import jakarta.transaction.Transactional;
  29. import jakarta.validation.Valid;
  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.util.Collections;
  48. import java.util.List;
  49. import java.util.Optional;
  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. @Consumes(MediaType.APPLICATION_JSON)
  61. @Produces(MediaType.APPLICATION_JSON)
  62. @Path(Constants.EXPERIMENTAL_TIMESERIES_CONTAINERS)
  63. @RequestScoped
  64. @IfBuildProperty(name = ExperimentalTimeseriesFeatureToggle.TOGGLE_PROPERTY, stringValue = "true")
  65. public class ExperimentalTimeseriesRest {

  66.   private ExperimentalTimeseriesService timeseriesService;
  67.   private ExperimentalTimeseriesCsvService timeseriesCsvService;
  68.   private ExperimentalTimeseriesContainerService timeseriesContainerService;
  69.   private PermissionsService permissionsService;
  70.   private PermissionsUtil permissionsUtil;

  71.   @Context
  72.   private SecurityContext securityContext;

  73.   ExperimentalTimeseriesRest() {}

  74.   @Inject
  75.   public ExperimentalTimeseriesRest(
  76.     ExperimentalTimeseriesService timeseriesService,
  77.     ExperimentalTimeseriesCsvService timeseriesCsvService,
  78.     ExperimentalTimeseriesContainerService timeseriesContainerService,
  79.     SecurityContext securityContext,
  80.     PermissionsService permissionsService,
  81.     PermissionsUtil permissionsUtil
  82.   ) {
  83.     this.timeseriesService = timeseriesService;
  84.     this.timeseriesCsvService = timeseriesCsvService;
  85.     this.timeseriesContainerService = timeseriesContainerService;
  86.     this.securityContext = securityContext;
  87.     this.permissionsService = permissionsService;
  88.     this.permissionsUtil = permissionsUtil;
  89.   }

  90.   @GET
  91.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  92.   @Operation(description = "Get all timeseries containers")
  93.   @APIResponse(
  94.     description = "ok",
  95.     responseCode = "200",
  96.     content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = TimeseriesContainerIO.class))
  97.   )
  98.   @APIResponse(description = "not found", responseCode = "404")
  99.   @Parameter(name = Constants.QP_NAME)
  100.   @Parameter(name = Constants.QP_PAGE)
  101.   @Parameter(name = Constants.QP_SIZE)
  102.   @Parameter(name = Constants.QP_ORDER_BY_ATTRIBUTE)
  103.   @Parameter(name = Constants.QP_ORDER_DESC)
  104.   public Response getAllExperimentalTimeseriesContainers(
  105.     @QueryParam(Constants.QP_NAME) String name,
  106.     @QueryParam(Constants.QP_PAGE) Integer page,
  107.     @QueryParam(Constants.QP_SIZE) Integer size,
  108.     @QueryParam(Constants.QP_ORDER_BY_ATTRIBUTE) ContainerAttributes orderBy,
  109.     @QueryParam(Constants.QP_ORDER_DESC) Boolean orderDesc
  110.   ) {
  111.     var params = new QueryParamHelper();
  112.     if (name != null) params = params.withName(name);
  113.     if (page != null && size != null) params = params.withPageAndSize(page, size);
  114.     if (orderBy != null) params = params.withOrderByAttribute(orderBy, orderDesc);
  115.     var containers = timeseriesContainerService.getContainers(params, securityContext.getUserPrincipal().getName());
  116.     var result = TimeseriesContainerIOMapper.map(containers);

  117.     return Response.ok(result).build();
  118.   }

  119.   @GET
  120.   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}")
  121.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  122.   @Operation(description = "Get timeseries container")
  123.   @APIResponse(
  124.     description = "ok",
  125.     responseCode = "200",
  126.     content = @Content(schema = @Schema(implementation = TimeseriesContainerIO.class))
  127.   )
  128.   @APIResponse(description = "not found", responseCode = "404")
  129.   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
  130.   public Response getExperimentalTimeseriesContainer(
  131.     @PathParam(Constants.TIMESERIES_CONTAINER_ID) long timeseriesContainerId
  132.   ) {
  133.     var container = timeseriesContainerService.getContainer(timeseriesContainerId);
  134.     return Response.ok(TimeseriesContainerIOMapper.map(container)).build();
  135.   }

  136.   @POST
  137.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  138.   @Operation(description = "Create a new timeseries container")
  139.   @APIResponse(
  140.     description = "created",
  141.     responseCode = "201",
  142.     content = @Content(schema = @Schema(implementation = TimeseriesContainerIO.class))
  143.   )
  144.   @APIResponse(description = "not found", responseCode = "404")
  145.   @Transactional
  146.   public Response createExperimentalTimeseriesContainer(
  147.     @RequestBody(
  148.       required = true,
  149.       content = @Content(schema = @Schema(implementation = TimeseriesContainerIO.class))
  150.     ) @Valid TimeseriesContainerIO timeseriesContainer
  151.   ) {
  152.     var container = timeseriesContainerService.createContainer(
  153.       timeseriesContainer.getName(),
  154.       securityContext.getUserPrincipal().getName()
  155.     );

  156.     return Response.ok(TimeseriesContainerIOMapper.map(container)).status(Status.CREATED).build();
  157.   }

  158.   @DELETE
  159.   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}")
  160.   @Subscribable
  161.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  162.   @Operation(description = "Delete timeseries container")
  163.   @APIResponse(description = "deleted", responseCode = "204")
  164.   @APIResponse(description = "not found", responseCode = "404")
  165.   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
  166.   @Transactional
  167.   public Response deleteExperimentalTimeseriesContainer(
  168.     @PathParam(Constants.TIMESERIES_CONTAINER_ID) long timeseriesContainerId
  169.   ) {
  170.     timeseriesContainerService.deleteContainer(timeseriesContainerId, securityContext.getUserPrincipal().getName());

  171.     return Response.status(Status.NO_CONTENT).build();
  172.   }

  173.   @POST
  174.   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PAYLOAD)
  175.   @Subscribable
  176.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  177.   @Operation(description = "Upload timeseries to container")
  178.   @APIResponse(
  179.     description = "created",
  180.     responseCode = "201",
  181.     content = @Content(schema = @Schema(implementation = ExperimentalTimeseries.class))
  182.   )
  183.   @APIResponse(description = "not found", responseCode = "404")
  184.   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
  185.   @Transactional
  186.   public Response createExperimentalTimeseries(
  187.     @PathParam(Constants.TIMESERIES_CONTAINER_ID) long containerId,
  188.     @RequestBody(
  189.       required = true,
  190.       content = @Content(schema = @Schema(implementation = ExperimentalTimeseriesWithDataPoints.class))
  191.     ) @Valid ExperimentalTimeseriesWithDataPoints payload
  192.   ) {
  193.     Optional<TimeseriesContainer> containerOptional = this.timeseriesContainerService.getContainerOptional(containerId);

  194.     if (containerOptional.isEmpty()) {
  195.       throw new InvalidBodyException("Timeseries container with id %s is null or deleted.", containerId);
  196.     }

  197.     ExperimentalTimeseriesEntity timeseriesEntity = timeseriesService.saveDataPoints(
  198.       containerOptional.get(),
  199.       payload.getTimeseries(),
  200.       payload.getPoints()
  201.     );

  202.     return Response.ok(new ExperimentalTimeseries(timeseriesEntity)).status(Status.CREATED).build();
  203.   }

  204.   @GET
  205.   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.AVAILABLE)
  206.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  207.   @Operation(description = "Get timeseries available")
  208.   @APIResponse(
  209.     description = "ok",
  210.     responseCode = "200",
  211.     content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = ExperimentalTimeseries.class))
  212.   )
  213.   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
  214.   public Response getExperimentalTimeseriesAvailable(
  215.     @PathParam(Constants.TIMESERIES_CONTAINER_ID) long timeseriesContainerId
  216.   ) {
  217.     Optional<TimeseriesContainer> containerOptional =
  218.       this.timeseriesContainerService.getContainerOptional(timeseriesContainerId);

  219.     if (containerOptional.isEmpty()) {
  220.       return Response.ok(Collections.emptyList()).build();
  221.     }

  222.     List<ExperimentalTimeseriesEntity> timeseriesEntityList = timeseriesService.getTimeseriesAvailable(
  223.       timeseriesContainerId
  224.     );

  225.     List<ExperimentalTimeseries> timeseriesListWithoutId = timeseriesEntityList
  226.       .stream()
  227.       .map(entity -> new ExperimentalTimeseries(entity))
  228.       .toList();

  229.     return Response.ok(timeseriesListWithoutId).build();
  230.   }

  231.   @GET
  232.   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PAYLOAD)
  233.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  234.   @Operation(description = "Get timeseries payload")
  235.   @APIResponse(
  236.     description = "ok",
  237.     responseCode = "200",
  238.     content = @Content(schema = @Schema(implementation = ExperimentalTimeseriesWithDataPoints.class))
  239.   )
  240.   @APIResponse(description = "not found", responseCode = "404")
  241.   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
  242.   @Parameter(name = Constants.MEASUREMENT, required = true)
  243.   @Parameter(name = Constants.LOCATION, required = true)
  244.   @Parameter(name = Constants.DEVICE, required = true)
  245.   @Parameter(name = Constants.SYMBOLICNAME, required = true)
  246.   @Parameter(name = Constants.FIELD, required = true)
  247.   @Parameter(name = Constants.START, required = true)
  248.   @Parameter(name = Constants.END, required = true)
  249.   @Parameter(name = Constants.FUNCTION)
  250.   @Parameter(name = Constants.GROUP_BY)
  251.   @Parameter(name = Constants.FILLOPTION)
  252.   public Response getExperimentalTimeseries(
  253.     @PathParam(Constants.TIMESERIES_CONTAINER_ID) long timeseriesContainerId,
  254.     @QueryParam(Constants.MEASUREMENT) String measurement,
  255.     @QueryParam(Constants.LOCATION) String location,
  256.     @QueryParam(Constants.DEVICE) String device,
  257.     @QueryParam(Constants.SYMBOLICNAME) String symbolicName,
  258.     @QueryParam(Constants.FIELD) String field,
  259.     @QueryParam(Constants.START) long start,
  260.     @QueryParam(Constants.END) long end,
  261.     @QueryParam(Constants.FUNCTION) AggregateFunction function,
  262.     @QueryParam(Constants.GROUP_BY) Long groupBy,
  263.     @QueryParam(Constants.FILLOPTION) FillOption fillOption
  264.   ) throws Exception {
  265.     if (measurement == null || location == null || device == null || symbolicName == null || field == null) {
  266.       throw new InvalidRequestException(
  267.         "Some query params are missing. Make sure that 'measurement', 'location', 'device', 'symbolicName' and 'field' are set."
  268.       );
  269.     }
  270.     var timeseries = new ExperimentalTimeseries(measurement, device, location, symbolicName, field);

  271.     ExperimentalTimeseriesDataPointsQueryParams queryParams = new ExperimentalTimeseriesDataPointsQueryParams(
  272.       start,
  273.       end,
  274.       groupBy,
  275.       fillOption,
  276.       function
  277.     );

  278.     var timeseriesData = timeseriesService.getDataPointsByTimeseries(timeseriesContainerId, timeseries, queryParams);

  279.     ExperimentalTimeseriesWithDataPoints timeseriesWithData = new ExperimentalTimeseriesWithDataPoints(
  280.       timeseries,
  281.       timeseriesData
  282.     );

  283.     return Response.ok(timeseriesWithData).build();
  284.   }

  285.   @GET
  286.   @Produces({ MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON })
  287.   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.EXPORT)
  288.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  289.   @Operation(description = "Export timeseries payload")
  290.   @APIResponse(
  291.     description = "ok",
  292.     responseCode = "200",
  293.     content = @Content(
  294.       mediaType = MediaType.APPLICATION_OCTET_STREAM,
  295.       schema = @Schema(type = SchemaType.STRING, format = "binary")
  296.     )
  297.   )
  298.   @APIResponse(description = "not found", responseCode = "404")
  299.   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
  300.   @Parameter(name = Constants.MEASUREMENT, required = true)
  301.   @Parameter(name = Constants.LOCATION, required = true)
  302.   @Parameter(name = Constants.DEVICE, required = true)
  303.   @Parameter(name = Constants.SYMBOLICNAME, required = true)
  304.   @Parameter(name = Constants.FIELD, required = true)
  305.   @Parameter(name = Constants.START, required = true)
  306.   @Parameter(name = Constants.END, required = true)
  307.   @Parameter(name = Constants.FUNCTION)
  308.   @Parameter(name = Constants.GROUP_BY)
  309.   @Parameter(name = Constants.FILLOPTION)
  310.   public Response exportExperimentalTimeseries(
  311.     @PathParam(Constants.TIMESERIES_CONTAINER_ID) long timeseriesContainerId,
  312.     @QueryParam(Constants.MEASUREMENT) String measurement,
  313.     @QueryParam(Constants.LOCATION) String location,
  314.     @QueryParam(Constants.DEVICE) String device,
  315.     @QueryParam(Constants.SYMBOLICNAME) String symbolicName,
  316.     @QueryParam(Constants.FIELD) String field,
  317.     @QueryParam(Constants.START) long start,
  318.     @QueryParam(Constants.END) long end,
  319.     @QueryParam(Constants.FUNCTION) AggregateFunction function,
  320.     @QueryParam(Constants.GROUP_BY) Long groupBy,
  321.     @QueryParam(Constants.FILLOPTION) FillOption fillOption
  322.   ) throws IOException {
  323.     if (measurement == null || location == null || device == null || symbolicName == null || field == null) {
  324.       throw new InvalidRequestException("Some query params are missing");
  325.     }

  326.     Optional<TimeseriesContainer> containerOptional =
  327.       this.timeseriesContainerService.getContainerOptional(timeseriesContainerId);

  328.     if (containerOptional.isEmpty()) {
  329.       throw new InvalidBodyException("Timeseries container with id %s is null or deleted.", timeseriesContainerId);
  330.     }

  331.     var timeseries = new ExperimentalTimeseries(measurement, device, location, symbolicName, field);
  332.     ExperimentalTimeseriesDataPointsQueryParams queryParams = new ExperimentalTimeseriesDataPointsQueryParams(
  333.       start,
  334.       end,
  335.       groupBy,
  336.       fillOption,
  337.       function
  338.     );
  339.     var inputStream = timeseriesCsvService.exportTimeseriesDataToCsv(
  340.       containerOptional.get().getId(),
  341.       timeseries,
  342.       queryParams
  343.     );

  344.     return Response.ok(inputStream, MediaType.APPLICATION_OCTET_STREAM)
  345.       .header("Content-Disposition", "attachment; filename=\"timeseries-export.csv\"")
  346.       .build();
  347.   }

  348.   @POST
  349.   @Consumes(MediaType.MULTIPART_FORM_DATA)
  350.   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.IMPORT)
  351.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  352.   @Operation(description = "Import timeseries payload")
  353.   @APIResponse(description = "ok", responseCode = "200")
  354.   @APIResponse(description = "not found", responseCode = "404")
  355.   @Subscribable
  356.   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
  357.   @Transactional
  358.   public Response importExperimentalTimeseries(
  359.     @PathParam(Constants.TIMESERIES_CONTAINER_ID) long timeseriesContainerId,
  360.     MultipartBodyFileUpload body
  361.   ) throws IOException {
  362.     String filePath = body.fileUpload != null ? body.fileUpload.uploadedFile().toString() : null;

  363.     if (filePath == null) {
  364.       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
  365.     }

  366.     Optional<TimeseriesContainer> containerOptional =
  367.       this.timeseriesContainerService.getContainerOptional(timeseriesContainerId);

  368.     if (containerOptional.isEmpty()) {
  369.       throw new InvalidBodyException("Timeseries container with id %s is null or deleted.", timeseriesContainerId);
  370.     }

  371.     timeseriesCsvService.importTimeseriesFromCsv(containerOptional.get(), filePath);
  372.     return Response.ok().build();
  373.   }

  374.   @GET
  375.   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PERMISSIONS)
  376.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  377.   @Operation(description = "Get permissions")
  378.   @APIResponse(
  379.     description = "ok",
  380.     responseCode = "200",
  381.     content = @Content(schema = @Schema(implementation = PermissionsIO.class))
  382.   )
  383.   @APIResponse(description = "not found", responseCode = "404")
  384.   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
  385.   public PermissionsIO getExperimentalTimeseriesPermissions(
  386.     @PathParam(Constants.TIMESERIES_CONTAINER_ID) long timeseriesContainerId
  387.   ) {
  388.     var permissions = permissionsService.getPermissionsByNeo4jId(timeseriesContainerId);
  389.     if (permissions == null) throw new NotFoundException();
  390.     return new PermissionsIO(permissions);
  391.   }

  392.   @PUT
  393.   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.PERMISSIONS)
  394.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  395.   @Operation(description = "Edit permissions")
  396.   @APIResponse(
  397.     description = "ok",
  398.     responseCode = "200",
  399.     content = @Content(schema = @Schema(implementation = PermissionsIO.class))
  400.   )
  401.   @APIResponse(description = "not found", responseCode = "404")
  402.   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
  403.   public PermissionsIO editExperimentalTimeseriesPermissions(
  404.     @PathParam(Constants.TIMESERIES_CONTAINER_ID) long timeseriesContainerId,
  405.     @RequestBody(
  406.       required = true,
  407.       content = @Content(schema = @Schema(implementation = PermissionsIO.class))
  408.     ) @Valid PermissionsIO permissions
  409.   ) {
  410.     var updatedPermissions = permissionsService.updatePermissionsByNeo4jId(permissions, timeseriesContainerId);
  411.     if (updatedPermissions == null) throw new NotFoundException();
  412.     return new PermissionsIO(updatedPermissions);
  413.   }

  414.   @GET
  415.   @Path("/{" + Constants.TIMESERIES_CONTAINER_ID + "}/" + Constants.ROLES)
  416.   @Tag(name = Constants.EXPERIMENTAL_TIMESERIES_CONTAINER)
  417.   @Operation(description = "Get roles")
  418.   @APIResponse(
  419.     description = "ok",
  420.     responseCode = "200",
  421.     content = @Content(schema = @Schema(implementation = RolesIO.class))
  422.   )
  423.   @APIResponse(description = "not found", responseCode = "404")
  424.   @Parameter(name = Constants.TIMESERIES_CONTAINER_ID)
  425.   public RolesIO getExperimentalTimeseriesRoles(
  426.     @PathParam(Constants.TIMESERIES_CONTAINER_ID) long timeseriesContainerId
  427.   ) {
  428.     var roles = permissionsUtil.getRolesByNeo4jId(timeseriesContainerId, securityContext.getUserPrincipal().getName());
  429.     if (roles == null) throw new NotFoundException();
  430.     return roles;
  431.   }

  432.   @Schema(type = SchemaType.STRING, format = "binary", description = "Timeseries as CSV")
  433.   public interface UploadItemSchema {}

  434.   public class UploadFormSchema {

  435.     @Schema(required = true)
  436.     public UploadItemSchema file;
  437.   }

  438.   @Schema(implementation = UploadFormSchema.class)
  439.   public static class MultipartBodyFileUpload {

  440.     @RestForm(Constants.FILE)
  441.     public FileUpload fileUpload;
  442.   }
  443. }