Neo4jQueryBuilder.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.neo4j.entities.ContainerType;
import de.dlr.shepard.common.search.io.SearchScope;
import de.dlr.shepard.common.util.Constants;
import de.dlr.shepard.common.util.CypherQueryHelper;
import de.dlr.shepard.common.util.Neo4jLabels;
import de.dlr.shepard.common.util.SearchConstants;
import de.dlr.shepard.common.util.SortingHelper;
import de.dlr.shepard.common.util.TraversalRules;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

public class Neo4jQueryBuilder {

  private static final List<String> BOOLEAN_OPERATORS = List.of(
    SearchConstants.JSON_AND,
    SearchConstants.JSON_OR,
    SearchConstants.JSON_NOT,
    SearchConstants.JSON_XOR
  );
  private static final List<String> OP_ATTRIBUTES = List.of(
    SearchConstants.OP_PROPERTY,
    SearchConstants.OP_VALUE,
    SearchConstants.OP_OPERATOR
  );
  private static final List<String> NOT_ID_PROPERTIES = List.of(
    SearchConstants.JSON_CREATED_BY,
    SearchConstants.JSON_UPDATED_BY,
    SearchConstants.JSON_VALUE_IRI,
    SearchConstants.JSON_PROPERTY_IRI,
    SearchConstants.JSON_CREATED_AT,
    SearchConstants.JSON_UPDATED_AT,
    SearchConstants.JSON_HAS_ANNOTATION,
    SearchConstants.JSON_HAS_ANNOTATION_IRI,
    SearchConstants.JSON_TIMESERIES_REFERENCE_START,
    SearchConstants.JSON_TIMESERIES_REFERENCE_END,
    SearchConstants.JSON_REFERENCED_TIMESERIES_PROPERTY_IRI,
    SearchConstants.JSON_REFERENCED_TIMESERIES_VALUE_IRI,
    SearchConstants.JSON_REFERENCED_TIMESERIES_HAS_ANNOTATION,
    SearchConstants.JSON_REFERENCED_TIMESERIES_HAS_ANNOTATION_IRI
  );

  private static final List<String> ID_PROPERTIES = List.of(
    SearchConstants.JSON_ID,
    SearchConstants.JSON_REFERENCED_COLLECTION_ID,
    SearchConstants.JSON_REFERENCED_DATA_OBJECT_ID,
    SearchConstants.JSON_REFERENCED_TIMESERIES_ID,
    SearchConstants.JSON_FILE_CONTAINER_ID,
    SearchConstants.JSON_STRUCTURED_DATA_CONTAINER_ID,
    SearchConstants.JSON_TIMESERIES_CONTAINER_ID,
    SearchConstants.JSON_SUCCESSOR_IDS,
    SearchConstants.JSON_PREDECESSOR_IDS,
    SearchConstants.JSON_CHILDREN_IDS,
    SearchConstants.JSON_PARENT_IDS
  );

  private static final List<String> QUINTUPLE_PROPERTIES = List.of(
    SearchConstants.JSON_REFERENCED_TIMESERIES_MEASUREMENT,
    SearchConstants.JSON_REFERENCED_TIMESERIES_DEVICE,
    SearchConstants.JSON_REFERENCED_TIMESERIES_LOCATION,
    SearchConstants.JSON_REFERENCED_TIMESERIES_SYMBOLIC_NAME,
    SearchConstants.JSON_REFERENCED_TIMESERIES_FIELD
  );

  private static String getNeo4jWithNeo4jIdString(String jsonQuery, String variable) {
    ObjectMapper objectMapper = new ObjectMapper();
    JsonNode jsonNode = null;
    try {
      jsonNode = objectMapper.readValue(jsonQuery, JsonNode.class);
    } catch (JsonProcessingException e) {
      throw new ShepardParserException("could not parse JSON " + e.getMessage());
    }
    return getNeo4jStringWithNeo4jId(jsonNode, variable);
  }

  private static String getNeo4jStringWithNeo4jId(JsonNode rootNode, String variable) {
    String op = "";
    try {
      op = rootNode.fieldNames().next();
    } catch (NoSuchElementException e) {
      throw new ShepardParserException("error in parsing" + e.getMessage());
    }
    if (OP_ATTRIBUTES.contains(op)) {
      return primitiveClauseWithNeo4jId(rootNode, variable);
    }
    return complexClauseWithNeo4jId(rootNode, op, variable);
  }

  private static String complexClauseWithNeo4jId(JsonNode node, String operator, String variable) {
    if (!BOOLEAN_OPERATORS.contains(operator)) throw new ShepardParserException(
      "unknown boolean operator: " + operator
    );
    if (operator.equals(SearchConstants.JSON_NOT)) return notClauseWithNeo4jId(node, variable);
    else return multaryClauseWithNeo4jId(node, operator, variable);
  }

  private static String multaryClauseWithNeo4jId(JsonNode node, String operator, String variable) {
    Iterator<JsonNode> argumentsArray = node.get(operator).elements();
    String firstArgument = getNeo4jStringWithNeo4jId(argumentsArray.next(), variable);
    String ret = "(" + firstArgument;
    while (argumentsArray.hasNext()) {
      ret = ret + " " + operator + " " + getNeo4jStringWithNeo4jId(argumentsArray.next(), variable);
    }
    ret = ret + ")";
    return ret;
  }

  private static String notClauseWithNeo4jId(JsonNode node, String variable) {
    JsonNode body = node.get(SearchConstants.JSON_NOT);
    return "(NOT(" + getNeo4jStringWithNeo4jId(body, variable) + "))";
  }

