Clover icon

Magnolia REST Content Delivery 2.1

  1. Project Clover database Fri Mar 16 2018 18:21:08 CET
  2. Package info.magnolia.rest.delivery.jcr

File FilteringCondition.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart10.png
0% of files have more coverage

Code metrics

42
96
18
1
307
212
50
0.52
5.33
18
2.78

Classes

Class Line # Actions
FilteringCondition 58 96 0% 50 4
0.97435997.4%
 

Contributing tests

This file is covered by 46 tests. .

Source view

1    /**
2    * This file Copyright (c) 2017-2018 Magnolia International
3    * Ltd. (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10    * This file is distributed in the hope that it will be
11    * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12    * implied warranty of MERCHANTABILITY or FITNESS FOR A
13    * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14    * Redistribution, except as permitted by whichever of the GPL
15    * or MNA you select, is prohibited.
16    *
17    * 1. For the GPL license (GPL), you can redistribute and/or
18    * modify this file under the terms of the GNU General
19    * Public License, Version 3, as published by the Free Software
20    * Foundation. You should have received a copy of the GNU
21    * General Public License, Version 3 along with this program;
22    * if not, write to the Free Software Foundation, Inc., 51
23    * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24    *
25    * 2. For the Magnolia Network Agreement (MNA), this file
26    * and the accompanying materials are made available under the
27    * terms of the MNA which accompanies this distribution, and
28    * is available at http://www.magnolia-cms.com/mna.html
29    *
30    * Any modifications to this file must keep this entire header
31    * intact.
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    * This class is used to get filtering condition SQL.
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    * Ranges are inclusive.
83    * For example, "price[in]=100~200" is translated into "price >= 100 AND price <= 200".
84    */
85    private final List<String[]> rangeValues;
86   
 
87  54 toggle public FilteringCondition(String propertyString, String valueString) {
88  54 if (StringUtils.isEmpty(propertyString)) {
89  1 throw new IllegalArgumentException("Property string is invalid.");
90    }
91   
92  53 operator = extractOperator(propertyString);
93  52 property = StringUtils.substringBeforeLast(propertyString, OPERATOR_OPEN);
94  52 values = valueString.split(VALUES_SPLITTER);
95   
96  52 validateValueString();
97   
98  50 isDateQuery = isDateQuery();
99   
100  50 if (isDateQuery) {
101  16 rangeValues = extractDateRangeValues(values);
102    } else {
103  34 rangeValues = extractRangeValues(values);
104    }
105    }
106   
 
107  52 toggle private void validateValueString() {
108    // If any value containing range symbol but not having enough 2 bounds, exception thrown.
109  52 if (Arrays.stream(values)
110    .anyMatch(value -> value.contains(RANGE_SYMBOL)
111    && value.split(RANGE_SYMBOL).length != 2)) {
112  1 throw new BadRequestException(INVALID_VALUE_STRING);
113    }
114   
115    // If date and range of dates are mixed, exception thrown.
116  51 if (Arrays.stream(values)
117    .anyMatch(value -> dateMatches(DATE_FORMATTER, value))
118    && Arrays.stream(values)
119    .anyMatch(value -> value.contains(RANGE_SYMBOL))) {
120  1 throw new BadRequestException(INVALID_VALUE_STRING);
121    }
122    }
123   
 
124  16 toggle private List<String[]> extractDateRangeValues(String[] values) {
125  16 List<String[]> rangeValues = new ArrayList<>();
126  16 for (String value : values) {
127  17 if (value.contains(RANGE_SYMBOL)) {
128  10 String[] bounds = value.split(RANGE_SYMBOL);
129  10 rangeValues.add(bounds);
130    } else {
131  7 String[] bounds = new String[] { value, value };
132  7 rangeValues.add(bounds);
133    }
134    }
135  16 return rangeValues;
136    }
137   
 
138  50 toggle private boolean isDateQuery() {
139    // If all values are dates, the condition is a date query.
140  50 if (Arrays.stream(values)
141    .allMatch(value -> dateMatches(DATE_FORMATTER, value))) {
142  7 return true;
143    }
144   
145    // If any value having invalid range value, the condition is not a date query.
146  43 for (String value : values) {
147  44 if (Arrays.stream(value.split(RANGE_SYMBOL))
148    .anyMatch(bound -> !dateMatches(DATE_FORMATTER, bound))) {
149  34 return false;
150    }
151    }
152   
153  9 return true;
154    }
155   
 
156  53 toggle private Operator extractOperator(String propertyString) {
157  53 if (!propertyString.contains(OPERATOR_OPEN) && !propertyString.contains(OPERATOR_CLOSE)) {
158  22 return Operator.EQ;
159    }
160  31 return Operator.from(StringUtils.substringBetween(propertyString, OPERATOR_OPEN, OPERATOR_CLOSE));
161    }
162   
 
163  34 toggle private List<String[]> extractRangeValues(String[] values) {
164  34 return Arrays.stream(values)
165    .map(value -> value.split(RANGE_SYMBOL))
166    .filter(bounds -> bounds.length == 2)
167    .collect(Collectors.toList());
168    }
169   
 
170  50 toggle public String asSqlString() {
171  50 if (isDateQuery) {
172  16 return rangeValues.stream()
173    .map(this::getDateConditionClause)
174    .collect(joining(" OR "));
175    }
176  34 if (operator == IN) {
177  5 return rangeValues.stream()
178    .map(this::getInConditionClause)
179    .collect(joining(" OR "));
180    }
181  29 if (operator == NOT_IN) {
182  2 return rangeValues.stream()
183    .map(this::getNotInConditionClause)
184    .collect(joining(" OR "));
185    }
186  27 return Arrays.stream(values)
187    .map(this::getConditionClause)
188    .collect(joining(" OR "));
189    }
190   
 
