blob: 8c927c69a324d55a87424780b46f8aa2968f9db9 [file] [log] [blame]
/*
* 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.sparql.graph;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.function.BiConsumer;
import org.apache.jena.ext.xerces.util.XMLChar;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.util.SplitIRI;
/**
* Framework for implementing {@link PrefixMapping}. It is stateless (unlike
* {@code PrefixMappingImpl}) and implements the contract of {@link PrefixMapping},
* providing the key algorithm and delegating storage to the subclasses.
* <p>
* Reverse mapping, looking up a URI to find a prefix is complex because there may be several
* possibilities. Applications should not rely on every implementation being consistent
* when there is a choice of which prefix to use to shorten a URI.
*/
public abstract class PrefixMappingBase implements PrefixMapping {
/* Reverse mappings.
* The strict contract of PrefixMapping requires a separate reverse mapping to be stored and manipulated,
* which in turn adds complexity to the storage implementations.
* However, applications removing prefixes is unusual so we end up with a lot of complexity with little value.
*
* Beware of the details of removing a mapping when there is another to the same URI.
* If we had:
*
* Add (pref1, U)
* Add (pref2, U)
*
* so that {@code U} reverse maps ({@link #getNsURIPrefix}) to {@code pref2} (it was
* done second) then
*
* Remove (pref2)
*
* it causes {@code U} to reverse map to {@code pref1}.
*
* This feature is quite a burden on implementations and should be regarded as "legacy" -
* an implementation may not support this complex effect.
*
* PrefixMappingMem does.
* Database backed ones typically don't.
*/
protected PrefixMappingBase() {}
// The storage operations of an implementation.
/** Add prefix */
abstract protected void add(String prefix, String uri);
/** Remove prefix. */
abstract protected void remove(String prefix);
/** Clear all mappings */
abstract protected void clear();
abstract protected boolean isEmpty();
abstract protected int size();
/** Return the URI that the prefix maps to. */
abstract protected String prefixToUri(String prefix);
/** Return a prefix that maps to the URI.
* There may be several; the answer is any one of them.
*/
abstract protected String uriToPrefix(String uri);
/** Return as a map. This map is only used within this class.
* It can be as efficient as possible.
* It will not be modified.
* It will not be returned to a caller of {@code PrefixMappingBase}.
*/
abstract protected Map<String, String> asMap();
/**
* Return as a map. The map return is not connected to the prefix mapping implementation,
* does not reflect subsequent prefix mapping changes.
*/
abstract protected Map<String, String> asMapCopy();
/** Apply the {@link BiConsumer} to each (prefix, uri) pair. */
abstract protected void apply(BiConsumer<String, String> action);
/**
* This part of the subclass API and may be overridden if an implementation can do
* better This general implementation is based on asMap() which may be a copy or may
* be a view of the storage directly.
*/
protected Optional<Entry<String, String>> findMapping( String uri, boolean partial ) {
return asMap().entrySet().stream().sequential().filter(e->{
String ss = e.getValue();
if (uri.startsWith( ss ) && (partial || ss.length() == uri.length()))
return true;
return false;
}).findFirst();
}
@Override
public PrefixMapping setNsPrefix(String prefix, String uri) {
checkLegalPrefix(prefix);
add(prefix, uri);
return this;
}
@Override
public PrefixMapping removeNsPrefix(String prefix) {
remove(prefix);
return this;
}
@Override
public PrefixMapping clearNsPrefixMap() {
clear();
return this;
}
/**
* Checks that a prefix is "legal" - it must be a valid XML NCName or "". XML rules
* for RDF/XML output.
* <p>
* This is a recurring user question - why does {@code Resource.getNamespace},
* {@code Resource.getLocalname} not abbreviate when it is legal Turtle.
* <p>
* Answer - legacy for RDF/XML.
* <p>
* See also {@link #qnameFor}.
*/
public static void checkLegalPrefix(String prefix) {
if ( prefix == null )
throw new PrefixMapping.IllegalPrefixException("null for prefix");
if ( prefix.length() > 0 && !XMLChar.isValidNCName(prefix) )
throw new PrefixMapping.IllegalPrefixException(prefix);
}
@Override
public PrefixMapping setNsPrefixes(PrefixMapping pmap) {
if ( pmap instanceof PrefixMappingBase ) {
PrefixMappingBase pmap2 = (PrefixMappingBase)pmap;
pmap2.apply((p,u)->setNsPrefix(p, u));
return this;
}
// Need to create as a map (a copy) and then add.
setNsPrefixes(pmap.getNsPrefixMap());
return this;
}
@Override
public PrefixMapping setNsPrefixes(Map<String, String> map) {
map.forEach(this::setNsPrefix);
return this;
}
/* Not javadoc.
This is the unusual contract as defined by the interface:
Update this PrefixMapping with the bindings in <code>map</code>, only
adding those (p, u) pairs for which neither p nor u appears in this mapping.
*/
@Override
public PrefixMapping withDefaultMappings(PrefixMapping pmap) {
if ( pmap instanceof PrefixMappingBase ) {
// Direct. No intermediate Map<> object created,
PrefixMappingBase pmap2 = (PrefixMappingBase)pmap;
pmap2.apply(this::addWith);
return this;
}
// Creates a Map<>.
Map<String, String> map = pmap.getNsPrefixMap();
map.forEach(this::addWith);
return this;
}
private void addWith(String p, String u) {
if ( prefixToUri(p) == null && uriToPrefix(u) == null )
add(p,u);
}
@Override
public String getNsPrefixURI(String prefix) {
return prefixToUri(prefix);
}
@Override
public String getNsURIPrefix(String uri) {
return uriToPrefix(uri);
}
@Override
public Map<String, String> getNsPrefixMap() {
return asMapCopy();
}
@Override
public String expandPrefix(String prefixed) {
int colon = prefixed.indexOf(':');
if ( colon < 0 )
return prefixed;
else {
String prefix = prefixed.substring(0, colon);
String uri = getNsPrefixURI(prefix);
return uri == null ? prefixed : uri + prefixed.substring(colon + 1);
}
}
@Override
public String qnameFor(String uri) {
// Turtle. SplitIRI.splitpoint(uri);
int split = SplitIRI.splitXML(uri);
String ns = uri.substring(0, split);
String local = uri.substring(split);
if ( local.equals("") )
return null;
String prefix = getNsURIPrefix(ns);
return prefix == null ? null : prefix + ":" + local;
}
@Override
public String shortForm(String uri) {
Optional<Entry<String, String>> e = findMapping(uri, true);
if ( ! e.isPresent() )
return uri;
return e.get().getKey() + ":" + uri.substring((e.get().getValue()).length());
}
@Override
public boolean samePrefixMappingAs(PrefixMapping other) {
if ( numPrefixes() != other.numPrefixes() )
return false;
return getNsPrefixMap().equals(other.getNsPrefixMap());
}
@Override
public PrefixMapping lock() {
return this;
}
@Override
public boolean hasNoMappings() {
return isEmpty();
}
@Override
public int numPrefixes() {
return size();
}
@Override
public String toString() {
StringJoiner sj = new StringJoiner(", ");
apply((p,u)->sj.add(p+"->"+u));
return "pm:["+numPrefixes()+"]{"+sj.toString()+"}";
}
}