  private static String primitiveClauseWithNeo4jId(JsonNode node, String variable) {
    String property = node.get(SearchConstants.OP_PROPERTY).textValue();
    property = changeAttributesDelimiter(property);
    if (NOT_ID_PROPERTIES.contains(property)) return simpleNotIdPropertyPart(node, variable);
    if (ID_PROPERTIES.contains(property)) return simpleIdPropertyPart(node, variable);
    if (QUINTUPLE_PROPERTIES.contains(property)) return simpleQuintuplePropertyPart(node, variable);
    String ret = "(";
    ret = ret + "toLower(" + variable + ".`" + property + "`) ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + valuePart(node).toLowerCase();
    ret = ret + ")";
    return ret;
  }

  /**
   * This is a fix described in:
   * https://gitlab.com/dlr-shepard/shepard/-/issues/389
   * We use new delimiter characters for attributes ('||'), but want to support
   * the old search functionality using '.' as a delimiter.
   *
   * @param property
   * @return property string, if it contained 'attributes.', it is going to be
   * replaced by 'attributes||'
   */
  private static String changeAttributesDelimiter(String property) {
    if (property.startsWith("attributes.")) {
      return property.replaceFirst("attributes.", "attributes||");
    }
    return property;
  }

  private static String valuePart(JsonNode node) {
    String ret = "";
    if (node.get(SearchConstants.OP_OPERATOR).textValue().equals(SearchConstants.JSON_IN)) {
      ret = ret + "[";
      Iterator<JsonNode> setArray = node.get(SearchConstants.OP_VALUE).elements();
      if (setArray.hasNext()) {
        ret = ret + setArray.next();
        while (setArray.hasNext()) ret = ret + ", " + setArray.next();
        ret = ret + "]";
      } else {
        ret = ret + "]";
      }
    } else ret = ret + node.get(SearchConstants.OP_VALUE);
    return ret;
  }

  private static String simpleNotIdPropertyPart(JsonNode node, String variable) {
    String property = node.get(SearchConstants.OP_PROPERTY).textValue();
    // search for creating user
    if (
      property.equals(SearchConstants.JSON_CREATED_BY) || property.equals(SearchConstants.JSON_UPDATED_BY)
    ) return byPart(node, variable);
    // search for createdAt/updatedAt
    if (
      property.equals(SearchConstants.JSON_CREATED_AT) || property.equals(SearchConstants.JSON_UPDATED_AT)
    ) return atPart(node, variable);
    // search for SemanticAnnotationIRIs
    if (
      property.equals(SearchConstants.JSON_VALUE_IRI) || property.equals(SearchConstants.JSON_PROPERTY_IRI)
    ) return iRIPart(node, variable);
    // search for SemanticAnnotations
    if (property.equals(SearchConstants.JSON_HAS_ANNOTATION)) return hasAnnotationPart(node, variable);
    if (property.equals(SearchConstants.JSON_HAS_ANNOTATION_IRI)) return hasAnnotationIRIPart(node, variable);
    // search for start/end of TimeseriesReferences
    if (
      property.equals(SearchConstants.JSON_TIMESERIES_REFERENCE_START) ||
      property.equals(SearchConstants.JSON_TIMESERIES_REFERENCE_END)
    ) return timeFramePart(node, variable);
    // search for SemanticAnnotations of referenced Timeseries
    if (
      property.equals(SearchConstants.JSON_REFERENCED_TIMESERIES_VALUE_IRI) ||
      property.equals(SearchConstants.JSON_REFERENCED_TIMESERIES_PROPERTY_IRI)
    ) return referencedTimeseriesIRIPart(node, variable);
    if (
      property.equals(SearchConstants.JSON_REFERENCED_TIMESERIES_HAS_ANNOTATION) ||
      property.equals(SearchConstants.JSON_REFERENCED_TIMESERIES_HAS_ANNOTATION_IRI)
    ) return referencedTimeseriesAnnotationPart(node, variable);
    return null;
  }

  /**
   * method used for searching according to a timestamp
   * used for createdAt and updatedAt
   *
   * @param node the JsonNode containing a search triple with updatedAt or createdAt as property
   * @param variable the variable in the resulting neo4j query representing the noe4jNode under consideration
   * @return a condition in a neo4j query representing the criterion given in the search triple given by node applied to the neo4jNode represented by variable
   */
  private static String atPart(JsonNode node, String variable) {
    String ret = "(";
    String at =
      switch (node.get(SearchConstants.OP_PROPERTY).textValue()) {
        case SearchConstants.JSON_CREATED_AT -> Neo4jLabels.CREATED_AT;
        case SearchConstants.JSON_UPDATED_AT -> Neo4jLabels.UPDATED_AT;
        default -> "";
      };
    ret = ret + variable + "." + at + " ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + valuePart(node).toLowerCase();
    ret = ret + ")";
    return ret;
  }

  private static String timeFramePart(JsonNode node, String variable) {
    String ret = "(";
    String timeFrame =
      switch (node.get(SearchConstants.OP_PROPERTY).textValue()) {
        case SearchConstants.JSON_TIMESERIES_REFERENCE_START -> Neo4jLabels.TIMESERIES_REFERENCE_START;
        case SearchConstants.JSON_TIMESERIES_REFERENCE_END -> Neo4jLabels.TIMESERIES_REFERENCE_END;
        default -> "";
      };
    ret = ret + variable + "." + timeFrame + " ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + valuePart(node).toLowerCase();
    ret = ret + ")";
    return ret;
  }

