View Javadoc
1   package de.dlr.shepard.common.search.query;
2   
3   import com.fasterxml.jackson.core.JsonProcessingException;
4   import com.fasterxml.jackson.databind.JsonNode;
5   import com.fasterxml.jackson.databind.ObjectMapper;
6   import de.dlr.shepard.common.exceptions.ShepardParserException;
7   import de.dlr.shepard.common.util.CypherDslHelper;
8   import de.dlr.shepard.common.util.CypherQueryHelper;
9   import de.dlr.shepard.common.util.Neo4jLabels;
10  import de.dlr.shepard.common.util.SearchConstants;
11  import java.util.Iterator;
12  import java.util.List;
13  import java.util.NoSuchElementException;
14  import org.neo4j.cypherdsl.core.Condition;
15  import org.neo4j.cypherdsl.core.Cypher;
16  import org.neo4j.cypherdsl.core.Expression;
17  import org.neo4j.cypherdsl.core.NamedPath;
18  import org.neo4j.cypherdsl.core.Node;
19  
20  public class TimeseriesInContainerQueryBuilder {
21  
22    private static final List<String> booleanOperators = List.of(
23      SearchConstants.JSON_AND,
24      SearchConstants.JSON_OR,
25      SearchConstants.JSON_NOT,
26      SearchConstants.JSON_XOR
27    );
28    private static final List<String> opAttributes = List.of(
29      SearchConstants.OP_PROPERTY,
30      SearchConstants.OP_VALUE,
31      SearchConstants.OP_OPERATOR
32    );
33  
34    private static final List<String> notIdProperties = List.of(
35      SearchConstants.JSON_VALUE_IRI,
36      SearchConstants.JSON_PROPERTY_IRI,
37      SearchConstants.JSON_HAS_ANNOTATION,
38      SearchConstants.JSON_HAS_ANNOTATION_IRI,
39      SearchConstants.JSON_VALUE_TYPE
40    );
41  
42    private static final List<String> quintupleProperties = List.of(
43      SearchConstants.JSON_MEASUREMENT,
44      SearchConstants.JSON_DEVICE,
45      SearchConstants.JSON_LOCATION,
46      SearchConstants.JSON_SYMBOLIC_NAME,
47      SearchConstants.JSON_FIELD
48    );
49  
50    private static final List<String> IdProperties = List.of(SearchConstants.JSON_ID, SearchConstants.JSON_REPOSITORY_ID);
51  
52    public static String searchTimeseriesInContainerQuery(long containerId, String jsonQuery, String username) {
53      Node ts = Cypher.node(Neo4jLabels.TIMESERIES);
54      Node tscon = Cypher.node(Neo4jLabels.TIMESERIES_CONTAINER);
55      NamedPath path = Cypher.path("path").definedBy(ts.relationshipBetween(Cypher.anyNode()));
56      var statement = Cypher.match(ts.relationshipTo(tscon, Neo4jLabels.IS_IN_CONTAINER))
57        .where(
58          CypherDslHelper.notDeleted(ts)
59            .and(CypherDslHelper.isNotDeletedWithInternalId(tscon, containerId))
60            .and(getAccessRightsCondition(tscon, username))
61            .and(getCondition(jsonQuery, ts))
62        )
63        .withDistinct(ts)
64        .match(path)
65        .returning(
66          ts.asExpression(),
67          Cypher.nodes(Cypher.path("path").get()),
68          Cypher.relationships(Cypher.path("path").get())
69        );
70      return statement.build().getCypher();
71    }
72  
73    private static Condition getCondition(JsonNode jsonNode, Node cypherNode) {
74      try {
75        String op = jsonNode.fieldNames().next();
76        if (opAttributes.contains(op)) {
77          return getPrimitiveCondition(jsonNode, cypherNode);
78        }
79        return getComplexCondition(jsonNode, op, cypherNode);
80      } catch (NoSuchElementException e) {
81        throw new ShepardParserException(SearchConstants.PARSING_ERROR_MESSAGE + e.getMessage());
82      }
83    }
84  
85    private static Condition getCondition(String jsonQuery, Node cypherNode) {
86      if (jsonQuery == null || jsonQuery.equals("")) return Cypher.isTrue();
87      ObjectMapper objectMapper = new ObjectMapper();
88      try {
89        JsonNode jsonNode = objectMapper.readValue(jsonQuery, JsonNode.class);
90        return getCondition(jsonNode, cypherNode);
91      } catch (JsonProcessingException e) {
92        throw new ShepardParserException(SearchConstants.NO_JSON_PARSING_MESSAGE + e.getMessage());
93      }
94    }
95  
96    private static Condition getPrimitiveCondition(JsonNode jsonNode, Node cypherNode) {
97      String property = jsonNode.get(SearchConstants.OP_PROPERTY).textValue();
98      if (notIdProperties.contains(property)) return getSimpleNotIdCondition(jsonNode, cypherNode);
99      if (IdProperties.contains(property)) return getSimpleIdCondition(jsonNode, cypherNode);
100     if (quintupleProperties.contains(property)) return getQuintupleCondition(jsonNode, cypherNode);
101     throw new ShepardParserException(SearchConstants.UNKNOWN_PROPERTY_MESSAGE + property);
102   }
103 
104   private static Condition getSimpleNotIdCondition(JsonNode jsonNode, Node cypherNode) {
105     String property = jsonNode.get(SearchConstants.OP_PROPERTY).textValue();
106     // for SemanticAnnotationIRIs
107     if (
108       property.equals(SearchConstants.JSON_VALUE_IRI) || property.equals(SearchConstants.JSON_PROPERTY_IRI)
109     ) return getIRICondition(jsonNode, cypherNode);
110     // for SemanticAnnotations
111     if (property.equals(SearchConstants.JSON_HAS_ANNOTATION)) return getAnnotationCondition(jsonNode, cypherNode);
112     // for SemanticAnnotations
113     if (property.equals(SearchConstants.JSON_HAS_ANNOTATION_IRI)) return getAnnotationIRICondition(
114       jsonNode,
115       cypherNode
116     );
117     // for valueType
118     if (property.equals(SearchConstants.JSON_VALUE_TYPE)) return hasStringProperty(
119       cypherNode,
120       property,
121       jsonNode.get(SearchConstants.OP_VALUE).textValue(),
122       jsonNode.get(SearchConstants.OP_OPERATOR).textValue(),
123       false
124     );
125     throw new ShepardParserException(SearchConstants.UNKNOWN_PROPERTY_MESSAGE + property);
126   }
127 
128   private static Condition getIRICondition(JsonNode jsonNode, Node cypherNode) {
129     String iriType = jsonNode.get(SearchConstants.OP_PROPERTY).textValue();
130     String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
131     String value = jsonNode.get(SearchConstants.OP_VALUE).textValue();
132     Node annotationNode = Cypher.node(Neo4jLabels.SEMANTIC_ANNOTATION);
133     return Cypher.match(cypherNode.relationshipTo(annotationNode, Neo4jLabels.HAS_ANNOTATION))
134       .where(hasStringProperty(annotationNode, iriType, value, operator))
135       .asCondition();
136   }
137 
138   private static Condition getQuintupleCondition(JsonNode jsonNode, Node cypherNode) {
139     String quintupleProperty = jsonNode.get(SearchConstants.OP_PROPERTY).textValue();
140     String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
141     String value = jsonNode.get(SearchConstants.OP_VALUE).textValue();
142     Node timeseriesTupleNode = Cypher.node(Neo4jLabels.TIMESERIES_TUPLE);
143     return Cypher.match(cypherNode.relationshipTo(timeseriesTupleNode, Neo4jLabels.HAS_TIMESERIES_TUPLE))
144       .where(hasStringProperty(timeseriesTupleNode, quintupleProperty, value, operator))
145       .asCondition();
146   }
147 
148   private static Condition getAnnotationCondition(JsonNode jsonNode, Node cypherNode) {
149     String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
150     String[] pair = CypherQueryHelper.splitByExactlyOneDoubleColon(jsonNode.get(SearchConstants.OP_VALUE).textValue());
151     String propertyName = pair[0];
152     String valueName = pair[1];
153     Node annotationNode = Cypher.node(Neo4jLabels.SEMANTIC_ANNOTATION);
154     return Cypher.match(cypherNode.relationshipTo(annotationNode, Neo4jLabels.HAS_ANNOTATION))
155       .where(
156         hasStringProperty(annotationNode, Neo4jLabels.PROPERTY_NAME, propertyName, operator).and(
157           hasStringProperty(annotationNode, Neo4jLabels.VALUE_NAME, valueName, operator)
158         )
159       )
160       .asCondition();
161   }
162 
163   private static Condition getAnnotationIRICondition(JsonNode jsonNode, Node cypherNode) {
164     String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
165     String[] pair = CypherQueryHelper.splitByExactlyOneDoubleColon(jsonNode.get(SearchConstants.OP_VALUE).textValue());
166     String propertyIRI = pair[0];
167     String valueIRI = pair[1];
168     Node annotationNode = Cypher.node(Neo4jLabels.SEMANTIC_ANNOTATION);
169     return Cypher.match(cypherNode.relationshipTo(annotationNode, Neo4jLabels.HAS_ANNOTATION))
170       .where(
171         hasStringProperty(annotationNode, Neo4jLabels.PROPERTY_IRI, propertyIRI, operator).and(
172           hasStringProperty(annotationNode, Neo4jLabels.VALUE_IRI, valueIRI, operator)
173         )
174       )
175       .asCondition();
176   }
177 
178   private static Condition getSimpleIdCondition(JsonNode jsonNode, Node cypherNode) {
179     String property = jsonNode.get(SearchConstants.OP_PROPERTY).textValue();
180     if (property.equals(SearchConstants.JSON_ID)) return getTimeseriesIdCondition(jsonNode, cypherNode);
181     if (property.equals(SearchConstants.JSON_REPOSITORY_ID)) return getHasRepositoryIdCondition(jsonNode, cypherNode);
182     throw new ShepardParserException(SearchConstants.UNKNOWN_PROPERTY_MESSAGE + property);
183   }
184 
185   private static Condition getTimeseriesIdCondition(JsonNode jsonNode, Node cypherNode) {
186     String property = Neo4jLabels.TIMESERIES_ID;
187     String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
188     long value = jsonNode.get(SearchConstants.OP_VALUE).longValue();
189     return hasLongProperty(cypherNode, property, value, operator);
190   }
191 
192   private static Condition getHasRepositoryIdCondition(JsonNode jsonNode, Node cypherNode) {
193     String operator = jsonNode.get(SearchConstants.OP_OPERATOR).textValue();
194     long value = jsonNode.get(SearchConstants.OP_VALUE).longValue();
195     Node annotation = Cypher.node(Neo4jLabels.SEMANTIC_ANNOTATION);
196     Node repository = Cypher.node(Neo4jLabels.SEMANTIC_REPOSITORY);
197     return Cypher.match(
198       cypherNode
199         .relationshipTo(annotation, Neo4jLabels.HAS_ANNOTATION)
200         .relationshipTo(repository, Neo4jLabels.VALUE_REPOSITORY, Neo4jLabels.PROPERTY_REPOSITORY)
201     )
202       .where(CypherDslHelper.notDeleted(repository))
203       .and(hasInternalIdProperty(repository, value, operator))
204       .asCondition();
205   }
206 
207   private static Condition getComplexCondition(JsonNode jsonNode, String op, Node cypherNode) {
208     if (!booleanOperators.contains(op)) throw new ShepardParserException(
209       SearchConstants.UNKNOWN_BOOLEAN_OPERATOR_MESSAGE + op
210     );
211     if (op.equals(SearchConstants.JSON_NOT)) return getNegatedCondition(jsonNode, cypherNode);
212     else return getMultaryCondition(jsonNode, op, cypherNode);
213   }
214 
215   private static Condition getNegatedCondition(JsonNode jsonNode, Node cypherNode) {
216     return getCondition(jsonNode.get(SearchConstants.JSON_NOT), cypherNode).not();
217   }
218 
219   private static Condition getMultaryCondition(JsonNode jsonNode, String op, Node cypherNode) {
220     Iterator<JsonNode> argumentsArray = jsonNode.get(op).elements();
221     Condition condition = getCondition(argumentsArray.next(), cypherNode);
222     while (argumentsArray.hasNext()) {
223       if (op.equals(SearchConstants.JSON_AND)) condition = condition.and(
224         getCondition(argumentsArray.next(), cypherNode)
225       );
226       if (op.equals(SearchConstants.JSON_OR)) condition = condition.or(getCondition(argumentsArray.next(), cypherNode));
227       if (op.equals(SearchConstants.JSON_XOR)) condition = condition.xor(
228         getCondition(argumentsArray.next(), cypherNode)
229       );
230     }
231     return condition;
232   }
233 
234   private static Condition getAccessRightsCondition(Node entity, String username) {
235     // no permission for legacy reasons
236     Condition noPermission = Cypher.not(
237       Cypher.exists(entity.relationshipTo(Cypher.anyNode(), Neo4jLabels.HAS_PERMISSIONS))
238     );
239     // explicitly readable
240     Condition readableBy = Cypher.exists(
241       entity
242         .relationshipTo(Cypher.anyNode(), Neo4jLabels.HAS_PERMISSIONS)
243         .relationshipTo(
244           Cypher.anyNode().withProperties(Neo4jLabels.USERNAME, Cypher.literalOf(username)),
245           Neo4jLabels.READABLE_BY,
246           Neo4jLabels.OWNED_BY
247         )
248     );
249     // public permission
250     Condition publicPermission = Cypher.exists(
251       entity.relationshipTo(
252         Cypher.anyNode().withProperties(Neo4jLabels.PERMISSION_TYPE, Cypher.literalOf(Neo4jLabels.PUBLIC)),
253         Neo4jLabels.HAS_PERMISSIONS
254       )
255     );
256     // public readable
257     Condition publicReadable = Cypher.exists(
258       entity.relationshipTo(
259         Cypher.anyNode().withProperties(Neo4jLabels.PERMISSION_TYPE, Cypher.literalOf(Neo4jLabels.PUBLIC_READABLE)),
260         Neo4jLabels.HAS_PERMISSIONS
261       )
262     );
263     // redableByGroup
264     Condition readableByGroup = Cypher.exists(
265       entity
266         .relationshipTo(Cypher.anyNode(), Neo4jLabels.HAS_PERMISSIONS)
267         .relationshipTo(Cypher.anyNode(), Neo4jLabels.READABLE_BY_GROUP)
268         .relationshipFrom(
269           Cypher.anyNode().withProperties(Neo4jLabels.USERNAME, Cypher.literalOf(username)),
270           Neo4jLabels.IS_IN_GROUP
271         )
272     );
273     Condition accessRights = noPermission.or(readableBy).or(publicPermission).or(publicReadable).or(readableByGroup);
274     return accessRights;
275   }
276 
277   private static Condition toCondition(String operator, Expression exp, Object value) {
278     return switch (operator) {
279       case SearchConstants.JSON_EQ -> exp.eq(Cypher.literalOf(value));
280       case SearchConstants.JSON_NE -> exp.ne(Cypher.literalOf(value));
281       case SearchConstants.JSON_GE -> exp.gte(Cypher.literalOf(value));
282       case SearchConstants.JSON_LE -> exp.lte(Cypher.literalOf(value));
283       case SearchConstants.JSON_LT -> exp.lt(Cypher.literalOf(value));
284       case SearchConstants.JSON_GT -> exp.gt(Cypher.literalOf(value));
285       case SearchConstants.JSON_CONTAINS -> exp.contains(Cypher.literalOf(value));
286       case SearchConstants.JSON_REGMATCH -> exp.matches(Cypher.literalOf(value));
287       default -> throw new ShepardParserException(SearchConstants.UNKNOWN_COMPARISON_OPERATOR_MESSAGE + operator);
288     };
289   }
290 
291   public static Condition hasStringProperty(Node node, String property, String value, String operator) {
292     return toCondition(operator, node.property(property), value);
293   }
294 
295   private static Condition hasLongProperty(Node node, String property, long value, String operator) {
296     return switch (operator) {
297       case SearchConstants.JSON_CONTAINS, SearchConstants.JSON_REGMATCH -> throw new ShepardParserException(
298         SearchConstants.UNKNOWN_COMPARISON_OPERATOR_MESSAGE + operator
299       );
300       default -> toCondition(operator, node.property(property), value);
301     };
302   }
303 
304   private static Condition hasInternalIdProperty(Node node, long value, String operator) {
305     return switch (operator) {
306       case SearchConstants.JSON_CONTAINS, SearchConstants.JSON_REGMATCH -> throw new ShepardParserException(
307         SearchConstants.UNKNOWN_COMPARISON_OPERATOR_MESSAGE + operator
308       );
309       default -> toCondition(operator, node.internalId(), value);
310     };
311   }
312 
313   public static Condition hasStringProperty(
314     Node node,
315     String property,
316     String value,
317     String operator,
318     boolean caseSensitive
319   ) {
320     if (caseSensitive) return hasStringProperty(node, property, value, operator);
321     else return toCondition(operator, Cypher.toLower(node.property(property)), value.toLowerCase());
322   }
323 }