/**
* Copyright 2011 meltmedia
*
* 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 org.xchain.framework.sax;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.TransformerHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xchain.framework.lifecycle.XmlFactoryLifecycle;
import org.xchain.framework.factory.TemplatesFactory;
import org.xchain.framework.strategy.TemplatesConsumerStrategy;
import org.xchain.framework.strategy.SourceSourceStrategy;
import org.xchain.framework.strategy.InputSourceSourceStrategy;
import org.xchain.framework.util.ParserUtil;
import org.xchain.framework.util.ParseException;
import org.xchain.framework.util.ParsedTransformer;
import org.xchain.framework.net.DependencyTracker;
import org.xchain.framework.net.UrlFactoryUriResolver;
import org.xchain.framework.net.UrlFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.DTDHandler;
import org.xml.sax.SAXException;
import org.xml.sax.Locator;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.XMLFilterImpl;
/**
* This filter handles <?xchain-stylesheet?> processing instructions. These can be used to style the contents
* of the xchain before it is loaded.
* <?xchain-stylesheet system-id="" parameters="qname='value', qname='value', qname='value'"?>
*
* @author Christian Trimble
* @author Devon Tackett
* @author Jason Rose
* @author Josh Kennedy
*/
public class XChainDeclFilter
extends XMLFilterImpl
{
private static Logger log = LoggerFactory.getLogger(XChainDeclFilter.class);
public static String XCHAIN_STYLESHEET_TARGET = "xchain-stylesheet";
/** True if we are in the prolog, false otherwise. */
private boolean inProlog = false;
/** The list of filters that we need to add into the filter chain. */
private List<TransformerHandler> handlerList = new ArrayList<TransformerHandler>();
/** The list of sax events that we are caching until the prolog is finished. */
private List<SaxEvent> eventList = new ArrayList<SaxEvent>();
private ContentHandler contentHandler;
private ErrorHandler errorHandler;
private EntityResolver entityResolver;
private DTDHandler dtdHandler;
private Locator locator;
private String baseUri;
public void setContentHandler( ContentHandler contentHandler ) { this.contentHandler = contentHandler; }
public ContentHandler getContentHandler() { return this.contentHandler; }
public void setErrorHandler( ErrorHandler errorHandler ) { this.errorHandler = errorHandler; }
public ErrorHandler getErrorHandler() { return this.errorHandler; }
public void setEntityResolver( EntityResolver entityResolver ) { this.entityResolver = entityResolver; }
public EntityResolver getEntityResolver() { return this.entityResolver; }
public void setDTDHandler( DTDHandler dtdHandler ) { this.dtdHandler = dtdHandler; }
public DTDHandler getDTDHandler() { return this.dtdHandler; }
public void setDocumentLocator( Locator locator ) { this.locator = locator; }
public Locator getDocumentLocator() { return this.locator; }
public void parse( String systemId )
throws SAXException, IOException
{
baseUri = systemId;
super.parse(systemId);
}
public void parse( InputSource source )
throws SAXException, IOException
{
baseUri = source.getSystemId();
super.parse(source);
}
/**
* Prepares the filter to start tracking prolog events and caches the start document event.
*/
public void startDocument()
throws SAXException
{
// we are now in the prolog.
inProlog = true;
handlerList.clear();
eventList.clear();
// track the start document event.
eventList.add(new StartDocumentEvent());
}
public void endDocument()
throws SAXException
{
if( inProlog ) {
endProlog();
}
super.endDocument();
}
/**
* Handles <?xchain-stylesheet?> processing instructions and caches all others.
*/
public void processingInstruction(String target, String data)
throws SAXException
{
if( XCHAIN_STYLESHEET_TARGET.equals(target) && inProlog ) {
addTransformerHandler(data);
}
else if( inProlog ) {
// cache the processing instruction.
eventList.add(new ProcessingInstructionEvent(target, data));
}
else {
super.processingInstruction(target, data);
}
}
/**
* Caches notation decl events until the prolog is finished.
*/
public void notationDecl(String name, String publicId, String systemId)
throws SAXException
{
if( inProlog ) {
eventList.add(new NotationDeclEvent(name, publicId, systemId));
}
else {
super.notationDecl(name, publicId, systemId);
}
}
/**
* Caches unparsed entity decl events until the prolog is finished.
*/
public void unparsedEntityDecl(String name, String publicId, String systemId, String notationName)
throws SAXException
{
if( inProlog ) {
eventList.add(new UnparsedEntityDeclEvent(name, publicId, systemId, notationName));
}
else {
super.unparsedEntityDecl(name, publicId, systemId, notationName);
}
}
/**
* If this start element is the end of the prolog, then endProlog() is called. Then start
* elements are passed along.
*/
public void startElement( String uri, String localName, String qName, Attributes attributes )
throws SAXException
{
if( inProlog ) {
endProlog();
}
super.startElement( uri, localName, qName, attributes );
}
/**
* If this start prefix mapping is the end of the prolog, then endProlog() is called. Then
* the start prefix mapping event is passed along.
*/
public void startPrefixMapping( String prefix, String uri )
throws SAXException
{
if( inProlog ) {
endProlog();
}
super.startPrefixMapping( prefix, uri );
}
/**
* Parses a data section and creates a new transformer handler.
*/
private void addTransformerHandler( String data )
throws SAXException
{
ParsedTransformer parsedTransformer = null;
TransformerHandler handler = null;
try {
parsedTransformer = ParserUtil.parseTransformer(data);
}
catch( ParseException pe ) {
throw new SAXException("Could not parse the xchain-stylesheet processing instruction.", pe);
}
String systemId = parsedTransformer.getAttributes().get("system-id");
// we must have a system id.
if( systemId == null ) {
throw new SAXException("<?xchain-stylesheet?> processing instructions require the system-id attribute.");
}
// resolve the systemId against our parents locator.
if( baseUri != null ) {
systemId = URI.create(baseUri).resolve(systemId).toString();
}
DependencyTracker.getInstance().startTracking();
// create the strategies for loading templates.
try {
DependencyTracker.getInstance().dependencyFound(UrlFactory.getInstance().newUrl(systemId));
// create the transformer handler for the templates object.
handler = XmlFactoryLifecycle.newTransformerHandler(systemId);
Transformer transformer = handler.getTransformer();
//transformer.setURIResolver(DependencyTracker.getInstance().createDependencyUriResolver(new UrlFactoryUriResolver()));
transformer.setURIResolver(new LoggingUriResolver(DependencyTracker.getInstance().createDependencyUriResolver(new UrlFactoryUriResolver())));
for( Map.Entry<String, String> parameter : parsedTransformer.getParameters().entrySet() ) {
transformer.setParameter(parameter.getKey(), parameter.getValue());
}
for( Map.Entry<String, String> outputProperty : parsedTransformer.getOutputProperties().entrySet() ) {
transformer.setOutputProperty(outputProperty.getKey(), outputProperty.getValue());
}
}
//catch( SAXException saxe ) {
//throw saxe;
//}
catch( Exception e ) {
throw new SAXException("Could not create transformer for system id '"+systemId+"' due to an exception.", e);
}
finally {
Set<URL> dependencies = DependencyTracker.getInstance().stopTracking();
if( log.isDebugEnabled() ) {
StringBuilder logBuilder = new StringBuilder();
logBuilder.append("The system id '"+systemId+"' has the following dependencies:\n");
for( URL url : dependencies ) {
logBuilder.append(url.toExternalForm()).append("\n");
}
log.debug(logBuilder.toString());
}
}
handlerList.add(handler);
}
private String resolveEntities( String data )
{
return data;
}
/**
* Starts passing events to the filter.
*/
private void endProlog()
throws SAXException
{
inProlog = false;
// if the filter list is not empty, then reconfigure the filter chain.
if( handlerList.isEmpty() ) {
super.setContentHandler(contentHandler);
super.setDTDHandler(dtdHandler);
super.setEntityResolver(entityResolver);
super.setErrorHandler(errorHandler);
contentHandler.setDocumentLocator(locator);
}
else {
// this instance if the parent of the first filter.
// the first through nth filters are attached to the previous filter.
for( int i = 0; i < handlerList.size()-1; i++ ) {
handlerList.get(i).setDocumentLocator(locator);
handlerList.get(i).setResult(new SAXResult(handlerList.get(i+1)));
}
handlerList.get(handlerList.size()-1).setResult(new SAXResult(contentHandler));
handlerList.get(0).setDocumentLocator(locator);
super.setContentHandler(handlerList.get(0));
if( handlerList.get(0) instanceof ErrorHandler ) {
super.setErrorHandler((ErrorHandler)handlerList.get(0));
}
if( handlerList.get(0) instanceof EntityResolver ) {
super.setEntityResolver((EntityResolver)handlerList.get(0));
}
if( handlerList.get(0) instanceof DTDHandler ) {
super.setDTDHandler((DTDHandler)handlerList.get(0));
}
}
// pass all of the cached events to the handlers.
for( SaxEvent event : eventList ) {
event.flushEvent();
}
eventList.clear();
}
/**
* The interface for cached sax events.
*/
private interface SaxEvent
{
/**
* Flushes this event through to the parent implementation.
*/
public void flushEvent()
throws SAXException;
}
private class ProcessingInstructionEvent
implements SaxEvent
{
private String target;
private String data;
public ProcessingInstructionEvent( String target, String data )
{
this.target = target;
this.data = data;
}
public void flushEvent()
throws SAXException
{
XChainDeclFilter.super.processingInstruction( target, data );
}
}
private class StartDocumentEvent
implements SaxEvent
{
public void flushEvent()
throws SAXException
{
XChainDeclFilter.super.startDocument();
}
}
private class NotationDeclEvent
implements SaxEvent
{
private String name;
private String publicId;
private String systemId;
public NotationDeclEvent( String name, String publicId, String systemId )
{
this.name = name;
this.publicId = publicId;
this.systemId = systemId;
}
public void flushEvent()
throws SAXException
{
XChainDeclFilter.super.notationDecl(name, publicId, systemId);
}
}
private class UnparsedEntityDeclEvent
implements SaxEvent
{
private String name;
private String publicId;
private String systemId;
private String notationName;
public UnparsedEntityDeclEvent( String name, String publicId, String systemId, String notationName )
{
this.name = name;
this.publicId = publicId;
this.systemId = systemId;
this.notationName = notationName;
}
public void flushEvent()
throws SAXException
{
XChainDeclFilter.super.unparsedEntityDecl(name, publicId, systemId, notationName);
}
}
public static class LoggingUriResolver
implements URIResolver
{
private URIResolver wrapped;
public LoggingUriResolver( URIResolver wrapped )
{
this.wrapped = wrapped;
}
public Source resolve(String href, String base)
throws TransformerException
{
try {
return wrapped.resolve(href, base);
}
catch( TransformerException te ) {
if( log.isDebugEnabled() ) {
log.debug("Could not load document for href '"+href+"' and base '"+base+"'.", te);
}
throw te;
}
catch( RuntimeException re ) {
if( log.isDebugEnabled() ) {
log.debug("Could not load document for href '"+href+"' and base '"+base+"'.", re);
}
throw re;
}
}
}
}