  private static String hasAnnotationIRIPart(JsonNode node, String variable) {
    String[] pair = CypherQueryHelper.splitByExactlyOneDoubleColon(node.get(SearchConstants.OP_VALUE).textValue());
    String propertyIRI = pair[0];
    String valueIRI = pair[1];
    String ret = "(";
    ret =
      ret +
      "EXISTS {MATCH (" +
      variable +
      ") - [:" +
      Neo4jLabels.HAS_ANNOTATION +
      "] -> (sem:" +
      Neo4jLabels.SEMANTIC_ANNOTATION +
      ") WHERE (sem." +
      Neo4jLabels.PROPERTY_IRI +
      " ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " \"" + propertyIRI + "\" AND ";
    ret =
      ret +
      " sem." +
      Neo4jLabels.VALUE_IRI +
      " " +
      operatorString(node.get(SearchConstants.OP_OPERATOR)) +
      " \"" +
      valueIRI;
    ret = ret + "\")})";
    return ret;
  }

  private static String hasAnnotationPart(JsonNode node, String variable) {
    String[] pair = CypherQueryHelper.splitByExactlyOneDoubleColon(node.get(SearchConstants.OP_VALUE).textValue());
    String propertyName = pair[0];
    String valueName = pair[1];
    String ret = "(";
    ret =
      ret +
      "EXISTS {MATCH (" +
      variable +
      ") - [:" +
      Neo4jLabels.HAS_ANNOTATION +
      "] -> (sem:" +
      Neo4jLabels.SEMANTIC_ANNOTATION +
      ") WHERE (sem." +
      Neo4jLabels.PROPERTY_NAME +
      " ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " \"" + propertyName + "\" AND ";
    ret =
      ret +
      " sem." +
      Neo4jLabels.VALUE_NAME +
      " " +
      operatorString(node.get(SearchConstants.OP_OPERATOR)) +
      " \"" +
      valueName;
    ret = ret + "\")})";
    return ret;
  }

  private static String simpleIdPropertyPart(JsonNode node, String variable) {
    String property = node.get(SearchConstants.OP_PROPERTY).textValue();
    // for simple id
    if (property.equals(SearchConstants.JSON_ID)) return neo4jIdPart(node, variable);
    // for CollectionReferences
    if (property.equals(SearchConstants.JSON_REFERENCED_COLLECTION_ID)) return referencedCollectionNeo4jIdPart(
      node,
      variable
    );
    // for DataObjectReferences
    if (property.equals(SearchConstants.JSON_REFERENCED_DATA_OBJECT_ID)) return referencedDataObjectNeo4jIdPart(
      node,
      variable
    );
    // for FileReferences
    if (property.equals(SearchConstants.JSON_FILE_CONTAINER_ID)) return fileContainerIdPart(node, variable);
    // for StructuredDataReferences
    if (property.equals(SearchConstants.JSON_STRUCTURED_DATA_CONTAINER_ID)) return structuredDataContainerIdPart(
      node,
      variable
    );
    // for TimeseriesReferences
    if (property.equals(SearchConstants.JSON_TIMESERIES_CONTAINER_ID)) return timeseriesContainerIdPart(node, variable);
    if (property.equals(SearchConstants.JSON_REFERENCED_TIMESERIES_ID)) return referencedTimeseriesIdPart(
      node,
      variable
    );
    // for neighborhoodIds
    if (
      property.equals(SearchConstants.JSON_SUCCESSOR_IDS) ||
      property.equals(SearchConstants.JSON_PREDECESSOR_IDS) ||
      property.equals(SearchConstants.JSON_CHILDREN_IDS) ||
      property.equals(SearchConstants.JSON_PARENT_IDS)
    ) return neighborhoodIdsPart(node, variable);
    return null;
  }

  private static String byPart(JsonNode node, String variable) {
    String ret = "(";
    String by =
      switch (node.get(SearchConstants.OP_PROPERTY).textValue()) {
        case SearchConstants.JSON_CREATED_BY -> Neo4jLabels.CREATED_BY;
        case SearchConstants.JSON_UPDATED_BY -> Neo4jLabels.UPDATED_BY;
        default -> "";
      };
    ret =
      ret + "EXISTS {MATCH (" + variable + ") - [:" + by + "] -> (u) WHERE toLower(u." + Neo4jLabels.USERNAME + ") ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + node.get(SearchConstants.OP_VALUE).toString().toLowerCase() + " ";
    ret = ret + "})";
    return ret;
  }

  private static String simpleQuintuplePropertyPart(JsonNode node, String variable) {
    String ret = "(";
    String quintupleProperty =
      switch (node.get(SearchConstants.OP_PROPERTY).textValue()) {
        case SearchConstants.JSON_REFERENCED_TIMESERIES_MEASUREMENT -> Neo4jLabels.MEASUREMENT;
        case SearchConstants.JSON_REFERENCED_TIMESERIES_FIELD -> Neo4jLabels.FIELD;
        case SearchConstants.JSON_REFERENCED_TIMESERIES_DEVICE -> Neo4jLabels.DEVICE;
        case SearchConstants.JSON_REFERENCED_TIMESERIES_LOCATION -> Neo4jLabels.LOCATION;
        case SearchConstants.JSON_REFERENCED_TIMESERIES_SYMBOLIC_NAME -> Neo4jLabels.SYMBOLIC_NAME;
        default -> "";
      };
    ret =
      ret +
      "EXISTS {MATCH (" +
      variable +
      ") - [:" +
      Neo4jLabels.HAS_PAYLOAD +
      "] -> (tt:" +
      Neo4jLabels.TIMESERIES_TUPLE +
      ") WHERE tt." +
      quintupleProperty +
      " ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + node.get(SearchConstants.OP_VALUE).toString().toLowerCase() + " ";
    ret = ret + "})";
    return ret;
  }

