/* * The contents of this file are subject to the Mozilla Public License * Version 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and limitations * under the License. * * The Original Code is the Kowari Metadata Store. * * The Initial Developer of the Original Code is Plugged In Software Pty * Ltd (http://www.pisoftware.com, mailto:info@pisoftware.com). Portions * created by Plugged In Software Pty Ltd are Copyright (C) 2001,2002 * Plugged In Software Pty Ltd. All Rights Reserved. * * Contributor(s): N/A. * * [NOTE: The text of this Exhibit A may differ slightly from the text * of the notices in the Source Code files of the Original Code. You * should use the text of this Exhibit A rather than the text found in the * Original Code Source Code for Your Modifications.] * */ package org.mulgara.content.rdfxml.writer; // Java 2 standard packages import java.net.URI; import java.util.HashMap; import java.util.Map; import org.apache.log4j.Logger; import org.apache.xerces.util.XMLChar; import org.jrdf.graph.Node; import org.jrdf.graph.URIReference; import org.jrdf.vocabulary.RDF; import org.jrdf.vocabulary.RDFS; import org.mulgara.query.QueryException; import org.mulgara.query.TuplesException; import org.mulgara.resolver.spi.GlobalizeException; import org.mulgara.resolver.spi.ResolverSession; import org.mulgara.resolver.spi.Statements; /** * Map that contains all namespaces for a set of Statements. * * @created 2004-02-20 * * @author <a href="mailto:robert.turner@tucanatech.com">Robert Turner</a> * * @version $Revision: 1.8 $ * * @modified $Date: 2005/01/05 04:58:03 $ * * @maintenanceAuthor $Author: newmana $ * * @company <A href="mailto:info@PIsoftware.com">Plugged In Software</A> * * @copyright ©2001 <a href="http://www.pisoftware.com/">Plugged In * Software Pty Ltd</a> * * @licence <a href="{@docRoot}/../../LICENCE">Mozilla Public License v1.1</a> */ public class NamespaceMap extends HashMap<String,String> { /** For serialization */ private static final long serialVersionUID = 1161744419591660130L; /** Logger. This is named after the class. */ private final static Logger logger = Logger.getLogger(NamespaceMap.class.getName()); /** A mirror of this map (where keys and values are swapped) */ private Map<String,String> mirror = null; /** A mapping of user-supplied namespace URI to prefix string */ private Map<String,String> userPrefixes = null; /** Prefix used to abbreviate RDF Namespace */ private static final String RDF_PREFIX = "rdf"; /** Prefix used to abbreviate RDFS Namespace */ private static final String RDFS_PREFIX = "rdfs"; /** * Constructor. Pre-populates the map with prefixes for a set of default namespaces * (RDF, RDFS, OWL, DC), and then populates the map with generated prefixes for all * unique namespaces in the statements. * * @param statements The statements which will be parsed for namespaces. * @param session The session used to globalize statement URI's. * @throws QueryException if an error occurred reading the statements. */ public NamespaceMap(Statements statements, ResolverSession session) throws QueryException { this(statements, session, null); } /** * Constructor. Pre-populates the map with prefixes for a set of default namespaces * (RDF, RDFS, OWL, DC), and adds a set of user-defined initial namespace prefixes. * Any initial prefix mapping which attempts to redefine a default namespace or prefix, or which * contains a prefix that is not a valid XML NCName, will be ignored. It then populates * the map with generated prefixes for all unique namespaces in the statements that do not * match a default or initial namespace prefix mapping. User-supplied namespace prefixes * that do not appear in the statements will not appear in the RDF/XML. The default namespace may * be defined by including an initial mapping for the empty prefix in the namespace map. * * @param statements The statements which will be parsed for namespaces. * @param session The session used to globalize statement URI's. * @param initialPrefixes A set of user-defined namespace prefixes. * @throws QueryException if an error occurred reading the statements. */ public NamespaceMap(Statements statements, ResolverSession session, Map<String,URI> initialPrefixes) throws QueryException { mirror = new HashMap<String,String>(); //add default namespaces put(RDF_PREFIX, RDF.BASE_URI.toString()); put(RDFS_PREFIX, RDFS.BASE_URI.toString()); put("owl", "http://www.w3.org/2002/07/owl#"); put("dc", "http://purl.org/dc/elements/1.1/"); if (initialPrefixes != null) { userPrefixes = validateUserPrefixes(initialPrefixes); } //read namespaces from the statements try { populate(statements, session); } catch (TuplesException tuplesException) { throw new QueryException("Could not read statements.", tuplesException); } catch (GlobalizeException globalException) { throw new QueryException("Could not globalize statements.", globalException); } } /** * Pre-populates the namespace mapping with a user-defined set of initial prefixes. * All prefixes are validated as XML NCNames, and prefixes for namespaces that have * already been defined will be ignored. * @param existingMap A mapping of prefix to namespace URI. */ private Map<String,String> validateUserPrefixes(Map<String,URI> existingMap) { Map<String,String> mappings = new HashMap<String,String>(); for (Map.Entry<String,URI> entry : existingMap.entrySet()) { String prefix = entry.getKey(); if (prefix != null) { // If the value is a valid XML namespace, it will be untouched. If it is not a namespace, the // namespace portion will be extracted and used. String namespace = toNamespaceURI(entry.getValue().toString()); if (namespace != null && !mappings.containsKey(namespace) && validatePrefix(prefix)) { mappings.put(namespace, prefix); } } } return mappings; } /** * Validates a user-defined prefix. A prefix is rejected if it meets any of the following conditions: * <ul> * <li>Is not a valid NCName according to the XML specification, or empty to represent the default namespace.</li> * <li>Attempts to redefine one of the existing default prefixes (rdf, rdfs, owl, dc).</li> * <li>Begins with the sequence "ns[0-9]" as this could conflict with a generated prefix.</li> * </ul> * @param prefix The prefix to validate. * @return <code>true</code> if it is safe to include the prefix in the available namespace definitions. */ private boolean validatePrefix(String prefix) { // Only accept prefixes that can be used in XML qnames. if (!(XMLChar.isValidNCName(prefix) || prefix.equals(""))) return false; // Don't allow existing prefixes to be redefined. if (containsKey(prefix)) return false; // Prefixes starting with "ns1", "ns2", etc. may conflict with generated prefixes. if (prefix.matches("^ns\\d")) return false; return true; } /** * Evaluates the statements and adds namespace mappings for all unique namespaces. * * @param statements The statements to be parsed for namespaces. * @param session ResolverSession Session used to globalize nodes from the statments. * @throws TuplesException if an error occurred iterating the statements. * @throws GlobalizeException if an error occurred globalizing a statement node. */ private void populate(Statements statements, ResolverSession session) throws TuplesException, GlobalizeException { Statements clonedStatements = (Statements)statements.clone(); boolean success = false; try { //last nodes to be evaluated long subject = -1; long predicate = -1; long object = -1; //current nodes long newSubject = -1; long newPredicate = -1; long newObject = -1; clonedStatements.beforeFirst(); while (clonedStatements.next()) { newSubject = clonedStatements.getSubject(); newPredicate = clonedStatements.getPredicate(); newObject = clonedStatements.getObject(); //evaluate nodes that have changed if (newSubject != subject) { subject = newSubject; evaluateAndPut(subject, session); } if (newPredicate != predicate) { predicate = newPredicate; evaluateAndPut(predicate, session); } if (newObject != object) { object = newObject; evaluateAndPut(object, session); } } success = true; } finally { try { clonedStatements.close(); } catch (TuplesException e) { if (success) throw e; // This is a new exception, need to re-throw it. else logger.info("Suppressing exception cleaning up from failed read", e); // Log suppressed exception. } } } /** * Globalizes the node ID and adds it to the namespace mappings it if it is a URI. * * @param nodeID The local node ID. * @param session Session used to globalize the node. * @throws GlobalizeException if an error occurred during globalization. */ protected void evaluateAndPut(long nodeID, ResolverSession session) throws GlobalizeException { //only URI's need namespace substitution Node node = session.globalize(nodeID); if ((node != null) && (node instanceof URIReference)) { this.addNamespaceURI(((URIReference) node).getURI()); } } /** * Extracts the namespace from a URI, and adds the namespace to the mappings using a generated * prefix if the namespace is not already part of the mappings. * * @param uri The URI containing a namespace to add. */ protected void addNamespaceURI(URI uri) { if (uri == null) throw new IllegalArgumentException("URI argument is null."); //extract namespace from URI String uriString = uri.toString(); String newURI = toNamespaceURI(uriString); //only add namespace if it is new if ((newURI != null) && !containsValue(newURI)) { //add to namespaces String prefix = null; // Look for a user-defined prefix for the new namespace. if (userPrefixes != null) prefix = userPrefixes.get(newURI); // If no user-defined prefix exists, generate a new one. if (prefix == null) prefix = "ns" + size(); put(prefix, newURI); } } /** * Extracts the root namespace from an URI. The root namespace is defined here as the substring * extending from the start of the URI string to the final occurrence of '/', '#', or ':', or * the entire URI string if none of these characters occurs. * * @param uri An input URI. * @return The namespace of the URI, or the original URI if no namespace could be found. */ private String toNamespaceURI(String uri) { if (uri == null) throw new IllegalArgumentException("URI argument is null."); //return original string by default String nsURI = uri; //work backwards until a '/', '#' or ':' is encountered char currentChar = 0; for (int i = (uri.length() - 1); i >= 0; i--) { currentChar = uri.charAt(i); if ((currentChar == '/') || (currentChar == '#') || (currentChar == ':')) { //copy the string up to that point and return nsURI = uri.substring(0, i) + currentChar; return nsURI; } } assert nsURI != null : "Extracted namespace is null"; return nsURI; } /** * Returns the key used to represent RDF.baseURI: * (http://www.w3.org/1999/02/22-rdf-syntax-ns#). * * @return The RDF namespace prefix (always <code>rdf</code>). */ public String getRDFPrefix() { return RDF_PREFIX; } /** * Substitutes part of the uri with the corresponding namespace from the map. * If the URI contains no local part (i.e. is a namespace itself) then the entire URI is * replaced with an XML entity. * * @param uri The URI to perform substitution on. * @throws QueryException if the URI's namespace is not present in the map. * @return An XML QName representation of the URI suitable for use as an XML attribute name. */ public String replaceNamespace(String uri) throws QueryException { String newURI = null; String nsURI = toNamespaceURI(uri); String key = mirror.get(nsURI); if (key == null) throw new QueryException("Namespace: " + nsURI + " has not been mapped."); //should all or part of the URI be replaced? if (uri.equals(nsURI)) { //replace uri with entity newURI = "&" + key + ";"; //this may produce invalid XML logger.warn("Replacing URI: " + uri + " with ENTITY: " + newURI + ". Namepace replacement may be invalid XML."); } else if (uri.startsWith(nsURI)) { // URI's in the default namespace get shortened to local name only. String prefix = key.length() > 0 ? key + ":" : key; //replace namespace part with prefix newURI = uri.replaceAll(nsURI, prefix); } assert newURI != null; //replace any entities newURI = replaceCollection(newURI); return newURI; } /** * If the URI has a fragment representing a collection (eg. Bag) item, it is * replaced with li. * * @param original original URI. * @return new URI with any necessary li. */ private String replaceCollection(String original){ //value to be returned String uri = original; //validate URI if (original != null) uri = original.replaceAll("_[0-9]+", "li"); return uri; } /** * Overridden to allow for bi-directional mapping. Not intended to be called * outside this class. * * @param key The prefix string * @param value The namespace URI string. * @return The previous namespace URI associated with the prefix, or <code>null</code> if there was none. */ @Override public String put(String key, String value) { mirror.put(value, key); return super.put(key, value); } }