package org.mapfish.print.processor.jasper; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.io.Files; import net.sf.jasperreports.engine.JRException; import net.sf.jasperreports.engine.JasperCompileManager; import net.sf.jasperreports.engine.design.JRValidationException; import org.mapfish.print.config.Configuration; import org.mapfish.print.config.HasConfiguration; import org.mapfish.print.config.WorkingDirectories; import org.mapfish.print.processor.AbstractProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import java.io.File; import java.io.IOException; import java.nio.file.StandardCopyOption; import java.util.List; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** * <p>A processor that actually compiles a JasperReport template file.</p> * <p>Example</p> * <pre><code> * processors: * - !reportBuilder # compile all reports in current directory * directory: '.'</code></pre> * [[examples=verboseExample]] */ public final class JasperReportBuilder extends AbstractProcessor<JasperReportBuilder.Input, Void> implements HasConfiguration { private static final Logger LOGGER = LoggerFactory.getLogger(JasperReportBuilder.class); /** * Extension for Jasper XML Report Template files. */ public static final String JASPER_REPORT_XML_FILE_EXT = ".jrxml"; /** * Extension for Compiled Jasper Report Template files. */ public static final String JASPER_REPORT_COMPILED_FILE_EXT = ".jasper"; private File directory = null; private Configuration configuration; @Autowired private MetricRegistry metricRegistry; @Autowired private WorkingDirectories workingDirectories; /** * Constructor. */ protected JasperReportBuilder() { super(Void.class); } @Override public Void execute(final JasperReportBuilder.Input param, final ExecutionContext context) throws JRException { Timer.Context buildReports = this.metricRegistry.timer(getClass() + "_execute()").time(); try { for (final File jasperFile : jasperXmlFiles()) { checkCancelState(context); compileJasperReport(this.configuration, jasperFile); } return null; } finally { buildReports.stop(); } } File compileJasperReport(final Configuration config, final File jasperFile) throws JRException { final File buildFile = this.workingDirectories.getBuildFileFor(config, jasperFile, JASPER_REPORT_COMPILED_FILE_EXT, LOGGER); return compileJasperReport(buildFile, jasperFile); } File compileJasperReport(final File buildFile, final File jasperFile) throws JRException { if (!buildFile.exists() || jasperFile.lastModified() > buildFile.lastModified()) { try { // May be called from multiple threads at the same time for the same report. // Instead of trying to protect the compiled file against modification while // another thread is reading it, use a temporary file as a target instead and // move it (atomic operation) when done. Worst case: we compile a file twice instead // of once. File tmpBuildFile = File.createTempFile("temp_", JASPER_REPORT_COMPILED_FILE_EXT, buildFile.getParentFile()); LOGGER.info("Building Jasper report: {}", jasperFile.getAbsolutePath()); LOGGER.debug("To: {}", buildFile.getAbsolutePath()); final Timer.Context compileJasperReport = this.metricRegistry.timer( "compile_" + jasperFile).time(); try { JasperCompileManager.compileReportToFile(jasperFile.getAbsolutePath(), tmpBuildFile.getAbsolutePath()); } catch (JRValidationException e) { LOGGER.error("The report '{}' isn't valid.", jasperFile.getAbsolutePath()); throw e; } finally { final long compileTime = TimeUnit.MILLISECONDS.convert(compileJasperReport.stop(), TimeUnit.NANOSECONDS); LOGGER.info("Report built in {}ms.", compileTime); } java.nio.file.Files.move(tmpBuildFile.toPath(), buildFile.toPath(), StandardCopyOption.ATOMIC_MOVE); } catch (IOException e) { throw new JRException(e); } } else { LOGGER.debug("Destination file is already up to date: " + buildFile.getAbsolutePath()); } return buildFile; } private Iterable<File> jasperXmlFiles() { File directoryToSearch = this.directory; if (directoryToSearch == null) { directoryToSearch = this.configuration.getDirectory(); } final String configurationAbsolutePath = this.configuration.getDirectory().getAbsolutePath(); if (!directoryToSearch.getAbsolutePath().startsWith(configurationAbsolutePath)) { throw new IllegalArgumentException("All directories and files referenced in the configuration must be in the configuration " + "directory: " + directoryToSearch + " is not in " + this.configuration.getDirectory()); } final Iterable<File> children = Files.fileTreeTraverser().children(directoryToSearch); return Iterables.filter(children, new Predicate<File>() { @Override public boolean apply(@Nullable final File input) { return input != null && input.getName().endsWith(JASPER_REPORT_XML_FILE_EXT); } }); } @Override public JasperReportBuilder.Input createInputParameter() { return new JasperReportBuilder.Input(); } /** * Set the directory and test that the directory exists and is contained within the Configuration directory. * * @param directory the new directory */ public void setDirectory(final String directory) { this.directory = new File(this.configuration.getDirectory(), directory); if (!this.directory.exists()) { throw new IllegalArgumentException("Directory does not exist: " + this.directory + ".\nConfiguration contained value " + directory + " which is supposed to be relative to configuration directory"); } if (!this.directory.getAbsolutePath().startsWith(this.configuration.getDirectory().getAbsolutePath())) { throw new IllegalArgumentException("All files and directories must be contained in the configuration directory" + " the directory provided in the configuration breaks that contract: " + directory + " in config file resolved to " + this.directory); } } @Override public void setConfiguration(final Configuration configuration) { this.configuration = configuration; } @Override public String toString() { return getClass().getSimpleName() + "(" + this.directory + ")"; } /** * The input parameter object for {@link JasperReportBuilder}. */ public static final class Input { } @Override protected void extraValidation(final List<Throwable> validationErrors, final Configuration config) { // nothing to do } }