  private static String neighborhoodIdsPart(JsonNode node, String variable) {
    String operatorString = node.get(SearchConstants.OP_OPERATOR).textValue();
    String neighborhoodProperty = node.get(SearchConstants.OP_PROPERTY).textValue();
    String neighborhoodNeo4j =
      switch (neighborhoodProperty) {
        case SearchConstants.JSON_SUCCESSOR_IDS -> "-[:" + Neo4jLabels.HAS_SUCCESSOR + "]->";
        case SearchConstants.JSON_PREDECESSOR_IDS -> "<-[:" + Neo4jLabels.HAS_SUCCESSOR + "]-";
        case SearchConstants.JSON_CHILDREN_IDS -> "-[:" + Neo4jLabels.HAS_CHILD + "]->";
        case SearchConstants.JSON_PARENT_IDS -> "<-[:" + Neo4jLabels.HAS_CHILD + "]-";
        default -> "";
      };
    if (operatorString.equals(SearchConstants.JSON_CONTAINS)) return neighborhoodIdsContainsPart(
      node,
      variable,
      neighborhoodNeo4j
    );
    if (operatorString.equals(SearchConstants.JSON_IS_CONTAINED_IN)) return neighborhoodIdsIsContainedInPart(
      node,
      variable,
      neighborhoodNeo4j
    );
    if (operatorString.equals(SearchConstants.JSON_EQ)) return neighborhoodIdsEqualsPart(
      node,
      variable,
      neighborhoodNeo4j
    );
    throw new ShepardParserException("illegal comparison operator " + operatorString);
  }

  private static String neighborhoodIdsEqualsPart(JsonNode node, String variable, String neighborhoodNeo4j) {
    return (
      "((" +
      neighborhoodIdsIsContainedInPart(node, variable, neighborhoodNeo4j) +
      ") AND (" +
      neighborhoodIdsContainsPart(node, variable, neighborhoodNeo4j) +
      "))"
    );
  }

  private static String neighborhoodIdsIsContainedInPart(JsonNode node, String variable, String neighborhoodNeo4j) {
    String ret = "(";
    ArrayList<Long> neighborIds = new ArrayList<Long>();
    for (JsonNode jsonNode : node.get(SearchConstants.OP_VALUE)) {
      neighborIds.add(jsonNode.longValue());
    }
    if (neighborIds.size() == 0) return "(NOT EXISTS{MATCH (" + variable + ")" + neighborhoodNeo4j + "(neighborObj)})";
    String arrayString = "[";
    for (int i = 0; i < neighborIds.size() - 1; i++) arrayString = arrayString + neighborIds.get(i) + ",";
    arrayString = arrayString + neighborIds.getLast() + "]";
    ret =
      ret +
      "NOT EXISTS{MATCH (" +
      variable +
      ")" +
      neighborhoodNeo4j +
      "(neighborObj) WHERE (NOT id(neighborObj) IN " +
      arrayString +
      ")})";
    return ret;
  }

  private static String neighborhoodIdsContainsPart(JsonNode node, String variable, String neighborhoodNeo4j) {
    String ret = "(";
    ArrayList<Long> neighborIds = new ArrayList<Long>();
    for (JsonNode jsonNode : node.get(SearchConstants.OP_VALUE)) {
      neighborIds.add(jsonNode.longValue());
    }
    if (neighborIds.size() == 0) return "(1=1)";
    for (int i = 0; i < neighborIds.size() - 1; i++) {
      ret =
        ret +
        "(EXISTS {MATCH (" +
        variable +
        ")" +
        neighborhoodNeo4j +
        "(neighborObj) WHERE id(neighborObj)=" +
        neighborIds.get(i) +
        "}) AND ";
    }
    ret =
      ret +
      "(EXISTS {MATCH (" +
      variable +
      ")" +
      neighborhoodNeo4j +
      "(neighborObj) WHERE id(neighborObj)=" +
      neighborIds.getLast() +
      "}))";
    return ret;
  }

  private static String iRIPart(JsonNode node, String variable) {
    String ret = "(";
    String iriType = node.get(SearchConstants.OP_PROPERTY).textValue();
    ret =
      ret +
      "EXISTS {MATCH (" +
      variable +
      ") - [] -> (sem:" +
      Neo4jLabels.SEMANTIC_ANNOTATION +
      ") WHERE (sem." +
      iriType +
      " ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + node.get(SearchConstants.OP_VALUE);
    ret = ret + ")})";
    return ret;
  }