191  6 toggle private String getInConditionClause(String[] bounds) {
192  6 return String.format("[%s] >= %s AND [%s] <= %s", property, resolveValue(bounds[0]), property, resolveValue(bounds[1]));
193    }
194   
 
195  2 toggle private String getNotInConditionClause(String[] bounds) {
196  2 return String.format("[%s] < %s OR [%s] > %s", property, resolveValue(bounds[0]), property, resolveValue(bounds[1]));
197    }
198   
 
199  17 toggle private String getDateConditionClause(String[] bounds) {
200  17 switch (operator) {
201  2 case GT:
202  2 rangeIsNotSupported(bounds);
203  1 return String.format("[%s] > %s", property, resolveDateValue(bounds[0], true));
204  2 case LT:
205  2 rangeIsNotSupported(bounds);
206  1 return String.format("[%s] < %s", property, resolveDateValue(bounds[0], false));
207  2 case GTE:
208  2 rangeIsNotSupported(bounds);
209  1 return String.format("[%s] >= %s", property, resolveDateValue(bounds[0], false));
210  2 case LTE:
211  2 rangeIsNotSupported(bounds);
212  1 return String.format("[%s] <= %s", property, resolveDateValue(bounds[0], true));
213  2 case NE:
214  2 rangeIsNotSupported(bounds);
215  1 return String.format("[%s] < %s OR [%s] > %s", property, resolveDateValue(bounds[0], false),
216    property, resolveDateValue(bounds[0], true));
217  7 default:
218  7 String lowerBound = resolveDateValue(bounds[0], false);
219  7 String upperBound = resolveDateValue(bounds[1], true);
220   
221  7 if (operator == IN) {
222  3 return String.format("[%s] >= %s AND [%s] <= %s", property, lowerBound, property, upperBound);
223  4 } else if (operator == NOT_IN) {
224  1 return String.format("[%s] < %s OR [%s] > %s", property, lowerBound, property, upperBound);
225    }
226   
227  3 rangeIsNotSupported(bounds);
228  2 return String.format("[%s] >= %s AND [%s] <= %s", property, lowerBound, property, upperBound);
229    }
230    }
231   
 
232  13 toggle private void rangeIsNotSupported(String[] bounds) {
233  13 if (bounds.length == 2 && !bounds[0].equals(bounds[1])) {
234  6 throw new BadRequestException("Range is not supported.");
235    }
236    }
237   
 
238  33 toggle private String getConditionClause(String value) {
239  33 if (PARENT_PROPERTY.equalsIgnoreCase(property)) {
240  1 return String.format("ISDESCENDANTNODE('%s')", value);
241    }
242   
243    /**
244    * By testing, "LOCALNAME" must go with "LIKE" to give correct results.
245    * This use of "LIKE" is equivalent to "=".
246    */
247  32 if (NAME_PROPERTY.equalsIgnoreCase(property)) {
248  4 return String.format("LOWER(LOCALNAME(t)) LIKE '%s'", value);
249    }
250   
251  28 return String.format("[%s] %s %s", property, operator.asSqlString(), resolveValue(value));
252    }
253   
 
254  44 toggle private String resolveValue(String value) {
255  44 if (NumberUtils.isNumber(value)) {
256  16 return value;
257    }
258   
259  28 if (dateMatches(DATE_TIME_FORMATTER, value)) {
260  10 return String.format("CAST('%s' AS DATE)", value);
261    }
262   
263    // Remove the escape characters, treat as String.
264  18 if (value.startsWith("\"") && value.endsWith("\"")) {
265  6 value = value.substring(1, value.length() - 1);
266    }
267   
268  18 return String.format("'%s'", value);
269    }
270   
 
271  20 toggle private String resolveDateValue(String value, boolean upperBound) {
272  20 if (dateMatches(DATE_FORMATTER, value)) {
273  20 if (upperBound) {
274  10 return String.format("CAST('%s' AS DATE)", getEndOfDay(value));
275    } else {
276  10 return String.format("CAST('%s' AS DATE)", getBeginningOfDay(value));
277    }
278    }
279  0 return String.format("CAST('%s' AS DATE)", value);
280    }
281   
 
282  213 toggle private boolean dateMatches(DateTimeFormatter dateTimeFormatter, String value) {
283  213 try {
284  213 dateTimeFormatter.parse(value);
285  65 return true;
286    } catch (DateTimeParseException e) {
287  148 return false;
288    }
289    }
290   
 
291  10 toggle private static String getBeginningOfDay(String date) {
292  10 LocalDate localDate = LocalDate.parse(date, DATE_FORMATTER);
293  10 ZonedDateTime zonedDateTime = localDate.atStartOfDay(ZoneId.systemDefault());
294  10 return zonedDateTime.format(DATE_TIME_FORMATTER);
295    }
296   
 
297  10 toggle private static String getEndOfDay(String date) {
298  10 LocalDate localDate = LocalDate.parse(date, DATE_FORMATTER);
299  10 ZonedDateTime zonedDateTime = localDate.atTime(LocalTime.MAX).atZone(ZoneId.systemDefault());
300  10 return zonedDateTime.format(DATE_TIME_FORMATTER);
301    }
302   
 
303  0 toggle @Override
304    public String toString() {
305  0 return asSqlString();
306    }
307    }