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