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.IN;
37 import static java.util.stream.Collectors.joining;
38
39 import java.time.LocalDate;
40 import java.time.ZoneId;
41 import java.time.ZonedDateTime;
42 import java.time.format.DateTimeFormatter;
43 import java.time.format.DateTimeParseException;
44 import java.util.Arrays;
45 import java.util.List;
46 import java.util.stream.Collectors;
47
48 import org.apache.commons.lang3.StringUtils;
49 import org.apache.commons.lang3.math.NumberUtils;
50
51
52
53
54 public class FilteringCondition {
55
56 private static final String NAME_PROPERTY = "@name";
57 private static final String PARENT_PROPERTY = "@ancestor";
58
59 static final String JCR_OFFSET_DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
60 static final String SIMPLE_DATE_PATTERN = "yyyy-MM-dd";
61
62 private static final String VALUES_SPLITTER = "\\|";
63
64 private static final String OPERATOR_OPEN = "[";
65 private static final String OPERATOR_CLOSE = "]";
66
67 private static final String RANGE_SYMBOL = "~";
68
69 private final String property;
70 private final Operator operator;
71 private final String[] values;
72
73
74
75
76
77 private final List<String[]> rangeValues;
78
79 public FilteringCondition(String propertyString, String valueString) {
80 if (StringUtils.isEmpty(propertyString)) {
81 throw new IllegalArgumentException("Property string is invalid.");
82 }
83
84 operator = extractOperator(propertyString);
85 property = StringUtils.substringBeforeLast(propertyString, OPERATOR_OPEN);
86 values = valueString.split(VALUES_SPLITTER);
87
88 rangeValues = extractRangeValues(values);
89 }
90
91 private Operator extractOperator(String propertyString) {
92 if (!propertyString.contains(OPERATOR_OPEN) && !propertyString.contains(OPERATOR_CLOSE)) {
93 return Operator.EQ;
94 }
95 return Operator.from(StringUtils.substringBetween(propertyString, OPERATOR_OPEN, OPERATOR_CLOSE));
96 }
97
98 private List<String[]> extractRangeValues(String[] values) {
99 return Arrays.stream(values)
100 .map(value -> value.split(RANGE_SYMBOL))
101 .filter(bounds -> bounds.length == 2)
102 .collect(Collectors.toList());
103 }
104
105 public String asSqlString() {
106 if (operator == IN) {
107 return rangeValues.stream()
108 .map(this::getInConditionClause)
109 .collect(joining(" OR "));
110 }
111 if (operator == Operator.NOT_IN) {
112 return rangeValues.stream()
113 .map(this::getNotInConditionClause)
114 .collect(joining(" OR "));
115 }
116 return Arrays.stream(values)
117 .map(this::getConditionClause)
118 .collect(joining(" OR "));
119 }
120
121 private String getInConditionClause(String[] bounds) {
122 return String.format("[%s] >= %s AND [%s] <= %s", property, resolveValue(bounds[0]), property, resolveValue(bounds[1]));
123 }
124
125 private String getNotInConditionClause(String[] bounds) {
126 return String.format("[%s] < %s OR [%s] > %s", property, resolveValue(bounds[0]), property, resolveValue(bounds[1]));
127 }
128
129 private String getConditionClause(String value) {
130 if (PARENT_PROPERTY.equalsIgnoreCase(property)) {
131 return String.format("ISDESCENDANTNODE('%s')", value);
132 }
133
134
135
136
137
138 if (NAME_PROPERTY.equalsIgnoreCase(property)) {
139 return String.format("LOWER(LOCALNAME(t)) LIKE '%s'", value);
140 }
141
142 return String.format("[%s] %s %s", property, operator.asSqlString(), resolveValue(value));
143 }
144
145 private String resolveValue(String value) {
146 if (NumberUtils.isNumber(value)) {
147 return value;
148 }
149
150 if (isMatchDateFormat(value, JCR_OFFSET_DATETIME_PATTERN)) {
151 return String.format("CAST('%s' AS DATE)", value);
152 }
153
154 if (isMatchDateFormat(value, SIMPLE_DATE_PATTERN)) {
155 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(SIMPLE_DATE_PATTERN);
156 LocalDate localDate = LocalDate.parse(value, dateTimeFormatter);
157 ZonedDateTime zonedDateTime = localDate.atStartOfDay(ZoneId.systemDefault());
158 String formattedDateTime = zonedDateTime.format(DateTimeFormatter.ofPattern(JCR_OFFSET_DATETIME_PATTERN));
159 return String.format("CAST('%s' AS DATE)", formattedDateTime);
160 }
161
162
163 if (value.startsWith("\"") && value.endsWith("\"")) {
164 value = value.substring(1, value.length() - 1);
165 }
166
167 return String.format("'%s'", value);
168 }
169
170 private boolean isMatchDateFormat(String value, String dateFormatString) {
171 try {
172 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormatString);
173 dateTimeFormatter.parse(value);
174
175 return true;
176 } catch (DateTimeParseException e) {
177
178 }
179 return false;
180 }
181
182 @Override
183 public String toString() {
184 return asSqlString();
185 }
186 }