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.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      * Split an attribute value recurring on provided literals/tokens.
182      *
183      * @param attrValue value to be split
184      * @param literals literals/tokens
185      * @return split value
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         // Schema keys
243         List<String> identifiers = new ArrayList<>();
244 
245         // Literals
246         List<String> literals = new ArrayList<>();
247 
248         // Get schema keys and literals
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         // Sort literals in order to process later literals included into others
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         // Split value on provided literals
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         // builder to build the clauses
289         StringBuilder bld = new StringBuilder();
290 
291         // Contains used identifiers in order to avoid replications
292         Set<String> used = new HashSet<>();
293 
294         // Create several clauses: one for eanch identifiers
295         for (int i = 0; i < identifiers.size(); i++) {
296             if (!used.contains(identifiers.get(i))) {
297                 // verify schema existence and get schema type
298                 PlainSchema schema = plainSchemaDAO.find(identifiers.get(i));
299                 if (schema == null) {
300                     LOG.error("Invalid schema '{}', ignoring", identifiers.get(i));
301                 } else {
302                     // clear builder
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         // check UNIQUE constraints
339         // cannot move to functional style due to the same issue reported at
340         // https://medium.com/xiumeteo-labs/stream-and-concurrentmodificationexception-2d14ed8ff4b2
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         // update sysInfo
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 }