/*
* Copyright (c) 2009-2015
* IT-Consulting Stephan Schloepke (http://www.schloepke.de/)
* klemm software consulting Mirko Klemm (http://www.klemm-scs.com/)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jbasics.parser;
import org.jbasics.parser.invoker.Invoker;
import org.jbasics.types.tuples.Pair;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.ext.DefaultHandler2;
import javax.xml.namespace.QName;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
@SuppressWarnings("unchecked")
public class BuilderContentHandler<T> extends DefaultHandler2 {
private final BuilderParserContext<T> context;
private final AtomicBoolean parsing;
private StateStack<BuildHandler> states;
private T result;
private StringBuilder characterBuffer;
private StringBuilder commentBuffer;
private Locator locator;
private NamespacePrefixStack prefixes;
private CustomParser activeCustomParser;
private ContentHandler activeCustomParserContentHandler;
private int customParserDepth;
public BuilderContentHandler(final BuilderParserContext<T> context) {
this.context = context;
this.parsing = new AtomicBoolean(false);
}
public T getParsingResult() {
return this.result;
}
@Override
public void setDocumentLocator(final Locator locator) {
super.setDocumentLocator(locator);
this.locator = locator;
}
@Override
public void startDocument() throws SAXException {
if (this.parsing.compareAndSet(false, true)) {
this.states = new StateStack<BuildHandler>();
this.characterBuffer = new StringBuilder();
this.commentBuffer = new StringBuilder();
this.prefixes = new NamespacePrefixStack();
this.result = null;
} else {
throw new IllegalStateException("Start of document event occured while already parsing another document"); //$NON-NLS-1$
}
}
@Override
public void endDocument() throws SAXException {
// TODO: setResult. Maybe using a sort of push handler?
if (!this.parsing.compareAndSet(true, false)) {
throw new IllegalStateException("End of document event occured while not parsing");
}
this.prefixes = null;
this.characterBuffer = null;
this.states = null;
}
@Override
public void startPrefixMapping(final String prefix, final String uri) throws SAXException {
if (this.activeCustomParserContentHandler != null) {
this.activeCustomParserContentHandler.startPrefixMapping(prefix, uri);
} else {
this.prefixes.pushMapping(prefix, URI.create(uri));
}
}
@Override
public void endPrefixMapping(final String prefix) throws SAXException {
if (this.activeCustomParserContentHandler != null) {
this.activeCustomParserContentHandler.endPrefixMapping(prefix);
} else {
this.prefixes.popMapping(prefix);
}
}
@Override
public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
if (!this.parsing.get()) {
throw new IllegalStateException("Start of Element event occured while not parsing"); //$NON-NLS-1$
}
try {
if (this.activeCustomParserContentHandler != null) {
this.activeCustomParserContentHandler.startElement(uri, localName, qName, attributes);
this.customParserDepth++;
return;
}
QName name = createQualifiedName(uri, localName, qName);
ParsingInfo parseInfo = null;
if (this.states.isEmpty()) {
// Root processing
parseInfo = this.context.getParsingInfo(name);
if (parseInfo == null) {
throw new SAXException("Unknown root element " + name);
}
} else {
BuildHandler current = this.states.peek();
if (this.characterBuffer.length() > 0) {
current.addText(this.characterBuffer.toString());
this.characterBuffer.setLength(0);
}
if (this.commentBuffer.length() > 0) {
current.addComment(this.commentBuffer.toString());
this.commentBuffer.setLength(0);
}
if (current instanceof CustomParserRegistry) {
// TODO: We need to handle the attributes here but ignoring this right now
Map<QName, String> attTemp = Collections.emptyMap();
CustomParser customParser = ((CustomParserRegistry) current).getCustomParser(name, attTemp);
if (customParser != null) {
this.activeCustomParser = customParser;
this.customParserDepth = 0;
this.activeCustomParserContentHandler = customParser.beginParsing();
this.activeCustomParserContentHandler.setDocumentLocator(this.locator);
this.activeCustomParserContentHandler.startDocument();
for (Pair<String, URI> prefix : this.prefixes) {
this.activeCustomParserContentHandler.startPrefixMapping(prefix.left(), prefix.right().toString());
}
this.activeCustomParserContentHandler.startElement(uri, localName, qName, attributes);
// here we need to end execution
return;
}
}
parseInfo = current.getParsingInfo();
if (parseInfo != null) {
Pair<ParsingInfo, Invoker<?, ?>> x = parseInfo.getElementInvoker(name);
if (x != null) {
parseInfo = x.first();
} else {
parseInfo = null;
}
}
}
if (parseInfo == null) {
StringBuilder message = new StringBuilder("Unrecognized element ").append(name);
if (this.locator != null) {
message.append(" (").append(this.locator.getLineNumber()).append("/").append(this.locator.getColumnNumber()).append(")");
}
throw new SAXParseException(message.toString(), this.locator);
}
// TODO: Here we need to discover if we have a content handler delegate set so we can delegate
// everything instead of the following
BuildHandler handler = new BuildHandlerImpl(name, parseInfo);
for (int i = 0; i < attributes.getLength(); i++) {
QName attrName = createQualifiedName(attributes.getURI(i), attributes.getLocalName(i), attributes.getQName(i));
String attrValue = attributes.getValue(i);
handler.setAttribute(attrName, attrValue);
}
this.states.push(handler);
} catch (RuntimeException eo) {
throw createParsingException(eo);
}
}
@Override
public void endElement(final String uri, final String localName, final String qName) throws SAXException {
if (this.parsing.get()) {
try {
if (this.activeCustomParserContentHandler != null) {
this.activeCustomParserContentHandler.endElement(uri, localName, qName);
if (this.customParserDepth <= 0) {
this.activeCustomParserContentHandler.endDocument();
this.activeCustomParser.finishParsing();
this.activeCustomParserContentHandler = null;
this.activeCustomParser = null;
} else {
this.customParserDepth--;
}
} else {
QName name = createQualifiedName(uri, localName, qName);
BuildHandler current = this.states.pop();
if (this.characterBuffer.length() > 0) {
current.addText(this.characterBuffer.toString());
this.characterBuffer.setLength(0);
}
if (this.commentBuffer.length() > 0) {
current.addComment(this.commentBuffer.toString());
this.commentBuffer.setLength(0);
}
if (this.states.isEmpty()) {
this.result = (T) current.getResult();
} else {
BuildHandler parent = this.states.peek();
parent.addElement(name, current.getResult());
}
}
} catch (RuntimeException e) {
throw createParsingException(e);
}
} else {
throw new IllegalStateException("Start of Element event occured while not parsing");
}
}
@Override
public void characters(final char[] ch, final int start, final int length) throws SAXException {
if (this.activeCustomParserContentHandler != null) {
this.activeCustomParserContentHandler.characters(ch, start, length);
} else {
if (this.commentBuffer.length() > 0) {
BuildHandler current = this.states.peek();
current.addComment(this.commentBuffer.toString());
this.commentBuffer.setLength(0);
}
this.characterBuffer.append(ch, start, length);
}
}
@Override
public void ignorableWhitespace(final char[] ch, final int start, final int length) throws SAXException {
if (this.activeCustomParserContentHandler != null) {
this.activeCustomParserContentHandler.ignorableWhitespace(ch, start, length);
} else {
System.out.println("Found ignorable whitespace");
}
}
private QName createQualifiedName(String namespace, final String localname, final String qName) {
String prefix = null;
if (qName != null) {
int temp = qName.indexOf(':');
if (temp > 0) {
prefix = qName.substring(0, temp);
}
}
// Some people append a / at the end of the namespace. However this is quite problematic really so we remove it
// in general.
// FIXME: We need to check that with the xml spec!!
if (namespace.endsWith("/")) {
namespace = namespace.substring(0, namespace.length() - 1);
}
if (prefix != null) {
return new QName(namespace, localname, prefix);
} else {
return new QName(namespace, localname);
}
}
private SAXParseException createParsingException(final RuntimeException eo) {
Throwable e = eo;
while (e.getCause() != null && e.getCause() != e) {
e = e.getCause();
}
StringBuilder message = new StringBuilder();
message.append("[").append(e.getClass().getSimpleName()).append("] ").append(e.getMessage());
if (this.locator != null) {
message.append(" (").append(this.locator.getLineNumber()).append("/").append(this.locator.getColumnNumber()).append(")");
}
SAXParseException en = new SAXParseException(message.toString(), this.locator);
en.setStackTrace(e.getStackTrace());
return en;
}
/*
* (non-Javadoc)
* @see org.xml.sax.ext.LexicalHandler#comment(char[], int, int)
*/
@Override
public void comment(final char[] ch, final int start, final int length) throws SAXException {
// Ok we need to hand over the comment since in some cases we actually want to receive comments (JavaScript in
// HTML for example)
if (this.characterBuffer.length() > 0) {
BuildHandler current = this.states.peek();
current.addText(this.characterBuffer.toString());
this.characterBuffer.setLength(0);
}
this.commentBuffer.append(ch, start, length);
}
}