/*
* Copyright (c) 2010-2015 Evolveum
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.evolveum.midpoint.report.impl;
import com.evolveum.midpoint.model.api.ModelService;
import com.evolveum.midpoint.model.api.context.ModelContext;
import com.evolveum.midpoint.model.api.context.ModelElementContext;
import com.evolveum.midpoint.model.api.context.ModelState;
import com.evolveum.midpoint.model.api.hooks.ChangeHook;
import com.evolveum.midpoint.model.api.hooks.HookOperationMode;
import com.evolveum.midpoint.model.api.hooks.HookRegistry;
import com.evolveum.midpoint.model.api.hooks.ReadHook;
import com.evolveum.midpoint.prism.PrismContainer;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.delta.ObjectDelta;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.query.builder.QueryBuilder;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.report.api.ReportManager;
import com.evolveum.midpoint.schema.GetOperationOptions;
import com.evolveum.midpoint.schema.SelectorOptions;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.result.OperationResultStatus;
import com.evolveum.midpoint.schema.util.MiscSchemaUtil;
import com.evolveum.midpoint.schema.util.ReportTypeUtil;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.task.api.TaskManager;
import com.evolveum.midpoint.util.exception.*;
import com.evolveum.midpoint.util.logging.LoggingUtils;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.design.JasperDesign;
import net.sf.jasperreports.engine.xml.JRXmlLoader;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* @author lazyman, garbika
*/
@Service(value = "reportManager")
public class ReportManagerImpl implements ReportManager, ChangeHook, ReadHook {
public static final String HOOK_URI = "http://midpoint.evolveum.com/model/report-hook-1";
private static final Trace LOGGER = TraceManager.getTrace(ReportManagerImpl.class);
private static final String CLASS_NAME_WITH_DOT = ReportManagerImpl.class + ".";
private static final String CLEANUP_REPORT_OUTPUTS = CLASS_NAME_WITH_DOT + "cleanupReportOutputs";
private static final String DELETE_REPORT_OUTPUT = CLASS_NAME_WITH_DOT + "deleteReportOutput";
private static final String REPORT_OUTPUT_DATA = CLASS_NAME_WITH_DOT + "getReportOutputData";
@Autowired
private HookRegistry hookRegistry;
@Autowired
private TaskManager taskManager;
@Autowired
private PrismContext prismContext;
@Autowired
private ModelService modelService;
@PostConstruct
public void init() {
hookRegistry.registerChangeHook(HOOK_URI, this);
hookRegistry.registerReadHook(HOOK_URI, this);
}
@Override
public <T extends ObjectType> void invoke(PrismObject<T> object,
Collection<SelectorOptions<GetOperationOptions>> options, Task task,
OperationResult parentResult) throws SchemaException,
ObjectNotFoundException, SecurityViolationException, CommunicationException, ConfigurationException {
if (!ReportType.class.equals(object.getCompileTimeClass())) {
return;
}
boolean raw = isRaw(options);
if (!raw) {
ReportTypeUtil.applyDefinition((PrismObject<ReportType>) object, prismContext);
}
}
private boolean isRaw(Collection<SelectorOptions<GetOperationOptions>> options) {
GetOperationOptions rootOptions = SelectorOptions.findRootOptions(options);
return rootOptions == null ? false : GetOperationOptions.isRaw(rootOptions);
}
/**
* Creates and starts task with proper handler, also adds necessary information to task
* (like ReportType reference and so on).
*
* @param object
* @param task
* @param parentResult describes report which has to be created
*/
@Override
public void runReport(PrismObject<ReportType> object, PrismContainer<ReportParameterType> paramContainer, Task task, OperationResult parentResult) {
task.setHandlerUri(ReportCreateTaskHandler.REPORT_CREATE_TASK_URI);
task.setObjectRef(object.getOid(), ReportType.COMPLEX_TYPE);
try {
if (paramContainer != null && !paramContainer.isEmpty()){
task.setExtensionContainer(paramContainer);
}
} catch (SchemaException e) {
throw new SystemException(e);
}
task.setThreadStopAction(ThreadStopActionType.CLOSE);
task.makeSingle();
taskManager.switchToBackground(task, parentResult);
parentResult.setBackgroundTaskOid(task.getOid());
}
/**
* Transforms change:
* 1/ ReportOutputType DELETE to MODIFY some attribute to mark it for deletion.
* 2/ ReportType ADD and MODIFY should compute jasper design and styles if necessary
*
* @param context
* @param task
* @param result
* @return
* @throws UnsupportedEncodingException
*/
@Override
public HookOperationMode invoke(@NotNull ModelContext context, @NotNull Task task, @NotNull OperationResult parentResult) {
ModelState state = context.getState();
if (state != ModelState.FINAL) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("report manager called in state = " + state + ", exiting.");
}
return HookOperationMode.FOREGROUND;
} else {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("report manager called in state = " + state + ", proceeding.");
}
}
boolean relatesToReport = false;
boolean isDeletion = false;
PrismObject<?> object = null;
for (Object o : context.getProjectionContexts()) {
boolean deletion = false;
object = ((ModelElementContext<?>) o).getObjectNew();
if (object == null) {
deletion = true;
object = ((ModelElementContext<?>) o).getObjectOld();
}
if (object == null) {
LOGGER.warn("Probably invalid projection context: both old and new objects are null");
} else if (object.getCompileTimeClass().isAssignableFrom(ReportType.class)) {
relatesToReport = true;
isDeletion = deletion;
}
}
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("change relates to report: " + relatesToReport + ", is deletion: " + isDeletion);
}
if (!relatesToReport) {
LOGGER.trace("invoke() EXITING: Changes not related to report");
return HookOperationMode.FOREGROUND;
}
if (isDeletion) {
LOGGER.trace("invoke() EXITING because operation is DELETION");
return HookOperationMode.FOREGROUND;
}
OperationResult result = parentResult.createSubresult(CLASS_NAME_WITH_DOT + "invoke");
try {
ReportType reportType = (ReportType) object.asObjectable();
JasperDesign jasperDesign = null;
if (reportType.getTemplate() == null){
String message = "Report template must not be null";
LOGGER.error(message);
result.recordFatalError(message, new SystemException());
}
// {
// PrismSchema reportSchema = null;
// PrismContainer<ReportConfigurationType> parameterConfiguration = null;
// try
// {
// reportSchema = ReportUtils.getParametersSchema(reportType, prismContext);
// parameterConfiguration = ReportUtils.getParametersContainer(reportType, reportSchema);
//
// } catch (Exception ex){
// String message = "Cannot create parameter configuration: " + ex.getMessage();
// LOGGER.error(message);
// result.recordFatalError(message, ex);
// }
//
// jasperDesign = ReportUtils.createJasperDesign(reportType, parameterConfiguration, reportSchema) ;
// LOGGER.trace("create jasper design : {}", jasperDesign);
// }
else
{
byte[] reportTemplateBase64 = reportType.getTemplate();
byte[] reportTemplate = Base64.decodeBase64(reportTemplateBase64);
InputStream inputStreamJRXML = new ByteArrayInputStream(reportTemplate);
jasperDesign = JRXmlLoader.load(inputStreamJRXML);
LOGGER.trace("load jasper design : {}", jasperDesign);
}
// Compile template
JasperReport jasperReport = JasperCompileManager.compileReport(jasperDesign);
LOGGER.trace("compile jasper design, create jasper report : {}", jasperReport);
//result.computeStatus();
result.recordSuccessIfUnknown();
}
catch (JRException ex) {
String message = "Cannot load or compile jasper report: " + ex.getMessage();
LOGGER.error(message);
result.recordFatalError(message, ex);
}
return HookOperationMode.FOREGROUND;
}
@Override
public void invokeOnException(@NotNull ModelContext context, @NotNull Throwable throwable, @NotNull Task task, @NotNull OperationResult result) {
}
@Override
public void cleanupReports(CleanupPolicyType cleanupPolicy, OperationResult parentResult) {
OperationResult result = parentResult.createSubresult(CLEANUP_REPORT_OUTPUTS);
if (cleanupPolicy.getMaxAge() == null) {
return;
}
Duration duration = cleanupPolicy.getMaxAge();
if (duration.getSign() > 0) {
duration = duration.negate();
}
Date deleteReportOutputsTo = new Date();
duration.addTo(deleteReportOutputsTo);
LOGGER.info("Starting cleanup for report outputs deleting up to {} (duration '{}').",
new Object[]{deleteReportOutputsTo, duration});
XMLGregorianCalendar timeXml = XmlTypeConverter.createXMLGregorianCalendar(deleteReportOutputsTo.getTime());
List<PrismObject<ReportOutputType>> obsoleteReportOutputs = new ArrayList<PrismObject<ReportOutputType>>();
try {
ObjectQuery obsoleteReportOutputsQuery = QueryBuilder.queryFor(ReportOutputType.class, prismContext)
.item(ReportOutputType.F_METADATA, MetadataType.F_CREATE_TIMESTAMP).le(timeXml)
.build();
obsoleteReportOutputs = modelService.searchObjects(ReportOutputType.class, obsoleteReportOutputsQuery, null, null, result);
} catch (Exception e) {
throw new SystemException("Couldn't get the list of obsolete report outputs: " + e.getMessage(), e);
}
LOGGER.debug("Found {} report output(s) to be cleaned up", obsoleteReportOutputs.size());
boolean interrupted = false;
int deleted = 0;
int problems = 0;
for (PrismObject<ReportOutputType> reportOutputPrism : obsoleteReportOutputs){
ReportOutputType reportOutput = reportOutputPrism.asObjectable();
LOGGER.trace("Removing report output {} along with {} file.", reportOutput.getName().getOrig(),
reportOutput.getFilePath());
boolean problem = false;
try {
deleteReportOutput(reportOutput, result);
} catch (Exception e) {
LoggingUtils.logException(LOGGER, "Couldn't delete obsolete report output {} due to a exception", e, reportOutput);
problem = true;
}
if (problem) {
problems++;
} else {
deleted++;
}
}
result.computeStatusIfUnknown();
LOGGER.info("Report cleanup procedure " +
(interrupted ? "was interrupted" : "finished") +
". Successfully deleted {} report outputs; there were problems with deleting {} report ouptuts.", deleted, problems);
String suffix = interrupted ? " Interrupted." : "";
if (problems == 0) {
parentResult.createSubresult(CLEANUP_REPORT_OUTPUTS + ".statistics").recordStatus(OperationResultStatus.SUCCESS,
"Successfully deleted " + deleted + " report output(s)." + suffix);
} else {
parentResult.createSubresult(CLEANUP_REPORT_OUTPUTS + ".statistics").recordPartialError("Successfully deleted " +
deleted + " report output(s), "
+ "there was problems with deleting " + problems + " report outputs.");
}
}
private void deleteReportOutput(ReportOutputType reportOutput, OperationResult parentResult) throws Exception {
String oid = reportOutput.getOid();
Task task = taskManager.createTaskInstance(DELETE_REPORT_OUTPUT);
parentResult.addSubresult(task.getResult());
OperationResult result = parentResult.createSubresult(DELETE_REPORT_OUTPUT);
result.addParam("oid", oid);
try {
File reportFile = new File(reportOutput.getFilePath());
reportFile.delete();
ObjectDelta<ReportOutputType> delta = ObjectDelta.createDeleteDelta(ReportOutputType.class, oid, prismContext);
Collection<ObjectDelta<? extends ObjectType>> deltas = MiscSchemaUtil.createCollection(delta);
modelService.executeChanges(deltas, null, task, result);
result.recordSuccessIfUnknown();
}
catch (Exception e) {
result.recordFatalError("Cannot delete the report output because of a exception.", e);
throw e;
}
}
@Override
public InputStream getReportOutputData(String reportOutputOid, OperationResult parentResult) {
Task task = taskManager.createTaskInstance(REPORT_OUTPUT_DATA);
OperationResult result = parentResult.createSubresult(REPORT_OUTPUT_DATA);
result.addParam("oid", reportOutputOid);
InputStream reportData = null;
try {
ReportOutputType reportOutput = modelService.getObject(ReportOutputType.class, reportOutputOid, null,
task, result).asObjectable();
String filePath = reportOutput.getFilePath();
if (StringUtils.isEmpty(filePath)) {
parentResult.recordFatalError("Report output file path is not defined.");
return null;
}
File file = new File(filePath);
reportData = FileUtils.openInputStream(file);
result.recordSuccessIfUnknown();
} catch (Exception e) {
LOGGER.trace("Cannot read the report data : {}", e.getMessage());
result.recordFatalError("Cannot read the report data.", e);
} finally {
result.computeStatusIfUnknown();
}
return reportData;
}
}