/* Aalto XML processor * * Copyright (c) 2006- Tatu Saloranta, tatu.saloranta@iki.fi * * Licensed under the License specified in the file LICENSE which is * included with the source code. * You may not use this file except in compliance with the License. * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fasterxml.aalto.out; import java.util.*; import javax.xml.namespace.QName; import javax.xml.stream.*; import org.codehaus.stax2.ri.typed.AsciiValueEncoder; import com.fasterxml.aalto.impl.ErrorConsts; /** * Concrete implementation of {@link StreamWriterBase}, which * implements the "namespace repairing" mode of operation. * This means that the writer ensures correctness and validity * of namespace bindings, as based on namespace URIs caller * passes, by adding necessary namespace declarations and using * prefixes as required to obtain expected results. */ public final class RepairingStreamWriter extends StreamWriterBase { /* ///////////////////////////////////////////////////// // Prefix generation settings ///////////////////////////////////////////////////// */ // // // Additional specific config flags base class doesn't have final String _cfgAutomaticNsPrefix; /* //////////////////////////////////////////////////// // Additional state //////////////////////////////////////////////////// */ /** * Sequence number used for generating dynamic namespace prefixes. * Array used as a wrapper to allow for easy sharing of the sequence * number. */ int[] _autoNsSeq = null; String _suggestedDefNs = null; /** * Map that contains URI-to-prefix entries that point out suggested * prefixes for URIs. These are populated by calls to * {@link #setPrefix}, and they are only used as hints for binding; * if there are conflicts, repairing writer can just use some other * prefix. */ HashMap<String,String> _suggestedPrefixes = null; /* ///////////////////////////////////////////////////// // Construction, init ///////////////////////////////////////////////////// */ public RepairingStreamWriter(WriterConfig cfg, XmlWriter writer, WNameTable symbols) { super(cfg, writer, symbols); _cfgAutomaticNsPrefix = cfg.getAutomaticNsPrefix(); } /* ///////////////////////////////////////////////////// // Implementations of abstract methods from base class, // Stax 1.0 methods ///////////////////////////////////////////////////// */ /** * With repairing writer, this is only taken as a suggestion as to how * the caller would prefer prefixes to be mapped. */ @Override public void setDefaultNamespace(String uri) throws XMLStreamException { _suggestedDefNs = (uri == null || uri.length() == 0) ? null : uri; } @Override public void _setPrefix(String prefix, String uri) { // note: sub-class has verified that prefix != null, uri != null /* Ok; let's assume that passing an empty String as * the URI means that we don't want passed prefix to be preferred * for any URI (since there's no way to map a prefix to the default * namespace) */ if (uri == null || uri.length() == 0) { if (_suggestedPrefixes != null) { for (Iterator<Map.Entry<String,String>> it = _suggestedPrefixes.entrySet().iterator(); it.hasNext(); ) { Map.Entry<String,String> en = it.next(); if (en.getValue().equals(prefix)) { it.remove(); } } } } else { if (_suggestedPrefixes == null) { _suggestedPrefixes = new HashMap<String,String>(16); } _suggestedPrefixes.put(uri, prefix); } } //public void writeAttribute(String localName, String value) @Override public void writeAttribute(String nsURI, String localName, String value) throws XMLStreamException { if (!_stateStartElementOpen) { throwOutputError(ErrorConsts.WERR_ATTR_NO_ELEM); } // no URI? No prefix. Otherwise, need to find or bind: WName name = (nsURI == null || nsURI.length() == 0) ? _symbols.findSymbol(localName) : _generateAttrName(null, localName, nsURI); _writeAttribute(name, value); } @Override public void writeAttribute(String prefix, String nsURI, String localName, String value) throws XMLStreamException { if (!_stateStartElementOpen) { throwOutputError(ErrorConsts.WERR_ATTR_NO_ELEM); } // no URI? No prefix. Otherwise, need to find or bind: WName name = (nsURI == null || nsURI.length() == 0) ? _symbols.findSymbol(localName) : _generateAttrName(prefix, localName, nsURI); _writeAttribute(name, value); } @Override public void writeDefaultNamespace(String nsURI) throws XMLStreamException { if (!_stateStartElementOpen) { throwOutputError(ErrorConsts.WERR_NS_NO_ELEM); } /* ... We have one complication though: if the current element * uses default namespace, can not change it (attributes don't * matter -- they never use the default namespace, but either don't * belong to a namespace, or belong to one using explicit prefix) */ if (!_currElem.hasPrefix()) { _currElem.setDefaultNsURI(nsURI); _writeDefaultNamespace(nsURI); } _writeDefaultNamespace(nsURI); } //public void writeDTD(String dtd) //public void writeEmptyElement(String localName) @Override public void writeEmptyElement(String nsURI, String localName) throws XMLStreamException { _writeStartOrEmpty(null, localName, nsURI, true); } @Override public void writeEmptyElement(String prefix, String localName, String nsURI) throws XMLStreamException { _writeStartOrEmpty(prefix, localName, nsURI, true); } //public void writeEndElement() @Override public void writeNamespace(String prefix, String nsURI) throws XMLStreamException { if (prefix == null || prefix.length() == 0) { writeDefaultNamespace(nsURI); return; } if (!_stateStartElementOpen) { throwOutputError(ErrorConsts.WERR_NS_NO_ELEM); } /* Let's only add the declaration if the prefix * is as of yet unbound. If we have to re-bind things in future, * so be it -- for now, this should suffice (and if we have to * add re-binding, must verify that no attribute, nor element * itself, is using overridden prefix) */ if (_currElem.isPrefixUnbound(prefix, _rootNsContext)) { _currElem.addPrefix(prefix, nsURI); _writeNamespace(prefix, nsURI); } } //public void writeStartElement(String localName) @Override public void writeStartElement(String nsURI, String localName) throws XMLStreamException { _writeStartOrEmpty(null, localName, nsURI, false); } @Override public void writeStartElement(String prefix, String localName, String nsURI) throws XMLStreamException { _writeStartOrEmpty(prefix, localName, nsURI, false); } /* ///////////////////////////////////////////////////// // Implementations of abstract methods from base class, // Stax2 Typed API Access (v3.0) ///////////////////////////////////////////////////// */ @Override public void writeTypedAttribute(String prefix, String nsURI, String localName, AsciiValueEncoder enc) throws XMLStreamException { if (!_stateStartElementOpen) { throwOutputError(ErrorConsts.WERR_ATTR_NO_ELEM); } WName name = (prefix == null || prefix.length() == 0) ? _symbols.findSymbol(localName) : _symbols.findSymbol(prefix, localName); _writeAttribute(name, enc); } @Override protected String _serializeQName(QName name) throws XMLStreamException { /* Gets bit more complicated: we need to ensure that given URI * is properly bound... */ String uri = name.getNamespaceURI(); String prefix = name.getPrefix(); String local = name.getLocalPart(); // Perhaps prefix is fine as is? if (_currElem.isPrefixBoundTo(prefix, uri, _rootNsContext)) { if (prefix == null || prefix.length() == 0) { return local; } return prefix + ":" + local; } /* Nope. Need to find or generate a new one... let's treat like an * attribute (i.e. get an explicit prefix, not use default ns). * Not optimal, need not be; not a hot spot method. */ WName newName = _generateAttrName(prefix, local, uri); return newName.getPrefixedName(); } /* ///////////////////////////////////////////////////// // Internal methods ///////////////////////////////////////////////////// */ /** * @param uri Non-empty namespace URI that will be used for the * attribute */ protected WName _generateAttrName(String suggPrefix, String localName, String uri) throws XMLStreamException { if (suggPrefix != null && suggPrefix.length() > 0) { // prefer this prefix switch (_currElem.checkPrefixValidity(suggPrefix, uri, _rootNsContext)) { case UNBOUND: // ok, need to bind _writeNamespace(suggPrefix, uri); _currElem.addPrefix(suggPrefix, uri); // fall through: case OK: // fine as is return _symbols.findSymbol(suggPrefix, localName); case MISBOUND: // can't bind, need another prefix // fall through } } /* Had no suggested prefix, or one given didn't work. Either way, * need to find a match to URI, or generate new one. */ String prefix = _currElem.getExplicitPrefix(uri, _rootNsContext); if (prefix == null) { // none found, generate if (_autoNsSeq == null) { _autoNsSeq = new int[1]; _autoNsSeq[0] = 1; } prefix = _currElem.generatePrefix(_rootNsContext, _cfgAutomaticNsPrefix, _autoNsSeq); _writeNamespace(prefix, uri); _currElem.addPrefix(prefix, uri); } return _symbols.findSymbol(prefix, localName); } public void _writeStartOrEmpty(String prefix, String localName, String nsURI, boolean isEmpty) throws XMLStreamException { // In repairing mode, better ensure validity. // First: do we want the "no namespace"? Separate, distinct handling if (nsURI == null || nsURI.length() == 0) { // either way, can not have non-empty prefix _verifyStartElement(null, localName); // must output the start-tag-start to add xmlns decl (if needed) WName name = _symbols.findSymbol(localName); _writeStartTag(name, isEmpty, null); if (!_currElem.hasEmptyDefaultNs()) { // is bound, need to rebind... _writeDefaultNamespace(nsURI); // also: changes default ns of curr elem etc: _currElem.setDefaultNsURI(""); } if (_validator != null) { _validator.validateElementStart(localName, "", ""); } return; } // Otherwise a non-empty namespace. Do we have a suggested prefix? if (prefix == null) { // nope, anything goes prefix = _currElem.getPrefix(nsURI); // And we do have something bound already! if (prefix != null) { _writeStartAndVerify(prefix, localName, nsURI, isEmpty); } else { // nothing suitable bound, let's generate prefix = _generateElemPrefix(nsURI); if (_writeStartAndVerify(prefix, localName, nsURI, isEmpty)) { // default ns _writeDefaultNamespace(nsURI); _currElem.setDefaultNsURI(nsURI); } else { // non-default, explicit prefix _writeNamespace(prefix, nsURI); _currElem.addPrefix(prefix, nsURI); } } } else { boolean isDef = _writeStartAndVerify(prefix, localName, nsURI, isEmpty); // Is the prefix already properly bound? if (!_currElem.isPrefixBoundTo(prefix, nsURI, _rootNsContext)) { // yup // Nope, need to rebind if (isDef) { _writeDefaultNamespace(nsURI); _currElem.setDefaultNsURI(nsURI); } else { // explicit prefix _writeNamespace(prefix, nsURI); _currElem.addPrefix(prefix, nsURI); } } } // And after all that, validation? if (_validator != null) { _validator.validateElementStart(localName, "", ((prefix == null) ? "" : prefix)); } return; } /** * @return True, if prefix indicates default namespace (is null or empty); * false otherwise */ private final boolean _writeStartAndVerify(String prefix, String localName, String nsURI, boolean isEmpty) throws XMLStreamException { if (prefix == null || prefix.length() == 0) { // default ns _verifyStartElement(null, localName); _writeStartTag(_symbols.findSymbol(localName), isEmpty, nsURI); return true; } // non-default, i.e. real prefix _verifyStartElement(prefix, localName); _writeStartTag(_symbols.findSymbol(prefix, localName), isEmpty, nsURI); return false; } /** * Method called if given URI is not yet bound, and no suggested prefix * is given (or one given can't be used). If so, methods is * to create a not-yet-bound-prefix for the namespace. */ protected final String _generateElemPrefix(String uri) throws XMLStreamException { // First: is it the 'recommended' default ns? if (_suggestedDefNs != null && _suggestedDefNs.equals(uri)) { return null; } // If not, maybe one of other 'recommended' bindings has it? if (_suggestedPrefixes != null) { String prefix = _suggestedPrefixes.get(uri); if (prefix != null) { return prefix; } } // Otherwise, generate a new unbound prefix /* We have 2 choices here, essentially; * could make elements always try to override the def * ns... or can just generate new one. Let's do latter * for now. */ if (_autoNsSeq == null) { _autoNsSeq = new int[1]; _autoNsSeq[0] = 1; } return _currElem.generatePrefix(_rootNsContext, _cfgAutomaticNsPrefix, _autoNsSeq); } }