1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.rest.delivery.jcr;
35
36 import static info.magnolia.rest.delivery.jcr.Operator.*;
37 import static java.util.stream.Collectors.joining;
38
39 import java.time.LocalDate;
40 import java.time.LocalTime;
41 import java.time.ZoneId;
42 import java.time.ZonedDateTime;
43 import java.time.format.DateTimeFormatter;
44 import java.time.format.DateTimeParseException;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.List;
48 import java.util.stream.Collectors;
49
50 import javax.ws.rs.BadRequestException;
51
52 import org.apache.commons.lang3.StringUtils;
53 import org.apache.commons.lang3.math.NumberUtils;
54
55
56
57
58 public class FilteringCondition {
59
60 private static final String NAME_PROPERTY = "@name";
61 private static final String PARENT_PROPERTY = "@ancestor";
62
63 static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
64 static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
65
66 private static final String VALUES_SPLITTER = "\\|";
67
68 private static final String OPERATOR_OPEN = "[";
69 private static final String OPERATOR_CLOSE = "]";
70
71 private static final String RANGE_SYMBOL = "~";
72
73 private final String INVALID_VALUE_STRING = "Value string is invalid.";
74
75 private final String property;
76 private final Operator operator;
77 private final String[] values;
78
79 private boolean isDateQuery = false;
80
81
82
83
84
85 private final List<String[]> rangeValues;
86
87 public FilteringCondition(String propertyString, String valueString) {
88 if (StringUtils.isEmpty(propertyString)) {
89 throw new IllegalArgumentException("Property string is invalid.");
90 }
91
92 operator = extractOperator(propertyString);
93 property = StringUtils.substringBeforeLast(propertyString, OPERATOR_OPEN);
94 values = valueString.split(VALUES_SPLITTER);
95
96 validateValueString();
97
98 isDateQuery = isDateQuery();
99
100 if (isDateQuery) {
101 rangeValues = extractDateRangeValues(values);
102 } else {
103 rangeValues = extractRangeValues(values);
104 }
105 }
106
107 private void validateValueString() {
108
109 if (Arrays.stream(values)
110 .anyMatch(value -> value.contains(RANGE_SYMBOL)
111 && value.split(RANGE_SYMBOL).length != 2)) {
112 throw new BadRequestException(INVALID_VALUE_STRING);
113 }
114
115
116 if (Arrays.stream(values)
117 .anyMatch(value -> dateMatches(DATE_FORMATTER, value))
118 && Arrays.stream(values)
119 .anyMatch(value -> value.contains(RANGE_SYMBOL))) {
120 throw new BadRequestException(INVALID_VALUE_STRING);
121 }
122 }
123
124 private List<String[]> extractDateRangeValues(String[] values) {
125 List<String[]> rangeValues = new ArrayList<>();
126 for (String value : values) {
127 if (value.contains(RANGE_SYMBOL)) {
128 String[] bounds = value.split(RANGE_SYMBOL);
129 rangeValues.add(bounds);
130 } else {
131 String[] bounds = new String[] { value, value };
132 rangeValues.add(bounds);
133 }
134 }
135 return rangeValues;
136 }
137
138 private boolean isDateQuery() {
139
140 if (Arrays.stream(values)
141 .allMatch(value -> dateMatches(DATE_FORMATTER, value))) {
142 return true;
143 }
144
145
146 for (String value : values) {
147 if (Arrays.stream(value.split(RANGE_SYMBOL))
148 .anyMatch(bound -> !dateMatches(DATE_FORMATTER, bound))) {
149 return false;
150 }
151 }
152
153 return true;
154 }
155
156 private Operator extractOperator(String propertyString) {
157 if (!propertyString.contains(OPERATOR_OPEN) && !propertyString.contains(OPERATOR_CLOSE)) {
158 return Operator.EQ;
159 }
160 return Operator.from(StringUtils.substringBetween(propertyString, OPERATOR_OPEN, OPERATOR_CLOSE));
161 }
162
163 private List<String[]> extractRangeValues(String[] values) {
164 return Arrays.stream(values)
165 .map(value -> value.split(RANGE_SYMBOL))
166 .filter(bounds -> bounds.length == 2)
167 .collect(Collectors.toList());
168 }
169
170 public String asSqlString() {
171 if (operator == NULL) {
172 return String.format("[%s] IS %sNULL", property, "true".equalsIgnoreCase(values[0]) ? "" : "NOT ");
173 }
174
175 if (isDateQuery) {
176 return rangeValues.stream()
177 .map(this::getDateConditionClause)
178 .collect(joining(" OR "));
179 }
180 if (operator == IN) {
181 return rangeValues.stream()
182 .map(this::getInConditionClause)
183 .collect(joining(" OR "));
184 }
185 if (operator == NOT_IN) {
186 return rangeValues.stream()
187 .map(this::getNotInConditionClause)
188 .collect(joining(" OR "));
189 }
190 return Arrays.stream(values)
191 .map(this::getConditionClause)
192 .collect(joining(" OR "));
193 }
194
195 private String getInConditionClause(String[] bounds) {
196 return String.format("[%s] >= %s AND [%s] <= %s", property, resolveValue(bounds[0]), property, resolveValue(bounds[1]));
197 }
198
199 private String getNotInConditionClause(String[] bounds) {
200 return String.format("[%s] < %s OR [%s] > %s", property, resolveValue(bounds[0]), property, resolveValue(bounds[1]));
201 }
202
203 private String getDateConditionClause(String[] bounds) {
204 switch (operator) {
205 case GT:
206 rangeIsNotSupported(bounds);
207 return String.format("[%s] > %s", property, resolveDateValue(bounds[0], true));
208 case LT:
209 rangeIsNotSupported(bounds);
210 return String.format("[%s] < %s", property, resolveDateValue(bounds[0], false));
211 case GTE:
212 rangeIsNotSupported(bounds);
213 return String.format("[%s] >= %s", property, resolveDateValue(bounds[0], false));
214 case LTE:
215 rangeIsNotSupported(bounds);
216 return String.format("[%s] <= %s", property, resolveDateValue(bounds[0], true));
217 case NE:
218 rangeIsNotSupported(bounds);
219 return String.format("[%s] < %s OR [%s] > %s", property, resolveDateValue(bounds[0], false),
220 property, resolveDateValue(bounds[0], true));
221 default:
222 String lowerBound = resolveDateValue(bounds[0], false);
223 String upperBound = resolveDateValue(bounds[1], true);
224
225 if (operator == IN) {
226 return String.format("[%s] >= %s AND [%s] <= %s", property, lowerBound, property, upperBound);
227 } else if (operator == NOT_IN) {
228 return String.format("[%s] < %s OR [%s] > %s", property, lowerBound, property, upperBound);
229 }
230
231 rangeIsNotSupported(bounds);
232 return String.format("[%s] >= %s AND [%s] <= %s", property, lowerBound, property, upperBound);
233 }
234 }
235
236 private void rangeIsNotSupported(String[] bounds) {
237 if (bounds.length == 2 && !bounds[0].equals(bounds[1])) {
238 throw new BadRequestException("Range is not supported.");
239 }
240 }
241
242 private String getConditionClause(String value) {
243 if (PARENT_PROPERTY.equalsIgnoreCase(property)) {
244 return String.format("ISDESCENDANTNODE('%s')", value);
245 }
246
247
248
249
250
251 if (NAME_PROPERTY.equalsIgnoreCase(property)) {
252 return String.format("LOWER(LOCALNAME(t)) LIKE '%s'", value);
253 }
254
255 return String.format("[%s] %s %s", property, operator.asSqlString(), resolveValue(value));
256 }
257
258 private String resolveValue(String value) {
259 if (NumberUtils.isNumber(value)) {
260 return value;
261 }
262
263 if (dateMatches(DATE_TIME_FORMATTER, value)) {
264 return String.format("CAST('%s' AS DATE)", value);
265 }
266
267
268 if (value.startsWith("\"") && value.endsWith("\"")) {
269 value = value.substring(1, value.length() - 1);
270 }
271
272 return String.format("'%s'", value);
273 }
274
275 private String resolveDateValue(String value, boolean upperBound) {
276 if (dateMatches(DATE_FORMATTER, value)) {
277 if (upperBound) {
278 return String.format("CAST('%s' AS DATE)", getEndOfDay(value));
279 } else {
280 return String.format("CAST('%s' AS DATE)", getBeginningOfDay(value));
281 }
282 }
283 return String.format("CAST('%s' AS DATE)", value);
284 }
285
286 private boolean dateMatches(DateTimeFormatter dateTimeFormatter, String value) {
287 try {
288 dateTimeFormatter.parse(value);
289 return true;
290 } catch (DateTimeParseException e) {
291 return false;
292 }
293 }
294
295 private static String getBeginningOfDay(String date) {
296 LocalDate localDate = LocalDate.parse(date, DATE_FORMATTER);
297 ZonedDateTime zonedDateTime = localDate.atStartOfDay(ZoneId.systemDefault());
298 return zonedDateTime.format(DATE_TIME_FORMATTER);
299 }
300
301 private static String getEndOfDay(String date) {
302 LocalDate localDate = LocalDate.parse(date, DATE_FORMATTER);
303 ZonedDateTime zonedDateTime = localDate.atTime(LocalTime.MAX).atZone(ZoneId.systemDefault());
304 return zonedDateTime.format(DATE_TIME_FORMATTER);
305 }
306
307 @Override
308 public String toString() {
309 return asSqlString();
310 }
311 }