/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.jena.shared.impl; import java.util.ArrayList ; import java.util.List ; import java.util.Map ; import java.util.Map.Entry; import org.apache.jena.rdf.model.impl.Util ; import org.apache.jena.shared.PrefixMapping ; import org.apache.jena.util.CollectionFactory ; import org.apache.xerces.util.XMLChar; /** An implementation of PrefixMapping. The mappings are stored in a pair of hash tables, one per direction. The test for a legal prefix is left to xerces's XMLChar.isValidNCName() predicate. */ public class PrefixMappingImpl implements PrefixMapping { protected Map<String, String> prefixToURI; protected Map<String, String> URItoPrefix; protected boolean locked; public PrefixMappingImpl() { prefixToURI = CollectionFactory.createHashedMap(); URItoPrefix = CollectionFactory.createHashedMap(); } protected void set(String prefix, String uri) { prefixToURI.put(prefix, uri) ; URItoPrefix.put(uri, prefix) ; } protected String get(String prefix) { return prefixToURI.get(prefix) ; } protected void remove(String prefix) { prefixToURI.remove(prefix) ; } @Override public PrefixMapping lock() { locked = true; return this; } @Override public PrefixMapping setNsPrefix( String prefix, String uri ) { checkUnlocked(); checkLegal( prefix ); if (!prefix.equals( "" )) checkProper( uri ); if (uri == null) throw new NullPointerException( "null URIs are prohibited as arguments to setNsPrefix" ); set( prefix, uri ); return this; } @Override public PrefixMapping removeNsPrefix( String prefix ) { checkUnlocked(); remove(prefix); regenerateReverseMapping() ; return this; } @Override public PrefixMapping clearNsPrefixMap() { checkUnlocked(); // Do by calling down via the interception point remove(prefix). // Copy prefixes to avoid possible concurrent modification exceptions List<String> prefixes = new ArrayList<>(prefixToURI.keySet()) ; prefixes.forEach(p->remove(p)); regenerateReverseMapping() ; return this ; } protected void regenerateReverseMapping() { URItoPrefix.clear(); for (Map.Entry<String, String> e: prefixToURI.entrySet()) URItoPrefix.put( e.getValue(), e.getKey() ); } protected void checkUnlocked() { if (locked) throw new JenaLockedException( this ); } private void checkProper( String uri ) { // suppressed by popular demand. TODO consider optionality // if (!isNiceURI( uri )) throw new NamespaceEndsWithNameCharException( uri ); } public static boolean isNiceURI( String uri ) { if (uri.equals( "" )) return false; char last = uri.charAt( uri.length() - 1 ); return Util.notNameChar( last ); } /** Add the bindings of other to our own. We defer to the general case because we have to ensure the URIs are checked. @param other the PrefixMapping whose bindings we are to add to this. */ @Override public PrefixMapping setNsPrefixes( PrefixMapping other ) { return setNsPrefixes( other.getNsPrefixMap() ); } /** Answer this PrefixMapping after updating it with the <code>(p, u)</code> mappings in <code>other</code> where neither <code>p</code> nor <code>u</code> appear in this mapping. */ @Override public PrefixMapping withDefaultMappings( PrefixMapping other ) { checkUnlocked(); for (Entry<String, String> e: other.getNsPrefixMap().entrySet()) { String prefix = e.getKey(), uri = e.getValue(); if (getNsPrefixURI( prefix ) == null && getNsURIPrefix( uri ) == null) setNsPrefix( prefix, uri ); } return this; } /** Add the bindings in the prefixToURI to our own. This will fail with a ClassCastException if any key or value is not a String; we make no guarantees about order or completeness if this happens. It will fail with an IllegalPrefixException if any prefix is illegal; similar provisos apply. @param other the Map whose bindings we are to add to this. */ @Override public PrefixMapping setNsPrefixes( Map<String, String> other ) { checkUnlocked(); for (Entry<String, String> e: other.entrySet()) setNsPrefix( e.getKey(), e.getValue() ); return this; } /** Checks that a prefix is "legal" - it must be a valid XML NCName. */ private void checkLegal( String prefix ) { if (prefix.length() > 0 && !XMLChar.isValidNCName( prefix )) throw new PrefixMapping.IllegalPrefixException( prefix ); } @Override public String getNsPrefixURI( String prefix ) { return get( prefix ); } @Override public Map<String, String> getNsPrefixMap() { return CollectionFactory.createHashedMap( prefixToURI ); } @Override public String getNsURIPrefix( String uri ) { return URItoPrefix.get( uri ); } /** Expand a prefixed URI. There's an assumption that any URI of the form Head:Tail is subject to mapping if Head is in the prefix mapping. So, if someone takes it into their heads to define eg "http" or "ftp" we have problems. */ @Override public String expandPrefix( String prefixed ) { int colon = prefixed.indexOf( ':' ); if (colon < 0) return prefixed; else { String uri = get( prefixed.substring( 0, colon ) ); return uri == null ? prefixed : uri + prefixed.substring( colon + 1 ); } } /** Answer a readable (we hope) representation of this prefix mapping. */ @Override public String toString() { return "pm:" + prefixToURI; } /** Answer the qname for <code>uri</code> which uses a prefix from this mapping, or null if there isn't one. <p> Relies on <code>splitNamespace</code> to carve uri into namespace and localname components; this ensures that the localname is legal and we just have to (reverse-)lookup the namespace in the prefix table. @see org.apache.jena.shared.PrefixMapping#qnameFor(java.lang.String) */ @Override public String qnameFor( String uri ) { int split = Util.splitNamespaceXML( uri ); String ns = uri.substring( 0, split ), local = uri.substring( split ); if (local.equals( "" )) return null; String prefix = URItoPrefix.get( ns ); return prefix == null ? null : prefix + ":" + local; } /** Compress the URI using the prefix mapping. This version of the code looks through all the maplets and checks each candidate prefix URI for being a leading substring of the argument URI. There's probably a much more efficient algorithm available, preprocessing the prefix strings into some kind of search table, but for the moment we don't need it. */ @Override public String shortForm( String uri ) { Entry<String, String> e = findMapping( uri, true ); return e == null ? uri : e.getKey() + ":" + uri.substring( (e.getValue()).length() ); } @Override public boolean samePrefixMappingAs( PrefixMapping other ) { return other instanceof PrefixMappingImpl ? equals( (PrefixMappingImpl) other ) : equalsByMap( other ) ; } protected boolean equals( PrefixMappingImpl other ) { return other.sameAs( this ); } protected boolean sameAs( PrefixMappingImpl other ) { return prefixToURI.equals( other.prefixToURI ); } protected final boolean equalsByMap( PrefixMapping other ) { return getNsPrefixMap().equals( other.getNsPrefixMap() ); } /** Answer a prefixToURI entry in which the value is an initial substring of <code>uri</code>. If <code>partial</code> is false, then the value must equal <code>uri</code>. Does a linear search of the entire prefixToURI, so not terribly efficient for large maps. @param uri the value to search for @param partial true if the match can be any leading substring, false for exact match @return some entry (k, v) such that uri starts with v [equal for partial=false] */ private Entry<String, String> findMapping( String uri, boolean partial ) { for (Entry<String, String> e: prefixToURI.entrySet()) { String ss = e.getValue(); if (uri.startsWith( ss ) && (partial || ss.length() == uri.length())) return e; } return null; } }