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