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
107 if (
108 property.equals(SearchConstants.JSON_VALUE_IRI) || property.equals(SearchConstants.JSON_PROPERTY_IRI)
109 ) return getIRICondition(jsonNode, cypherNode);
110
111 if (property.equals(SearchConstants.JSON_HAS_ANNOTATION)) return getAnnotationCondition(jsonNode, cypherNode);
112
113 if (property.equals(SearchConstants.JSON_HAS_ANNOTATION_IRI)) return getAnnotationIRICondition(
114 jsonNode,
115 cypherNode
116 );
117
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
236 Condition noPermission = Cypher.not(
237 Cypher.exists(entity.relationshipTo(Cypher.anyNode(), Neo4jLabels.HAS_PERMISSIONS))
238 );
239
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
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
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
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 }