ExperimentalTimeseriesService.java

package de.dlr.shepard.timeseries.services;

import de.dlr.shepard.exceptions.InvalidBodyException;
import de.dlr.shepard.neo4Core.entities.TimeseriesContainer;
import de.dlr.shepard.timeseries.model.ExperimentalTimeseries;
import de.dlr.shepard.timeseries.model.ExperimentalTimeseriesDataPoint;
import de.dlr.shepard.timeseries.model.ExperimentalTimeseriesDataPointsQueryParams;
import de.dlr.shepard.timeseries.model.ExperimentalTimeseriesEntity;
import de.dlr.shepard.timeseries.model.enums.ExperimentalDataPointValueType;
import de.dlr.shepard.timeseries.repositories.ExperimentalTimeseriesDataPointRepository;
import de.dlr.shepard.timeseries.repositories.ExperimentalTimeseriesRepository;
import de.dlr.shepard.timeseries.utilities.ObjectTypeEvaluator;
import de.dlr.shepard.timeseries.utilities.TimeseriesValidator;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

@RequestScoped
public class ExperimentalTimeseriesService {

  private ExperimentalTimeseriesRepository timeseriesRepository;
  private ExperimentalTimeseriesDataPointRepository timeseriesDataPointRepository;

  ExperimentalTimeseriesService() {}

  @Inject
  public ExperimentalTimeseriesService(
    ExperimentalTimeseriesRepository timeseriesRepository,
    ExperimentalTimeseriesDataPointRepository timeseriesDataPointRepository
  ) {
    this.timeseriesRepository = timeseriesRepository;
    this.timeseriesDataPointRepository = timeseriesDataPointRepository;
  }

  /**
   * Returns a list of timeseries objects that are in the given database.
   *
   * @param containerId of the given timeseries container
   * @return a list of timeseries entities
   */
  public List<ExperimentalTimeseriesEntity> getTimeseriesAvailable(long containerId) {
    return timeseriesRepository.list("containerId", containerId);
  }

  public void deleteTimeseriesByContainerId(long containerId) {
    timeseriesRepository.delete("containerId", containerId);
  }

  /**
   * Retrieve a list of DataPoints for a time-interval with options to grouping/ time slicing, filling and aggregating.
   *
   * @return List of TimeseriesDataPoint
   */
  public List<ExperimentalTimeseriesDataPoint> getDataPointsByTimeseries(
    long containerId,
    ExperimentalTimeseries timeseries,
    ExperimentalTimeseriesDataPointsQueryParams queryParams
  ) {
    Optional<ExperimentalTimeseriesEntity> timeseriesEntity =
      this.timeseriesRepository.findTimeseries(containerId, timeseries);

    if (timeseriesEntity.isEmpty()) return Collections.emptyList();

    int timeseriesId = timeseriesEntity.get().getId();
    ExperimentalDataPointValueType valueType = timeseriesEntity.get().getValueType();

    return this.timeseriesDataPointRepository.queryDataPoints(timeseriesId, valueType, queryParams);
  }

  /**
   * Saves data points in the database.
   * If the corresponding timeseries did not exist before, it will be persisted in the database.
   *
   * @param timeseriesContainerId    Identifies the TimeseriesContainer
   * @param timeseries               The timeseries identifiers
   * @param dataPoints               Data points to be added to the timeseries
   * @return created timeseries
   */
  public ExperimentalTimeseriesEntity saveDataPoints(
    TimeseriesContainer timeseriesContainer,
    ExperimentalTimeseries timeseries,
    List<ExperimentalTimeseriesDataPoint> dataPoints
  ) {
    ExperimentalDataPointValueType incomingValueType = ObjectTypeEvaluator.determineType(
      dataPoints.get(0).getValue()
    ).orElseThrow(() -> new InvalidBodyException());

    ExperimentalTimeseriesEntity timeseriesEntity = getOrCreateTimeseries(
      timeseriesContainer.getId(),
      timeseries,
      incomingValueType
    );

    assertDataPointsMatchTimeseriesValueType(timeseriesEntity, dataPoints);

    timeseriesDataPointRepository.insertManyDataPoints(dataPoints, timeseriesEntity);

    return timeseriesEntity;
  }

  private ExperimentalTimeseriesEntity getOrCreateTimeseries(
    long containerId,
    ExperimentalTimeseries timeseries,
    ExperimentalDataPointValueType incomingValueType
  ) {
    // try to find timeseries in db
    Optional<ExperimentalTimeseriesEntity> matchingTimeseries = timeseriesRepository.findTimeseries(
      containerId,
      timeseries
    );

    if (matchingTimeseries.isPresent()) return matchingTimeseries.get();

    TimeseriesValidator.assertTimeseriesPropertiesAreValid(timeseries);

    // create new timeseries because it does not exist
    ExperimentalTimeseriesEntity timeseriesEntity = new ExperimentalTimeseriesEntity(
      containerId,
      timeseries,
      incomingValueType
    );
    this.timeseriesRepository.persist(timeseriesEntity);
    return timeseriesEntity;
  }

  private static void assertDataPointsMatchTimeseriesValueType(
    ExperimentalTimeseriesEntity timeseriesEntity,
    List<ExperimentalTimeseriesDataPoint> dataPoints
  ) {
    for (var dataPoint : dataPoints) {
      var expectedType = ObjectTypeEvaluator.determineType(dataPoint.getValue()).orElseThrow(() ->
        new InvalidBodyException()
      );
      assertValueTypeMatchesTimeseries(timeseriesEntity, expectedType);
    }
  }

  private static void assertValueTypeMatchesTimeseries(
    ExperimentalTimeseriesEntity timeseries,
    ExperimentalDataPointValueType incomingValueType
  ) {
    if (timeseries.getValueType() != incomingValueType) throw new InvalidBodyException(
      "Timeseries already exists for data type %s but new data points are of type %s",
      timeseries.getValueType(),
      incomingValueType
    );
  }
}