/** * Copyright (C) 2010 Orbeon, Inc. * * 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 * 2.1 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 Lesser General Public License for more details. * * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html */ package org.orbeon.oxf.processor.converter; import org.orbeon.dom.Element; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.xml.XMLReceiver; import org.orbeon.oxf.processor.*; import org.orbeon.oxf.processor.impl.CacheableTransformerOutputImpl; import org.orbeon.oxf.xml.ForwardingXMLReceiver; import org.orbeon.oxf.xml.NamespaceContext; import org.xml.sax.Attributes; import org.xml.sax.SAXException; /** * The QName converter converts elements' local names and/or namespace URIs. * * NOTE: This implementation probably needs to do more work to detect and adjust to namespace * declarations conflicts. * * TODO: attributes should probably also be updated, or removed! */ public class QNameConverter extends ProcessorImpl { public static final String QNAME_CONVERTER_CONFIG_NAMESPACE_URI = "http://www.orbeon.com/oxf/converter/qname"; public QNameConverter() { addInputInfo(new ProcessorInputOutputInfo(INPUT_DATA)); addInputInfo(new ProcessorInputOutputInfo(INPUT_CONFIG, QNAME_CONVERTER_CONFIG_NAMESPACE_URI)); addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_DATA)); } @Override public ProcessorOutput createOutput(String name) { final ProcessorOutput output = new CacheableTransformerOutputImpl(QNameConverter.this, name) { public void readImpl(PipelineContext context, XMLReceiver xmlReceiver) { // Read config input final Config config = readCacheInputAsObject(context, getInputByName(INPUT_CONFIG), new CacheableInputReader<Config>() { public Config read(org.orbeon.oxf.pipeline.api.PipelineContext context, ProcessorInput input) { Config result = new Config(); Element configElement = readInputAsOrbeonDom(context, input).getRootElement(); { Element matchURIElement = configElement.element("match").element("uri"); result.matchURI = (matchURIElement == null) ? null : matchURIElement.getStringValue(); Element matchPrefixElement = configElement.element("match").element("prefix"); result.matchPrefix = (matchPrefixElement == null) ? null : matchPrefixElement.getStringValue(); } { Element replaceURIElement = configElement.element("replace").element("uri"); result.replaceURI = (replaceURIElement == null) ? null : replaceURIElement.getStringValue(); Element replacePrefixElement = configElement.element("replace").element("prefix"); result.replacePrefix = (replacePrefixElement == null) ? null : replacePrefixElement.getStringValue(); } return result; } }); // Do the conversion readInputAsSAX(context, INPUT_DATA, new ForwardingXMLReceiver(xmlReceiver) { private NamespaceContext namespaceContext = new NamespaceContext(); @Override public void startElement(String uri, String localname, String qName, Attributes attributes) throws SAXException { namespaceContext.startElement(); if (config.matchURI == null || config.matchURI.equals(uri)) { int colonIndex = qName.indexOf(':'); String prefix = (colonIndex == -1) ? "" : qName.substring(0, colonIndex); if (config.matchPrefix == null || config.matchPrefix.equals(prefix)) { // Match: replace prefix or URI or both String newURI = (config.replaceURI == null) ? uri : config.replaceURI; String newQName; if (config.replacePrefix == null) { newQName = qName; } else if (colonIndex == -1) { final StringBuilder sb= new StringBuilder(config.replacePrefix.length() + qName.length() + 1); sb.append(config.replacePrefix); sb.append(':'); sb.append(qName); newQName = sb.toString(); } else { final StringBuilder sb= new StringBuilder(config.replacePrefix.length() + qName.length()); sb.append(config.replacePrefix); sb.append(qName.substring(colonIndex)); newQName = sb.toString(); } checkNamespace(uri, newURI, prefix, (config.replacePrefix == null) ? prefix : config.replacePrefix, true); super.startElement(newURI, localname, newQName, attributes); } else { // No match super.startElement(uri, localname, qName, attributes); } } else { // No match super.startElement(uri, localname, qName, attributes); } } @Override public void endElement(String uri, String localname, String qName) throws SAXException { if (config.matchURI == null || config.matchURI.equals(uri)) { int colonIndex = qName.indexOf(':'); String prefix = (colonIndex == -1) ? "" : qName.substring(0, colonIndex); if (config.matchPrefix == null || config.matchPrefix.equals(prefix)) { // Match: replace prefix or URI or both String newURI = (config.replaceURI == null) ? uri : config.replaceURI; String newQName; if (config.replacePrefix == null) { newQName = qName; } else if (colonIndex == -1) { final StringBuilder sb= new StringBuilder(config.replacePrefix.length() + qName.length() + 1); sb.append(config.replacePrefix); sb.append(':'); sb.append(qName); newQName = sb.toString(); } else { final StringBuilder sb= new StringBuilder(config.replacePrefix.length() + qName.length()); sb.append(config.replacePrefix); sb.append(qName.substring(colonIndex)); newQName = sb.toString(); } super.endElement(newURI, localname, newQName); checkNamespace(uri, newURI, prefix, (config.replacePrefix == null) ? prefix : config.replacePrefix, false); } else { // No match super.endElement(uri, localname, qName); } } else { // No match super.endElement(uri, localname, qName); } namespaceContext.endElement(); } @Override public void startPrefixMapping(String prefix, String uri) throws SAXException { namespaceContext.startPrefixMapping(prefix, uri); super.startPrefixMapping(prefix, uri); } @Override public void endPrefixMapping(String prefix) throws SAXException { super.endPrefixMapping(prefix); } private void checkNamespace(String matchURI, String newURI, String matchPrefix, String newPrefix, boolean start) throws SAXException { if (matchURI.equals(newURI) && !matchPrefix.equals(newPrefix)) { // Changing prefixes but keeping URI if (!isURIInScopeForPrefix(newPrefix, newURI)) { // new prefix -> URI not in scope if (start) { super.startPrefixMapping(newPrefix, newURI); if (namespaceContext.getURI(newPrefix) == null) namespaceContext.startPrefixMapping(newPrefix, newURI); } else { super.endPrefixMapping(newPrefix); } } } } private boolean isURIInScopeForPrefix(String prefix, String uri) { String inScopeURIForPrefix = namespaceContext.getURI(prefix); return (inScopeURIForPrefix != null) && inScopeURIForPrefix.equals(uri); } }); } }; addOutput(name, output); return output; } private static class Config { public String matchPrefix; // null means all prefixes are matched public String matchURI; // null means all URIs are matched public String replacePrefix; // null means don't replace the prefix public String replaceURI; // null means don't replace the URI } }