View Javadoc
1   package de.dlr.shepard.common.util;
2   
3   import de.dlr.shepard.common.configuration.feature.toggles.VersioningFeatureToggle;
4   import de.dlr.shepard.common.exceptions.ShepardParserException;
5   import de.dlr.shepard.common.neo4j.endpoints.OrderByAttribute;
6   import de.dlr.shepard.common.search.endpoints.BasicContainerAttributes;
7   import java.util.List;
8   import java.util.UUID;
9   import java.util.stream.Collectors;
10  
11  public class CypherQueryHelper {
12  
13    public enum Neighborhood {
14      EVERYTHING,
15      OUTGOING,
16      ESSENTIAL,
17    }
18  
19    private CypherQueryHelper() {}
20  
21    public static String getObjectPartWithVersion(String variable, String type, boolean hasName, String versionVariable) {
22      String ret = getObjectPart(variable, type, hasName);
23      ret = ret + "-[:has_version]->(" + versionVariable + ":Version)";
24      return ret;
25    }
26  
27    public static String getObjectPart(String variable, String type, boolean hasName) {
28      if (hasName) return getObjectPartWithName(variable, type);
29      else return getObjectPartWithoutName(variable, type);
30    }
31  
32    private static String getObjectPartWithName(String variable, String type) {
33      var namePart = "{ name : $name, deleted: FALSE }";
34      var result = "(%s:%s %s)".formatted(variable, type, namePart);
35      return result;
36    }
37  
38    private static String getObjectPartWithoutName(String variable, String type) {
39      var namePart = "{ deleted: FALSE }";
40      var result = "(%s:%s %s)".formatted(variable, type, namePart);
41      return result;
42    }
43  
44    public static String getPaginationPart() {
45      return "SKIP $offset LIMIT $size";
46    }
47  
48    public static String getPaginationPart(PaginationHelper paginationParams) {
49      return "SKIP %d LIMIT %d".formatted(paginationParams.getOffset(), paginationParams.getSize());
50    }
51  
52    public static String getReturnPart(String entity) {
53      return getReturnPart(entity, Neighborhood.EVERYTHING, 1);
54    }
55  
56    public static String getReturnPart(String entity, int depth) {
57      return getReturnPart(entity, Neighborhood.EVERYTHING, depth);
58    }
59  
60    public static String getReturnPart(String entity, Neighborhood neighborhood) {
61      return getReturnPart(entity, neighborhood, 1);
62    }
63  
64    public static String getReturnPart(String entity, Neighborhood neighborhood, PaginationHelper pagination) {
65      return getReturnPart(entity, neighborhood, 1, pagination);
66    }
67  
68    public static String getReturnCountPart(String entity, Neighborhood neighborhood) {
69      return (getNeighborhoodPart(entity, neighborhood, 1) + " RETURN " + "COUNT(%s)".formatted(entity));
70    }
71  
72    public static String getReturnPart(String entity, Neighborhood neighborhood, int depth) {
73      return (
74        getNeighborhoodPart(entity, neighborhood, depth) +
75        " RETURN " +
76        "%s, nodes(path), relationships(path)".formatted(entity)
77      );
78    }
79  
80    public static String getReturnPart(String entity, Neighborhood neighborhood, int depth, PaginationHelper pagination) {
81      return (
82        getNeighborhoodPart(entity, neighborhood, depth) +
83        (pagination != null ? " " + CypherQueryHelper.getPaginationPart(pagination) : "") +
84        " RETURN " +
85        "%s, nodes(path), relationships(path)".formatted(entity)
86      );
87    }
88  
89    private static String getNeighborhoodPart(String entity, Neighborhood neighborhood, int depth) {
90      // Clamp the depth between 1 and 3 nodes
91      depth = Math.max(1, Math.min(3, depth));
92      String match =
93        switch (neighborhood) {
94          case EVERYTHING -> "path=(%s)-[*0..%d]-(n) WHERE n.deleted = FALSE OR n.deleted IS NULL";
95          case OUTGOING -> "path=(%s)-[*0..%d]->(n) WHERE n.deleted = FALSE OR n.deleted IS NULL";
96          case ESSENTIAL -> "path=(%s)-[*0..%d]->(n) WHERE n:Permission OR n:User";
97        };
98      return "MATCH " + match.formatted(entity, depth);
99    }
100 
101   public static String getReturnPartLight(String entity) {
102     return "RETURN " + entity;
103   }
104 
105   public static String getOrderByPart(String variable, OrderByAttribute orderByAttribute, Boolean orderDesc) {
106     String ret;
107     boolean isString = orderByAttribute.isString();
108     if (!isString) ret = "ORDER BY " + variable + "." + orderByAttribute;
109     else if (
110       orderByAttribute instanceof BasicContainerAttributes attributes && attributes == BasicContainerAttributes.type
111     ) ret = "ORDER BY LABELS(" + variable + ")";
112     else ret = "ORDER BY toLower(" + variable + "." + orderByAttribute + ")";
113     if (orderByAttribute.toString() == "id") ret = "ORDER BY id(" + variable + ")";
114     if (orderDesc != null && orderDesc) ret = ret + " DESC";
115     return ret;
116   }
117 
118   public static String getShepardIdPart(String variable, long shepardId) {
119     return variable + "." + Constants.SHEPARD_ID + " = " + shepardId;
120   }
121 
122   public static String getShepardIdsPart(String variable, List<Long> shepardIds) {
123     String commaSeparatedIds = shepardIds.stream().map(String::valueOf).collect(Collectors.joining(","));
124     return variable + "." + Constants.SHEPARD_ID + " in [" + commaSeparatedIds + "]";
125   }
126 
127   public static String getReadableByQuery(String variable, String username) {
128     String ret =
129       """
130       (NOT exists((%s)-[:has_permissions]->(:Permissions)) \
131       OR exists((%s)-[:has_permissions]->(:Permissions)-[:readable_by|owned_by]->(:User { username: "%s" })) \
132       OR exists((%s)-[:has_permissions]->(:Permissions {permissionType: "Public"})) \
133       OR exists((%s)-[:has_permissions]->(:Permissions {permissionType: "PublicReadable"})) \
134       OR exists((%s)-[:has_permissions]->(:Permissions)-[:readable_by_group]->(:UserGroup)<-[:is_in_group]-(:User { username: "%s"})))""".formatted(
135           variable,
136           variable,
137           username,
138           variable,
139           variable,
140           variable,
141           username
142         );
143     return ret;
144   }
145 
146   public static String getVersionHeadPart(String variable) {
147     if (VersioningFeatureToggle.isEnabled()) {
148       return "(" + variable + ".isHEADVersion = true)";
149     }
150     return "(1=1)";
151   }
152 
153   public static String getVersionPart(String variable, UUID versionUID) {
154     return "(" + variable + ".uid = '" + versionUID + "')";
155   }
156 
157   public static String[] splitByExactlyOneDoubleColon(String doubleColonSeparatedString) {
158     //REMARK:
159     //the split function in JAVA behaves sometimes in an inconsistent way:
160     //"::".split("::") = String[0] {  }
161     //"x::y".split("::") = String[2] { "x", "y" }
162     //"::y".split("::") = String[2] { "", "y" }
163     //"x::".split("::") = String[1] { "x" }
164     //to extract the empty string from a double colon separated String correctly
165     // some additional work is needed
166     if (!doubleColonSeparatedString.matches(SearchConstants.JSON_COLON_MATCHER)) throw new ShepardParserException(
167       SearchConstants.INCORRECT_COLON_MESSAGE + doubleColonSeparatedString
168     );
169     String[] propertyValuePair = doubleColonSeparatedString.split("::");
170     String[] ret = new String[2];
171     //case ::
172     if (propertyValuePair.length == 0) {
173       ret[0] = "";
174       ret[1] = "";
175     }
176     //case x::
177     if (propertyValuePair.length == 1) {
178       ret[0] = propertyValuePair[0];
179       ret[1] = "";
180     }
181     //case x::y (where x may be the empty string)
182     if (propertyValuePair.length == 2) {
183       ret[0] = propertyValuePair[0];
184       ret[1] = propertyValuePair[1];
185     }
186     return ret;
187   }
188 }