TimeseriesInContainerQueryBuilder.java

package de.dlr.shepard.common.search.query;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.dlr.shepard.common.exceptions.ShepardParserException;
import de.dlr.shepard.common.util.CypherDslHelper;
import de.dlr.shepard.common.util.CypherQueryHelper;
import de.dlr.shepard.common.util.Neo4jLabels;
import de.dlr.shepard.common.util.SearchConstants;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import org.neo4j.cypherdsl.core.Condition;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.NamedPath;
import org.neo4j.cypherdsl.core.Node;

public class TimeseriesInContainerQueryBuilder {

  private static final List<String> booleanOperators = List.of(
    SearchConstants.JSON_AND,
    SearchConstants.JSON_OR,
    SearchConstants.JSON_NOT,
    SearchConstants.JSON_XOR
  );
  private static final List<String> opAttributes = List.of(
    SearchConstants.OP_PROPERTY,
    SearchConstants.OP_VALUE,
    SearchConstants.OP_OPERATOR
  );

  private static final List<String> notIdProperties = List.of(
    SearchConstants.JSON_VALUE_IRI,
    SearchConstants.JSON_PROPERTY_IRI,
    SearchConstants.JSON_HAS_ANNOTATION,
    SearchConstants.JSON_HAS_ANNOTATION_IRI,
    SearchConstants.JSON_VALUE_TYPE
  );

  private static final List<String> quintupleProperties = List.of(
    SearchConstants.JSON_MEASUREMENT,
    SearchConstants.JSON_DEVICE,
    SearchConstants.JSON_LOCATION,
    SearchConstants.JSON_SYMBOLIC_NAME,
    SearchConstants.JSON_FIELD
  );

  private static final List<String> IdProperties = List.of(SearchConstants.JSON_ID, SearchConstants.JSON_REPOSITORY_ID);

  public static String searchTimeseriesInContainerQuery(long containerId, String jsonQuery, String username) {
    Node ts = Cypher.node(Neo4jLabels.TIMESERIES);
    Node tscon = Cypher.node(Neo4jLabels.TIMESERIES_CONTAINER);
    NamedPath path = Cypher.path("path").definedBy(ts.relationshipBetween(Cypher.anyNode()));
    var statement = Cypher.match(ts.relationshipTo(tscon, Neo4jLabels.IS_IN_CONTAINER))
      .where(
        CypherDslHelper.notDeleted(ts)
          .and(CypherDslHelper.isNotDeletedWithInternalId(tscon, containerId))
          .and(getAccessRightsCondition(tscon, username))
          .and(getCondition(jsonQuery, ts))
      )
      .withDistinct(ts)
      .match(path)
      .returning(
        ts.asExpression(),
        Cypher.nodes(Cypher.path("path").get()),
        Cypher.relationships(Cypher.path("path").get())
      );
    return statement.build().getCypher();
  }

  private static Condition getCondition(JsonNode jsonNode, Node cypherNode) {
    try {
      String op = jsonNode.fieldNames().next();
      if (opAttributes.contains(op)) {
        return getPrimitiveCondition(jsonNode, cypherNode);
      }
      return getComplexCondition(jsonNode, op, cypherNode);
    } catch (NoSuchElementException e) {
      throw new ShepardParserException(SearchConstants.PARSING_ERROR_MESSAGE + e.getMessage());
    }
  }

  private static Condition getCondition(String jsonQuery, Node cypherNode) {
    if (jsonQuery == null || jsonQuery.equals("")) return Cypher.isTrue();
    ObjectMapper objectMapper = new ObjectMapper();
    try {
      JsonNode jsonNode = objectMapper.readValue(jsonQuery, JsonNode.class);
      return getCondition(jsonNode, cypherNode);
    } catch (JsonProcessingException e) {
      throw new ShepardParserException(SearchConstants.NO_JSON_PARSING_MESSAGE + e.getMessage());
    }
  }

  private static Condition getPrimitiveCondition(JsonNode jsonNode, Node cypherNode) {
    String property = jsonNode.get(SearchConstants.OP_PROPERTY).textValue();
    if (notIdProperties.contains(property)) return getSimpleNotIdCondition(jsonNode, cypherNode);
    if (IdProperties.contains(property)) return getSimpleIdCondition(jsonNode, cypherNode);
    if (quintupleProperties.contains(property)) return getQuintupleCondition(jsonNode, cypherNode);
    throw new ShepardParserException(SearchConstants.UNKNOWN_PROPERTY_MESSAGE + property);
  }

