/*
* Copyright (C) 2011 Laurent Caillette
*
* This program 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 3 of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.novelang.rendering;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.Charset;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.TransformerHandler;
import com.google.common.collect.ImmutableMap;
import org.apache.xalan.transformer.TransformerImpl;
import org.xml.sax.ContentHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.SAXException;
import static com.google.common.base.Preconditions.checkNotNull;
import org.novelang.common.SyntacticTree;
import org.novelang.common.metadata.DocumentMetadata;
import org.novelang.common.metadata.PageIdentifier;
import org.novelang.configuration.RenderingConfiguration;
import org.novelang.configuration.RenditionKinematic;
import org.novelang.logger.Logger;
import org.novelang.logger.LoggerFactory;
import org.novelang.outfit.DefaultCharset;
import org.novelang.outfit.loader.ResourceLoader;
import org.novelang.outfit.loader.ResourceName;
import org.novelang.outfit.xml.EntityEscapeSelector;
import org.novelang.outfit.xml.LocalEntityResolver;
import org.novelang.outfit.xml.LocalUriResolver;
import org.novelang.outfit.xml.SaxPipeline;
import org.novelang.outfit.xml.SaxRecorder;
import org.novelang.outfit.xml.TransformerCompositeException;
import org.novelang.outfit.xml.TransformerErrorListener;
import org.novelang.outfit.xml.XmlNamespaces;
import org.novelang.outfit.xml.XslTransformerFactory;
import org.novelang.parser.NodeKindTools;
import org.novelang.rendering.multipage.PagesExtractor;
import org.novelang.rendering.multipage.XslMultipageStylesheetCapture;
import org.novelang.rendering.multipage.XslPageIdentifierExtractor;
import org.novelang.rendering.xslt.validate.SaxConnectorForVerifier;
/**
* Writes XML basing on an XSLT stylesheet.
*
* @author Laurent Caillette
*/
public class XslWriter extends XmlWriter implements PagesExtractor {
private static final Logger LOGGER = LoggerFactory.getLogger( XslWriter.class ) ;
protected final EntityResolver entityResolver;
protected final URIResolver uriResolver;
protected static final RenditionMimeType DEFAULT_RENDITION_MIME_TYPE = RenditionMimeType.XML ;
protected final ResourceName xslFileName ;
protected final ResourceLoader resourceLoader ;
protected final EntityEscapeSelector entityEscapeSelector ;
private static final ResourceName IDENTITY_XSL_FILE_NAME = new ResourceName( "identity.xsl" ) ;
private final RenditionKinematic renditionKinematic;
/**
* Accumulates problems during XSL parsing and transformation.
* May need some extra care if we want to make the {@link XslWriter} reusable.
*/
private final TransformerErrorListener transformerErrorListener = new TransformerErrorListener() ;
private TransformerHandler transformerHandler;
public XslWriter( final RenderingConfiguration configuration, final ResourceName xslFileName )
throws IOException, TransformerConfigurationException, SAXException, TransformerCompositeException
{
this( configuration, xslFileName, DefaultCharset.RENDERING, DEFAULT_RENDITION_MIME_TYPE ) ;
}
public XslWriter(
final String namespaceUri,
final String nameQualifier,
final RenderingConfiguration configuration,
final ResourceName xslFileName
) throws IOException, TransformerConfigurationException, SAXException, TransformerCompositeException
{
this(
namespaceUri,
nameQualifier,
configuration,
xslFileName,
DEFAULT_RENDITION_MIME_TYPE
) ;
}
public XslWriter(
final String namespaceUri,
final String nameQualifier,
final RenderingConfiguration configuration,
final ResourceName xslFileName,
final RenditionMimeType mimeType
) throws IOException, TransformerConfigurationException, SAXException, TransformerCompositeException
{
this(
namespaceUri,
nameQualifier,
configuration,
xslFileName,
DefaultCharset.RENDERING,
mimeType,
EntityEscapeSelector.NO_ENTITY_ESCAPE
) ;
}
public XslWriter(
final RenderingConfiguration configuration,
final ResourceName xslFileName,
final Charset charset,
final RenditionMimeType mimeType
)
throws IOException, TransformerConfigurationException, SAXException, TransformerCompositeException
{
this( configuration, xslFileName, charset, mimeType, EntityEscapeSelector.NO_ENTITY_ESCAPE ) ;
}
public XslWriter(
final RenderingConfiguration configuration,
final ResourceName xslFileName,
final Charset charset,
final RenditionMimeType mimeType,
final EntityEscapeSelector entityEscapeSelector
)
throws IOException, TransformerConfigurationException, SAXException, TransformerCompositeException
{
this(
XmlNamespaces.TREE_NAMESPACE_URI,
XmlNamespaces.TREE_NAME_QUALIFIER,
configuration,
xslFileName,
charset,
mimeType,
entityEscapeSelector
) ;
}
public XslWriter(
final String namespaceUri,
final String nameQualifier,
final RenderingConfiguration configuration,
final ResourceName xslFileName,
final Charset charset,
final RenditionMimeType mimeType,
final EntityEscapeSelector entityEscapeSelector
)
throws IOException, TransformerConfigurationException, SAXException, TransformerCompositeException
{
super( namespaceUri, nameQualifier, charset, mimeType ) ;
this.entityEscapeSelector = checkNotNull( entityEscapeSelector ) ;
this.resourceLoader = checkNotNull( configuration.getResourceLoader() ) ;
this.renditionKinematic = checkNotNull( configuration.getRenderingKinematic() ) ;
final ResourceName safeXslFileName;
if( null == xslFileName ) {
safeXslFileName = IDENTITY_XSL_FILE_NAME;
} else {
safeXslFileName = xslFileName ;
}
this.xslFileName = safeXslFileName ;
entityResolver = new LocalEntityResolver( resourceLoader, entityEscapeSelector ) ;
uriResolver = new LocalUriResolver( resourceLoader, entityResolver ) {
@Override
protected ContentHandler decorate( final ContentHandler original ) {
return xslTransformerFactoryDecoratorInstaller.decorate( original ) ;
}
} ;
// Triggers XSL parsing.
transformerHandler = new XslTransformerFactory.FromResource(
resourceLoader,
xslFileName,
entityResolver,
uriResolver,
xslTransformerFactoryDecoratorInstaller,
transformerErrorListener
).newTransformerHandler() ;
LOGGER.debug( "Created ", getClass().getName(), " with stylesheet ", safeXslFileName ) ;
logLastParsedStylesheet() ;
}
@Override
protected ContentHandler createContentHandler(
final OutputStream outputStream,
final DocumentMetadata documentMetadata,
final Charset charset
)
throws Exception
{
LOGGER.debug( "Creating ContentHandler with charset ", charset.name() );
configure( transformerHandler.getTransformer(), documentMetadata ) ;
final ContentHandler sinkContentHandler =
createSinkContentHandler( outputStream, documentMetadata, charset ) ;
transformerHandler.setResult( new SAXResult( sinkContentHandler ) ) ;
final TransformerImpl transformer = ( TransformerImpl ) transformerHandler.getTransformer() ;
transformer.setErrorListener( transformerErrorListener ) ;
// Workaround to XALANJ-101. Works along with hacked TransformerImpl.
// Returning tranformerHandler alone was good enough until trying to reuse the transformer
// (for multipage output).
return transformer.getInputContentHandler( true ) ;
}
private void configure(
final Transformer transformer,
final DocumentMetadata documentMetadata
) {
transformer.setParameter( "timestamp", documentMetadata.getCreationTimestamp() ) ;
transformer.setParameter( "charset", documentMetadata.getCharset().name() ) ;
final String contentDirectoryUrl = removeTrailingSolidus( documentMetadata.getContentDirectory() ) ;
transformer.setParameter( "content-directory", contentDirectoryUrl ) ;
transformer.setParameter( "rendition-kinematic", renditionKinematic.name() ) ;
}
private static String removeTrailingSolidus( final URL contentDirectoryUrl ) {
final String externalForm = contentDirectoryUrl.toExternalForm() ;
if( externalForm.endsWith( "/" ) ) {
return externalForm.substring( 0, externalForm.length() - 1 ) ;
} else {
return externalForm ;
}
}
/**
* Hook for letting subclasses post-process XSL output.
*/
protected ContentHandler createSinkContentHandler(
final OutputStream outputStream,
final DocumentMetadata documentMetadata,
final Charset charset
) throws Exception
{
return super.createContentHandler( outputStream, documentMetadata, charset ) ;
}
@Override
public void finishWriting() throws Exception {
super.finishWriting() ;
transformerErrorListener.flush() ;
}
// ===========
// SaxPipeline
// ===========
/**
* Decorates {@link XslTransformerFactory}'s {@code TemplatesHandler} with a fresh
* {@link SaxPipeline} which performs stylesheet element name validation
* (using {@link SaxConnectorForVerifier}) and captures a nested stylesheet
* (using {@link XslMultipageStylesheetCapture} if any.
* Because of XSL's import mechanism, there can be multiple nested stylesheets.
* But since <import> is the first instruction in a stylesheet, we can safely
* assume that last nested stylesheet is the one to apply.
* Except for element name validation (which remains silent if everything's fine),
* the only side-effect of this method is to call
* {@link #setLastParsedStylesheet(org.novelang.outfit.xml.SaxRecorder.Player)}
*/
private final XslTransformerFactory.DecoratorInstaller xslTransformerFactoryDecoratorInstaller =
new XslTransformerFactory.DecoratorInstaller() {
@Override
public ContentHandler decorate( final ContentHandler original ) {
final SaxPipeline pipeline = new SaxPipeline( original ) ;
pipeline.add( new SaxPipeline.ForkingStage( new SaxConnectorForVerifier(
XmlNamespaces.TREE_NAMESPACE_URI,
NodeKindTools.getRenderingNames()
) ), 0 ) ;
pipeline.add( new XslMultipageStylesheetCapture( entityResolver ) {
@Override
protected void onStylesheetDocumentBuilt( final SaxRecorder.Player stylesheetPlayer ) {
setLastParsedStylesheet( stylesheetPlayer ) ;
}
}, 1 ) ;
return pipeline ;
}
}
;
// ==================
// Stylesheet capture
// ==================
@Override
public ImmutableMap< PageIdentifier, String > extractPages(
final SyntacticTree documentTree
) throws Exception
{
return new XslPageIdentifierExtractor(
entityResolver,
uriResolver,
getLastParsedStylesheet()
).extractPages( documentTree ) ;
}
private SaxRecorder.Player lastParsedStylesheet = null ;
private void setLastParsedStylesheet( final SaxRecorder.Player stylesheetPlayer ) {
this.lastParsedStylesheet = checkNotNull( stylesheetPlayer ) ;
}
/**
* @return a possibly null object.
*/
private SaxRecorder.Player getLastParsedStylesheet() {
return lastParsedStylesheet ;
}
private void logLastParsedStylesheet() {
final String xml ;
if( lastParsedStylesheet == null ) {
xml = null ;
} else {
try {
xml = SaxRecorder.asXml( lastParsedStylesheet ) ;
} catch( SAXException e ) {
throw new RuntimeException( e ) ;
} catch( IOException e ) {
throw new RuntimeException( e ) ;
}
}
LOGGER.debug( "Parsed nested stylesheet: ", xml ) ;
}
}