blob: 15d337bb71ad78170af585ee77738d8292fe86f3 [file] [log] [blame]
package org.apache.maven.doxia.docrenderer.pdf.itext;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.maven.doxia.docrenderer.DocumentRendererException;
import org.apache.maven.doxia.docrenderer.pdf.AbstractPdfRenderer;
import org.apache.maven.doxia.document.DocumentCover;
import org.apache.maven.doxia.document.DocumentMeta;
import org.apache.maven.doxia.document.DocumentModel;
import org.apache.maven.doxia.document.DocumentTOCItem;
import org.apache.maven.doxia.module.itext.ITextSink;
import org.apache.maven.doxia.module.itext.ITextSinkFactory;
import org.apache.maven.doxia.module.itext.ITextUtil;
import org.apache.maven.doxia.module.site.SiteModule;
import org.apache.xml.utils.DefaultErrorHandler;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import com.lowagie.text.ElementTags;
/**
* Abstract <code>document</code> render with the <code>iText</code> framework
*
* @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
* @author ltheussl
* @version $Id$
* @since 1.1
* @plexus.component role="org.apache.maven.doxia.docrenderer.pdf.PdfRenderer" role-hint="itext"
*/
public class ITextPdfRenderer
extends AbstractPdfRenderer
{
/** The xslt style sheet used to transform a Document to an iText file. */
private static final String XSLT_RESOURCE = "TOC.xslt";
/** The TransformerFactory. */
private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
/** The DocumentBuilderFactory. */
private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
/** The DocumentBuilder. */
private static final DocumentBuilder DOCUMENT_BUILDER;
static
{
TRANSFORMER_FACTORY.setErrorListener( new DefaultErrorHandler() );
try
{
DOCUMENT_BUILDER = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
}
catch ( ParserConfigurationException e )
{
throw new RuntimeException( "Error building document :" + e.getMessage() );
}
}
/** {@inheritDoc} */
public void generatePdf( File inputFile, File pdfFile )
throws DocumentRendererException
{
if ( getLogger().isDebugEnabled() )
{
getLogger().debug( "Generating : " + pdfFile );
}
try
{
ITextUtil.writePdf( new FileInputStream( inputFile ), new FileOutputStream( pdfFile ) );
}
catch ( IOException e )
{
throw new DocumentRendererException( "Cannot create PDF from " + inputFile + ": " + e.getMessage(), e );
}
catch ( RuntimeException e )
{
throw new DocumentRendererException( "Error creating PDF from " + inputFile + ": " + e.getMessage(), e );
}
}
/** {@inheritDoc} */
public void render( Map filesToProcess, File outputDirectory, DocumentModel documentModel )
throws DocumentRendererException, IOException
{
// copy resources, images, etc.
copyResources( outputDirectory );
if ( documentModel == null )
{
getLogger().debug( "No document model, generating all documents individually." );
renderIndividual( filesToProcess, outputDirectory );
return;
}
String outputName = getOutputName( documentModel );
File outputITextFile = new File( outputDirectory, outputName + ".xml" );
if ( !outputITextFile.getParentFile().exists() )
{
outputITextFile.getParentFile().mkdirs();
}
File pdfOutputFile = new File( outputDirectory, outputName + ".pdf" );
if ( !pdfOutputFile.getParentFile().exists() )
{
pdfOutputFile.getParentFile().mkdirs();
}
List iTextFiles;
if ( ( documentModel.getToc() == null ) || ( documentModel.getToc().getItems() == null ) )
{
getLogger().info( "No TOC is defined in the document descriptor. Merging all documents." );
iTextFiles = parseAllFiles( filesToProcess, outputDirectory );
}
else
{
getLogger().debug( "Using TOC defined in the document descriptor." );
iTextFiles = parseTOCFiles( filesToProcess, outputDirectory, documentModel );
}
File iTextFile = new File( outputDirectory, outputName + ".xml" );
File iTextOutput = new File( outputDirectory, outputName + "." + getOutputExtension() );
Document document = generateDocument( iTextFiles );
transform( documentModel, document, iTextFile );
generatePdf( iTextFile, iTextOutput );
}
/** {@inheritDoc} */
public void renderIndividual( Map filesToProcess, File outputDirectory )
throws DocumentRendererException, IOException
{
for ( Iterator it = filesToProcess.keySet().iterator(); it.hasNext(); )
{
String key = (String) it.next();
SiteModule module = (SiteModule) filesToProcess.get( key );
File fullDoc = new File( getBaseDir(), module.getSourceDirectory() + File.separator + key );
String output = key;
String lowerCaseExtension = module.getExtension().toLowerCase( Locale.ENGLISH );
if ( output.toLowerCase( Locale.ENGLISH ).indexOf( "." + lowerCaseExtension ) != -1 )
{
output =
output.substring( 0, output.toLowerCase( Locale.ENGLISH ).indexOf( "." + lowerCaseExtension ) );
}
File outputITextFile = new File( outputDirectory, output + ".xml" );
if ( !outputITextFile.getParentFile().exists() )
{
outputITextFile.getParentFile().mkdirs();
}
File pdfOutputFile = new File( outputDirectory, output + ".pdf" );
if ( !pdfOutputFile.getParentFile().exists() )
{
pdfOutputFile.getParentFile().mkdirs();
}
parse( fullDoc, module, outputITextFile );
generatePdf( outputITextFile, pdfOutputFile );
}
}
//--------------------------------------------
//
//--------------------------------------------
/**
* Parse a source document and emit results into a sink.
*
* @param fullDocPath file to the source document.
* @param module the site module associated with the source document (determines the parser to use).
* @param iTextFile the resulting iText xml file.
* @throws DocumentRendererException in case of a parsing problem.
* @throws IOException if the source and/or target document cannot be opened.
*/
private void parse( File fullDoc, SiteModule module, File iTextFile )
throws DocumentRendererException, IOException
{
if ( getLogger().isDebugEnabled() )
{
getLogger().debug( "Parsing file " + fullDoc.getAbsolutePath() );
}
System.setProperty( "itext.basedir", iTextFile.getParentFile().getAbsolutePath() );
Writer writer = null;
ITextSink sink = null;
try
{
writer = WriterFactory.newXmlWriter( iTextFile );
sink = (ITextSink) new ITextSinkFactory().createSink( writer );
sink.setClassLoader( new URLClassLoader( new URL[] { iTextFile.getParentFile().toURI().toURL() } ) );
parse( fullDoc.getAbsolutePath(), module.getParserId(), sink );
}
finally
{
if ( sink != null )
{
sink.flush();
sink.close();
}
IOUtil.close( writer );
System.getProperties().remove( "itext.basedir" );
}
}
/**
* Merge all iTextFiles to a single one.
*
* @param iTextFiles list of iText xml files.
* @return Document.
* @throws DocumentRendererException if any.
* @throws IOException if any.
*/
private Document generateDocument( List iTextFiles )
throws DocumentRendererException, IOException
{
Document document = DOCUMENT_BUILDER.newDocument();
document.appendChild( document.createElement( ElementTags.ITEXT ) ); // Used only to set a root
for ( int i = 0; i < iTextFiles.size(); i++ )
{
File iTextFile = (File) iTextFiles.get( i );
Document iTextDocument;
try
{
iTextDocument = DOCUMENT_BUILDER.parse( iTextFile );
}
catch ( SAXException e )
{
throw new DocumentRendererException( "SAX Error : " + e.getMessage() );
}
// Only one chapter per doc
Node chapter = iTextDocument.getElementsByTagName( ElementTags.CHAPTER ).item( 0 );
try
{
document.getDocumentElement().appendChild( document.importNode( chapter, true ) );
}
catch ( DOMException e )
{
throw new DocumentRendererException( "Error appending chapter for "
+ iTextFile + " : " + e.getMessage() );
}
}
return document;
}
/**
* Initialize the transformer object.
*
* @return an instance of a transformer object.
* @throws DocumentRendererException if any.
*/
private Transformer initTransformer()
throws DocumentRendererException
{
try
{
Transformer transformer = TRANSFORMER_FACTORY.newTransformer( new StreamSource( ITextPdfRenderer.class
.getResourceAsStream( XSLT_RESOURCE ) ) );
transformer.setErrorListener( TRANSFORMER_FACTORY.getErrorListener() );
transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "false" );
transformer.setOutputProperty( OutputKeys.INDENT, "yes" );
transformer.setOutputProperty( OutputKeys.METHOD, "xml" );
transformer.setOutputProperty( OutputKeys.ENCODING, "UTF-8" );
// No doctype since itext doctype is not up to date!
return transformer;
}
catch ( TransformerConfigurationException e )
{
throw new DocumentRendererException( "Error configuring Transformer for " + XSLT_RESOURCE + ": "
+ e.getMessage() );
}
catch ( IllegalArgumentException e )
{
throw new DocumentRendererException( "Error configuring Transformer for " + XSLT_RESOURCE + ": "
+ e.getMessage() );
}
}
/**
* Add transformer parameters from a DocumentModel.
*
* @param transformer the Transformer to set the parameters.
* @param documentModel the DocumentModel to take the parameters from, could be null.
* @param iTextFile the iTextFile not null for the relative paths.
*/
private void addTransformerParameters( Transformer transformer, DocumentModel documentModel, File iTextFile )
{
if ( documentModel == null )
{
return;
}
// Meta parameters
boolean hasNullMeta = false;
if ( documentModel.getMeta() == null )
{
hasNullMeta = true;
documentModel.setMeta( new DocumentMeta() );
}
addTransformerParameter( transformer, "meta.author", documentModel.getMeta().getAllAuthorNames(),
System.getProperty( "user.name", "null" ) );
addTransformerParameter( transformer, "meta.creator", documentModel.getMeta().getCreator(),
System.getProperty( "user.name", "null" ) );
// see com.lowagie.text.Document#addCreationDate()
SimpleDateFormat sdf = new SimpleDateFormat( "EEE MMM dd HH:mm:ss zzz yyyy" );
addTransformerParameter( transformer, "meta.creationdate", documentModel.getMeta().getCreationdate(),
sdf.format( new Date() ) );
addTransformerParameter( transformer, "meta.keywords", documentModel.getMeta().getAllKeyWords() );
addTransformerParameter( transformer, "meta.pagesize", documentModel.getMeta().getPageSize(),
ITextUtil.getPageSize( ITextUtil.getDefaultPageSize() ) );
addTransformerParameter( transformer, "meta.producer", documentModel.getMeta().getGenerator(),
"Apache Doxia iText" );
addTransformerParameter( transformer, "meta.subject", documentModel.getMeta().getSubject(),
( documentModel.getMeta().getTitle() != null ? documentModel.getMeta().getTitle()
: "" ) );
addTransformerParameter( transformer, "meta.title", documentModel.getMeta().getTitle() );
if ( hasNullMeta )
{
documentModel.setMeta( null );
}
// cover parameter
boolean hasNullCover = false;
if ( documentModel.getCover() == null )
{
hasNullCover = true;
documentModel.setCover( new DocumentCover() );
}
addTransformerParameter( transformer, "cover.author", documentModel.getCover().getAllAuthorNames(),
System.getProperty( "user.name", "null" ) );
String companyLogo = getLogoURL( documentModel.getCover().getCompanyLogo(), iTextFile.getParentFile() );
addTransformerParameter( transformer, "cover.companyLogo", companyLogo );
addTransformerParameter( transformer, "cover.companyName", documentModel.getCover().getCompanyName() );
if ( documentModel.getCover().getCoverdate() == null )
{
documentModel.getCover().setCoverDate( new Date() );
addTransformerParameter( transformer, "cover.date", documentModel.getCover().getCoverdate() );
documentModel.getCover().setCoverDate( null );
}
else
{
addTransformerParameter( transformer, "cover.date", documentModel.getCover().getCoverdate() );
}
addTransformerParameter( transformer, "cover.subtitle", documentModel.getCover().getCoverSubTitle() );
addTransformerParameter( transformer, "cover.title", documentModel.getCover().getCoverTitle() );
addTransformerParameter( transformer, "cover.type", documentModel.getCover().getCoverType() );
addTransformerParameter( transformer, "cover.version", documentModel.getCover().getCoverVersion() );
String projectLogo = getLogoURL( documentModel.getCover().getProjectLogo(), iTextFile.getParentFile() );
addTransformerParameter( transformer, "cover.projectLogo", projectLogo );
addTransformerParameter( transformer, "cover.projectName", documentModel.getCover().getProjectName() );
if ( hasNullCover )
{
documentModel.setCover( null );
}
}
/**
* @param transformer not null
* @param name not null
* @param value could be empty
* @param defaultValue could be empty
* @since 1.1.1
*/
private void addTransformerParameter( Transformer transformer, String name, String value, String defaultValue )
{
if ( StringUtils.isEmpty( value ) )
{
addTransformerParameter( transformer, name, defaultValue );
}
else
{
addTransformerParameter( transformer, name, value );
}
}
/**
* @param transformer not null
* @param name not null
* @param value could be empty
* @since 1.1.1
*/
private void addTransformerParameter( Transformer transformer, String name, String value )
{
if ( StringUtils.isEmpty( value ) )
{
return;
}
transformer.setParameter( name, value );
}
/**
* Transform a document to an iTextFile.
*
* @param documentModel the DocumentModel to take the parameters from, could be null.
* @param document the Document to transform.
* @param iTextFile the resulting iText xml file.
* @throws DocumentRendererException in case of a transformation error.
*/
private void transform( DocumentModel documentModel, Document document, File iTextFile )
throws DocumentRendererException
{
Transformer transformer = initTransformer();
addTransformerParameters( transformer, documentModel, iTextFile );
try
{
transformer.transform( new DOMSource( document ), new StreamResult( iTextFile ) );
}
catch ( TransformerException e )
{
throw new DocumentRendererException( "Error transforming Document " + document + ": " + e.getMessage() );
}
}
/**
* @param filesToProcess not null
* @param outputDirectory not null
* @return a list of all parsed files.
* @throws DocumentRendererException if any
* @throws IOException if any
* @since 1.1.1
*/
private List parseAllFiles( Map filesToProcess, File outputDirectory )
throws DocumentRendererException, IOException
{
List iTextFiles = new LinkedList();
for ( Iterator it = filesToProcess.keySet().iterator(); it.hasNext(); )
{
String key = (String) it.next();
SiteModule module = (SiteModule) filesToProcess.get( key );
File fullDoc = new File( getBaseDir(), module.getSourceDirectory() + File.separator + key );
String outputITextName = key.substring( 0, key.lastIndexOf( "." ) + 1 ) + "xml";
File outputITextFileTmp = new File( outputDirectory, outputITextName );
outputITextFileTmp.deleteOnExit();
if ( !outputITextFileTmp.getParentFile().exists() )
{
outputITextFileTmp.getParentFile().mkdirs();
}
iTextFiles.add( outputITextFileTmp );
parse( fullDoc, module, outputITextFileTmp );
}
return iTextFiles;
}
/**
* @param filesToProcess not null
* @param outputDirectory not null
* @return a list of all parsed files.
* @throws DocumentRendererException if any
* @throws IOException if any
* @since 1.1.1
*/
private List parseTOCFiles( Map filesToProcess, File outputDirectory, DocumentModel documentModel )
throws DocumentRendererException, IOException
{
List iTextFiles = new LinkedList();
for ( Iterator it = documentModel.getToc().getItems().iterator(); it.hasNext(); )
{
DocumentTOCItem tocItem = (DocumentTOCItem) it.next();
if ( tocItem.getRef() == null )
{
getLogger().debug(
"No ref defined for the tocItem '" + tocItem.getName()
+ "' in the document descriptor. IGNORING" );
continue;
}
String href = StringUtils.replace( tocItem.getRef(), "\\", "/" );
if ( href.lastIndexOf( "." ) != -1 )
{
href = href.substring( 0, href.lastIndexOf( "." ) );
}
for ( Iterator i = siteModuleManager.getSiteModules().iterator(); i.hasNext(); )
{
SiteModule module = (SiteModule) i.next();
File moduleBasedir = new File( getBaseDir(), module.getSourceDirectory() );
if ( moduleBasedir.exists() )
{
String doc = href + "." + module.getExtension();
File source = new File( moduleBasedir, doc );
if ( source.exists() )
{
String outputITextName = doc.substring( 0, doc.lastIndexOf( "." ) + 1 ) + "xml";
File outputITextFileTmp = new File( outputDirectory, outputITextName );
outputITextFileTmp.deleteOnExit();
if ( !outputITextFileTmp.getParentFile().exists() )
{
outputITextFileTmp.getParentFile().mkdirs();
}
iTextFiles.add( outputITextFileTmp );
parse( source, module, outputITextFileTmp );
}
}
}
}
return iTextFiles;
}
/**
* @param logo
* @param parentFile
* @return the logo url or null if unable to create it.
* @since 1.1.1
*/
private String getLogoURL( String logo, File parentFile )
{
if ( logo == null )
{
return null;
}
try
{
return new URL( logo ).toString();
}
catch ( MalformedURLException e )
{
try
{
File f = new File( parentFile, logo );
if ( !f.exists() )
{
getLogger().warn( "The logo " + f.getAbsolutePath() + " doesnt exist. IGNORING" );
}
else
{
return f.toURL().toString();
}
}
catch ( MalformedURLException e1 )
{
// nope
}
}
return null;
}
}