  private static Condition getSimpleNotIdCondition(JsonNode jsonNode, Node cypherNode) {
    String property = jsonNode.get(SearchConstants.OP_PROPERTY).textValue();
    // for SemanticAnnotationIRIs
    if (
      property.equals(SearchConstants.JSON_VALUE_IRI) || property.equals(SearchConstants.JSON_PROPERTY_IRI)
    ) return getIRICondition(jsonNode, cypherNode);
    // for SemanticAnnotations
    if (property.equals(SearchConstants.JSON_HAS_ANNOTATION)) return getAnnotationCondition(jsonNode, cypherNode);
    // for SemanticAnnotations
    if (property.equals(SearchConstants.JSON_HAS_ANNOTATION_IRI)) return getAnnotationIRICondition(
      jsonNode,
      cypherNode
    );
    // for valueType
    if (property.equals(SearchConstants.JSON_VALUE_TYPE)) return hasStringProperty(
      cypherNode,
      property,
      jsonNode.get(SearchConstants.OP_VALUE).textValue(),
      jsonNode.get(SearchConstants.OP_OPERATOR).textValue(),
      false
    );
    throw new ShepardParserException(SearchConstants.UNKNOWN_PROPERTY_MESSAGE + property);
  }

  private static Condition getIRICondition(JsonNode jsonNode, Node cypherNode) {
    String iriType = jsonNode.get(SearchConstants.OP_PROPERTY).textValue();
    String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
    String value = jsonNode.get(SearchConstants.OP_VALUE).textValue();
    Node annotationNode = Cypher.node(Neo4jLabels.SEMANTIC_ANNOTATION);
    return Cypher.match(cypherNode.relationshipTo(annotationNode, Neo4jLabels.HAS_ANNOTATION))
      .where(hasStringProperty(annotationNode, iriType, value, operator))
      .asCondition();
  }

  private static Condition getQuintupleCondition(JsonNode jsonNode, Node cypherNode) {
    String quintupleProperty = jsonNode.get(SearchConstants.OP_PROPERTY).textValue();
    String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
    String value = jsonNode.get(SearchConstants.OP_VALUE).textValue();
    Node timeseriesTupleNode = Cypher.node(Neo4jLabels.TIMESERIES_TUPLE);
    return Cypher.match(cypherNode.relationshipTo(timeseriesTupleNode, Neo4jLabels.HAS_TIMESERIES_TUPLE))
      .where(hasStringProperty(timeseriesTupleNode, quintupleProperty, value, operator))
      .asCondition();
  }

  private static Condition getAnnotationCondition(JsonNode jsonNode, Node cypherNode) {
    String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
    String[] pair = CypherQueryHelper.splitByExactlyOneDoubleColon(jsonNode.get(SearchConstants.OP_VALUE).textValue());
    String propertyName = pair[0];
    String valueName = pair[1];
    Node annotationNode = Cypher.node(Neo4jLabels.SEMANTIC_ANNOTATION);
    return Cypher.match(cypherNode.relationshipTo(annotationNode, Neo4jLabels.HAS_ANNOTATION))
      .where(
        hasStringProperty(annotationNode, Neo4jLabels.PROPERTY_NAME, propertyName, operator).and(
          hasStringProperty(annotationNode, Neo4jLabels.VALUE_NAME, valueName, operator)
        )
      )
      .asCondition();
  }

  private static Condition getAnnotationIRICondition(JsonNode jsonNode, Node cypherNode) {
    String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
    String[] pair = CypherQueryHelper.splitByExactlyOneDoubleColon(jsonNode.get(SearchConstants.OP_VALUE).textValue());
    String propertyIRI = pair[0];
    String valueIRI = pair[1];
    Node annotationNode = Cypher.node(Neo4jLabels.SEMANTIC_ANNOTATION);
    return Cypher.match(cypherNode.relationshipTo(annotationNode, Neo4jLabels.HAS_ANNOTATION))
      .where(
        hasStringProperty(annotationNode, Neo4jLabels.PROPERTY_IRI, propertyIRI, operator).and(
          hasStringProperty(annotationNode, Neo4jLabels.VALUE_IRI, valueIRI, operator)
        )
      )
      .asCondition();
  }