  private static String referencedTimeseriesIRIPart(JsonNode node, String variable) {
    String ret = "(";
    String iriType =
      switch (node.get(SearchConstants.OP_PROPERTY).textValue()) {
        case SearchConstants.JSON_REFERENCED_TIMESERIES_VALUE_IRI -> Neo4jLabels.VALUE_IRI;
        case SearchConstants.JSON_REFERENCED_TIMESERIES_PROPERTY_IRI -> Neo4jLabels.PROPERTY_IRI;
        default -> "";
      };
    ret =
      ret +
      "EXISTS {MATCH (" +
      variable +
      ") - [:" +
      Neo4jLabels.HAS_PAYLOAD +
      "] -> (tt:" +
      Neo4jLabels.TIMESERIES_TUPLE +
      ") <- [:" +
      Neo4jLabels.HAS_TIMESERIES_TUPLE +
      "] - (ts:" +
      Neo4jLabels.TIMESERIES +
      ") - [:" +
      Neo4jLabels.HAS_ANNOTATION +
      "] -> (sem:" +
      Neo4jLabels.SEMANTIC_ANNOTATION +
      "), (" +
      variable +
      ") - [:" +
      Neo4jLabels.IS_IN_CONTAINER +
      "] -> (tsc:" +
      Neo4jLabels.TIMESERIES_CONTAINER +
      ") <- [:" +
      Neo4jLabels.IS_IN_CONTAINER +
      "] - (ts:" +
      Neo4jLabels.TIMESERIES +
      ") WHERE sem." +
      iriType +
      " ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + node.get(SearchConstants.OP_VALUE);
    ret = ret + "})";
    return ret;
  }

  private static String referencedTimeseriesIdPart(JsonNode node, String variable) {
    String ret = "(";
    ret =
      ret +
      "EXISTS {MATCH (" +
      variable +
      ")-[:" +
      Neo4jLabels.HAS_PAYLOAD +
      "]->(tt:" +
      Neo4jLabels.TIMESERIES_TUPLE +
      ")<-[:" +
      Neo4jLabels.HAS_TIMESERIES_TUPLE +
      "]-(ts:" +
      Neo4jLabels.TIMESERIES +
      "),  (" +
      variable +
      ") - [:" +
      Neo4jLabels.IS_IN_CONTAINER +
      "] -> (tsc:" +
      Neo4jLabels.TIMESERIES_CONTAINER +
      ") <- [:" +
      Neo4jLabels.IS_IN_CONTAINER +
      "] - (ts:" +
      Neo4jLabels.TIMESERIES +
      ") WHERE ts." +
      Neo4jLabels.TIMESERIES_ID +
      " ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + node.get(SearchConstants.OP_VALUE) + " ";
    ret = ret + "})";
    return ret;
  }

  private static String referencedTimeseriesAnnotationPart(JsonNode node, String variable) {
    String propertyType =
      switch (node.get(SearchConstants.OP_PROPERTY).textValue()) {
        case SearchConstants.JSON_REFERENCED_TIMESERIES_HAS_ANNOTATION -> Neo4jLabels.PROPERTY_NAME;
        case SearchConstants.JSON_REFERENCED_TIMESERIES_HAS_ANNOTATION_IRI -> Neo4jLabels.PROPERTY_IRI;
        default -> "";
      };
    String valueType =
      switch (node.get(SearchConstants.OP_PROPERTY).textValue()) {
        case SearchConstants.JSON_REFERENCED_TIMESERIES_HAS_ANNOTATION -> Neo4jLabels.VALUE_NAME;
        case SearchConstants.JSON_REFERENCED_TIMESERIES_HAS_ANNOTATION_IRI -> Neo4jLabels.VALUE_IRI;
        default -> "";
      };
    String[] pair = CypherQueryHelper.splitByExactlyOneDoubleColon(node.get(SearchConstants.OP_VALUE).textValue());
    String property = pair[0];
    String value = pair[1];
    String ret = "(";
    ret =
      ret +
      "EXISTS {MATCH (" +
      variable +
      ") - [:" +
      Neo4jLabels.HAS_PAYLOAD +
      "] -> (tt:" +
      Neo4jLabels.TIMESERIES_TUPLE +
      ") <- [:" +
      Neo4jLabels.HAS_TIMESERIES_TUPLE +
      "] - (ts:" +
      Neo4jLabels.TIMESERIES +
      ") - [:" +
      Neo4jLabels.HAS_ANNOTATION +
      "] -> (sem:" +
      Neo4jLabels.SEMANTIC_ANNOTATION +
      "), (" +
      variable +
      ") - [:is_in_container] -> (tsc:TimeseriesContainer) <- [:is_in_container] - (ts:Timeseries) WHERE sem." +
      propertyType +
      " ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + "\"" + property + "\"" + " AND sem." + valueType + " ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + "\"" + value + "\"";
    ret = ret + "})";
    return ret;
  }

  private static String timeseriesContainerIdPart(JsonNode node, String variable) {
    return containerIdPart(node, variable, Neo4jLabels.TIMESERIES_CONTAINER);
  }

  private static String structuredDataContainerIdPart(JsonNode node, String variable) {
    return containerIdPart(node, variable, Neo4jLabels.STRUCTURED_DATA_CONTAINER);
  }

  private static String fileContainerIdPart(JsonNode node, String variable) {
    return containerIdPart(node, variable, Neo4jLabels.FILE_CONTAINER);
  }

  private static String containerIdPart(JsonNode node, String variable, String containerType) {
    String ret = "(";
    ret =
      ret +
      "EXISTS {MATCH (" +
      variable +
      ")-[:" +
      Neo4jLabels.IS_IN_CONTAINER +
      "]->(refCon:" +
      containerType +
      ") WHERE id(refCon) ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + node.get(SearchConstants.OP_VALUE) + " ";
    ret = ret + "})";
    return ret;
  }

  private static String referencedDataObjectNeo4jIdPart(JsonNode node, String variable) {
    String ret = "(";
    ret =
      ret +
      "EXISTS {MATCH (" +
      variable +
      ")-[:" +
      Neo4jLabels.POINTS_TO +
      "]->(refDo:" +
      Neo4jLabels.DATA_OBJECT +
      ") WHERE id(refDo) ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + node.get(SearchConstants.OP_VALUE) + " ";
    ret = ret + "})";
    return ret;
  }

