/* eXist Native XML Database * Copyright (C) 2000-03, Wolfgang M. Meier (wolfgang@exist-db.org) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This library 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * $Id$ */ package org.exist.util.serializer; import java.io.Writer; import java.util.HashMap; import java.util.Map; import java.util.Properties; import javax.xml.XMLConstants; import javax.xml.transform.TransformerException; import org.exist.Namespaces; import org.exist.dom.INodeHandle; import org.exist.dom.QName; import org.exist.storage.serializers.EXistOutputKeys; import org.exist.util.XMLString; import org.w3c.dom.Document; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.ext.LexicalHandler; import org.xml.sax.helpers.NamespaceSupport; public class SAXSerializer extends AbstractSerializer implements ContentHandler, LexicalHandler, Receiver { private final NamespaceSupport nsSupport = new NamespaceSupport(); private final Map<String, String> namespaceDecls = new HashMap<>(); private final Map<String, String> optionalNamespaceDecls = new HashMap<>(); private boolean enforceXHTML = false; public SAXSerializer() { super(); } public SAXSerializer(final Writer writer, final Properties outputProperties) { super(); setOutput(writer, outputProperties); } @Override public final void setOutput(final Writer writer, final Properties properties) { super.setOutput(writer, properties); // if set, enforce XHTML namespace on elements with no namespace final String xhtml = outputProperties.getProperty(EXistOutputKeys.ENFORCE_XHTML, "no"); enforceXHTML = xhtml.equalsIgnoreCase("yes"); } public Writer getWriter() { return receiver.getWriter(); } public void setReceiver(final XMLWriter receiver) { this.receiver = receiver; } @Override public void reset() { super.reset(); nsSupport.reset(); namespaceDecls.clear(); optionalNamespaceDecls.clear(); } @Override public void setDocumentLocator(final Locator locator) { //Nothing to do ? } @Override public void startDocument() throws SAXException { try { receiver.startDocument(); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void endDocument() throws SAXException { try { receiver.endDocument(); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void startPrefixMapping(String prefix, String namespaceURI) throws SAXException { if (namespaceURI.equals(Namespaces.XML_NS)) { return; } if(prefix == null) { prefix = XMLConstants.DEFAULT_NS_PREFIX; } final String ns = nsSupport.getURI(prefix); if (enforceXHTML && !Namespaces.XHTML_NS.equals(namespaceURI)) { namespaceURI = Namespaces.XHTML_NS; } if(ns == null || (!ns.equals(namespaceURI))) { optionalNamespaceDecls.put(prefix, namespaceURI); } } @Override public void endPrefixMapping(final String prefix) throws SAXException { optionalNamespaceDecls.remove(prefix); } @Override public void startElement(String namespaceURI, final String localName, final String qname, final Attributes attribs) throws SAXException { try { namespaceDecls.clear(); nsSupport.pushContext(); receiver.startElement(namespaceURI, localName, qname); String elemPrefix = XMLConstants.DEFAULT_NS_PREFIX; int p = qname.indexOf(':'); if (p > 0) { elemPrefix = qname.substring(0, p); } if (namespaceURI == null) { namespaceURI = XMLConstants.NULL_NS_URI; } if (enforceXHTML && elemPrefix.length() == 0 && namespaceURI.length() == 0) { namespaceURI = Namespaces.XHTML_NS; } if (nsSupport.getURI(elemPrefix) == null) { namespaceDecls.put(elemPrefix, namespaceURI); nsSupport.declarePrefix(elemPrefix, namespaceURI); } // check attributes for required namespace declarations String attrName; String uri; if(attribs != null) { for (int i = 0; i < attribs.getLength(); i++) { attrName = attribs.getQName(i); if (XMLConstants.XMLNS_ATTRIBUTE.equals(attrName)) { if (nsSupport.getURI(XMLConstants.DEFAULT_NS_PREFIX) == null) { uri = attribs.getValue(i); if (enforceXHTML && !Namespaces.XHTML_NS.equals(uri)) { uri = Namespaces.XHTML_NS; } namespaceDecls.put(XMLConstants.DEFAULT_NS_PREFIX, uri); nsSupport.declarePrefix(XMLConstants.DEFAULT_NS_PREFIX, uri); } } else if (attrName.startsWith(XMLConstants.XMLNS_ATTRIBUTE + ":")) { final String prefix = attrName.substring(6); if (nsSupport.getURI(prefix) == null) { uri = attribs.getValue(i); namespaceDecls.put(prefix, uri); nsSupport.declarePrefix(prefix, uri); } } else if ((p = attrName.indexOf(':')) > 0) { final String prefix = attrName.substring(0, p); uri = attribs.getURI(i); if (nsSupport.getURI(prefix) == null) { namespaceDecls.put(prefix, uri); nsSupport.declarePrefix(prefix, uri); } } } } for (final Map.Entry<String, String> nsEntry : optionalNamespaceDecls.entrySet()) { final String prefix = nsEntry.getKey(); uri = nsEntry.getValue(); receiver.namespace(prefix, uri); nsSupport.declarePrefix(prefix, uri); //nsSupport.declarePrefix(prefix, namespaceURI); } // output all namespace declarations for (final Map.Entry<String, String> nsEntry : namespaceDecls.entrySet()) { final String prefix = nsEntry.getKey(); uri = nsEntry.getValue(); if(!optionalNamespaceDecls.containsKey(prefix)) { receiver.namespace(prefix, uri); } } //cancels current xmlns if relevant if (XMLConstants.DEFAULT_NS_PREFIX.equals(elemPrefix) && !namespaceURI.equals(receiver.getDefaultNamespace())) { receiver.namespace(XMLConstants.DEFAULT_NS_PREFIX, namespaceURI); nsSupport.declarePrefix(XMLConstants.DEFAULT_NS_PREFIX, namespaceURI); } optionalNamespaceDecls.clear(); // output attributes if(attribs != null) { for (int i = 0; i < attribs.getLength(); i++) { if (!attribs.getQName(i).startsWith(XMLConstants.XMLNS_ATTRIBUTE)) { receiver.attribute(attribs.getQName(i), attribs.getValue(i)); } } } } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void startElement(final QName qname, final AttrList attribs) throws SAXException { try { namespaceDecls.clear(); nsSupport.pushContext(); String prefix = qname.getPrefix(); String namespaceURI = qname.getNamespaceURI(); if(prefix == null) { prefix = XMLConstants.DEFAULT_NS_PREFIX; } if(namespaceURI == null) { namespaceURI = XMLConstants.NULL_NS_URI; } if(enforceXHTML && prefix.length() == 0 && namespaceURI.length() == 0) { namespaceURI = Namespaces.XHTML_NS; receiver.startElement(new QName(qname.getLocalPart(), namespaceURI, qname.getPrefix())); } else { receiver.startElement(qname); } if (nsSupport.getURI(prefix) == null) { namespaceDecls.put(prefix, namespaceURI); nsSupport.declarePrefix(prefix, namespaceURI); } // check attributes for required namespace declarations QName attrQName; String uri; if(attribs != null) { for (int i = 0; i < attribs.getLength(); i++) { attrQName = attribs.getQName(i); if (XMLConstants.XMLNS_ATTRIBUTE.equals(attrQName.getLocalPart())) { if (nsSupport.getURI(XMLConstants.DEFAULT_NS_PREFIX) == null) { uri = attribs.getValue(i); if (enforceXHTML && !Namespaces.XHTML_NS.equals(uri)) { uri = Namespaces.XHTML_NS; } namespaceDecls.put(XMLConstants.DEFAULT_NS_PREFIX, uri); nsSupport.declarePrefix(XMLConstants.DEFAULT_NS_PREFIX, uri); } } else if (attrQName.getPrefix() != null && attrQName.getPrefix().length() > 0) { prefix = attrQName.getPrefix(); if((XMLConstants.XMLNS_ATTRIBUTE + ":").equals(prefix)) { if (nsSupport.getURI(prefix) == null) { uri = attribs.getValue(i); prefix = attrQName.getLocalPart(); namespaceDecls.put(prefix, uri); nsSupport.declarePrefix(prefix, uri); } } else { if (nsSupport.getURI(prefix) == null) { uri = attrQName.getNamespaceURI(); namespaceDecls.put(prefix, uri); nsSupport.declarePrefix(prefix, uri); } } } } } String optPrefix; for (final Map.Entry<String, String> nsEntry : optionalNamespaceDecls.entrySet()) { optPrefix = nsEntry.getKey(); uri = nsEntry.getValue(); receiver.namespace(optPrefix, uri); nsSupport.declarePrefix(optPrefix, uri); } // output all namespace declarations for (final Map.Entry<String, String> nsEntry : namespaceDecls.entrySet()) { optPrefix = nsEntry.getKey(); if (XMLConstants.XMLNS_ATTRIBUTE.equals(optPrefix)) { continue; } uri = nsEntry.getValue(); if(!optionalNamespaceDecls.containsKey(optPrefix)) { receiver.namespace(optPrefix, uri); } } optionalNamespaceDecls.clear(); //cancels current xmlns if relevant if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix) && !namespaceURI.equals(receiver.getDefaultNamespace())) { receiver.namespace(XMLConstants.DEFAULT_NS_PREFIX, namespaceURI); nsSupport.declarePrefix(XMLConstants.DEFAULT_NS_PREFIX, namespaceURI); } if(attribs != null) { // output attributes for (int i = 0; i < attribs.getLength(); i++) { if (!attribs.getQName(i).getLocalPart().startsWith(XMLConstants.XMLNS_ATTRIBUTE)) { receiver.attribute(attribs.getQName(i), attribs.getValue(i)); } } } } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void endElement(final String namespaceURI, final String localName, final String qname) throws SAXException { try { nsSupport.popContext(); receiver.endElement(namespaceURI, localName, qname); receiver.setDefaultNamespace(nsSupport.getURI(XMLConstants.DEFAULT_NS_PREFIX)); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void endElement(final QName qname) throws SAXException { try { nsSupport.popContext(); String prefix = qname.getPrefix(); String namespaceURI = qname.getNamespaceURI(); if(prefix == null) { prefix = XMLConstants.DEFAULT_NS_PREFIX; } if(namespaceURI == null) { namespaceURI = XMLConstants.NULL_NS_URI; } if(enforceXHTML && prefix.length() == 0 && namespaceURI.length() == 0) { namespaceURI = Namespaces.XHTML_NS; receiver.endElement(new QName(qname.getLocalPart(), namespaceURI, qname.getPrefix())); } else { receiver.endElement(qname); } receiver.setDefaultNamespace(nsSupport.getURI(XMLConstants.DEFAULT_NS_PREFIX)); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void attribute(final QName qname, final String value) throws SAXException { // ignore namespace declaration attributes if((qname.getPrefix() != null && XMLConstants.XMLNS_ATTRIBUTE.equals(qname.getPrefix())) || XMLConstants.XMLNS_ATTRIBUTE.equals(qname.getLocalPart())) { return; } try { receiver.attribute(qname, value); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void characters(final char[] ch, final int start, final int len) throws SAXException { try { receiver.characters(ch, start, len); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void characters(final CharSequence seq) throws SAXException { try { receiver.characters(seq); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void ignorableWhitespace(final char[] ch, final int start, final int len) throws SAXException { try { receiver.characters(ch, start, len); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void processingInstruction(final String target, final String data) throws SAXException { try { receiver.processingInstruction(target, data); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void cdataSection(final char[] ch, final int start, final int len) throws SAXException { try { receiver.cdataSection(ch, start, len); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void skippedEntity(final String name) throws SAXException { //Nothing to do } @Override public void startDTD(final String name, final String publicId, final String systemId) throws SAXException { //Nothing to do } @Override public void endDTD() throws SAXException { //Nothing to do } @Override public void documentType(final String name, final String publicId, final String systemId) throws SAXException { try { receiver.documentType(name, publicId, systemId); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void highlightText(final CharSequence seq) { // not supported with this receiver } @Override public void startEntity(final String name) throws SAXException { //Nothing to do } @Override public void endEntity(final String name) throws SAXException { //Nothing to do } @Override public void startCDATA() throws SAXException { //Nothing to do } @Override public void endCDATA() throws SAXException { //Nothing to do } @Override public void comment(final char[] ch, final int start, final int len) throws SAXException { try { receiver.comment(new XMLString(ch, start, len)); } catch (final TransformerException e) { throw new SAXException(e.getMessage(), e); } } @Override public void setCurrentNode(final INodeHandle node) { // just ignore. } @Override public Document getDocument() { //just ignore. return null; } }