/* * Copyright 2004-2008 Andy Clark * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * 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 org.cyberneko.html.filters; import java.util.Enumeration; import java.util.Locale; import java.util.Vector; import mf.org.apache.xerces.xni.*; import mf.org.apache.xerces.xni.parser.XMLComponentManager; import mf.org.apache.xerces.xni.parser.XMLConfigurationException; import org.cyberneko.html.HTMLElements; import org.cyberneko.html.xercesbridge.XercesBridge; /** * This filter binds namespaces if namespace processing is turned on * by setting the feature "http://xml.org/sax/features/namespaces" is * set to <code>true</code>. * <p> * This configuration recognizes the following features: * <ul> * <li>http://xml.org/sax/features/namespaces * </ul> * * @author Andy Clark * * @version $Id: NamespaceBinder.java,v 1.8 2005/05/30 00:19:28 andyc Exp $ */ public class NamespaceBinder extends DefaultFilter { // // Constants // // namespace uris /** XHTML 1.0 namespace URI (http://www.w3.org/1999/xhtml). */ public static final String XHTML_1_0_URI = "http://www.w3.org/1999/xhtml"; /** XML namespace URI (http://www.w3.org/XML/1998/namespace). */ public static final String XML_URI = "http://www.w3.org/XML/1998/namespace"; /** XMLNS namespace URI (http://www.w3.org/2000/xmlns/). */ public static final String XMLNS_URI = "http://www.w3.org/2000/xmlns/"; // features /** Namespaces. */ protected static final String NAMESPACES = "http://xml.org/sax/features/namespaces"; /** Override namespace binding URI. */ protected static final String OVERRIDE_NAMESPACES = "http://cyberneko.org/html/features/override-namespaces"; /** Insert namespace binding URIs. */ protected static final String INSERT_NAMESPACES = "http://cyberneko.org/html/features/insert-namespaces"; /** Recognized features. */ private static final String[] RECOGNIZED_FEATURES = { NAMESPACES, OVERRIDE_NAMESPACES, INSERT_NAMESPACES, }; /** Feature defaults. */ private static final Boolean[] FEATURE_DEFAULTS = { null, Boolean.FALSE, Boolean.FALSE, }; // properties /** Modify HTML element names: { "upper", "lower", "default" }. */ protected static final String NAMES_ELEMS = "http://cyberneko.org/html/properties/names/elems"; /** Modify HTML attribute names: { "upper", "lower", "default" }. */ protected static final String NAMES_ATTRS = "http://cyberneko.org/html/properties/names/attrs"; /** Namespaces URI. */ protected static final String NAMESPACES_URI = "http://cyberneko.org/html/properties/namespaces-uri"; /** Recognized properties. */ private static final String[] RECOGNIZED_PROPERTIES = new String[] { NAMES_ELEMS, NAMES_ATTRS, NAMESPACES_URI, }; /** Property defaults. */ private static final Object[] PROPERTY_DEFAULTS = { null, null, XHTML_1_0_URI, }; // modify HTML names /** Don't modify HTML names. */ protected static final short NAMES_NO_CHANGE = 0; /** Uppercase HTML names. */ protected static final short NAMES_UPPERCASE = 1; /** Lowercase HTML names. */ protected static final short NAMES_LOWERCASE = 2; // // Data // // features /** Namespaces. */ protected boolean fNamespaces; /** Namespace prefixes. */ protected boolean fNamespacePrefixes; /** Override namespaces. */ protected boolean fOverrideNamespaces; /** Insert namespaces. */ protected boolean fInsertNamespaces; // properties /** Modify HTML element names. */ protected short fNamesElems; /** Modify HTML attribute names. */ protected short fNamesAttrs; /** Namespaces URI. */ protected String fNamespacesURI; // state /** Namespace context. */ protected final NamespaceSupport fNamespaceContext = new NamespaceSupport(); // temp vars /** QName. */ private final QName fQName = new QName(); // // HTMLComponent methods // /** * Returns a list of feature identifiers that are recognized by * this component. This method may return null if no features * are recognized by this component. */ public String[] getRecognizedFeatures() { return merge(super.getRecognizedFeatures(), RECOGNIZED_FEATURES); } // getRecognizedFeatures():String[] /** * Returns the default state for a feature, or null if this * component does not want to report a default value for this * feature. */ public Boolean getFeatureDefault(String featureId) { for (int i = 0; i < RECOGNIZED_FEATURES.length; i++) { if (RECOGNIZED_FEATURES[i].equals(featureId)) { return FEATURE_DEFAULTS[i]; } } return super.getFeatureDefault(featureId); } // getFeatureDefault(String):Boolean /** * Returns a list of property identifiers that are recognized by * this component. This method may return null if no properties * are recognized by this component. */ public String[] getRecognizedProperties() { return merge(super.getRecognizedProperties(), RECOGNIZED_PROPERTIES); } // getRecognizedProperties():String[] /** * Returns the default value for a property, or null if this * component does not want to report a default value for this * property. */ public Object getPropertyDefault(String propertyId) { for (int i = 0; i < RECOGNIZED_PROPERTIES.length; i++) { if (RECOGNIZED_PROPERTIES[i].equals(propertyId)) { return PROPERTY_DEFAULTS[i]; } } return super.getPropertyDefault(propertyId); } // getPropertyDefault(String):Object /** * Resets the component. The component can query the component manager * about any features and properties that affect the operation of the * component. * * @param manager The component manager. * * @throws XNIException Thrown by component on initialization error. */ public void reset(XMLComponentManager manager) throws XMLConfigurationException { super.reset(manager); // features fNamespaces = manager.getFeature(NAMESPACES); fOverrideNamespaces = manager.getFeature(OVERRIDE_NAMESPACES); fInsertNamespaces = manager.getFeature(INSERT_NAMESPACES); // get properties fNamesElems = getNamesValue(String.valueOf(manager.getProperty(NAMES_ELEMS))); fNamesAttrs = getNamesValue(String.valueOf(manager.getProperty(NAMES_ATTRS))); fNamespacesURI = String.valueOf(manager.getProperty(NAMESPACES_URI)); // initialize state fNamespaceContext.reset(); } // reset(XMLComponentManager) // // XMLDocumentHandler methods // /** Start document. */ public void startDocument(XMLLocator locator, String encoding, NamespaceContext nscontext, Augmentations augs) throws XNIException { // perform default handling // NOTE: using own namespace context super.startDocument(locator,encoding,fNamespaceContext,augs); } // startDocument(XMLLocator,String,NamespaceContext,Augmentations) /** Start element. */ public void startElement(QName element, XMLAttributes attrs, Augmentations augs) throws XNIException { // bind namespaces, if needed if (fNamespaces) { fNamespaceContext.pushContext(); bindNamespaces(element, attrs); int dcount = fNamespaceContext.getDeclaredPrefixCount(); if (fDocumentHandler != null && dcount > 0) { for (int i = 0; i < dcount; i++) { String prefix = fNamespaceContext.getDeclaredPrefixAt(i); String uri = fNamespaceContext.getURI(prefix); XercesBridge.getInstance().XMLDocumentHandler_startPrefixMapping(fDocumentHandler, prefix, uri, augs); } } } // perform default handling super.startElement(element, attrs, augs); } // startElement(QName,XMLAttributes,Augmentations) /** Empty element. */ public void emptyElement(QName element, XMLAttributes attrs, Augmentations augs) throws XNIException { // bind namespaces, if needed if (fNamespaces) { fNamespaceContext.pushContext(); bindNamespaces(element, attrs); int dcount = fNamespaceContext.getDeclaredPrefixCount(); if (fDocumentHandler != null && dcount > 0) { for (int i = 0; i < dcount; i++) { String prefix = fNamespaceContext.getDeclaredPrefixAt(i); String uri = fNamespaceContext.getURI(prefix); XercesBridge.getInstance().XMLDocumentHandler_startPrefixMapping(fDocumentHandler, prefix, uri, augs); } } } // perform default handling super.emptyElement(element, attrs, augs); // pop context if (fNamespaces) { int dcount = fNamespaceContext.getDeclaredPrefixCount(); if (fDocumentHandler != null && dcount > 0) { for (int i = dcount-1; i >= 0; i--) { String prefix = fNamespaceContext.getDeclaredPrefixAt(i); XercesBridge.getInstance().XMLDocumentHandler_endPrefixMapping(fDocumentHandler, prefix, augs); } } fNamespaceContext.popContext(); } } // startElement(QName,XMLAttributes,Augmentations) /** End element. */ public void endElement(QName element, Augmentations augs) throws XNIException { // bind namespaces, if needed if (fNamespaces) { bindNamespaces(element, null); } // perform default handling super.endElement(element, augs); // pop context if (fNamespaces) { int dcount = fNamespaceContext.getDeclaredPrefixCount(); if (fDocumentHandler != null && dcount > 0) { for (int i = dcount-1; i >= 0; i--) { String prefix = fNamespaceContext.getDeclaredPrefixAt(i); XercesBridge.getInstance().XMLDocumentHandler_endPrefixMapping(fDocumentHandler, prefix, augs); } } fNamespaceContext.popContext(); } } // endElement(QName,Augmentations) // // Protected static methods // /** Splits a qualified name. */ protected static void splitQName(QName qname) { int index = qname.rawname.indexOf(':'); if (index != -1) { qname.prefix = qname.rawname.substring(0,index); qname.localpart = qname.rawname.substring(index+1); } } // splitQName(QName) /** * Converts HTML names string value to constant value. * * @see #NAMES_NO_CHANGE * @see #NAMES_LOWERCASE * @see #NAMES_UPPERCASE */ protected static final short getNamesValue(String value) { if (value.equals("lower")) { return NAMES_LOWERCASE; } if (value.equals("upper")) { return NAMES_UPPERCASE; } return NAMES_NO_CHANGE; } // getNamesValue(String):short /** Modifies the given name based on the specified mode. */ protected static final String modifyName(String name, short mode) { switch (mode) { case NAMES_UPPERCASE: return name.toUpperCase(Locale.ENGLISH); case NAMES_LOWERCASE: return name.toLowerCase(Locale.ENGLISH); } return name; } // modifyName(String,short):String // // Protected methods // /** Binds namespaces. */ protected void bindNamespaces(QName element, XMLAttributes attrs) { // split element qname splitQName(element); // declare namespace prefixes int attrCount = attrs != null ? attrs.getLength() : 0; for (int i = attrCount - 1; i >= 0; i--) { attrs.getName(i, fQName); String aname = fQName.rawname; String ANAME = aname.toUpperCase(Locale.ENGLISH); if (ANAME.startsWith("XMLNS:") || ANAME.equals("XMLNS")) { int anamelen = aname.length(); // get parts String aprefix = anamelen > 5 ? aname.substring(0,5) : null; String alocal = anamelen > 5 ? aname.substring(6) : aname; String avalue = attrs.getValue(i); // re-case parts and set them back into attributes if (anamelen > 5) { aprefix = modifyName(aprefix, NAMES_LOWERCASE); alocal = modifyName(alocal, fNamesElems); aname = aprefix + ':' + alocal; } else { alocal = modifyName(alocal, NAMES_LOWERCASE); aname = alocal; } fQName.setValues(aprefix, alocal, aname, null); attrs.setName(i, fQName); // declare prefix String prefix = alocal != aname ? alocal : ""; String uri = avalue.length() > 0 ? avalue : null; if (fOverrideNamespaces && prefix.equals(element.prefix) && HTMLElements.getElement(element.localpart, null) != null) { uri = fNamespacesURI; } fNamespaceContext.declarePrefix(prefix, uri); } } // bind element String prefix = element.prefix != null ? element.prefix : ""; element.uri = fNamespaceContext.getURI(prefix); // REVISIT: The prefix of a qualified element name that is // bound to a namespace is passed (as recent as // Xerces 2.4.0) as "" for start elements and null // for end elements. Why? One of them is a bug, // clearly. -Ac if (element.uri != null && element.prefix == null) { element.prefix = ""; } // do we need to insert namespace bindings? if (fInsertNamespaces && attrs != null && HTMLElements.getElement(element.localpart,null) != null) { if (element.prefix == null || fNamespaceContext.getURI(element.prefix) == null) { String xmlns = "xmlns" + ((element.prefix != null) ? ":"+element.prefix : ""); fQName.setValues(null, xmlns, xmlns, null); attrs.addAttribute(fQName, "CDATA", fNamespacesURI); bindNamespaces(element, attrs); return; } } // bind attributes attrCount = attrs != null ? attrs.getLength() : 0; for (int i = 0; i < attrCount; i++) { attrs.getName(i, fQName); splitQName(fQName); prefix = !fQName.rawname.equals("xmlns") ? (fQName.prefix != null ? fQName.prefix : "") : "xmlns"; // PATCH: Joseph Walton if (!prefix.equals("")) { fQName.uri = prefix.equals("xml") ? XML_URI : fNamespaceContext.getURI(prefix); } // NOTE: You would think the xmlns namespace would be handled // by NamespaceSupport but it's not. -Ac if (prefix.equals("xmlns") && fQName.uri == null) { fQName.uri = XMLNS_URI; } attrs.setName(i, fQName); } } // bindNamespaces(QName,XMLAttributes) // // Classes // /** * This namespace context object implements the old and new XNI * <code>NamespaceContext</code> interface methods so that it can * be used across all versions of Xerces2. */ public static class NamespaceSupport implements NamespaceContext { // // Data // /** Top of the levels list. */ protected int fTop = 0; /** The levels of the entries. */ protected int[] fLevels = new int[10]; /** The entries. */ protected Entry[] fEntries = new Entry[10]; // // Constructors // /** Default constructor. */ public NamespaceSupport() { pushContext(); declarePrefix("xml", NamespaceContext.XML_URI); declarePrefix("xmlns", NamespaceContext.XMLNS_URI); } // <init>() // // NamespaceContext methods // // since Xerces 2.0.0-beta2 (old XNI namespaces) /** Get URI. */ public String getURI(String prefix) { for (int i = fLevels[fTop]-1; i >= 0; i--) { final Entry entry = fEntries[i]; if (entry.prefix.equals(prefix)) { return entry.uri; } } return null; } // getURI(String):String /** Get declared prefix count. */ public int getDeclaredPrefixCount() { return fLevels[fTop] - fLevels[fTop-1]; } // getDeclaredPrefixCount():int /** Get declared prefix at. */ public String getDeclaredPrefixAt(int index) { return fEntries[fLevels[fTop-1] + index].prefix; } // getDeclaredPrefixAt(int):String /** Get parent context. */ public NamespaceContext getParentContext() { return this; } // getParentContext():NamespaceContext // since Xerces #.#.# (new XNI namespaces) /** Reset. */ public void reset() { fLevels[fTop = 1] = fLevels[fTop-1]; } // reset() /** Push context. */ public void pushContext() { if (++fTop == fLevels.length) { int[] iarray = new int[fLevels.length + 10]; System.arraycopy(fLevels, 0, iarray, 0, fLevels.length); fLevels = iarray; } fLevels[fTop] = fLevels[fTop-1]; } // pushContext() /** Pop context. */ public void popContext() { if (fTop > 1) { fTop--; } } // popContext() /** Declare prefix. */ public boolean declarePrefix(String prefix, String uri) { int count = getDeclaredPrefixCount(); for (int i = 0; i < count; i++) { String dprefix = getDeclaredPrefixAt(i); if (dprefix.equals(prefix)) { return false; } } Entry entry = new Entry(prefix, uri); if (fLevels[fTop] == fEntries.length) { Entry[] earray = new Entry[fEntries.length + 10]; System.arraycopy(fEntries, 0, earray, 0, fEntries.length); fEntries = earray; } fEntries[fLevels[fTop]++] = entry; return true; } // declarePrefix(String,String):boolean /** Get prefix. */ public String getPrefix(String uri) { for (int i = fLevels[fTop]-1; i >= 0; i--) { final Entry entry = fEntries[i]; if (entry.uri.equals(uri)) { return entry.prefix; } } return null; } // getPrefix(String):String /** Get all prefixes. */ public Enumeration getAllPrefixes() { Vector prefixes = new Vector(); for (int i = fLevels[1]; i < fLevels[fTop]; i++) { String prefix = fEntries[i].prefix; if (!prefixes.contains(prefix)) { prefixes.addElement(prefix); } } return prefixes.elements(); } // getAllPrefixes():Enumeration // // Classes // /** A namespace binding entry. */ static class Entry { // // Data // /** Prefix. */ public String prefix; /** URI. */ public String uri; // // Constructors // /** Constructs an entry. */ public Entry(String prefix, String uri) { this.prefix = prefix; this.uri = uri; } // <init>(String,String) } // class Entry } // class NamespaceSupport } // class NamespaceBinder