  private static Condition getSimpleIdCondition(JsonNode jsonNode, Node cypherNode) {
    String property = jsonNode.get(SearchConstants.OP_PROPERTY).textValue();
    if (property.equals(SearchConstants.JSON_ID)) return getTimeseriesIdCondition(jsonNode, cypherNode);
    if (property.equals(SearchConstants.JSON_REPOSITORY_ID)) return getHasRepositoryIdCondition(jsonNode, cypherNode);
    throw new ShepardParserException(SearchConstants.UNKNOWN_PROPERTY_MESSAGE + property);
  }

  private static Condition getTimeseriesIdCondition(JsonNode jsonNode, Node cypherNode) {
    String property = Neo4jLabels.TIMESERIES_ID;
    String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
    long value = jsonNode.get(SearchConstants.OP_VALUE).longValue();
    return hasLongProperty(cypherNode, property, value, operator);
  }

  private static Condition getHasRepositoryIdCondition(JsonNode jsonNode, Node cypherNode) {
    String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
    long value = jsonNode.get(SearchConstants.OP_VALUE).longValue();
    Node annotation = Cypher.node(Neo4jLabels.SEMANTIC_ANNOTATION);
    Node repository = Cypher.node(Neo4jLabels.SEMANTIC_REPOSITORY);
    return Cypher.match(
      cypherNode
        .relationshipTo(annotation, Neo4jLabels.HAS_ANNOTATION)
        .relationshipTo(repository, Neo4jLabels.VALUE_REPOSITORY, Neo4jLabels.PROPERTY_REPOSITORY)
    )
      .where(CypherDslHelper.notDeleted(repository))
      .and(hasInternalIdProperty(repository, value, operator))
      .asCondition();
  }

  private static Condition getComplexCondition(JsonNode jsonNode, String op, Node cypherNode) {
    if (!booleanOperators.contains(op)) throw new ShepardParserException(
      SearchConstants.UNKNOWN_BOOLEAN_OPERATOR_MESSAGE + op
    );
    if (op.equals(SearchConstants.JSON_NOT)) return getNegatedCondition(jsonNode, cypherNode);
    else return getMultaryCondition(jsonNode, op, cypherNode);
  }

  private static Condition getNegatedCondition(JsonNode jsonNode, Node cypherNode) {
    return getCondition(jsonNode.get(SearchConstants.JSON_NOT), cypherNode).not();
  }

  private static Condition getMultaryCondition(JsonNode jsonNode, String op, Node cypherNode) {
    Iterator<JsonNode> argumentsArray = jsonNode.get(op).elements();
    Condition condition = getCondition(argumentsArray.next(), cypherNode);
    while (argumentsArray.hasNext()) {
      if (op.equals(SearchConstants.JSON_AND)) condition = condition.and(
        getCondition(argumentsArray.next(), cypherNode)
      );
      if (op.equals(SearchConstants.JSON_OR)) condition = condition.or(getCondition(argumentsArray.next(), cypherNode));
      if (op.equals(SearchConstants.JSON_XOR)) condition = condition.xor(
        getCondition(argumentsArray.next(), cypherNode)
      );
    }
    return condition;
  }

  private static Condition getAccessRightsCondition(Node entity, String username) {
    //no permission for legacy reasons
    Condition noPermission = Cypher.not(
      Cypher.exists(entity.relationshipTo(Cypher.anyNode(), Neo4jLabels.HAS_PERMISSIONS))
    );
    HashMap<String, String> usernameProperty = new HashMap<String, String>();
    //explicitly readable
    usernameProperty.put(Neo4jLabels.USERNAME, username);
    Condition readableBy = Cypher.exists(
      entity
        .relationshipTo(Cypher.anyNode(), Neo4jLabels.HAS_PERMISSIONS)
        .relationshipTo(
          Cypher.anyNode().withProperties(Neo4jLabels.USERNAME, Cypher.literalOf(username)),
          Neo4jLabels.READABLE_BY,
          Neo4jLabels.OWNED_BY
        )
    );
    //public permission
    Condition publicPermission = Cypher.exists(
      entity.relationshipTo(
        Cypher.anyNode().withProperties(Neo4jLabels.PERMISSION_TYPE, Cypher.literalOf(Neo4jLabels.PUBLIC)),
        Neo4jLabels.HAS_PERMISSIONS
      )
    );
    //public readable
    Condition publicReadable = Cypher.exists(
      entity.relationshipTo(
        Cypher.anyNode().withProperties(Neo4jLabels.PERMISSION_TYPE, Cypher.literalOf(Neo4jLabels.PUBLIC_READABLE)),
        Neo4jLabels.HAS_PERMISSIONS
      )
    );
    //redableByGroup
    Condition readableByGroup = Cypher.exists(
      entity
        .relationshipTo(Cypher.anyNode(), Neo4jLabels.HAS_PERMISSIONS)
        .relationshipTo(Cypher.anyNode(), Neo4jLabels.READABLE_BY_GROUP)
        .relationshipFrom(
          Cypher.anyNode().withProperties(Neo4jLabels.USERNAME, Cypher.literalOf(username)),
          Neo4jLabels.IS_IN_GROUP
        )
    );
    Condition accessRights = noPermission.or(readableBy).or(publicPermission).or(publicReadable).or(readableByGroup);
    return accessRights;
  }