  private static String referencedCollectionNeo4jIdPart(JsonNode node, String variable) {
    String ret = "(";
    ret =
      ret +
      "EXISTS {MATCH (" +
      variable +
      ")-[:" +
      Neo4jLabels.POINTS_TO +
      "]->(refCol:" +
      Neo4jLabels.COLLECTION +
      ") WHERE id(refCol) ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + node.get(SearchConstants.OP_VALUE) + " ";
    ret = ret + "})";
    return ret;
  }

  private static String neo4jIdPart(JsonNode node, String variable) {
    String ret = "(";
    ret = ret + "id(" + variable + ") ";
    ret = ret + operatorString(node.get(SearchConstants.OP_OPERATOR)) + " ";
    ret = ret + valuePart(node);
    ret = ret + ")";
    return ret;
  }

  private static String operatorString(JsonNode node) {
    String operator = node.textValue();
    return switch (operator) {
      case SearchConstants.JSON_EQ -> "=";
      case SearchConstants.JSON_CONTAINS -> "contains";
      case SearchConstants.JSON_GT -> ">";
      case SearchConstants.JSON_LT -> "<";
      case SearchConstants.JSON_GE -> ">=";
      case SearchConstants.JSON_LE -> "<=";
      case SearchConstants.JSON_IN -> "IN";
      case SearchConstants.JSON_NE -> "<>";
      case SearchConstants.JSON_REGMATCH -> "=~";
      default -> throw new ShepardParserException("unknown comparison operator " + operator);
    };
  }

  private static String collectionDataObjectMatchPartWithoutVersion() {
    String ret =
      "MATCH (" +
      SearchConstants.COLLECTION_IN_QUERY +
      ":" +
      Neo4jLabels.COLLECTION +
      ")-[:has_dataobject]->(" +
      SearchConstants.DATAOBJECT_IN_QUERY +
      ":" +
      Neo4jLabels.DATA_OBJECT +
      ")";
    return ret;
  }

  private static String collectionNeo4jIdWherePart(Long collectionId) {
    String ret = "(id(" + SearchConstants.COLLECTION_IN_QUERY + ") = " + collectionId + ")";
    return ret;
  }

  private static String notDeletedPart(String variable) {
    String ret = "(" + variable + ".deleted = FALSE)";
    return ret;
  }

  private static String collectionDataObjectNeo4jIdWherePart(Long collectionId, Long dataObjectId) {
    String ret =
      "(id(" +
      SearchConstants.COLLECTION_IN_QUERY +
      ") = " +
      collectionId +
      " AND id(" +
      SearchConstants.DATAOBJECT_IN_QUERY +
      ") = " +
      dataObjectId +
      ")";
    return ret;
  }

  private static String collectionDataObjectTraversalNeo4jIdWherePart(Long collectionId, Long dataObjectId) {
    String ret =
      "(id(" + SearchConstants.COLLECTION_IN_QUERY + ") = " + collectionId + " AND id(d) = " + dataObjectId + ")";
    return ret;
  }

  private static String basicReferenceMatchPartWithoutVersion() {
    String ret =
      "MATCH (" +
      SearchConstants.COLLECTION_IN_QUERY +
      ":" +
      Neo4jLabels.COLLECTION +
      ")-[:" +
      Neo4jLabels.HAS_DATAOBJECT +
      "]->(" +
      SearchConstants.DATAOBJECT_IN_QUERY +
      ":" +
      Neo4jLabels.DATA_OBJECT +
      ")-[:" +
      Neo4jLabels.HAS_REFERENCE +
      "]->(" +
      SearchConstants.REFERENCE_IN_QUERY +
      ":" +
      Neo4jLabels.BASIC_REFERENCE +
      ")";
    return ret;
  }

