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.logic;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.OutputStream;
23  import java.lang.reflect.Method;
24  import java.time.OffsetDateTime;
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Optional;
29  import java.util.stream.Collectors;
30  import java.util.zip.ZipInputStream;
31  import javax.ws.rs.core.Response;
32  import org.apache.commons.lang3.ArrayUtils;
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.commons.lang3.tuple.Pair;
35  import org.apache.commons.lang3.tuple.Triple;
36  import org.apache.syncope.common.lib.SyncopeClientException;
37  import org.apache.syncope.common.lib.to.ExecTO;
38  import org.apache.syncope.common.lib.to.JobTO;
39  import org.apache.syncope.common.lib.to.ReportTO;
40  import org.apache.syncope.common.lib.types.ClientExceptionType;
41  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
42  import org.apache.syncope.common.lib.types.JobAction;
43  import org.apache.syncope.common.lib.types.JobType;
44  import org.apache.syncope.common.rest.api.RESTHeaders;
45  import org.apache.syncope.common.rest.api.batch.BatchResponseItem;
46  import org.apache.syncope.core.persistence.api.dao.JobStatusDAO;
47  import org.apache.syncope.core.persistence.api.dao.NotFoundException;
48  import org.apache.syncope.core.persistence.api.dao.ReportDAO;
49  import org.apache.syncope.core.persistence.api.dao.ReportExecDAO;
50  import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
51  import org.apache.syncope.core.persistence.api.entity.EntityFactory;
52  import org.apache.syncope.core.persistence.api.entity.Report;
53  import org.apache.syncope.core.persistence.api.entity.ReportExec;
54  import org.apache.syncope.core.provisioning.api.data.ReportDataBinder;
55  import org.apache.syncope.core.provisioning.api.job.JobManager;
56  import org.apache.syncope.core.provisioning.api.job.JobNamer;
57  import org.apache.syncope.core.provisioning.api.utils.ExceptionUtils2;
58  import org.apache.syncope.core.provisioning.java.job.report.ReportJob;
59  import org.apache.syncope.core.spring.security.AuthContextUtils;
60  import org.quartz.JobKey;
61  import org.quartz.SchedulerException;
62  import org.springframework.scheduling.quartz.SchedulerFactoryBean;
63  import org.springframework.security.access.prepost.PreAuthorize;
64  import org.springframework.transaction.annotation.Transactional;
65  
66  public class ReportLogic extends AbstractExecutableLogic<ReportTO> {
67  
68      protected final ReportDAO reportDAO;
69  
70      protected final ReportExecDAO reportExecDAO;
71  
72      protected final ReportDataBinder binder;
73  
74      protected final EntityFactory entityFactory;
75  
76      public ReportLogic(
77              final JobManager jobManager,
78              final SchedulerFactoryBean scheduler,
79              final JobStatusDAO jobStatusDAO,
80              final ReportDAO reportDAO,
81              final ReportExecDAO reportExecDAO,
82              final ReportDataBinder binder,
83              final EntityFactory entityFactory) {
84  
85          super(jobManager, scheduler, jobStatusDAO);
86  
87          this.reportDAO = reportDAO;
88          this.reportExecDAO = reportExecDAO;
89          this.binder = binder;
90          this.entityFactory = entityFactory;
91      }
92  
93      @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_CREATE + "')")
94      public ReportTO create(final ReportTO reportTO) {
95          Report report = entityFactory.newEntity(Report.class);
96          binder.getReport(report, reportTO);
97          report = reportDAO.save(report);
98          try {
99              jobManager.register(
100                     report,
101                     null,
102                     AuthContextUtils.getUsername());
103         } catch (Exception e) {
104             LOG.error("While registering quartz job for report " + report.getKey(), e);
105 
106             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
107             sce.getElements().add(e.getMessage());
108             throw sce;
109         }
110 
111         return binder.getReportTO(report);
112     }
113 
114     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_UPDATE + "')")
115     public ReportTO update(final ReportTO reportTO) {
116         Report report = Optional.ofNullable(reportDAO.find(reportTO.getKey())).
117                 orElseThrow(() -> new NotFoundException("Report " + reportTO.getKey()));
118 
119         binder.getReport(report, reportTO);
120         report = reportDAO.save(report);
121         try {
122             jobManager.register(
123                     report,
124                     null,
125                     AuthContextUtils.getUsername());
126         } catch (Exception e) {
127             LOG.error("While registering quartz job for report " + report.getKey(), e);
128 
129             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
130             sce.getElements().add(e.getMessage());
131             throw sce;
132         }
133 
134         return binder.getReportTO(report);
135     }
136 
137     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_LIST + "')")
138     @Transactional(readOnly = true)
139     public List<ReportTO> list() {
140         return reportDAO.findAll().stream().map(binder::getReportTO).collect(Collectors.toList());
141     }
142 
143     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_READ + "')")
144     @Transactional(readOnly = true)
145     public ReportTO read(final String key) {
146         Report report = Optional.ofNullable(reportDAO.find(key)).
147                 orElseThrow(() -> new NotFoundException("Report " + key));
148         return binder.getReportTO(report);
149     }
150 
151     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_EXECUTE + "')")
152     @Override
153     public ExecTO execute(final String key, final OffsetDateTime startAt, final boolean dryRun) {
154         Report report = Optional.ofNullable(reportDAO.find(key)).
155                 orElseThrow(() -> new NotFoundException("Report " + key));
156 
157         if (!report.isActive()) {
158             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
159             sce.getElements().add("Report " + key + " is not active");
160             throw sce;
161         }
162 
163         try {
164             Map<String, Object> jobDataMap = jobManager.register(
165                     report,
166                     startAt,
167                     AuthContextUtils.getUsername());
168             jobDataMap.put(JobManager.DRY_RUN_JOBDETAIL_KEY, dryRun);
169 
170             if (startAt == null) {
171                 scheduler.getScheduler().triggerJob(JobNamer.getJobKey(report));
172             }
173         } catch (Exception e) {
174             LOG.error("While executing report {}", report, e);
175 
176             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
177             sce.getElements().add(e.getMessage());
178             throw sce;
179         }
180 
181         ExecTO result = new ExecTO();
182         result.setJobType(JobType.REPORT);
183         result.setRefKey(report.getKey());
184         result.setRefDesc(binder.buildRefDesc(report));
185         result.setStart(OffsetDateTime.now());
186         result.setStatus("JOB_FIRED");
187         result.setMessage("Job fired; waiting for results...");
188         result.setExecutor(AuthContextUtils.getUsername());
189         return result;
190     }
191 
192     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_READ + "')")
193     @Transactional(readOnly = true)
194     public String getFilename(final String executionKey) {
195         ReportExec reportExec = Optional.ofNullable(reportExecDAO.find(executionKey)).
196                 orElseThrow(() -> new NotFoundException("Report execution " + executionKey));
197 
198         return reportExec.getReport().getName()
199                 + "."
200                 + StringUtils.removeStart(reportExec.getReport().getFileExt(), ".");
201     }
202 
203     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_READ + "')")
204     @Transactional(readOnly = true)
205     public void exportExecutionResult(
206             final OutputStream os,
207             final String executionKey) {
208 
209         ReportExec reportExec = Optional.ofNullable(reportExecDAO.find(executionKey)).
210                 orElseThrow(() -> new NotFoundException("Report execution " + executionKey));
211 
212         if (reportExec.getExecResult() == null || !ReportJob.Status.SUCCESS.name().equals(reportExec.getStatus())) {
213             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidReportExec);
214             sce.getElements().add(reportExec.getExecResult() == null
215                     ? "No report data produced"
216                     : "Report did not run successfully");
217             throw sce;
218         }
219 
220         // streaming output from a compressed byte array stream
221         try (ByteArrayInputStream bais = new ByteArrayInputStream(reportExec.getExecResult()); ZipInputStream zis =
222                 new ZipInputStream(bais)) {
223 
224             // a single ZipEntry in the ZipInputStream
225             zis.getNextEntry();
226 
227             zis.transferTo(os);
228         } catch (Exception e) {
229             LOG.error("While exporting content", e);
230         }
231     }
232 
233     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_DELETE + "')")
234     public ReportTO delete(final String key) {
235         Report report = Optional.ofNullable(reportDAO.find(key)).
236                 orElseThrow(() -> new NotFoundException("Report " + key));
237 
238         ReportTO deletedReport = binder.getReportTO(report);
239         jobManager.unregister(report);
240         reportDAO.delete(report);
241         return deletedReport;
242     }
243 
244     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_READ + "')")
245     @Override
246     public Pair<Integer, List<ExecTO>> listExecutions(
247             final String key,
248             final OffsetDateTime before,
249             final OffsetDateTime after,
250             final int page,
251             final int size,
252             final List<OrderByClause> orderByClauses) {
253 
254         Report report = Optional.ofNullable(reportDAO.find(key)).
255                 orElseThrow(() -> new NotFoundException("Report " + key));
256 
257         Integer count = reportExecDAO.count(report, before, after);
258 
259         List<ExecTO> result = reportExecDAO.findAll(report, before, after, page, size, orderByClauses).stream().
260                 map(reportExec -> binder.getExecTO(reportExec)).collect(Collectors.toList());
261 
262         return Pair.of(count, result);
263     }
264 
265     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_LIST + "')")
266     @Override
267     public List<ExecTO> listRecentExecutions(final int max) {
268         return reportExecDAO.findRecent(max).stream().
269                 map(reportExec -> binder.getExecTO(reportExec)).collect(Collectors.toList());
270     }
271 
272     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_DELETE + "')")
273     @Override
274     public ExecTO deleteExecution(final String executionKey) {
275         ReportExec reportExec = Optional.ofNullable(reportExecDAO.find(executionKey)).
276                 orElseThrow(() -> new NotFoundException("Report execution " + executionKey));
277 
278         ExecTO reportExecToDelete = binder.getExecTO(reportExec);
279         reportExecDAO.delete(reportExec);
280         return reportExecToDelete;
281     }
282 
283     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_DELETE + "')")
284     @Override
285     public List<BatchResponseItem> deleteExecutions(
286             final String key,
287             final OffsetDateTime before,
288             final OffsetDateTime after) {
289 
290         Report report = Optional.ofNullable(reportDAO.find(key)).
291                 orElseThrow(() -> new NotFoundException("Report " + key));
292 
293         List<BatchResponseItem> batchResponseItems = new ArrayList<>();
294 
295         reportExecDAO.findAll(report, before, after, -1, -1, List.of()).forEach(exec -> {
296             BatchResponseItem item = new BatchResponseItem();
297             item.getHeaders().put(RESTHeaders.RESOURCE_KEY, List.of(exec.getKey()));
298             batchResponseItems.add(item);
299 
300             try {
301                 reportExecDAO.delete(exec);
302                 item.setStatus(Response.Status.OK.getStatusCode());
303             } catch (Exception e) {
304                 LOG.error("Error deleting execution {} of report {}", exec.getKey(), key, e);
305                 item.setStatus(Response.Status.BAD_REQUEST.getStatusCode());
306                 item.setContent(ExceptionUtils2.getFullStackTrace(e));
307             }
308         });
309 
310         return batchResponseItems;
311     }
312 
313     @Override
314     protected Triple<JobType, String, String> getReference(final JobKey jobKey) {
315         String key = JobNamer.getReportKeyFromJobName(jobKey.getName());
316 
317         return Optional.ofNullable(reportDAO.find(key)).
318                 map(f -> Triple.of(JobType.REPORT, key, binder.buildRefDesc(f))).orElse(null);
319     }
320 
321     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_LIST + "')")
322     @Override
323     public List<JobTO> listJobs() {
324         return super.doListJobs(false);
325     }
326 
327     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_READ + "')")
328     @Override
329     public JobTO getJob(final String key) {
330         Report report = Optional.ofNullable(reportDAO.find(key)).
331                 orElseThrow(() -> new NotFoundException("Report " + key));
332 
333         JobTO jobTO = null;
334         try {
335             jobTO = getJobTO(JobNamer.getJobKey(report), false);
336         } catch (SchedulerException e) {
337             LOG.error("Problems while retrieving scheduled job {}", JobNamer.getJobKey(report), e);
338 
339             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
340             sce.getElements().add(e.getMessage());
341             throw sce;
342         }
343         if (jobTO == null) {
344             throw new NotFoundException("Job for report " + key);
345         }
346         return jobTO;
347     }
348 
349     @PreAuthorize("hasRole('" + IdRepoEntitlement.REPORT_EXECUTE + "')")
350     @Override
351     public void actionJob(final String key, final JobAction action) {
352         Report report = Optional.ofNullable(reportDAO.find(key)).
353                 orElseThrow(() -> new NotFoundException("Report " + key));
354 
355         doActionJob(JobNamer.getJobKey(report), action);
356     }
357 
358     @Override
359     protected ReportTO resolveReference(final Method method, final Object... args)
360             throws UnresolvedReferenceException {
361 
362         String key = null;
363 
364         if (ArrayUtils.isNotEmpty(args) && ("create".equals(method.getName())
365                 || "update".equals(method.getName())
366                 || "delete".equals(method.getName()))) {
367             for (int i = 0; key == null && i < args.length; i++) {
368                 if (args[i] instanceof String) {
369                     key = (String) args[i];
370                 } else if (args[i] instanceof ReportTO) {
371                     key = ((ReportTO) args[i]).getKey();
372                 }
373             }
374         }
375 
376         if (key != null) {
377             try {
378                 return binder.getReportTO(reportDAO.find(key));
379             } catch (Throwable ignore) {
380                 LOG.debug("Unresolved reference", ignore);
381                 throw new UnresolvedReferenceException(ignore);
382             }
383         }
384 
385         throw new UnresolvedReferenceException();
386     }
387 }