/*
* FopTask.java
* Copyright 2016 Connor Petty <cpmeister@users.sourceforge.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Created on Jan 3, 2016, 9:14:07 PM
*/
package pcgen.util.fop;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.SourceLocator;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopConfParser;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.FopFactoryBuilder;
import org.apache.fop.apps.MimeConstants;
import org.apache.fop.events.Event;
import org.apache.fop.events.EventFormatter;
import org.apache.fop.events.EventListener;
import org.apache.fop.events.model.EventSeverity;
import org.apache.fop.render.Renderer;
import pcgen.cdom.base.Constants;
import pcgen.system.ConfigurationSettings;
import pcgen.util.Logging;
/**
* This class is used to generate pdf files from an xml source. There are two ways to define the
* source of the task: files or inputstreams. The output of this task can either be an OutputStream
* which you can point to a file, or a Renderer. The Renderer is used by print preview and for
* direct printing.
*
* @author Connor Petty <cpmeister@users.sourceforge.net>
*/
public final class FopTask implements Runnable
{
private static final FopFactory FOP_FACTORY = createFopFactory();
private static FOUserAgent userAgent;
private static final TransformerFactory TRANS_FACTORY = TransformerFactory.newInstance();
private static FopFactory createFopFactory()
{
// Allow optional customization with configuration file
String configPath = ConfigurationSettings.getOutputSheetsDir() + File.separator + "fop.xconf";
Logging.log(Logging.INFO, "FoPTask checking for config file at " + configPath);
File userConfigFile = new File(configPath);
FopFactoryBuilder builder;
if (userConfigFile.exists())
{
Logging.log(Logging.INFO, "FoPTask using config file " + configPath);
FopConfParser parser;
try
{
parser = new FopConfParser(userConfigFile);
}
catch (Exception e)
{
Logging.errorPrint("FoPTask encountered a problem with FOP configuration "
+ configPath + ": ", e);
return null;
}
builder = parser.getFopFactoryBuilder();
}
else
{
Logging.log(Logging.INFO, "FoPTask using default config");
builder = new FopFactoryBuilder(new File(".").toURI());
builder.setStrictFOValidation(false);
}
return builder.build();
}
private final StreamSource inputSource;
private final StreamSource xsltSource;
private final Renderer renderer;
private final OutputStream outputStream;
private final StringBuilder errorBuilder = new StringBuilder(32);
private FopTask(StreamSource inputXml, StreamSource xsltSource, Renderer renderer, OutputStream outputStream)
{
this.inputSource = inputXml;
this.xsltSource = xsltSource;
this.renderer = renderer;
this.outputStream = outputStream;
}
private static StreamSource createXsltStreamSource(File xsltFile) throws FileNotFoundException
{
if (xsltFile == null)
{
return null;
}
if (!xsltFile.exists())
{
throw new FileNotFoundException("xsl file "
+ xsltFile.getAbsolutePath() + " not found ");
}
return new StreamSource(xsltFile);
}
public static FopFactory getFactory()
{
return FOP_FACTORY;
}
/**
* Creates a new FopTask that transforms the input stream using the given xsltFile and outputs a
* pdf document to the given output stream. The output can be saved to a file if a
* FileOutputStream is used.
*
* @param inputXmlStream the fop xml input stream
* @param xsltFile the transform template file, if null then the identity transformer is used
* @param outputPdf output stream for pdf document
* @return a FopTask to be executed
* @throws FileNotFoundException if xsltFile is not null and does not exist
*/
public static FopTask newFopTask(InputStream inputXmlStream, File xsltFile, OutputStream outputPdf) throws FileNotFoundException
{
StreamSource xsltSource = createXsltStreamSource(xsltFile);
userAgent = FOP_FACTORY.newFOUserAgent();
return new FopTask(new StreamSource(inputXmlStream), xsltSource, null, outputPdf);
}
/**
* Creates a new FopTask that transforms the input stream using the given xsltFile and outputs a
* pdf document to the given Renderer. This task can can be used for both previewing a pdf
* document as well as printing a pdf
*
* @param inputXmlStream the fop xml input stream
* @param xsltFile the transform template file, if null then the identity transformer is used
* @param renderer the Renderer to output a pdf document to.
* @return a FopTask to be executed
* @throws FileNotFoundException if xsltFile is not null and does not exist
*/
public static FopTask newFopTask(InputStream inputXmlStream, File xsltFile, Renderer renderer) throws FileNotFoundException
{
StreamSource xsltSource = createXsltStreamSource(xsltFile);
userAgent = renderer.getUserAgent();
return new FopTask(new StreamSource(inputXmlStream), xsltSource, renderer, null);
}
public String getErrorMessages()
{
return errorBuilder.toString();
}
/**
* Run the FO to PDF/AWT conversion. This automatically closes any provided OutputStream for
* this FopTask.
*/
@Override
public void run()
{
try(OutputStream out = outputStream)
{
userAgent.setProducer("PC Gen Character Generator");
userAgent.setAuthor(System.getProperty("user.name"));
userAgent.setCreationDate(new Date());
userAgent.getEventBroadcaster().addEventListener(new FOPEventListener());
String mimeType;
if (renderer != null)
{
userAgent.setKeywords("PCGEN FOP PREVIEW");
mimeType = MimeConstants.MIME_FOP_AWT_PREVIEW;
}
else
{
userAgent.setKeywords("PCGEN FOP PDF");
mimeType = MimeConstants.MIME_PDF;
}
Fop fop;
if (out != null)
{
fop = FOP_FACTORY.newFop(mimeType, userAgent, out);
}
else
{
fop = FOP_FACTORY.newFop(mimeType, userAgent);
}
Transformer transformer;
if (xsltSource != null)
{
transformer = TRANS_FACTORY.newTransformer(xsltSource);
}
else
{
transformer = TRANS_FACTORY.newTransformer();// identity transformer
}
transformer.setErrorListener(new FOPErrorListener());
transformer.transform(inputSource, new SAXResult(fop.getDefaultHandler()));
}
catch (TransformerException | FOPException | IOException e)
{
errorBuilder.append(e.getMessage()).append(Constants.LINE_SEPARATOR);
Logging.errorPrint("Exception in FopTask:run", e);
}
catch (RuntimeException ex)
{
errorBuilder.append(ex.getMessage()).append(Constants.LINE_SEPARATOR);
Logging.errorPrint("Unexpected exception in FopTask:run: ", ex);
}
}
/**
* The Class {@code FOPErrorListener} listens for notifications of issues when generating
* PDF files and responds accordingly.
*/
private static class FOPErrorListener implements ErrorListener
{
@Override
public void error(TransformerException exception)
throws TransformerException
{
SourceLocator locator = exception.getLocator();
Logging.errorPrint("FOP Error " + exception.getMessage() + " at " + getLocation(locator));
throw exception;
}
@Override
public void fatalError(TransformerException exception)
throws TransformerException
{
SourceLocator locator = exception.getLocator();
Logging.errorPrint("FOP Fatal Error " + exception.getMessage() + " at " + getLocation(locator));
throw exception;
}
@Override
public void warning(TransformerException exception)
throws TransformerException
{
SourceLocator locator = exception.getLocator();
Logging.log(Logging.WARNING, getLocation(locator) + exception.getMessage());
}
private static String getLocation(SourceLocator locator)
{
if (locator == null)
{
return "Unknown; ";
}
StringBuilder builder = new StringBuilder();
if (locator.getSystemId() != null)
{
builder.append(locator.getSystemId());
builder.append("; ");
}
if (locator.getLineNumber() > -1)
{
builder.append("Line#: ");
builder.append(locator.getLineNumber());
builder.append("; ");
}
if (locator.getColumnNumber() > -1)
{
builder.append("Column#: ");
builder.append(locator.getColumnNumber());
builder.append("; ");
}
return builder.toString();
}
}
private static class FOPEventListener implements EventListener
{
/**
* @{inheritdoc}
*/
@Override
public void processEvent(final Event event)
{
String msg = "[FOP] " + EventFormatter.format(event);
// filter out some erroneous FOP warnings about not finding internal fonts
// this is an ancient, but still unfixed FOP bug
// see https://issues.apache.org/jira/browse/FOP-1667
if (msg.contains("not found") && (msg.contains("Symbol,normal") || msg.contains("ZapfDingbats,normal")))
{
return;
}
EventSeverity severity = event.getSeverity();
if (severity == EventSeverity.INFO)
{
Logging.log(Logging.INFO, msg);
}
else if (severity == EventSeverity.WARN)
{
Logging.log(Logging.WARNING, msg);
}
else if (severity == EventSeverity.ERROR || severity == EventSeverity.FATAL)
{
Logging.log(Logging.ERROR, msg);
}
else
{
assert false;
}
}
}
}