  public static String collectionSelectionQueryWithNeo4jId(
    String searchBodyQuery,
    String userName,
    SortingHelper sortOrder
  ) {
    String ret = "MATCH (" + SearchConstants.COLLECTION_IN_QUERY + ":" + Neo4jLabels.COLLECTION + ")";
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(searchBodyQuery, SearchConstants.COLLECTION_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + notDeletedPart(SearchConstants.COLLECTION_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + readableByPart(userName);
    if (sortOrder.hasOrderByAttribute()) {
      ret +=
        " " +
        CypherQueryHelper.getOrderByPart(
          SearchConstants.COLLECTION_IN_QUERY,
          sortOrder.getOrderByAttribute(),
          sortOrder.getOrderDesc()
        );
    }
    return ret;
  }

  public static String containerSelectionQueryWithNeo4jId(
    String JSONQuery,
    ContainerType containerType,
    SortingHelper sortOrder,
    String userName
  ) {
    String ret = "MATCH (" + containerType.getTypeAlias() + ":" + containerType.getTypeName() + ")";
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(JSONQuery, containerType.getTypeAlias());
    ret = ret + " AND ";
    ret = ret + notDeletedPart(containerType.getTypeAlias());
    ret = ret + " AND ";
    ret = ret + CypherQueryHelper.getReadableByQuery(containerType.getTypeAlias(), userName);
    if (sortOrder.hasOrderByAttribute()) {
      ret +=
        " " +
        CypherQueryHelper.getOrderByPart(
          containerType.getTypeAlias(),
          sortOrder.getOrderByAttribute(),
          sortOrder.getOrderDesc()
        );
    }
    return ret;
  }

  public static String collectionDataObjectSelectionQueryWithNeo4jId(
    Long collectionId,
    String searchBodyQuery,
    String username
  ) {
    String ret = "";
    ret = ret + collectionDataObjectMatchPartWithoutVersion();
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(searchBodyQuery, SearchConstants.DATAOBJECT_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + collectionNeo4jIdWherePart(collectionId);
    ret = ret + " AND ";
    ret = ret + notDeletedPart(SearchConstants.DATAOBJECT_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + readableByPart(username);
    return ret;
  }

  public static String collectionDataObjectDataObjectSelectionQueryWithNeo4jId(
    SearchScope scope,
    TraversalRules traversalRule,
    String searchBodyQuery,
    String username
  ) {
    String ret = "";
    ret = ret + collectionDataObjectDataObjectMatchPartWithoutVersion(traversalRule);
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(searchBodyQuery, SearchConstants.DATAOBJECT_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + collectionDataObjectTraversalNeo4jIdWherePart(scope.getCollectionId(), scope.getDataObjectId());
    ret = ret + " AND ";
    ret = ret + notDeletedPart(SearchConstants.DATAOBJECT_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + readableByPart(username);
    return ret;
  }

  public static String collectionDataObjectDataObjectSelectionQueryWithNeo4jId(
    SearchScope scope,
    String searchBodyQuery,
    String username
  ) {
    String ret = "";
    ret = ret + collectionDataObjectMatchPartWithoutVersion();
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(searchBodyQuery, SearchConstants.DATAOBJECT_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + collectionDataObjectNeo4jIdWherePart(scope.getCollectionId(), scope.getDataObjectId());
    ret = ret + " AND ";
    ret = ret + notDeletedPart(SearchConstants.DATAOBJECT_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + readableByPart(username);
    return ret;
  }

  public static String dataObjectSelectionQueryWithNeo4jId(String searchBodyQuery, String username) {
    String ret = "";
    ret = ret + collectionDataObjectMatchPartWithoutVersion();
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(searchBodyQuery, SearchConstants.DATAOBJECT_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + notDeletedPart(SearchConstants.DATAOBJECT_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + readableByPart(username);
    return ret;
  }

  public static String basicReferenceSelectionQueryWithNeo4jId(String searchBodyQuery, String username) {
    String ret = "";
    ret = ret + basicReferenceMatchPartWithoutVersion();
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(searchBodyQuery, SearchConstants.REFERENCE_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + notDeletedPart(SearchConstants.REFERENCE_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + readableByPart(username);
    return ret;
  }

  public static String collectionBasicReferenceSelectionQueryWithNeo4jId(
    String searchBodyQuery,
    Long collectionId,
    String username
  ) {
    String ret = "";
    ret = ret + basicReferenceMatchPartWithoutVersion();
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(searchBodyQuery, SearchConstants.REFERENCE_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + collectionNeo4jIdWherePart(collectionId);
    ret = ret + " AND ";
    ret = ret + notDeletedPart(SearchConstants.REFERENCE_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + readableByPart(username);
    return ret;
  }

  public static String collectionDataObjectReferenceSelectionQueryWithNeo4jId(
    SearchScope scope,
    String searchBodyQuery,
    String username
  ) {
    String ret = "";
    ret = ret + basicReferenceMatchPartWithoutVersion();
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(searchBodyQuery, SearchConstants.REFERENCE_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + collectionDataObjectNeo4jIdWherePart(scope.getCollectionId(), scope.getDataObjectId());
    ret = ret + " AND ";
    ret = ret + notDeletedPart(SearchConstants.REFERENCE_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + readableByPart(username);
    return ret;
  }

  public static String collectionDataObjectBasicReferenceSelectionQueryWithNeo4jId(
    SearchScope scope,
    TraversalRules traversalRule,
    String searchBodyQuery,
    String username
  ) {
    String ret = "";
    ret = ret + collectionDataObjectBasicReferenceMatchPartWithoutVersion(traversalRule);
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(searchBodyQuery, SearchConstants.REFERENCE_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + collectionDataObjectTraversalNeo4jIdWherePart(scope.getCollectionId(), scope.getDataObjectId());
    ret = ret + " AND ";
    ret = ret + notDeletedPart(SearchConstants.REFERENCE_IN_QUERY);
    ret = ret + " AND ";
    ret = ret + readableByPart(username);
    return ret;
  }

  private static String collectionDataObjectDataObjectMatchPartWithoutVersion(TraversalRules traversalRule) {
    String ret =
      switch (traversalRule) {
        case children -> "MATCH (" +
        SearchConstants.COLLECTION_IN_QUERY +
        ":" +
        Neo4jLabels.COLLECTION +
        ")-[:" +
        Neo4jLabels.HAS_DATAOBJECT +
        "]->(d:" +
        Neo4jLabels.DATA_OBJECT +
        ")-[:" +
        Neo4jLabels.HAS_CHILD +
        "*0..]->(" +
        SearchConstants.DATAOBJECT_IN_QUERY +
        ":" +
        Neo4jLabels.DATA_OBJECT +
        ")";
        case parents -> "MATCH (" +
        SearchConstants.COLLECTION_IN_QUERY +
        ":" +
        Neo4jLabels.COLLECTION +
        ")-[:" +
        Neo4jLabels.HAS_DATAOBJECT +
        "]->(d:" +
        Neo4jLabels.DATA_OBJECT +
        ")<-[:" +
        Neo4jLabels.HAS_CHILD +
        "*0..]-(" +
        SearchConstants.DATAOBJECT_IN_QUERY +
        ":" +
        Neo4jLabels.DATA_OBJECT +
        ")";
        case successors -> "MATCH (" +
        SearchConstants.COLLECTION_IN_QUERY +
        ":" +
        Neo4jLabels.COLLECTION +
        ")-[:" +
        Neo4jLabels.HAS_DATAOBJECT +
        "]->(d:" +
        Neo4jLabels.DATA_OBJECT +
        ")-[:" +
        Neo4jLabels.HAS_SUCCESSOR +
        "*0..]->(" +
        SearchConstants.DATAOBJECT_IN_QUERY +
        ":" +
        Neo4jLabels.DATA_OBJECT +
        ")";
        case predecessors -> "MATCH (" +
        SearchConstants.COLLECTION_IN_QUERY +
        ":" +
        Neo4jLabels.COLLECTION +
        ")-[:" +
        Neo4jLabels.HAS_DATAOBJECT +
        "]->(d:DataObject)<-[:has_successor*0..]-(" +
        SearchConstants.DATAOBJECT_IN_QUERY +
        ":" +
        Neo4jLabels.DATA_OBJECT +
        ")";
        default -> "";
      };
    return ret;
  }

  private static String collectionDataObjectBasicReferenceMatchPartWithoutVersion(TraversalRules traversalRule) {
    String ret =
      switch (traversalRule) {
        case children -> "MATCH (" +
        SearchConstants.COLLECTION_IN_QUERY +
        ":" +
        Neo4jLabels.COLLECTION +
        ")-[:" +
        Neo4jLabels.HAS_DATAOBJECT +
        "]->(d:" +
        Neo4jLabels.DATA_OBJECT +
        ")-[:" +
        Neo4jLabels.HAS_CHILD +
        "*0..]->(" +
        SearchConstants.DATAOBJECT_IN_QUERY +
        ":" +
        Neo4jLabels.DATA_OBJECT +
        ")-[:" +
        Neo4jLabels.HAS_REFERENCE +
        "]->(" +
        SearchConstants.REFERENCE_IN_QUERY +
        ":" +
        Neo4jLabels.BASIC_REFERENCE +
        ")";
        case parents -> "MATCH (" +
        SearchConstants.COLLECTION_IN_QUERY +
        ":" +
        Neo4jLabels.COLLECTION +
        ")-[:" +
        Neo4jLabels.HAS_DATAOBJECT +
        "]->(d:" +
        Neo4jLabels.DATA_OBJECT +
        ")<-[:" +
        Neo4jLabels.HAS_CHILD +
        "*0..]-(" +
        SearchConstants.DATAOBJECT_IN_QUERY +
        ":" +
        Neo4jLabels.DATA_OBJECT +
        ")-[:" +
        Neo4jLabels.HAS_REFERENCE +
        "]->(" +
        SearchConstants.REFERENCE_IN_QUERY +
        ":" +
        Neo4jLabels.BASIC_REFERENCE +
        ")";
        case successors -> "MATCH (" +
        SearchConstants.COLLECTION_IN_QUERY +
        ":" +
        Neo4jLabels.COLLECTION +
        ")-[:" +
        Neo4jLabels.HAS_DATAOBJECT +
        "]->(d:" +
        Neo4jLabels.DATA_OBJECT +
        ")-[:" +
        Neo4jLabels.HAS_SUCCESSOR +
        "*0..]->(" +
        SearchConstants.DATAOBJECT_IN_QUERY +
        ":" +
        Neo4jLabels.DATA_OBJECT +
        ")-[:" +
        Neo4jLabels.HAS_REFERENCE +
        "]->(" +
        SearchConstants.REFERENCE_IN_QUERY +
        ":" +
        Neo4jLabels.BASIC_REFERENCE +
        ")";
        case predecessors -> "MATCH (" +
        SearchConstants.COLLECTION_IN_QUERY +
        ":" +
        Neo4jLabels.COLLECTION +
        ")-[:" +
        Neo4jLabels.HAS_DATAOBJECT +
        "]->(d:" +
        Neo4jLabels.DATA_OBJECT +
        ")<-[:" +
        Neo4jLabels.HAS_SUCCESSOR +
        "*0..]-(" +
        SearchConstants.DATAOBJECT_IN_QUERY +
        ":" +
        Neo4jLabels.DATA_OBJECT +
        ")-[:" +
        Neo4jLabels.HAS_REFERENCE +
        "]->(" +
        SearchConstants.REFERENCE_IN_QUERY +
        ":" +
        Neo4jLabels.BASIC_REFERENCE +
        ")";
        default -> "";
      };
    return ret;
  }

  private static String readableByPart(String username) {
    String variable = SearchConstants.COLLECTION_IN_QUERY;
    return CypherQueryHelper.getReadableByQuery(variable, username);
  }

  public static String userSelectionQuery(String query) {
    String ret = "";
    ret = ret + userMatchPart();
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(query, Constants.USER_IN_QUERY);
    return ret;
  }

  private static String userMatchPart() {
    String ret = "";
    ret = ret + "MATCH (" + Constants.USER_IN_QUERY + ":" + Neo4jLabels.USER + ")";
    return ret;
  }

  public static String userGroupSelectionQuery(String query) {
    String ret = "";
    ret = ret + userGroupMatchPart();
    ret = ret + " WHERE ";
    ret = ret + getNeo4jWithNeo4jIdString(query, Constants.USERGROUP_IN_QUERY);
    return ret;
  }

  private static String userGroupMatchPart() {
    String ret = "";
    ret = ret + "MATCH (" + Constants.USERGROUP_IN_QUERY + ":" + Neo4jLabels.USER_GROUP + ")";
    return ret;
  }
}