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);
}
}