/* * 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.outfit.xml; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import org.apache.commons.lang.StringUtils; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import static com.google.common.base.Preconditions.checkNotNull; import org.novelang.logger.Logger; import org.novelang.logger.LoggerFactory; import org.novelang.outfit.TextTools; import org.novelang.outfit.loader.ResourceName; /** * Base class for processing elements from SAX events, where "buildup" objects represent * parts of a greater entity and get stored in a stack that follows XML element hierarchy. * * @author Laurent Caillette */ public abstract class StackBasedElementReader< ELEMENT, ATTRIBUTE, BUILDUP > extends NamespaceAwareContentHandlerAdapter { private static final Logger LOGGER = LoggerFactory.getLogger( StackBasedElementReader.class ) ; private final Function< String, ELEMENT > findFromLocalName ; private final Function< ATTRIBUTE, String > attributeToName ; private final ELEMENT rootElement ; private final BuildupStack< ELEMENT, BUILDUP > stack ; protected StackBasedElementReader( final String namespaceUri, final ImmutableSet< ImmutableList< ELEMENT > > elementPaths, final Function< ELEMENT, String > pathElementToString, final Function< ATTRIBUTE, String > attributeToName, final Function< String, ELEMENT > findFromLocalName ) { super( namespaceUri ) ; this.findFromLocalName = checkNotNull( findFromLocalName ) ; this.attributeToName = checkNotNull( attributeToName ) ; // Will throw some ugly exception if preconditions (at least one element) aren't met. this.rootElement = elementPaths.asList().get( 0 ).get( 0 ) ; this.stack = new BuildupStack< ELEMENT, BUILDUP >( elementPaths, pathElementToString ) ; } @SuppressWarnings( { "StringBufferField" } ) private final StringBuilder charactersCollector = new StringBuilder() ; protected void throwException( final String message ) throws IncorrectXmlException { throw new IncorrectXmlException( getNamespaceAwareness().buildMessageWithLocation( message ) + ( stack.isEmpty() ? "" : " (in " + stack.getPathAsString() + ")" ) ) ; } @Override public void startElement( final String uri, final String localName, final String qName, final Attributes attributes ) throws SAXException { final ELEMENT element = findFromLocalName.apply( localName ) ; try { if( rootElement.equals( element ) ) { // We care of meta prefix only for local root FOP element. if( ! getNamespaceAwareness().isMetaPrefix( uri ) ) { throwException( "Expecting '" + element + "' element in " + "'" + getNamespaceAwareness().getNamespaceUri() + "' namespace" ) ; } } else if( ! stack.isEmpty() ) { if( element == null ) { throwException( "Unknown element: '" + localName + "'" ) ; } } if( element != null ) { stack.push( element, preparePush( element, attributes ) ) ; // StackBasedElementReader.LOGGER.debug( ">>> ", stack.getPathAsString() ) ; } } catch( BuildupStack.IllegalPathException e ) { throwException( e.getMessage() ) ; } } @Override public void endElement( final String uri, final String localName, final String qName ) throws SAXException { if( ! stack.isEmpty() ) { final BUILDUP newBuildup = preparePop() ; stack.pop() ; // StackBasedElementReader.LOGGER.debug( "<<< ", stack.getPathAsString() ) ; if( stack.isEmpty() ) { if( newBuildup != null ) { throw new IllegalStateException( "Stack is getting empty but got a non-null value frop preparePop(): " + newBuildup ) ; } } else { stack.setTopBuildup( newBuildup ) ; } } } protected abstract BUILDUP preparePush( ELEMENT element, Attributes attributes ) throws IncorrectXmlException; protected abstract BUILDUP preparePop() throws IncorrectXmlException; // ============ // Stack access // ============ protected final ELEMENT getTopSegment() { return stack.topSegment() ; } protected final BUILDUP getBuildupOnTop() { return stack.getBuildupOnTop() ; } protected final BUILDUP getBuildupUnderTop() { return stack.getBuildupUnderTop() ; } // ========== // Attributes // ========== protected final String getStringAttributeValue( final Attributes attributes, final ATTRIBUTE attribute ) throws IncorrectXmlException { final String actualValue = attributes.getValue( attributeToName.apply( attribute ) ) ; if( actualValue == null ) { throwException( "Missing '" + attributeToName.apply( attribute ) + "' attribute" ) ; return null ; // Never executes but compiler gets happy. } else { return actualValue ; } } protected final boolean getBooleanAttributeValue( final Attributes attributes, final ATTRIBUTE attribute, final boolean defaultValue ) throws IncorrectXmlException { final String actualValue = attributes.getValue( attributeToName.apply( attribute ) ) ; if( actualValue == null ) { return defaultValue ; } else { if( "true".equalsIgnoreCase( actualValue ) ) { return true ; } else if( "false".equalsIgnoreCase( actualValue ) ) { return false ; } else { throwException( "Unsupported boolean value '" + actualValue + "', must be 'true' or 'false'" ) ; return false ; // Never executes but compiler gets pleased. } } } // ==== // Text // ==== @Override public void characters( final char[] ch, final int start, final int length ) throws SAXException { charactersCollector.append( ch, start, length ) ; } protected String getAndClearCollectedText() { final String collectedText = charactersCollector.toString() ; TextTools.clear( charactersCollector ) ; return collectedText ; } /** * Side effect: clears collected text. */ protected final int getIntegerFromCollectedText() throws IncorrectXmlException { final String text = StringUtils.trim( getAndClearCollectedText() ) ; try { return Integer.parseInt( text ) ; } catch( NumberFormatException e ) { throwException( "Couldn't parse '" + text + "' as an integer value " ) ; return 0 ; // Never executes but makes compiler happy. } } /** * Side effect: clears collected text. */ protected final ResourceName getResourceNameFromCollectedText() throws IncorrectXmlException { final String text = StringUtils.trim( getAndClearCollectedText() ) ; try { if( StringUtils.isBlank( text ) ) { return null ; } else { return new ResourceName( text ) ; } } catch( IllegalArgumentException e ) { throwException( e.getMessage() ) ; return null ; // Never executes but makes compiler happy. } } }