View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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      * Split an attribute value recurring on provided literals/tokens.
253      *
254      * @param attrValue value to be split
255      * @param literals literals/tokens
256      * @return split value
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         // Schema keys
278         List<String> identifiers = new ArrayList<>();
279 
280         // Literals
281         List<String> literals = new ArrayList<>();
282 
283         // Get schema keys and literals
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         // Sort literals in order to process later literals included into others
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         // Split value on provided literals
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         // clauses to be used with INTERSECTed queries
322         Set<String> clauses = new HashSet<>();
323 
324         // builder to build the clauses
325         StringBuilder bld = new StringBuilder();
326 
327         // Contains used identifiers in order to avoid replications
328         Set<String> used = new HashSet<>();
329 
330         // Create several clauses: one for each identifiers
331         for (int i = 0; i < identifiers.size(); i++) {
332             if (!used.contains(identifiers.get(i))) {
333                 // verify schema existence and get schema type
334                 PlainSchema schema = plainSchemaDAO.find(identifiers.get(i));
335                 if (schema == null) {
336                     LOG.error("Invalid schema '{}', ignoring", identifiers.get(i));
337                 } else {
338                     // clear builder
339                     bld.delete(0, bld.length());
340 
341                     bld.append('(');
342 
343                     // set schema key
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                     // use a value clause different for each different schema type
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         // query string
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         // schemas given by type and aux classes
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         // schemas given by type extensions
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 }