/* * 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.io.IOException; import java.io.StringWriter; import com.google.common.collect.ImmutableList; import org.dom4j.io.SAXContentHandler; import org.dom4j.io.XMLWriter; 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.checkNotNull; /** * Records and replays SAX events. * We don't use {@link org.dom4j.io.SAXEventRecorder} which discards location. * Location is useful for correct error reports when reparsing nested stylesheets. * * {@link org.dom4j.io.SAXEventRecorder} probably does a better job as it implements: * <ul> * <li>{@link org.xml.sax.ext.DeclHandler}, * <li>{@link org.xml.sax.DTDHandler}, * <li>{@link org.xml.sax.EntityResolver}, * <li>{@link org.xml.sax.ErrorHandler}, * <li>{@link org.xml.sax.ext.LexicalHandler}. * </ul> * <p> * This class is not thread-safe. * * @author Laurent Caillette */ public final class SaxRecorder extends ContentHandlerAdapter { public interface Player { void playOn( final ContentHandler target ) throws SAXException ; } /** * Returns an immutable object able to replay captured events. * * @return a non-null object. */ public Player getPlayer() { final ImmutableList< Event > eventsToPlay = this.events.build() ; return new Player() { @Override public void playOn( final ContentHandler target ) throws SAXException { play( eventsToPlay, target ) ; } } ; } /** * Replays events. * <p> * Warning: this methods installs its own {@code Locator} in the target {@code ContentHandler} * and doesn't restore the previous one once done. This is because {@code ContentHandler} * doesn't expose something like a {@code getDocumentLocator} method. */ private static void play( final ImmutableList< Event > events, final ContentHandler target ) throws SAXException { final InstrumentedLocator locator = new InstrumentedLocator() ; target.setDocumentLocator( locator ) ; for( final Event replayedEvent : events ) { locator.setLocationRecord( replayedEvent.locationRecord ) ; replayedEvent.replay( target ) ; } } public static String asXml( final Player player ) throws SAXException, IOException { final SAXContentHandler saxContentHandler = new SAXContentHandler() ; player.playOn( saxContentHandler ) ; final StringWriter stringWriter = new StringWriter() ; // Don't pretty print, would mess original whitespaces. new XMLWriter( stringWriter/*, OutputFormat.createPrettyPrint()*/ ) .write( saxContentHandler.getDocument() ) ; return stringWriter.toString() ; } // ============== // ContentHandler // ============== @Override public void startPrefixMapping( final String prefix, final String uri ) { add( new Event( getLocationRecord() ) { @Override public void replay( final ContentHandler target ) throws SAXException { target.startPrefixMapping( prefix, uri ) ; } } ) ; } @Override public void startDocument() { add( new Event( getLocationRecord() ) { @Override public void replay( final ContentHandler target ) throws SAXException { target.startDocument() ; } } ) ; } @Override public void startElement( final String uri, final String localName, final String qName, final Attributes attributes ) { final Attributes attributesCopy = new ImmutableAttributes( attributes ) ; add( new Event( getLocationRecord() ) { @Override public void replay( final ContentHandler target ) throws SAXException { target.startElement( uri, localName, qName, attributesCopy ) ; } } ) ; } @Override public void characters( final char[] chars, final int start, final int length ) { final char[] charactersCopy = chars.clone() ; add( new Event( getLocationRecord() ) { @Override public void replay( final ContentHandler target ) throws SAXException { target.characters( charactersCopy, start, length ) ; } } ) ; } @Override public void ignorableWhitespace( final char[] chars, final int start, final int length ) { final char[] charactersCopy = chars.clone() ; add( new Event( getLocationRecord() ) { @Override public void replay( final ContentHandler target ) throws SAXException { target.ignorableWhitespace( charactersCopy, start, length ) ; } } ) ; } @Override public void processingInstruction( final String piTarget, final String data ) { add( new Event( getLocationRecord() ) { @Override public void replay( final ContentHandler target ) throws SAXException { target.processingInstruction( piTarget, data ) ; } } ) ; } @Override public void skippedEntity( final String name ) { add( new Event( getLocationRecord() ) { @Override public void replay( final ContentHandler target ) throws SAXException { target.skippedEntity( name ) ; } } ) ; } @Override public void endElement( final String uri, final String localName, final String qName ) { add( new Event( getLocationRecord() ) { @Override public void replay( final ContentHandler target ) throws SAXException { target.endElement( uri, localName, qName ) ; } } ) ; } @Override public void endDocument() { add( new Event( getLocationRecord() ) { @Override public void replay( final ContentHandler target ) throws SAXException { target.endDocument() ; } } ) ; } @Override public void endPrefixMapping( final String prefix ) { add( new Event( getLocationRecord() ) { @Override public void replay( final ContentHandler target ) throws SAXException { target.endPrefixMapping( prefix ) ; } } ) ; } // ================ // Recorder objects // ================ private ImmutableSourceLocator getLocationRecord() { return ImmutableSourceLocator.create( getDocumentLocator() ) ; } private final ImmutableList.Builder< Event > events = ImmutableList.builder() ; private void add( final Event event ) { events.add( event ) ; } private static abstract class Event { private final ImmutableSourceLocator locationRecord ; protected Event( final ImmutableSourceLocator locationRecord ) { this.locationRecord = checkNotNull( locationRecord ) ; } public abstract void replay( final ContentHandler target ) throws SAXException; } private static class InstrumentedLocator implements Locator { private ImmutableSourceLocator locationRecord = ImmutableSourceLocator.NULL ; public void setLocationRecord( final ImmutableSourceLocator locationRecord ) { this.locationRecord = checkNotNull( locationRecord ) ; } @Override public String getPublicId() { return locationRecord.getPublicId() ; } @Override public String getSystemId() { return locationRecord.getSystemId() ; } @Override public int getLineNumber() { return locationRecord.getLineNumber() ; } @Override public int getColumnNumber() { return locationRecord.getColumnNumber() ; } } }