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.sql.Connection;
22 import java.sql.PreparedStatement;
23 import java.sql.ResultSet;
24 import java.sql.SQLException;
25 import java.time.OffsetDateTime;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Optional;
33 import java.util.Set;
34 import java.util.regex.Pattern;
35 import javax.persistence.Query;
36 import javax.persistence.TypedQuery;
37 import org.apache.commons.jexl3.parser.Parser;
38 import org.apache.commons.jexl3.parser.ParserConstants;
39 import org.apache.commons.jexl3.parser.Token;
40 import org.apache.commons.lang3.StringUtils;
41 import org.apache.openjpa.persistence.OpenJPAPersistence;
42 import org.apache.syncope.core.persistence.api.dao.AllowedSchemas;
43 import org.apache.syncope.core.persistence.api.dao.AnyDAO;
44 import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
45 import org.apache.syncope.core.persistence.api.dao.DynRealmDAO;
46 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
47 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
48 import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
49 import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
50 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
51 import org.apache.syncope.core.persistence.api.entity.Any;
52 import org.apache.syncope.core.persistence.api.entity.AnyTypeClass;
53 import org.apache.syncope.core.persistence.api.entity.AnyUtils;
54 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
55 import org.apache.syncope.core.persistence.api.entity.DerSchema;
56 import org.apache.syncope.core.persistence.api.entity.DynRealm;
57 import org.apache.syncope.core.persistence.api.entity.ExternalResource;
58 import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue;
59 import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
60 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
61 import org.apache.syncope.core.persistence.api.entity.Schema;
62 import org.apache.syncope.core.persistence.api.entity.VirSchema;
63 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
64 import org.apache.syncope.core.persistence.api.entity.group.Group;
65 import org.apache.syncope.core.persistence.api.entity.user.User;
66 import org.springframework.transaction.annotation.Propagation;
67 import org.springframework.transaction.annotation.Transactional;
68
69 public abstract class AbstractAnyDAO<A extends Any<?>> extends AbstractDAO<A> implements AnyDAO<A> {
70
71 protected final AnyUtilsFactory anyUtilsFactory;
72
73 protected final PlainSchemaDAO plainSchemaDAO;
74
75 protected final DerSchemaDAO derSchemaDAO;
76
77 protected final DynRealmDAO dynRealmDAO;
78
79 private AnyUtils anyUtils;
80
81 public AbstractAnyDAO(
82 final AnyUtilsFactory anyUtilsFactory,
83 final PlainSchemaDAO plainSchemaDAO,
84 final DerSchemaDAO derSchemaDAO,
85 final DynRealmDAO dynRealmDAO) {
86
87 this.anyUtilsFactory = anyUtilsFactory;
88 this.plainSchemaDAO = plainSchemaDAO;
89 this.derSchemaDAO = derSchemaDAO;
90 this.dynRealmDAO = dynRealmDAO;
91 }
92
93 protected abstract AnyUtils init();
94
95 protected AnyUtils anyUtils() {
96 synchronized (this) {
97 if (anyUtils == null) {
98 anyUtils = init();
99 }
100 }
101 return anyUtils;
102 }
103
104 @SuppressWarnings("unchecked")
105 protected List<String> findAllKeys(final String table, final int page, final int itemsPerPage) {
106 Query query = entityManager().createNativeQuery(
107 "SELECT id FROM " + table + " ORDER BY id", String.class);
108 query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1));
109 query.setMaxResults(itemsPerPage);
110
111 List<String> result = new ArrayList<>();
112 query.getResultList().stream().map(resultKey -> resultKey instanceof Object[]
113 ? (String) ((Object[]) resultKey)[0]
114 : ((String) resultKey)).
115 forEach(actualKey -> result.add(actualKey.toString()));
116 return result;
117 }
118
119 protected OffsetDateTime findLastChange(final String key, final String table) {
120 OffsetDateTime creationDate = null;
121 OffsetDateTime lastChangeDate = null;
122
123 try (Connection conn = (Connection) OpenJPAPersistence.cast(entityManager()).getConnection()) {
124 try (PreparedStatement stmt =
125 conn.prepareStatement("SELECT creationDate, lastChangeDate FROM " + table + " WHERE id=?")) {
126 stmt.setString(1, key);
127
128 ResultSet rs = stmt.executeQuery();
129 if (rs.next()) {
130 creationDate = rs.getObject(1, OffsetDateTime.class);
131 lastChangeDate = rs.getObject(2, OffsetDateTime.class);
132 }
133 }
134 } catch (SQLException e) {
135 LOG.error("While reading {} from {}", key, table, e);
136 }
137
138 return Optional.ofNullable(lastChangeDate).orElse(creationDate);
139 }
140
141 protected abstract void securityChecks(A any);
142
143 @Transactional(readOnly = true)
144 @Override
145 public List<A> findByKeys(final List<String> keys) {
146 Class<A> entityClass = anyUtils().anyClass();
147 TypedQuery<A> query = entityManager().createQuery(
148 "SELECT e FROM " + entityClass.getSimpleName() + " e WHERE e.id IN (:keys)", entityClass);
149 query.setParameter("keys", keys);
150 return query.getResultList();
151 }
152
153 @Transactional(readOnly = true)
154 @Override
155 public A authFind(final String key) {
156 if (key == null) {
157 throw new NotFoundException("Null key");
158 }
159
160 A any = find(key);
161 if (any == null) {
162 throw new NotFoundException(StringUtils.substringBefore(
163 StringUtils.substringAfter(getClass().getSimpleName(), "JPA"), "DAO") + ' ' + key);
164 }
165
166 securityChecks(any);
167
168 return any;
169 }
170
171 @Transactional(readOnly = true)
172 @Override
173 @SuppressWarnings("unchecked")
174 public A find(final String key) {
175 return (A) entityManager().find(anyUtils().anyClass(), key);
176 }
177
178 private Query findByPlainAttrValueQuery(final String entityName, final boolean ignoreCaseMatch) {
179 String query = "SELECT e FROM " + entityName + " e"
180 + " WHERE e.attribute.schema.id = :schemaKey AND ((e.stringValue IS NOT NULL"
181 + " AND "
182 + (ignoreCaseMatch ? "LOWER(" : "") + "e.stringValue" + (ignoreCaseMatch ? ")" : "")
183 + " = "
184 + (ignoreCaseMatch ? "LOWER(" : "") + ":stringValue" + (ignoreCaseMatch ? ")" : "") + ')'
185 + " OR (e.booleanValue IS NOT NULL AND e.booleanValue = :booleanValue)"
186 + " OR (e.dateValue IS NOT NULL AND e.dateValue = :dateValue)"
187 + " OR (e.longValue IS NOT NULL AND e.longValue = :longValue)"
188 + " OR (e.doubleValue IS NOT NULL AND e.doubleValue = :doubleValue))";
189 return entityManager().createQuery(query);
190 }
191
192 @Override
193 @SuppressWarnings("unchecked")
194 public List<A> findByPlainAttrValue(
195 final PlainSchema schema,
196 final PlainAttrValue attrValue,
197 final boolean ignoreCaseMatch) {
198
199 if (schema == null) {
200 LOG.error("No PlainSchema");
201 return List.of();
202 }
203
204 String entityName = schema.isUniqueConstraint()
205 ? anyUtils().plainAttrUniqueValueClass().getName()
206 : anyUtils().plainAttrValueClass().getName();
207 Query query = findByPlainAttrValueQuery(entityName, ignoreCaseMatch);
208 query.setParameter("schemaKey", schema.getKey());
209 query.setParameter("stringValue", attrValue.getStringValue());
210 query.setParameter("booleanValue", attrValue.getBooleanValue());
211 if (attrValue.getDateValue() == null) {
212 query.setParameter("dateValue", null);
213 } else {
214 query.setParameter("dateValue", attrValue.getDateValue().toInstant());
215 }
216 query.setParameter("longValue", attrValue.getLongValue());
217 query.setParameter("doubleValue", attrValue.getDoubleValue());
218
219 List<A> result = new ArrayList<>();
220 ((List<PlainAttrValue>) query.getResultList()).stream().forEach(value -> {
221 A any = (A) value.getAttr().getOwner();
222 if (!result.contains(any)) {
223 result.add(any);
224 }
225 });
226
227 return result;
228 }
229
230 @Override
231 public Optional<A> findByPlainAttrUniqueValue(
232 final PlainSchema schema,
233 final PlainAttrUniqueValue attrUniqueValue,
234 final boolean ignoreCaseMatch) {
235
236 if (schema == null) {
237 LOG.error("No PlainSchema");
238 return Optional.empty();
239 }
240 if (!schema.isUniqueConstraint()) {
241 LOG.error("This schema has not unique constraint: '{}'", schema.getKey());
242 return Optional.empty();
243 }
244
245 List<A> result = findByPlainAttrValue(schema, attrUniqueValue, ignoreCaseMatch);
246 return result.isEmpty()
247 ? Optional.empty()
248 : Optional.of(result.get(0));
249 }
250
251
252
253
254
255
256
257
258 private static List<String> split(final String attrValue, final List<String> literals) {
259 final List<String> attrValues = new ArrayList<>();
260
261 if (literals.isEmpty()) {
262 attrValues.add(attrValue);
263 } else {
264 for (String token : attrValue.split(Pattern.quote(literals.get(0)))) {
265 if (!token.isEmpty()) {
266 attrValues.addAll(split(token, literals.subList(1, literals.size())));
267 }
268 }
269 }
270
271 return attrValues;
272 }
273
274 private Set<String> getWhereClause(final String expression, final String value, final boolean ignoreCaseMatch) {
275 Parser parser = new Parser(expression);
276
277
278 List<String> identifiers = new ArrayList<>();
279
280
281 List<String> literals = new ArrayList<>();
282
283
284 for (Token token = parser.getNextToken(); token != null && StringUtils.isNotBlank(token.toString());
285 token = parser.getNextToken()) {
286
287 if (token.kind == ParserConstants.STRING_LITERAL) {
288 literals.add(token.toString().substring(1, token.toString().length() - 1));
289 }
290
291 if (token.kind == ParserConstants.IDENTIFIER) {
292 identifiers.add(token.toString());
293 }
294 }
295
296
297 literals.sort((l1, l2) -> {
298 if (l1 == null && l2 == null) {
299 return 0;
300 } else if (l1 != null && l2 == null) {
301 return -1;
302 } else if (l1 == null) {
303 return 1;
304 } else if (l1.length() == l2.length()) {
305 return 0;
306 } else if (l1.length() > l2.length()) {
307 return -1;
308 } else {
309 return 1;
310 }
311 });
312
313
314 List<String> attrValues = split(value, literals);
315
316 if (attrValues.size() != identifiers.size()) {
317 LOG.error("Ambiguous JEXL expression resolution: literals and values have different size");
318 return Set.of();
319 }
320
321
322 Set<String> clauses = new HashSet<>();
323
324
325 StringBuilder bld = new StringBuilder();
326
327
328 Set<String> used = new HashSet<>();
329
330
331 for (int i = 0; i < identifiers.size(); i++) {
332 if (!used.contains(identifiers.get(i))) {
333
334 PlainSchema schema = plainSchemaDAO.find(identifiers.get(i));
335 if (schema == null) {
336 LOG.error("Invalid schema '{}', ignoring", identifiers.get(i));
337 } else {
338
339 bld.delete(0, bld.length());
340
341 bld.append('(');
342
343
344 bld.append("s.id = '").append(identifiers.get(i)).append('\'');
345
346 bld.append(" AND ");
347
348 bld.append("s.id = a.schema_id").append(" AND ");
349
350 bld.append("a.id = v.attribute_id");
351
352 bld.append(" AND ");
353
354
355 switch (schema.getType()) {
356 case Boolean:
357 bld.append("v.booleanValue = '").append(attrValues.get(i)).append('\'');
358 break;
359 case Long:
360 bld.append("v.longValue = ").append(attrValues.get(i));
361 break;
362 case Double:
363 bld.append("v.doubleValue = ").append(attrValues.get(i));
364 break;
365 case Date:
366 bld.append("v.dateValue = '").append(attrValues.get(i)).append('\'');
367 break;
368 default:
369 if (ignoreCaseMatch) {
370 bld.append("LOWER(v.stringValue) = '").
371 append(attrValues.get(i).toLowerCase()).append('\'');
372 } else {
373 bld.append("v.stringValue = '").
374 append(attrValues.get(i)).append('\'');
375 }
376 }
377
378 bld.append(')');
379
380 used.add(identifiers.get(i));
381
382 clauses.add(bld.toString());
383 }
384 }
385 }
386
387 LOG.debug("Generated where clauses {}", clauses);
388
389 return clauses;
390 }
391
392 @Override
393 public List<A> findByDerAttrValue(final DerSchema schema, final String value, final boolean ignoreCaseMatch) {
394 if (schema == null) {
395 LOG.error("No DerSchema");
396 return List.of();
397 }
398
399
400 StringBuilder querystring = new StringBuilder();
401
402 boolean subquery = false;
403 for (String clause : getWhereClause(schema.getExpression(), value, ignoreCaseMatch)) {
404 if (querystring.length() > 0) {
405 subquery = true;
406 querystring.append(" AND a.owner_id IN ( ");
407 }
408
409 querystring.append("SELECT a.owner_id ").
410 append("FROM ").append(anyUtils().plainAttrClass().getSimpleName().substring(3)).append(" a, ").
411 append(anyUtils().plainAttrValueClass().getSimpleName().substring(3)).append(" v, ").
412 append(PlainSchema.class.getSimpleName()).append(" s ").
413 append("WHERE ").append(clause);
414
415 if (subquery) {
416 querystring.append(')');
417 }
418 }
419
420 List<A> result = new ArrayList<>();
421 if (querystring.length() > 0) {
422 Query query = entityManager().createNativeQuery(querystring.toString());
423
424 for (Object anyKey : query.getResultList()) {
425 A any = find(anyKey.toString());
426 if (!result.contains(any)) {
427 result.add(any);
428 }
429 }
430 }
431
432 return result;
433 }
434
435 @SuppressWarnings("unchecked")
436 @Override
437 public List<A> findByResource(final ExternalResource resource) {
438 Query query = entityManager().createQuery("SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e "
439 + "WHERE :resource MEMBER OF e.resources");
440 query.setParameter("resource", resource);
441
442 return query.getResultList();
443 }
444
445 @Override
446 public SearchCond getAllMatchingCond() {
447 AnyCond idCond = new AnyCond(AttrCond.Type.ISNOTNULL);
448 idCond.setSchema("id");
449 return SearchCond.getLeaf(idCond);
450 }
451
452 @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
453 @Override
454 @SuppressWarnings("unchecked")
455 public <S extends Schema> AllowedSchemas<S> findAllowedSchemas(final A any, final Class<S> reference) {
456 AllowedSchemas<S> result = new AllowedSchemas<>();
457
458
459 Set<AnyTypeClass> typeOwnClasses = new HashSet<>();
460 typeOwnClasses.addAll(any.getType().getClasses());
461 typeOwnClasses.addAll(any.getAuxClasses());
462
463 typeOwnClasses.forEach(typeClass -> {
464 if (reference.equals(PlainSchema.class)) {
465 result.getForSelf().addAll((Collection<? extends S>) typeClass.getPlainSchemas());
466 } else if (reference.equals(DerSchema.class)) {
467 result.getForSelf().addAll((Collection<? extends S>) typeClass.getDerSchemas());
468 } else if (reference.equals(VirSchema.class)) {
469 result.getForSelf().addAll((Collection<? extends S>) typeClass.getVirSchemas());
470 }
471 });
472
473
474 Map<Group, List<? extends AnyTypeClass>> typeExtensionClasses = new HashMap<>();
475 if (any instanceof User) {
476 ((User) any).getMemberships().forEach(memb -> memb.getRightEnd().getTypeExtensions().
477 forEach(typeExt -> typeExtensionClasses.put(memb.getRightEnd(), typeExt.getAuxClasses())));
478 } else if (any instanceof AnyObject) {
479 ((AnyObject) any).getMemberships().forEach(memb -> memb.getRightEnd().getTypeExtensions().stream().
480 filter(typeExt -> any.getType().equals(typeExt.getAnyType())).
481 forEach(typeExt -> typeExtensionClasses.put(memb.getRightEnd(), typeExt.getAuxClasses())));
482 }
483
484 typeExtensionClasses.entrySet().stream().map(entry -> {
485 result.getForMemberships().put(entry.getKey(), new HashSet<>());
486 return entry;
487 }).forEach(entry -> entry.getValue().forEach(typeClass -> {
488 if (reference.equals(PlainSchema.class)) {
489 result.getForMemberships().get(entry.getKey()).
490 addAll((Collection<? extends S>) typeClass.getPlainSchemas());
491 } else if (reference.equals(DerSchema.class)) {
492 result.getForMemberships().get(entry.getKey()).
493 addAll((Collection<? extends S>) typeClass.getDerSchemas());
494 } else if (reference.equals(VirSchema.class)) {
495 result.getForMemberships().get(entry.getKey()).
496 addAll((Collection<? extends S>) typeClass.getVirSchemas());
497 }
498 }));
499
500 return result;
501 }
502
503 @Override
504 public A save(final A any) {
505 return entityManager().merge(any);
506 }
507
508 @Override
509 public void delete(final String key) {
510 A any = find(key);
511 if (any == null) {
512 return;
513 }
514
515 delete(any);
516 }
517
518 @Transactional(readOnly = true)
519 @Override
520 @SuppressWarnings("unchecked")
521 public List<String> findDynRealms(final String key) {
522 Query query = entityManager().createNativeQuery(
523 "SELECT dynRealm_id FROM " + JPADynRealmDAO.DYNMEMB_TABLE + " WHERE any_id=?");
524 query.setParameter(1, key);
525
526 List<String> result = new ArrayList<>();
527 query.getResultList().stream().map(resultKey -> resultKey instanceof Object[]
528 ? (String) ((Object[]) resultKey)[0]
529 : ((String) resultKey)).
530 forEach((actualKey) -> {
531 DynRealm dynRealm = dynRealmDAO.find(actualKey.toString());
532 if (dynRealm == null) {
533 LOG.error("Could not find dynRealm with id {}, even though returned by the native query",
534 actualKey);
535 } else if (!result.contains(actualKey.toString())) {
536 result.add(actualKey.toString());
537 }
538 });
539 return result;
540 }
541 }