/**
* 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.jsl;
import org.xchain.namespaces.jsl.AbstractTemplateCommand;
import org.xchain.namespaces.jsl.TemporaryCommand;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.apache.commons.digester.Digester;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.LinkedList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* @author Christian Trimble
* @author Josh Kennedy
*/
public class SaxTemplateHandler
extends AbstractSaxTemplateHandler
{
public static Logger log = LoggerFactory.getLogger(SaxTemplateHandler.class);
public static String TEMPLATE_LOCAL_NAME = "template";
public static String VALUE_OF_LOCAL_NAME = "value-of";
public static String TEXT_LOCAL_NAME = "text";
public static String COMMENT_LOCAL_NAME = "comment";
public static String ELEMENT_LOCAL_NAME = "element";
public static String ATTRIBUTE_LOCAL_NAME = "attribute";
public static String TEMPORARY_CHAIN_LOCAL_NAME = "temporary";
public static enum CharacterTarget{ NONE, SOURCE_BUILDER, DIGESTER };
protected static Pattern whitespacePattern = null;
static {
try {
whitespacePattern = Pattern.compile("\\A\\s*\\z");
}
catch( PatternSyntaxException pse ) {
if( log.isErrorEnabled() ) {
log.error("Could not compile the whitespace pattern.", pse);
}
}
}
/** A control stack for character targets. */
protected LinkedList<CharacterTarget> charactersTargetStack = new LinkedList<CharacterTarget>();
/** A control stack for ignorable whitespace. */
protected LinkedList<CharacterTarget> ignorableWhitespaceTargetStack = new LinkedList<CharacterTarget>();
/** The template builder that will be used to create the source files. */
protected TemplateSourceBuilder sourceBuilder = new TemplateSourceBuilder();
/** The digester that we will inject templates into. */
protected Digester digester = null;
/** The depth of jsl template elements in the current event stream. */
protected int templateElementDepth = 0;
public void setDigester( Digester digester ) { this.digester = digester; }
//
// Handlers for JSL element events.
//
/**
* Handles start element events for all JSL elements.
*/
protected void startJslElement( String uri, String localName, String qName, Attributes attributes )
throws SAXException
{
// scan for jsl elements that come out of order.
if( TEMPLATE_LOCAL_NAME.equals( localName ) ) {
templateElementDepth++;
}
else if( templateElementDepth == 0 ) {
throw new SAXException("The element {"+uri+"}"+localName+"was found outside of a {"+JSL_NAMESPACE+"}template element.");
}
boolean startSource = elementInfoStack.size() == 1 || elementInfoStack.get(1).getElementType() == ElementType.COMMAND_ELEMENT_TYPE;
// if the parent element is null or a command element, then we need to start a new template source.
if( startSource ) {
sourceBuilder.startSource(elementInfoStack.getFirst().getSourcePrefixMapping(), elementInfoStack.getFirst().getSourceExcludeResultPrefixSet(), TEMPLATE_LOCAL_NAME.equals(localName));
}
if( TEMPLATE_LOCAL_NAME.equals( localName ) ) {
startJslTemplateElement( uri, localName, qName, attributes );
}
else if( VALUE_OF_LOCAL_NAME.equals( localName ) ) {
startJslValueOfElement( uri, localName, qName, attributes );
}
else if( TEXT_LOCAL_NAME.equals( localName ) ) {
startJslTextElement( uri, localName, qName, attributes );
}
else if( COMMENT_LOCAL_NAME.equals( localName ) ) {
startJslCommentElement( uri, localName, qName, attributes );
}
else if( ELEMENT_LOCAL_NAME.equals( localName ) ) {
startJslDynamicElement( uri, localName, qName, attributes );
}
else if( ATTRIBUTE_LOCAL_NAME.equals( localName ) ) {
startJslDynamicAttribute( uri, localName, qName, attributes );
}
else {
throw new SAXException("Unknown template element '{"+uri+"}"+localName+"'.");
}
if( startSource ) {
// send start prefix mapping events to the digester.
for( Map.Entry<String, String> mapping : elementInfoStack.getFirst().getHandlerPrefixMapping().entrySet() ) {
getContentHandler().startPrefixMapping(mapping.getKey(), mapping.getValue());
}
// create a new attributes object with any attributes that are in an xchain namespace.
AttributesImpl digesterAttributes = new AttributesImpl();
for( int i = 0; i < attributes.getLength(); i++ ) {
if( commandNamespaceSet.contains(attributes.getURI(i)) ) {
digesterAttributes.addAttribute(attributes.getURI(i), attributes.getLocalName(i), attributes.getQName(i), attributes.getType(i), attributes.getValue(i));
}
}
// send the start template element to the digester.
getContentHandler().startElement( JSL_NAMESPACE, TEMPORARY_CHAIN_LOCAL_NAME, temporaryChainQName(), digesterAttributes );
}
}
/**
* Handles end element events for all JSL elements.
*/
protected void endJslElement( String uri, String localName, String qName )
throws SAXException
{
if( TEMPLATE_LOCAL_NAME.equals( localName ) ) {
// this element is a flag, ignore it.
endJslTemplateElement( uri, localName, qName );
}
else if( VALUE_OF_LOCAL_NAME.equals( localName ) ) {
endJslValueOfElement( uri, localName, qName );
}
else if( TEXT_LOCAL_NAME.equals( localName ) ) {
endJslTextElement( uri, localName, qName );
}
else if( COMMENT_LOCAL_NAME.equals( localName ) ) {
endJslCommentElement( uri, localName, qName );
}
else if( ELEMENT_LOCAL_NAME.equals( localName ) ) {
endJslDynamicElement( uri, localName, qName );
}
else if( ATTRIBUTE_LOCAL_NAME.equals( localName ) ) {
endJslDynamicAttribute( uri, localName, qName );
}
else {
throw new SAXException("Unknown template element '{"+uri+"}"+localName+"'.");
}
// if the parent element is null or a command element, then we need to end the template source.
if( elementInfoStack.size() == 1 || elementInfoStack.get(1).getElementType() == ElementType.COMMAND_ELEMENT_TYPE ) {
handleSource(sourceBuilder.endSource());
// send end prefix mapping events to the digester.
for( String prefix : elementInfoStack.getFirst().getHandlerPrefixMapping().keySet() ) {
getContentHandler().endPrefixMapping(prefix);
}
}
if( TEMPLATE_LOCAL_NAME.equals( localName ) ) {
templateElementDepth--;
if( templateElementDepth < 0 ) {
throw new SAXException("The template element depth has come out of synch.");
}
}
}
private void handleSource( SourceResult templateSource )
throws SAXException
{
AbstractTemplateCommand replacementCommand = null;
try {
replacementCommand = (AbstractTemplateCommand)sourceCompiler.compileTemplate(templateSource).newInstance();
}
catch( Exception e ) {
throw new SAXException("Could not instantiate template class.", e);
}
// remove the temporary command off of the digesters stack.
TemporaryCommand temporaryCommand = (TemporaryCommand)digester.pop();
// add all of the temporary commands children to the replacement command.
replacementCommand.getCommandList().addAll(temporaryCommand.getCommandList());
replacementCommand.setLocator(temporaryCommand.getLocator());
// push the replacement command onto the stack.
digester.push(replacementCommand);
// pass the end element event to the digester.
getContentHandler().endElement( JSL_NAMESPACE, TEMPORARY_CHAIN_LOCAL_NAME, temporaryChainQName());
}
private String temporaryChainQName()
{
String jslNamespacePrefix = prefixMappingContext.lookUpPrefix(JSL_NAMESPACE);
return "".equals(jslNamespacePrefix) ? TEMPORARY_CHAIN_LOCAL_NAME : jslNamespacePrefix + ":" + TEMPORARY_CHAIN_LOCAL_NAME;
}
protected void startJslTemplateElement( String uri, String localName, String qName, Attributes attributes )
throws SAXException
{
charactersTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
ignorableWhitespaceTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
}
protected void endJslTemplateElement( String uri, String localName, String qName )
throws SAXException
{
// configure the character target stacks.
charactersTargetStack.removeFirst();
ignorableWhitespaceTargetStack.removeFirst();
}
/**
* Handles start element events for JSL value-of elements.
*/
protected void startJslValueOfElement( String uri, String localName, String qName, Attributes attributes )
throws SAXException
{
// configure the character target stacks.
charactersTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
ignorableWhitespaceTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
String select = attributes.getValue("", "select");
if( select == null ) {
throw new SAXException("The {"+JSL_NAMESPACE+"}"+VALUE_OF_LOCAL_NAME+" requires a select attribute.");
}
for( Map.Entry<String, String> entry : elementInfoStack.getFirst().getPrefixMapping().entrySet() ) {
sourceBuilder.appendContextStartPrefixMapping(entry.getKey(), entry.getValue());
}
sourceBuilder.appendValueOf(select);
for( Map.Entry<String, String> entry : elementInfoStack.getFirst().getPrefixMapping().entrySet() ) {
sourceBuilder.appendContextEndPrefixMapping(entry.getKey());
}
}
/**
* Handles end element events for JSL value-of elements.
*/
protected void endJslValueOfElement( String uri, String localName, String qName )
throws SAXException
{
// configure the character target stacks.
charactersTargetStack.removeFirst();
ignorableWhitespaceTargetStack.removeFirst();
}
/**
* Handles start element events for JSL text elements.
*/
protected void startJslTextElement( String uri, String localName, String qName, Attributes attributes )
throws SAXException
{
// push characters target.
charactersTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
// push ignorable whitespace target.
ignorableWhitespaceTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
}
/**
* Handles end element events for JSL text elements.
*/
protected void endJslTextElement( String uri, String localName, String qName )
throws SAXException
{
// pop characters target.
charactersTargetStack.removeFirst();
// pop ignorable whitespace target.
ignorableWhitespaceTargetStack.removeFirst();
}
/**
* Handles start element events for JSL comment elements.
*/
protected void startJslCommentElement( String uri, String localName, String qName, Attributes attributes )
throws SAXException
{
// whitespace needs to go to the source builder.
charactersTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
ignorableWhitespaceTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
// start the comment.
sourceBuilder.appendStartComment();
}
/**
* Handles end element events for JSL comment elements.
*/
protected void endJslCommentElement( String uri, String localName, String qName )
throws SAXException
{
// end the comment.
sourceBuilder.appendEndComment();
// reset the whitespace targets.
charactersTargetStack.removeFirst();
ignorableWhitespaceTargetStack.removeFirst();
}
/**
* Handles start element events for JSL element elements.
*/
protected void startJslDynamicElement( String uri, String localName, String qName, Attributes attributes )
throws SAXException
{
// whitespace needs to go to the source builder.
charactersTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
ignorableWhitespaceTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
// start the element
String name = attributes.getValue("", "name");
String namespace = attributes.getValue("", "namespace");
// get the element info for the head of the stack.
ElementInfo elementInfo = elementInfoStack.getFirst();
sourceBuilder.startStartElement();
// send the prefix mappings to the source builder.
for( Map.Entry<String, String> prefixMapping : elementInfo.getPrefixMapping().entrySet() ) {
sourceBuilder.appendStartPrefixMapping(prefixMapping.getKey(), prefixMapping.getValue());
}
// send any exclude result prefix events that need to go out.
for( String excludeResultPrefix : elementInfo.getExcludeResultPrefixSet() ) {
sourceBuilder.appendStartExcludeResultPrefix(excludeResultPrefix);
}
sourceBuilder.appendStartDynamicElement(name, namespace);
sourceBuilder.endStartElement();
}
/**
* Handles end element events for JSL element elements.
*/
protected void endJslDynamicElement( String uri, String localName, String qName )
throws SAXException
{
ElementInfo elementInfo = elementInfoStack.getFirst();
sourceBuilder.startEndElement();
sourceBuilder.appendEndDynamicElement();
// send any exclude result prefix events that need to go out.
for( String excludeResultPrefix : elementInfo.getExcludeResultPrefixSet() ) {
sourceBuilder.appendEndExcludeResultPrefix(excludeResultPrefix);
}
// send the prefix mappings to the source builder.
for( Map.Entry<String, String> prefixMapping : elementInfo.getPrefixMapping().entrySet() ) {
sourceBuilder.appendEndPrefixMapping(prefixMapping.getKey());
}
sourceBuilder.endEndElement();
charactersTargetStack.removeFirst();
ignorableWhitespaceTargetStack.removeFirst();
}
/**
* Handles start element events for JSL attribute elements.
*/
protected void startJslDynamicAttribute( String uri, String localName, String qName, Attributes attributes )
throws SAXException
{
ElementInfo elementInfo = elementInfoStack.getFirst();
charactersTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
ignorableWhitespaceTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
// send the prefix mappings to the source builder.
for( Map.Entry<String, String> prefixMapping : elementInfo.getPrefixMapping().entrySet() ) {
sourceBuilder.appendStartPrefixMapping(prefixMapping.getKey(), prefixMapping.getValue());
}
// send any exclude result prefix events that need to go out.
for( String excludeResultPrefix : elementInfo.getExcludeResultPrefixSet() ) {
sourceBuilder.appendStartExcludeResultPrefix(excludeResultPrefix);
}
String name = attributes.getValue("", "name");
String namespace = attributes.getValue("", "namespace");
sourceBuilder.appendStartDynamicAttribute(name, namespace);
}
/**
* Handles end element events for JSL element elements.
*/
protected void endJslDynamicAttribute( String uri, String localName, String qName )
throws SAXException
{
ElementInfo elementInfo = elementInfoStack.getFirst();
sourceBuilder.appendEndDynamicAttribute();
// send any exclude result prefix events that need to go out.
for( String excludeResultPrefix : elementInfo.getExcludeResultPrefixSet() ) {
sourceBuilder.appendEndExcludeResultPrefix(excludeResultPrefix);
}
// send the prefix mappings to the source builder.
for( Map.Entry<String, String> prefixMapping : elementInfo.getPrefixMapping().entrySet() ) {
sourceBuilder.appendEndPrefixMapping(prefixMapping.getKey());
}
charactersTargetStack.removeFirst();
ignorableWhitespaceTargetStack.removeFirst();
}
/**
* Handles start element events for elements in an xchain namespace.
*/
protected void startCommandElement( String uri, String localName, String qName, Attributes attributes )
throws SAXException
{
// configure the character target stacks.
charactersTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
ignorableWhitespaceTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
// get the element info.
ElementInfo elementInfo = elementInfoStack.getFirst();
// if this parent element is not a command, then we need to append a command call and start
if( elementInfoStack.size() > 1 && elementInfoStack.get(1).getElementType() != ElementType.COMMAND_ELEMENT_TYPE ) {
sourceBuilder.appendCommandCall();
}
for( Map.Entry<String, String> prefixMapping : elementInfo.getHandlerPrefixMapping().entrySet() ) {
getContentHandler().startPrefixMapping(prefixMapping.getKey(), prefixMapping.getValue());
}
// send the start element to the digester.
getContentHandler().startElement( uri, localName, qName, attributes );
}
/**
* Handles end element events for elements in an xchain namespace.
*/
protected void endCommandElement( String uri, String localName, String qName )
throws SAXException
{
// pop the character target stacks.
charactersTargetStack.removeFirst();
ignorableWhitespaceTargetStack.removeFirst();
// get the element info.
ElementInfo elementInfo = elementInfoStack.getFirst();
// we do not need to do anything to the source builder.
// send the end element to the digester.
getContentHandler().endElement( uri, localName, qName );
for( Map.Entry<String, String> prefixMapping : elementInfo.getHandlerPrefixMapping().entrySet() ) {
getContentHandler().endPrefixMapping(prefixMapping.getKey());
}
}
/**
* Handles start element events for template elements.
*/
protected void startTemplateElement( String uri, String localName, String qName, Attributes attributes )
throws SAXException
{
if( templateElementDepth == 0 ) {
throw new SAXException("The element {"+uri+"}"+localName+" was found outside of a template element.");
}
// push characters target.
charactersTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
// push ignorable whitespace target.
ignorableWhitespaceTargetStack.addFirst(CharacterTarget.SOURCE_BUILDER);
boolean startSource = elementInfoStack.size() == 1 || elementInfoStack.get(1).getElementType() == ElementType.COMMAND_ELEMENT_TYPE;
// if the parent element is null or a command element, then we need to start a new template source.
if( startSource ) {
sourceBuilder.startSource(elementInfoStack.getFirst().getSourcePrefixMapping(), elementInfoStack.getFirst().getSourceExcludeResultPrefixSet(), false);
}
try {
// get the element info for the head of the stack.
ElementInfo elementInfo = elementInfoStack.getFirst();
// let the source builder know that we are starting a start element.
sourceBuilder.startStartElement();
// send the prefix mappings to the source builder.
for( Map.Entry<String, String> prefixMapping : elementInfo.getPrefixMapping().entrySet() ) {
sourceBuilder.appendStartPrefixMapping(prefixMapping.getKey(), prefixMapping.getValue());
}
// send any exclude result prefix events that need to go out.
for( String excludeResultPrefix : elementInfo.getExcludeResultPrefixSet() ) {
sourceBuilder.appendStartExcludeResultPrefix(excludeResultPrefix);
}
// send the attributes to the source builder.
for( int i = 0; i < attributes.getLength(); i++) {
sourceBuilder.appendAttributeValueTemplate(attributes.getURI(i), attributes.getLocalName(i), attributes.getQName(i), attributes.getValue(i));
}
// append the start element.
sourceBuilder.appendStartElement(uri, localName, qName);
// let the source builder know that we are ending a start element.
sourceBuilder.endStartElement();
}
catch( SAXException e ) {
throw e;
}
catch( Exception e ) {
throw new SAXException(e);
}
// we need to do this just before we stop templating, so this kind of code should be in start command, or end jsl template and end template when there was not
// a nested command. We need to delay this, since the passed through template must provide the namespaces state from all of the consumed elements.
if( startSource ) {
// send start prefix mapping events to the digester.
for( Map.Entry<String, String> mapping : elementInfoStack.getFirst().getHandlerPrefixMapping().entrySet() ) {
getContentHandler().startPrefixMapping(mapping.getKey(), mapping.getValue());
}
// create a new attributes object with any attributes that are in an xchain namespace.
AttributesImpl digesterAttributes = new AttributesImpl();
for( int i = 0; i < attributes.getLength(); i++ ) {
if( commandNamespaceSet.contains(attributes.getURI(i)) ) {
digesterAttributes.addAttribute(attributes.getURI(i), attributes.getLocalName(i), attributes.getQName(i), attributes.getType(i), attributes.getValue(i));
}
}
// send the start template element to the digester.
getContentHandler().startElement( JSL_NAMESPACE, TEMPORARY_CHAIN_LOCAL_NAME, temporaryChainQName(), digesterAttributes );
}
}
/**
* Handles end element events for template elements.
*/
public void endTemplateElement( String uri, String localName, String qName )
throws SAXException
{
// pop the character target stacks.
charactersTargetStack.removeFirst();
ignorableWhitespaceTargetStack.removeFirst();
// get the element info for the head of the stack.
ElementInfo elementInfo = elementInfoStack.getFirst();
// let the source builder know that we are starting a start element.
sourceBuilder.startEndElement();
// append the end element.
sourceBuilder.appendEndElement(uri, localName, qName);
// send any exclude result prefix events that need to go out.
for( String excludeResultPrefix : elementInfo.getExcludeResultPrefixSet() ) {
sourceBuilder.appendEndExcludeResultPrefix(excludeResultPrefix);
}
// send the prefix mappings to the source builder.
for( Map.Entry<String, String> prefixMapping : elementInfo.getPrefixMapping().entrySet() ) {
sourceBuilder.appendEndPrefixMapping(prefixMapping.getKey());
}
// let the source builder know that we are ending a start element.
sourceBuilder.endEndElement();
boolean startSource = elementInfoStack.size() == 1 || elementInfoStack.get(1).getElementType() == ElementType.COMMAND_ELEMENT_TYPE;
// if the parent element is null or a command element, then we need to start a new template source.
if( startSource ) {
handleSource(sourceBuilder.endSource());
// send end prefix mapping events to the digester.
for( String prefix : elementInfoStack.getFirst().getHandlerPrefixMapping().keySet() ) {
getContentHandler().endPrefixMapping(prefix);
}
}
}
//
// Handlers for element body content events.
//
/**
* Handles characters found in the source document.
*/
public void flushCharacters()
throws SAXException
{
if( !charactersTargetStack.isEmpty() ) {
// switch on the characters target.
switch( charactersTargetStack.getFirst() ) {
case NONE:
break;
case SOURCE_BUILDER:
if( !whitespacePattern.matcher(charactersBuilder).matches() ) {
// if we are in an xchain command, then we need to start a new source.
boolean startSource = elementInfoStack.size() == 0 || elementInfoStack.get(0).getElementType() == ElementType.COMMAND_ELEMENT_TYPE;
if( startSource ) {
// don't send any mappings for the element.
sourceBuilder.startSource(new HashMap<String, String>(), new HashSet<String>(), false);
getContentHandler().startElement( JSL_NAMESPACE, TEMPORARY_CHAIN_LOCAL_NAME, temporaryChainQName(), new AttributesImpl() );
}
sourceBuilder.appendCharacters(charactersBuilder.toString());
if( startSource ) {
handleSource(sourceBuilder.endSource());
}
}
break;
case DIGESTER:
char[] characters = charactersBuilder.toString().toCharArray();
getContentHandler().characters(characters, 0, characters.length);
break;
}
charactersBuilder = new StringBuilder();
}
}
}