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.lang.reflect.Field;
22  import java.time.OffsetDateTime;
23  import java.util.ArrayList;
24  import java.util.List;
25  import java.util.Objects;
26  import java.util.Optional;
27  import java.util.stream.Collectors;
28  import javax.persistence.ManyToOne;
29  import javax.persistence.NoResultException;
30  import javax.persistence.OneToMany;
31  import javax.persistence.OneToOne;
32  import javax.persistence.Query;
33  import javax.persistence.TypedQuery;
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.syncope.common.lib.to.PropagationTaskTO;
36  import org.apache.syncope.common.lib.types.AnyTypeKind;
37  import org.apache.syncope.common.lib.types.ExecStatus;
38  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
39  import org.apache.syncope.common.lib.types.TaskType;
40  import org.apache.syncope.core.persistence.api.dao.RealmDAO;
41  import org.apache.syncope.core.persistence.api.dao.RemediationDAO;
42  import org.apache.syncope.core.persistence.api.dao.TaskDAO;
43  import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
44  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
45  import org.apache.syncope.core.persistence.api.entity.Implementation;
46  import org.apache.syncope.core.persistence.api.entity.Notification;
47  import org.apache.syncope.core.persistence.api.entity.Realm;
48  import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
49  import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
50  import org.apache.syncope.core.persistence.api.entity.task.PullTask;
51  import org.apache.syncope.core.persistence.api.entity.task.PushTask;
52  import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
53  import org.apache.syncope.core.persistence.api.entity.task.Task;
54  import org.apache.syncope.core.persistence.api.entity.task.TaskUtils;
55  import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory;
56  import org.apache.syncope.core.persistence.jpa.entity.task.JPAMacroTask;
57  import org.apache.syncope.core.persistence.jpa.entity.task.JPANotificationTask;
58  import org.apache.syncope.core.persistence.jpa.entity.task.JPAPropagationTask;
59  import org.apache.syncope.core.persistence.jpa.entity.task.JPAPropagationTaskExec;
60  import org.apache.syncope.core.persistence.jpa.entity.task.JPAPullTask;
61  import org.apache.syncope.core.persistence.jpa.entity.task.JPAPushTask;
62  import org.apache.syncope.core.persistence.jpa.entity.task.JPASchedTask;
63  import org.apache.syncope.core.spring.security.AuthContextUtils;
64  import org.apache.syncope.core.spring.security.SecurityProperties;
65  import org.springframework.transaction.annotation.Transactional;
66  import org.springframework.util.CollectionUtils;
67  import org.springframework.util.ReflectionUtils;
68  
69  public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
70  
71      protected final RealmDAO realmDAO;
72  
73      protected final RemediationDAO remediationDAO;
74  
75      protected final TaskUtilsFactory taskUtilsFactory;
76  
77      protected final SecurityProperties securityProperties;
78  
79      public JPATaskDAO(
80              final RealmDAO realmDAO,
81              final RemediationDAO remediationDAO,
82              final TaskUtilsFactory taskUtilsFactory,
83              final SecurityProperties securityProperties) {
84  
85          this.realmDAO = realmDAO;
86          this.remediationDAO = remediationDAO;
87          this.taskUtilsFactory = taskUtilsFactory;
88          this.securityProperties = securityProperties;
89      }
90  
91      @Transactional(readOnly = true)
92      @Override
93      public boolean exists(final TaskType type, final String key) {
94          Query query = entityManager().createNativeQuery("SELECT id FROM "
95                  + taskUtilsFactory.getInstance(type).getTaskTable()
96                  + " WHERE id=?");
97          query.setParameter(1, key);
98  
99          return !query.getResultList().isEmpty();
100     }
101 
102     @Transactional(readOnly = true)
103     @SuppressWarnings("unchecked")
104     @Override
105     public <T extends Task<T>> T find(final TaskType type, final String key) {
106         return (T) entityManager().find(taskUtilsFactory.getInstance(type).getTaskEntity(), key);
107     }
108 
109     @Transactional(readOnly = true)
110     @SuppressWarnings("unchecked")
111     @Override
112     public <T extends SchedTask> Optional<T> findByName(final TaskType type, final String name) {
113         TaskUtils taskUtils = taskUtilsFactory.getInstance(type);
114         TypedQuery<T> query = (TypedQuery<T>) entityManager().createQuery(
115                 "SELECT e FROM " + taskUtils.getTaskEntity().getSimpleName() + " e WHERE e.name = :name",
116                 taskUtils.getTaskEntity());
117         query.setParameter("name", name);
118 
119         try {
120             return Optional.of(query.getSingleResult());
121         } catch (NoResultException e) {
122             LOG.debug("No task found with name {}", name, e);
123             return Optional.empty();
124         }
125     }
126 
127     @Override
128     public Optional<Task<?>> find(final String key) {
129         Task<?> task = find(TaskType.SCHEDULED, key);
130         if (task == null) {
131             task = find(TaskType.PULL, key);
132         }
133         if (task == null) {
134             task = find(TaskType.PUSH, key);
135         }
136         if (task == null) {
137             task = find(TaskType.MACRO, key);
138         }
139         if (task == null) {
140             task = find(TaskType.PROPAGATION, key);
141         }
142         if (task == null) {
143             task = find(TaskType.NOTIFICATION, key);
144         }
145 
146         return Optional.ofNullable(task);
147     }
148 
149     @Override
150     public List<SchedTask> findByDelegate(final Implementation delegate) {
151         TypedQuery<SchedTask> query = entityManager().createQuery(
152                 "SELECT e FROM " + JPASchedTask.class.getSimpleName()
153                 + " e WHERE e.jobDelegate=:delegate", SchedTask.class);
154         query.setParameter("delegate", delegate);
155 
156         return query.getResultList();
157     }
158 
159     @Override
160     public List<PullTask> findByReconFilterBuilder(final Implementation reconFilterBuilder) {
161         TypedQuery<PullTask> query = entityManager().createQuery(
162                 "SELECT e FROM " + JPAPullTask.class.getSimpleName()
163                 + " e WHERE e.reconFilterBuilder=:reconFilterBuilder", PullTask.class);
164         query.setParameter("reconFilterBuilder", reconFilterBuilder);
165 
166         return query.getResultList();
167     }
168 
169     @Override
170     public List<PullTask> findByPullActions(final Implementation pullActions) {
171         TypedQuery<PullTask> query = entityManager().createQuery(
172                 "SELECT e FROM " + JPAPullTask.class.getSimpleName() + " e "
173                 + "WHERE :pullActions MEMBER OF e.actions", PullTask.class);
174         query.setParameter("pullActions", pullActions);
175 
176         return query.getResultList();
177     }
178 
179     @Override
180     public List<PushTask> findByPushActions(final Implementation pushActions) {
181         TypedQuery<PushTask> query = entityManager().createQuery(
182                 "SELECT e FROM " + JPAPushTask.class.getSimpleName() + " e "
183                 + "WHERE :pushActions MEMBER OF e.actions", PushTask.class);
184         query.setParameter("pushActions", pushActions);
185 
186         return query.getResultList();
187     }
188 
189     @Override
190     public List<MacroTask> findByRealm(final Realm realm) {
191         TypedQuery<MacroTask> query = entityManager().createQuery(
192                 "SELECT e FROM " + JPAMacroTask.class.getSimpleName() + " e "
193                 + "WHERE e.realm=:realm", MacroTask.class);
194         query.setParameter("realm", realm);
195 
196         return query.getResultList();
197     }
198 
199     @Override
200     public List<MacroTask> findByCommand(final Implementation command) {
201         TypedQuery<MacroTask> query = entityManager().createQuery("SELECT e FROM " + JPAMacroTask.class.getSimpleName()
202                 + " e WHERE :command MEMBER OF e.commands", MacroTask.class);
203         query.setParameter("command", command);
204 
205         return query.getResultList();
206     }
207 
208     protected final <T extends Task<T>> StringBuilder buildFindAllQueryJPA(final TaskType type) {
209         StringBuilder builder = new StringBuilder("SELECT t FROM ").
210                 append(taskUtilsFactory.getInstance(type).getTaskEntity().getSimpleName()).
211                 append(" t WHERE ");
212         if (type == TaskType.SCHEDULED) {
213             builder.append("t.id NOT IN (SELECT t.id FROM ").
214                     append(JPAPushTask.class.getSimpleName()).append(" t) ").
215                     append("AND ").
216                     append("t.id NOT IN (SELECT t.id FROM ").
217                     append(JPAPullTask.class.getSimpleName()).append(" t)");
218         } else {
219             builder.append("1=1");
220         }
221 
222         return builder.append(' ');
223     }
224 
225     @Override
226     @SuppressWarnings("unchecked")
227     public <T extends Task<T>> List<T> findToExec(final TaskType type) {
228         StringBuilder queryString = buildFindAllQueryJPA(type).append("AND ");
229 
230         if (type == TaskType.NOTIFICATION) {
231             queryString.append("t.executed = false ");
232         } else {
233             queryString.append("t.executions IS EMPTY ");
234         }
235         queryString.append("ORDER BY t.id DESC");
236 
237         Query query = entityManager().createQuery(queryString.toString());
238         return query.getResultList();
239     }
240 
241     @Transactional(readOnly = true)
242     @Override
243     public <T extends Task<T>> List<T> findAll(final TaskType type) {
244         return findAll(type, null, null, null, null, -1, -1, List.of());
245     }
246 
247     protected int setParameter(final List<Object> parameters, final Object parameter) {
248         parameters.add(parameter);
249         return parameters.size();
250     }
251 
252     protected StringBuilder buildFindAllQuery(
253             final TaskType type,
254             final ExternalResource resource,
255             final Notification notification,
256             final AnyTypeKind anyTypeKind,
257             final String entityKey,
258             final boolean orderByTaskExecInfo,
259             final List<Object> parameters) {
260 
261         if (resource != null
262                 && type != TaskType.PROPAGATION && type != TaskType.PUSH && type != TaskType.PULL) {
263 
264             throw new IllegalArgumentException(type + " is not related to " + ExternalResource.class.getSimpleName());
265         }
266 
267         if ((anyTypeKind != null || entityKey != null)
268                 && type != TaskType.PROPAGATION && type != TaskType.NOTIFICATION) {
269 
270             throw new IllegalArgumentException(type + " is not related to users, groups or any objects");
271         }
272 
273         if (notification != null && type != TaskType.NOTIFICATION) {
274             throw new IllegalArgumentException(type + " is not related to notifications");
275         }
276 
277         String taskTable = taskUtilsFactory.getInstance(type).getTaskTable();
278         StringBuilder queryString = new StringBuilder("SELECT ").append(taskTable).append(".*");
279 
280         if (orderByTaskExecInfo) {
281             String taskExecTable = taskUtilsFactory.getInstance(type).getTaskExecTable();
282             queryString.append(',').append(taskExecTable).append(".startDate AS startDate").
283                     append(',').append(taskExecTable).append(".endDate AS endDate").
284                     append(',').append(taskExecTable).append(".status AS status").
285                     append(" FROM ").append(taskTable).
286                     append(',').append(taskExecTable).append(',').append("(SELECT ").
287                     append(taskExecTable).append(".task_id, ").
288                     append("MAX(").append(taskExecTable).append(".startDate) AS startDate").
289                     append(" FROM ").append(taskExecTable).
290                     append(" GROUP BY ").append(taskExecTable).append(".task_id) GRP").
291                     append(" WHERE ").
292                     append(taskTable).append(".id=").append(taskExecTable).append(".task_id").
293                     append(" AND ").append(taskTable).append(".id=").append("GRP.task_id").
294                     append(" AND ").
295                     append(taskExecTable).append(".startDate=").append("GRP.startDate");
296         } else {
297             queryString.append(", null AS startDate, null AS endDate, null AS status FROM ").append(taskTable).
298                     append(" WHERE 1=1");
299         }
300 
301         queryString.append(' ');
302 
303         if (resource != null) {
304             queryString.append(" AND ").
305                     append(taskTable).append(".resource_id=?").append(setParameter(parameters, resource.getKey()));
306         }
307         if (notification != null) {
308             queryString.append(" AND ").
309                     append(taskTable).
310                     append(".notification_id=?").append(setParameter(parameters, notification.getKey()));
311         }
312         if (anyTypeKind != null) {
313             queryString.append(" AND ").
314                     append(taskTable).append(".anyTypeKind=?").append(setParameter(parameters, anyTypeKind.name()));
315         }
316         if (entityKey != null) {
317             queryString.append(" AND ").
318                     append(taskTable).append(".entityKey=?").append(setParameter(parameters, entityKey));
319         }
320         if (type == TaskType.MACRO
321                 && !AuthContextUtils.getUsername().equals(securityProperties.getAdminUser())) {
322 
323             String realmKeysArg = AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.TASK_LIST).stream().
324                     map(realmDAO::findByFullPath).
325                     filter(Objects::nonNull).
326                     flatMap(r -> realmDAO.findDescendants(r.getFullPath(), null, -1, -1).stream()).
327                     map(Realm::getKey).
328                     distinct().
329                     map(realmKey -> "?" + setParameter(parameters, realmKey)).
330                     collect(Collectors.joining(","));
331             queryString.append(" AND ").
332                     append(taskTable).append(".realm_id IN (").append(realmKeysArg).append(")");
333         }
334 
335         return queryString;
336     }
337 
338     protected String toOrderByStatement(
339             final Class<? extends Task<?>> beanClass,
340             final List<OrderByClause> orderByClauses) {
341 
342         StringBuilder statement = new StringBuilder();
343 
344         statement.append(" ORDER BY ");
345 
346         StringBuilder subStatement = new StringBuilder();
347         orderByClauses.forEach(clause -> {
348             String field = clause.getField().trim();
349             switch (field) {
350                 case "latestExecStatus":
351                     field = "status";
352                     break;
353 
354                 case "start":
355                     field = "startDate";
356                     break;
357 
358                 case "end":
359                     field = "endDate";
360                     break;
361 
362                 default:
363                     Field beanField = ReflectionUtils.findField(beanClass, field);
364                     if (beanField != null
365                             && (beanField.getAnnotation(ManyToOne.class) != null
366                             || beanField.getAnnotation(OneToMany.class) != null
367                             || beanField.getAnnotation(OneToOne.class) != null)) {
368 
369                         field += "_id";
370                     }
371             }
372 
373             subStatement.append(field).append(' ').append(clause.getDirection().name()).append(',');
374         });
375 
376         if (subStatement.length() == 0) {
377             statement.append("id DESC");
378         } else {
379             subStatement.deleteCharAt(subStatement.length() - 1);
380             statement.append(subStatement);
381         }
382 
383         return statement.toString();
384     }
385 
386     @Override
387     public <T extends Task<T>> List<T> findAll(
388             final TaskType type,
389             final ExternalResource resource,
390             final Notification notification,
391             final AnyTypeKind anyTypeKind,
392             final String entityKey,
393             final int page,
394             final int itemsPerPage,
395             final List<OrderByClause> orderByClauses) {
396 
397         List<Object> parameters = new ArrayList<>();
398 
399         boolean orderByTaskExecInfo = orderByClauses.stream().
400                 anyMatch(clause -> clause.getField().equals("start")
401                 || clause.getField().equals("end")
402                 || clause.getField().equals("latestExecStatus")
403                 || clause.getField().equals("status"));
404 
405         StringBuilder queryString = buildFindAllQuery(
406                 type,
407                 resource,
408                 notification,
409                 anyTypeKind,
410                 entityKey,
411                 orderByTaskExecInfo,
412                 parameters);
413 
414         if (orderByTaskExecInfo) {
415             // UNION with tasks without executions...
416             queryString.insert(0, "SELECT T.id FROM ((").append(") UNION ALL (").
417                     append(buildFindAllQuery(
418                             type,
419                             resource,
420                             notification,
421                             anyTypeKind,
422                             entityKey,
423                             false,
424                             parameters)).
425                     append(" AND id NOT IN ").
426                     append("(SELECT task_id AS id FROM ").
427                     append(taskUtilsFactory.getInstance(type).getTaskExecTable()).
428                     append(')').
429                     append(")) T");
430         } else {
431             queryString.insert(0, "SELECT T.id FROM (").append(") T");
432         }
433 
434         queryString.append(toOrderByStatement(taskUtilsFactory.getInstance(type).getTaskEntity(), orderByClauses));
435 
436         Query query = entityManager().createNativeQuery(queryString.toString());
437 
438         for (int i = 1; i <= parameters.size(); i++) {
439             query.setParameter(i, parameters.get(i - 1));
440         }
441 
442         query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1));
443 
444         if (itemsPerPage > 0) {
445             query.setMaxResults(itemsPerPage);
446         }
447 
448         List<T> result = new ArrayList<>();
449 
450         @SuppressWarnings("unchecked")
451         List<Object> raw = query.getResultList();
452         raw.stream().map(key -> key instanceof Object[]
453                 ? (String) ((Object[]) key)[0]
454                 : ((String) key)).forEach(key -> {
455 
456             T task = find(type, key);
457             if (task == null) {
458                 LOG.error("Could not find task with key {}, even if returned by native query", key);
459             } else if (!result.contains(task)) {
460                 result.add(task);
461             }
462         });
463 
464         return result;
465     }
466 
467     @Override
468     public int count(
469             final TaskType type,
470             final ExternalResource resource,
471             final Notification notification,
472             final AnyTypeKind anyTypeKind,
473             final String entityKey) {
474 
475         List<Object> parameters = new ArrayList<>();
476 
477         StringBuilder queryString =
478                 buildFindAllQuery(type, resource, notification, anyTypeKind, entityKey, false, parameters);
479 
480         String table = taskUtilsFactory.getInstance(type).getTaskTable();
481         Query query = entityManager().createNativeQuery(StringUtils.replaceOnce(
482                 queryString.toString(),
483                 "SELECT " + table + ".*, null AS startDate, null AS endDate, null AS status",
484                 "SELECT COUNT(" + table + ".id)"));
485 
486         for (int i = 1; i <= parameters.size(); i++) {
487             query.setParameter(i, parameters.get(i - 1));
488         }
489 
490         return ((Number) query.getSingleResult()).intValue();
491     }
492 
493     @Transactional(rollbackFor = { Throwable.class })
494     @Override
495     public <T extends Task<T>> T save(final T task) {
496         if (task instanceof JPANotificationTask) {
497             ((JPANotificationTask) task).list2json();
498         } else if (task instanceof JPAPushTask) {
499             ((JPAPushTask) task).map2json();
500         }
501         return entityManager().merge(task);
502     }
503 
504     @Override
505     public void delete(final TaskType type, final String id) {
506         Task<?> task = find(type, id);
507         if (task == null) {
508             return;
509         }
510 
511         delete(task);
512     }
513 
514     @Override
515     public void delete(final Task<?> task) {
516         if (task instanceof PullTask) {
517             remediationDAO.findByPullTask((PullTask) task).forEach(remediation -> remediation.setPullTask(null));
518         }
519 
520         entityManager().remove(task);
521     }
522 
523     @Override
524     public void deleteAll(final ExternalResource resource, final TaskType type) {
525         findAll(type, resource, null, null, null, -1, -1, List.of()).
526                 stream().map(Task<?>::getKey).forEach(key -> delete(type, key));
527     }
528 
529     @Override
530     public List<PropagationTaskTO> purgePropagations(
531             final OffsetDateTime since,
532             final List<ExecStatus> statuses,
533             final List<ExternalResource> externalResources) {
534 
535         StringBuilder queryString = new StringBuilder("SELECT t.task_id "
536                 + "FROM " + JPAPropagationTaskExec.TABLE + " t "
537                 + "INNER JOIN " + JPAPropagationTask.TABLE + " z "
538                 + "ON t.task_id=z.id "
539                 + "WHERE t.enddate=(SELECT MAX(e.enddate) FROM " + JPAPropagationTaskExec.TABLE + " e "
540                 + "WHERE e.task_id=t.task_id) ");
541 
542         List<Object> queryParameters = new ArrayList<>();
543         if (since != null) {
544             queryParameters.add(since);
545             queryString.append("AND t.enddate <= ?").append(queryParameters.size()).append(' ');
546         }
547         if (!CollectionUtils.isEmpty(statuses)) {
548             queryString.append("AND (").
549                     append(statuses.stream().map(status -> {
550                         queryParameters.add(status.name());
551                         return "t.status = ?" + queryParameters.size();
552                     }).collect(Collectors.joining(" OR "))).
553                     append(")");
554         }
555         if (!CollectionUtils.isEmpty(externalResources)) {
556             queryString.append("AND (").
557                     append(externalResources.stream().map(externalResource -> {
558                         queryParameters.add(externalResource.getKey());
559                         return "z.resource_id = ?" + queryParameters.size();
560                     }).collect(Collectors.joining(" OR "))).
561                     append(")");
562         }
563 
564         Query query = entityManager().createNativeQuery(queryString.toString());
565         for (int i = 1; i <= queryParameters.size(); i++) {
566             query.setParameter(i, queryParameters.get(i - 1));
567         }
568 
569         @SuppressWarnings("unchecked")
570         List<Object> raw = query.getResultList();
571 
572         List<PropagationTaskTO> purged = new ArrayList<>();
573         raw.stream().map(Object::toString).distinct().forEach(key -> {
574             PropagationTask task = find(TaskType.PROPAGATION, key);
575             if (task != null) {
576                 PropagationTaskTO taskTO = new PropagationTaskTO();
577 
578                 taskTO.setOperation(task.getOperation());
579                 taskTO.setConnObjectKey(task.getConnObjectKey());
580                 taskTO.setOldConnObjectKey(task.getOldConnObjectKey());
581                 taskTO.setPropagationData(task.getSerializedPropagationData());
582                 taskTO.setResource(task.getResource().getKey());
583                 taskTO.setObjectClassName(task.getObjectClassName());
584                 taskTO.setAnyTypeKind(task.getAnyTypeKind());
585                 taskTO.setAnyType(task.getAnyType());
586                 taskTO.setEntityKey(task.getEntityKey());
587 
588                 purged.add(taskTO);
589 
590                 delete(task);
591             }
592         });
593 
594         return purged;
595     }
596 }