/*
* Copyright (C) 2008 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.outfit.xml;
import java.util.List;
import com.google.common.collect.Lists;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Chains multiple {@link ContentHandler}s as {@link Stage}s with the responsability for
* each {@link Stage} of calling the next one, allowing various kind of SAX methd calls
* interception.
* One {@link Stage} can only belong to one {@link org.novelang.outfit.xml.SaxPipeline} at
* a time.
*
* This class is not thread-safe.
*
* @author Laurent Caillette
*/
public class SaxPipeline extends DelegatingContentHandler {
private final List< Stage > stages = Lists.newArrayList() ;
private final ContentHandler end ;
public SaxPipeline( final ContentHandler end ) {
this.end = checkNotNull( end ) ;
}
@Override
protected final ContentHandler getDelegate() {
return getContentHandlerAt( 0 ) ;
}
/**
* Includes the {@link #end}.
*
* @return a value strictly greater than 0.
*/
protected final int getContentHandlerCount() {
return stages.size() + 1 ;
}
protected final ContentHandler getContentHandlerAt( final int position ) {
checkArgument( position >= 0, "Postion of %s lower than 0", position ) ;
checkArgument( position <= getContentHandlerCount(),
"Position of %s greater than ContentHandler count of %s", getContentHandlerCount() ) ;
if( position == getContentHandlerCount() - 1 ) {
return end ;
} else {
return stages.get( position ) ;
}
}
public final void add( final Stage stage, final int position ) {
checkNotNull( stage ) ;
checkArgument( position >= 0 ) ;
checkArgument( position < getContentHandlerCount() ) ;
checkArgument(
! isExplicitelySet( stage.delegate ), "Stage %s already belongs to a pipeline", stage ) ;
stages.add( position, stage ) ;
chainStagesAround( position ) ;
}
/**
* Replace an existing {@link Stage} (can't replace end).
*/
public final void replace( final int position, final Stage replacement ) {
checkNotNull( replacement ) ;
checkArgument( position >= 0 ) ;
checkArgument( position < getContentHandlerCount(),
"There must be at least %s stage(s), wrong position: %s", stages.size(), position ) ;
stages.get( position ).delegate = UNASSIGNED ;
stages.set( position, replacement ) ;
chainStagesAround( position ) ;
}
private void chainStagesAround( final int position ) {
if( position > 0 ) {
stages.get( position - 1 ).delegate = getContentHandlerAt( position ) ;
}
if( position < getContentHandlerCount() ) {
stages.get( position ).delegate = getContentHandlerAt( position + 1 ) ;
}
}
// Don't need more methods by now, we're not building a framework here.
// =====
// Stage
// =====
private static boolean isExplicitelySet( final ContentHandler contentHandler ) {
return contentHandler != null && contentHandler != UNASSIGNED ;
}
/**
* Magic non-null value to tell that a {@link Stage} doesn't belong to a {@link SaxPipeline} while
* keeping its {@link Stage#delegate} value non null for usage outside of a {@link SaxPipeline}.
*/
private static final ContentHandler UNASSIGNED = new ContentHandlerAdapter() {
@Override
public String toString() {
return SaxPipeline.class.getSimpleName() + ".UNASSIGNED" ;
}
} ;
public abstract static class Stage extends DelegatingContentHandler {
/**
* Set directly by the owning {@link SaxPipeline}. Never null.
*/
private ContentHandler delegate = UNASSIGNED ;
@Override
protected final ContentHandler getDelegate() {
return delegate ;
}
@Override
public String toString() {
return getClass().getSimpleName() + "{delegate=" + delegate + "}" ;
}
}
public static final class HollowStage extends Stage { }
/**
* A kind of {@link Stage} that forks SAX events into a simple {@link ContentHandler}
* before forwarding them to other {@link Stage}s unconditionally.
*/
public static class ForkingStage extends Stage {
private final ContentHandler fork ;
protected final ContentHandler getFork() {
return fork ;
}
public ForkingStage( final ContentHandler fork ) {
this.fork = checkNotNull( fork ) ;
}
@Override
public void setDocumentLocator( final Locator locator ) {
super.setDocumentLocator( locator ) ;
fork.setDocumentLocator( getDocumentLocator() ) ;
}
@Override
public void startPrefixMapping( final String prefix, final String uri ) throws SAXException {
fork.startPrefixMapping( prefix, uri ) ;
super.startPrefixMapping( prefix, uri );
}
@Override
public void endPrefixMapping( final String prefix ) throws SAXException {
fork.endPrefixMapping( prefix ) ;
super.endPrefixMapping( prefix );
}
@Override
public void startDocument() throws SAXException {
fork.startDocument() ;
super.startDocument();
}
@Override
public void endDocument() throws SAXException {
fork.endDocument() ;
super.endDocument();
}
@Override
public void startElement(
final String uri,
final String localName,
final String qName,
final Attributes attributes
) throws SAXException {
fork.startElement( uri, localName, qName, attributes ) ;
super.startElement( uri, localName, qName, attributes ) ;
}
@Override
public void endElement( final String uri, final String localName, final String qName )
throws SAXException
{
fork.endElement( uri, localName, qName ) ;
super.endElement( uri, localName, qName );
}
@Override
public void characters( final char[] chars, final int start, final int length )
throws SAXException
{
fork.characters( chars, start, length ) ;
super.characters( chars, start, length );
}
@Override
public void ignorableWhitespace( final char[] chars, final int start, final int length )
throws SAXException
{
fork.ignorableWhitespace( chars, start, length ) ;
super.ignorableWhitespace( chars, start, length ) ;
}
@Override
public void processingInstruction( final String target, final String data )
throws SAXException
{
fork.processingInstruction( target, data ) ;
super.processingInstruction( target, data );
}
@Override
public void skippedEntity( final String name ) throws SAXException {
fork.skippedEntity( name ) ;
super.skippedEntity( name );
}
}
}