  public static Condition hasStringProperty(Node node, String property, String value, String operator) {
    if (operator.equals(SearchConstants.JSON_EQ)) return node.property(property).eq(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_NE)) return node.property(property).ne(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_GE)) return node.property(property).gte(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_LE)) return node.property(property).lte(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_LT)) return node.property(property).lt(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_GT)) return node.property(property).gt(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_CONTAINS)) return node
      .property(property)
      .contains(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_REGMATCH)) return node.property(property).matches(Cypher.literalOf(value));
    throw new ShepardParserException(SearchConstants.UNKNOWN_COMPARISON_OPERATOR_MESSAGE + operator);
  }

  private static Condition hasLongProperty(Node node, String property, long value, String operator) {
    if (operator.equals(SearchConstants.JSON_EQ)) return node.property(property).eq(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_NE)) return node.property(property).ne(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_GE)) return node.property(property).gte(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_LE)) return node.property(property).lte(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_LT)) return node.property(property).lt(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_GT)) return node.property(property).gt(Cypher.literalOf(value));
    throw new ShepardParserException(SearchConstants.UNKNOWN_COMPARISON_OPERATOR_MESSAGE + operator);
  }

  private static Condition hasInternalIdProperty(Node node, long value, String operator) {
    if (operator.equals(SearchConstants.JSON_EQ)) return node.internalId().eq(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_NE)) return node.internalId().ne(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_GE)) return node.internalId().gte(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_LE)) return node.internalId().lte(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_LT)) return node.internalId().lt(Cypher.literalOf(value));
    if (operator.equals(SearchConstants.JSON_GT)) return node.internalId().gt(Cypher.literalOf(value));
    throw new ShepardParserException(SearchConstants.UNKNOWN_COMPARISON_OPERATOR_MESSAGE + operator);
  }

  public static Condition hasStringProperty(
    Node node,
    String property,
    String value,
    String operator,
    boolean caseSensitive
  ) {
    if (caseSensitive) return hasStringProperty(node, property, value, operator);
    if (operator.equals(SearchConstants.JSON_EQ)) return Cypher.toLower(node.property(property)).eq(
      Cypher.literalOf(value.toLowerCase())
    );
    if (operator.equals(SearchConstants.JSON_NE)) return Cypher.toLower(node.property(property)).ne(
      Cypher.literalOf(value.toLowerCase())
    );
    if (operator.equals(SearchConstants.JSON_GE)) return Cypher.toLower(node.property(property)).gte(
      Cypher.literalOf(value.toLowerCase())
    );
    if (operator.equals(SearchConstants.JSON_LE)) return Cypher.toLower(node.property(property)).lte(
      Cypher.literalOf(value.toLowerCase())
    );
    if (operator.equals(SearchConstants.JSON_LT)) return Cypher.toLower(node.property(property)).lt(
      Cypher.literalOf(value.toLowerCase())
    );
    if (operator.equals(SearchConstants.JSON_GT)) return Cypher.toLower(node.property(property)).gt(
      Cypher.literalOf(value.toLowerCase())
    );
    if (operator.equals(SearchConstants.JSON_CONTAINS)) return Cypher.toLower(node.property(property)).contains(
      Cypher.literalOf(value.toLowerCase())
    );
    if (operator.equals(SearchConstants.JSON_REGMATCH)) return Cypher.toLower(node.property(property)).matches(
      Cypher.literalOf(value.toLowerCase())
    );
    throw new ShepardParserException(SearchConstants.UNKNOWN_COMPARISON_OPERATOR_MESSAGE + operator);
  }
}