1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.syncope.core.persistence.jpa.dao;
20
21 import java.time.OffsetDateTime;
22 import java.util.ArrayList;
23 import java.util.HashSet;
24 import java.util.LinkedHashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Optional;
28 import java.util.Set;
29 import java.util.StringJoiner;
30 import java.util.regex.Pattern;
31 import javax.persistence.Query;
32 import org.apache.commons.jexl3.parser.Parser;
33 import org.apache.commons.jexl3.parser.ParserConstants;
34 import org.apache.commons.jexl3.parser.Token;
35 import org.apache.commons.lang3.StringUtils;
36 import org.apache.commons.lang3.tuple.Pair;
37 import org.apache.syncope.common.lib.types.AnyTypeKind;
38 import org.apache.syncope.common.lib.types.AttrSchemaType;
39 import org.apache.syncope.core.persistence.api.dao.DuplicateException;
40 import org.apache.syncope.core.persistence.api.dao.JPAJSONAnyDAO;
41 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
42 import org.apache.syncope.core.persistence.api.entity.Any;
43 import org.apache.syncope.core.persistence.api.entity.AnyUtils;
44 import org.apache.syncope.core.persistence.api.entity.DerSchema;
45 import org.apache.syncope.core.persistence.api.entity.JSONPlainAttr;
46 import org.apache.syncope.core.persistence.api.entity.PlainAttr;
47 import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue;
48 import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
49 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
50 import org.apache.syncope.core.persistence.jpa.entity.AbstractEntity;
51 import org.apache.syncope.core.spring.security.AuthContextUtils;
52 import org.springframework.transaction.annotation.Transactional;
53
54 abstract class AbstractJPAJSONAnyDAO extends AbstractDAO<AbstractEntity> implements JPAJSONAnyDAO {
55
56 protected final PlainSchemaDAO plainSchemaDAO;
57
58 protected AbstractJPAJSONAnyDAO(final PlainSchemaDAO plainSchemaDAO) {
59 this.plainSchemaDAO = plainSchemaDAO;
60 }
61
62 protected String view(final String table) {
63 return StringUtils.containsIgnoreCase(table, AnyTypeKind.USER.name())
64 ? "user_search"
65 : StringUtils.containsIgnoreCase(table, AnyTypeKind.GROUP.name())
66 ? "group_search"
67 : "anyObject_search";
68 }
69
70 protected abstract String queryBegin(String table);
71
72 protected Pair<String, Boolean> schemaInfo(final AttrSchemaType schemaType, final boolean ignoreCaseMatch) {
73 String key;
74 boolean lower = false;
75
76 switch (schemaType) {
77 case Boolean:
78 key = "booleanValue";
79 break;
80
81 case Date:
82 key = "dateValue";
83 break;
84
85 case Double:
86 key = "doubleValue";
87 break;
88
89 case Long:
90 key = "longValue";
91 break;
92
93 case Binary:
94 key = "binaryValue";
95 break;
96
97 default:
98 lower = ignoreCaseMatch;
99 key = "stringValue";
100 }
101
102 return Pair.of(key, lower);
103 }
104
105 protected abstract String attrValueMatch(
106 AnyUtils anyUtils,
107 PlainSchema schema,
108 PlainAttrValue attrValue,
109 boolean ignoreCaseMatch);
110
111 protected Object getAttrValue(
112 final PlainSchema schema,
113 final PlainAttrValue attrValue,
114 final boolean ignoreCaseMatch) {
115
116 return attrValue.getValue();
117 }
118
119 protected <A extends Any<?>> List<A> buildResult(final AnyUtils anyUtils, final List<Object> queryResult) {
120 List<A> result = new ArrayList<>();
121 queryResult.forEach(anyKey -> {
122 A any = anyUtils.<A>dao().find(anyKey.toString());
123 if (any == null) {
124 LOG.error("Could not find any for key {}", anyKey);
125 } else {
126 result.add(any);
127 }
128 });
129 return result;
130 }
131
132 @SuppressWarnings("unchecked")
133 @Transactional(readOnly = true)
134 @Override
135 public <A extends Any<?>> List<A> findByPlainAttrValue(
136 final String table,
137 final AnyUtils anyUtils,
138 final PlainSchema schema,
139 final PlainAttrValue attrValue,
140 final boolean ignoreCaseMatch) {
141
142 if (schema == null) {
143 LOG.error("No PlainSchema");
144 return List.of();
145 }
146
147 Query query = entityManager().createNativeQuery(
148 queryBegin(table)
149 + "WHERE " + attrValueMatch(anyUtils, schema, attrValue, ignoreCaseMatch));
150 query.setParameter(1, schema.getKey());
151 query.setParameter(2, getAttrValue(schema, attrValue, ignoreCaseMatch));
152
153 return buildResult(anyUtils, query.getResultList());
154 }
155
156 @Transactional(readOnly = true)
157 @Override
158 public <A extends Any<?>> Optional<A> findByPlainAttrUniqueValue(
159 final String table,
160 final AnyUtils anyUtils,
161 final PlainSchema schema,
162 final PlainAttrUniqueValue attrUniqueValue,
163 final boolean ignoreCaseMatch) {
164
165 if (schema == null) {
166 LOG.error("No PlainSchema");
167 return Optional.empty();
168 }
169 if (!schema.isUniqueConstraint()) {
170 LOG.error("This schema has not unique constraint: '{}'", schema.getKey());
171 return Optional.empty();
172 }
173
174 List<A> result = findByPlainAttrValue(table, anyUtils, schema, attrUniqueValue, ignoreCaseMatch);
175 return result.isEmpty()
176 ? Optional.empty()
177 : Optional.of(result.get(0));
178 }
179
180
181
182
183
184
185
186
187 protected List<String> split(final String attrValue, final List<String> literals) {
188 List<String> attrValues = new ArrayList<>();
189
190 if (literals.isEmpty()) {
191 attrValues.add(attrValue);
192 } else {
193 for (String token : attrValue.split(Pattern.quote(literals.get(0)))) {
194 if (!token.isEmpty()) {
195 attrValues.addAll(split(token, literals.subList(1, literals.size())));
196 }
197 }
198 }
199
200 return attrValues;
201 }
202
203 @SuppressWarnings("unchecked")
204 protected List<Object> findByDerAttrValue(
205 final String table,
206 final Map<String, List<Object>> clauses) {
207
208 StringJoiner actualClauses = new StringJoiner(" AND id IN ");
209 List<Object> queryParams = new ArrayList<>();
210
211 clauses.forEach((clause, parameters) -> {
212 actualClauses.add(clause);
213 queryParams.addAll(parameters);
214 });
215
216 Query query = entityManager().createNativeQuery(
217 "SELECT DISTINCT id FROM " + table + " u WHERE id IN " + actualClauses.toString());
218 for (int i = 0; i < queryParams.size(); i++) {
219 query.setParameter(i + 1, queryParams.get(i));
220 }
221
222 return query.getResultList();
223 }
224
225 @SuppressWarnings("unchecked")
226 @Transactional(readOnly = true)
227 @Override
228 public <A extends Any<?>> List<A> findByDerAttrValue(
229 final String table,
230 final AnyUtils anyUtils,
231 final DerSchema derSchema,
232 final String value,
233 final boolean ignoreCaseMatch) {
234
235 if (derSchema == null) {
236 LOG.error("No DerSchema");
237 return List.of();
238 }
239
240 Parser parser = new Parser(derSchema.getExpression());
241
242
243 List<String> identifiers = new ArrayList<>();
244
245
246 List<String> literals = new ArrayList<>();
247
248
249 for (Token token = parser.getNextToken(); token != null && StringUtils.isNotBlank(token.toString());
250 token = parser.getNextToken()) {
251
252 if (token.kind == ParserConstants.STRING_LITERAL) {
253 literals.add(token.toString().substring(1, token.toString().length() - 1));
254 }
255
256 if (token.kind == ParserConstants.IDENTIFIER) {
257 identifiers.add(token.toString());
258 }
259 }
260
261
262 literals.sort((l1, l2) -> {
263 if (l1 == null && l2 == null) {
264 return 0;
265 } else if (l1 != null && l2 == null) {
266 return -1;
267 } else if (l1 == null) {
268 return 1;
269 } else if (l1.length() == l2.length()) {
270 return 0;
271 } else if (l1.length() > l2.length()) {
272 return -1;
273 } else {
274 return 1;
275 }
276 });
277
278
279 List<String> attrValues = split(value, literals);
280
281 if (attrValues.size() != identifiers.size()) {
282 LOG.error("Ambiguous JEXL expression resolution: literals and values have different size");
283 return List.of();
284 }
285
286 Map<String, List<Object>> clauses = new LinkedHashMap<>();
287
288
289 StringBuilder bld = new StringBuilder();
290
291
292 Set<String> used = new HashSet<>();
293
294
295 for (int i = 0; i < identifiers.size(); i++) {
296 if (!used.contains(identifiers.get(i))) {
297
298 PlainSchema schema = plainSchemaDAO.find(identifiers.get(i));
299 if (schema == null) {
300 LOG.error("Invalid schema '{}', ignoring", identifiers.get(i));
301 } else {
302
303 bld.delete(0, bld.length());
304
305 PlainAttrValue attrValue;
306 if (schema.isUniqueConstraint()) {
307 attrValue = anyUtils.newPlainAttrUniqueValue();
308 } else {
309 attrValue = anyUtils.newPlainAttrValue();
310 }
311 attrValue.setStringValue(attrValues.get(i));
312
313 bld.append('(').
314 append(queryBegin(table)).
315 append("WHERE ").
316 append(attrValueMatch(anyUtils, schema, attrValue, ignoreCaseMatch)).
317 append(')');
318
319 used.add(identifiers.get(i));
320
321 List<Object> queryParams = new ArrayList<>();
322 queryParams.add(schema.getKey());
323 queryParams.add(getAttrValue(schema, attrValue, ignoreCaseMatch));
324
325 clauses.put(bld.toString(), queryParams);
326 }
327 }
328 }
329
330 LOG.debug("Generated where clauses {}", clauses);
331
332 return buildResult(anyUtils, findByDerAttrValue(table, clauses));
333 }
334
335 @Transactional
336 @Override
337 public <A extends Any<?>> void checkBeforeSave(final String table, final AnyUtils anyUtils, final A any) {
338
339
340
341 for (PlainAttr<?> attr : any.getPlainAttrs()) {
342 if (attr.getUniqueValue() != null && attr instanceof JSONPlainAttr) {
343 PlainSchema schema = attr.getSchema();
344 Optional<A> other = findByPlainAttrUniqueValue(table, anyUtils, schema, attr.getUniqueValue(), false);
345 if (other.isEmpty() || other.get().getKey().equals(any.getKey())) {
346 LOG.debug("No duplicate value found for {}", attr.getUniqueValue().getValueAsString());
347 } else {
348 throw new DuplicateException(
349 "Value " + attr.getUniqueValue().getValueAsString()
350 + " existing for " + schema.getKey());
351 }
352 }
353 }
354
355
356 OffsetDateTime now = OffsetDateTime.now();
357 String who = AuthContextUtils.getWho();
358 LOG.debug("Set last change date '{}' and modifier '{}' for '{}'", now, who, any);
359 any.setLastModifier(who);
360 any.setLastChangeDate(now);
361 }
362 }