/******************************************************************************* * Copyright 2012 Pearson Education * * 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.semantictools.jsonld; import java.io.Serializable; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; /** * LdContext provides an API for accessing terms within a JSON-LD context. * @author Greg McFall * */ public class LdContext implements Serializable { private static final long serialVersionUID = 1L; private static Random random = new Random(new Date().getTime()); private transient LdContext parentContext; private String contextURI; private List<LdTerm> termList; private Map<String,LdTerm> termMap; private List<LdContext> components; /** * Creates a new LdContext which inherits terms from the specified parent context. */ public LdContext(LdContext parentContext) { this.parentContext = parentContext; } public LdContext() {} /** * Returns a shallow copy of this LdContext. */ public LdContext copy() { LdContext copy = new LdContext(); copy.parentContext = parentContext; copy.termList = termList; copy.termMap = termMap; copy.components = components; copy.contextURI = contextURI; return copy; } /** * Returns the URI for this context, or null if the URI is not known. */ public String getContextURI() { return contextURI; } /** * Sets the URI for this JSON-LD context. */ public void setContextURI(String contextURI) { this.contextURI = contextURI; } /** * Sets the parent context from which terms are inherited. */ public void setParentContext(LdContext parent) { parentContext = parent; } /** * Adds a new term to this context. * @throws IllegalStateException If the context is closed. */ public void add(LdTerm term) throws IllegalStateException { if (termMap != null) { throw new IllegalStateException("Cannot add a new term because this context is closed."); } if (termList == null) { termList = new ArrayList<LdTerm>(); } termList.add(term); } /** * Return the list of terms declared locally within this context. * Does not return terms inherited from the parent context or * terms from components. */ public List<LdTerm> getTermList() { if (termList == null) { termList = new ArrayList<LdTerm>(); } return termList; } /** * Adds a component context. This corresponds to the case where multiple contexts are * declared within the scope of a JSON object. * @throws IllegalStateException If the context is closed. */ public void add(LdContext context) throws IllegalStateException { if (termMap != null) { throw new IllegalStateException("Cannot add a component context because the enapsulating context is closed."); } if (components == null) { components = new ArrayList<LdContext>(); } components.add(context); } /** * Close this LdContext to further modification. * It is an error to add a new LdTerm or a component LdContext to after this context has * been closed. * This method builds internal hash maps which allow the context to perform URI expansion * rapidly. */ public void close() { // Only build the termMap once. // We guard against the case where the close method is accidentally // called more than once. Instead of throwing an exception, we // bail out immediately. // if (termMap != null) return; if (termList!=null) { termMap = new HashMap<String, LdTerm>(); for (LdTerm term : termList) { String name = term.getShortName(); termMap.put(name, term); String rawIRI = term.getRawIRI(); termMap.put(rawIRI, term); if (isFullyQualified(rawIRI)) { term.setIRI(rawIRI); termMap.put(rawIRI, term); } String rawTypeIRI = term.getRawTypeIRI(); if (isFullyQualified(rawTypeIRI)) { term.setTypeIRI(rawTypeIRI); } } } if (components != null) { for (LdContext c : components) { c.close(); } } expandTerms(); } public List<LdTerm> listTerms() { return termList; } public List<LdContext> listComponents() { return components; } /** * Returns true if the given URI is fully qualified. */ private boolean isFullyQualified(String anyURI) { if (anyURI==null) return false; return anyURI.startsWith("http://") || anyURI.startsWith("urn:") || anyURI.contains("://") || anyURI.startsWith("cid:") || anyURI.startsWith("data:") || anyURI.startsWith("dav:") || anyURI.startsWith("dns:") || anyURI.startsWith("geo:") || anyURI.startsWith("go:") || anyURI.startsWith("gopher:") || anyURI.startsWith("h323:") || anyURI.startsWith("iax:") || anyURI.startsWith("im:") || anyURI.startsWith("mid:") || anyURI.startsWith("news:") || anyURI.startsWith("pres:") || anyURI.startsWith("sip:") || anyURI.startsWith("sms:") || anyURI.startsWith("snmp:") || anyURI.startsWith("tag:") || anyURI.startsWith("tel:") || anyURI.startsWith("uuid:") || anyURI.startsWith("ws:") || anyURI.startsWith("xmpp:"); } /** * Compute the fully qualified URI for local terms in this context. */ private void expandTerms() { if (termList == null || termMap==null) return; for (LdTerm term : termList) { String iri = term.getIRI(); if (iri == null) { String rawIRI = term.getRawIRI(); iri = expand(rawIRI); if (!iri.equals(rawIRI)) { term.setIRI(iri); termMap.put(iri, term); } } String typeIRI = term.getTypeIRI(); if (typeIRI == null) { String rawTypeIRI = term.getRawTypeIRI(); if (rawTypeIRI != null) { typeIRI = expand(rawTypeIRI); if (!typeIRI.equals(rawTypeIRI)) { term.setTypeIRI(typeIRI); } } } } } /** * Returns the fully qualified IRI for the specified key, or * returns the given key if it cannot be expanded. * @param key The string that should be expanded to a fully qualified IRI. * This value may be a compact IRI (with a colon separating a namespace prefix from a local name) * or it may be a simple name declared in the JSON-LD context. */ public String expand(String key) { if (key == null) return null; int colon = key.indexOf(':'); if (colon<0) { LdTerm term = getTerm(key); return term==null ? key : term.getIRI(); } String prefix = key.substring(0, colon); String namespaceIRI = null; LdTerm term = getTerm(prefix); if (term == null) { return key; } namespaceIRI = term.getIRI(); if (namespaceIRI == null) { // The namespace IRI needs to be expanded. namespaceIRI = term.getRawIRI(); String expandedIRI = expand(namespaceIRI); if (!namespaceIRI.equals(expandedIRI)) { term.setIRI(expandedIRI); namespaceIRI = expandedIRI; } } if (namespaceIRI == null) { return key; } String suffix = key.substring(colon+1); return namespaceIRI + suffix; } /** * Returns the specified term as defined within this JSON-LD context. * The return value may come from this context, or it may be inherited from * the parent context. * * @param key Either the short name for the term, or a compact IRI, or a fully qualified IRI. */ public LdTerm getTerm(String key) { if (key==null) return null; LdTerm term = (termMap != null) ? termMap.get(key) : null; if (term != null) return term; if (components != null) { for (int i=components.size()-1; i>=0; i--) { LdContext c = components.get(i); term = c.getTerm(key); if (term != null) return term; } } if (parentContext != null) { term = parentContext.getTerm(key); } return term; } /** * Returns true if this context has been enhanced with LdProperty * or LdClass data which is useful for validation. */ public boolean isEnhanced() { if (termList != null) { for (LdTerm term : termList) { // if (term.getProperty() != null) return true; if ( // TODO: The special case handling of owl:Thing is forced by a hack that added this RDF class by brute force. // Eliminate this hack. (term.getRdfClass() != null && !"http://www.w3.org/2002/07/owl#Thing".equals(term.getRdfClass().getURI())) || (term.getDatatype()!=null) || (term.getProperty()!=null) ) return true; } } return (parentContext==null) ? false : parentContext.isEnhanced(); } /** * Returns a representation of the specified RDF class. */ public LdClass getClass(String classURI) { LdTerm term = getTerm(classURI); LdClass result = (term==null) ? null : term.getRdfClass(); return (result==null && parentContext!=null) ? parentContext.getClass(classURI) : result; } public List<LdClass> listClasses() { List<LdClass> list = new ArrayList<LdClass>(); addClasses(list); return list; } private void addClasses(List<LdClass> list) { for (LdTerm term : termList) { LdClass c = term.getRdfClass(); if (c != null) { list.add(c); } } if (parentContext != null) { parentContext.addClasses(list); } } /** * Returns true if the subURI is the same as superURI, or if the * subURI is the URI for a class that is known to be a subclass of the * class referenced by superURI. */ public boolean isAssignableFrom(String subURI, String superURI) { if (subURI.equals(superURI)) return true; LdClass subDomain = getClass(subURI); return (subDomain == null) ? null : subDomain.hasSuperType(superURI); } public LdDatatype findDatatypeByURI(String datatypeURI) { if (datatypeURI==null) { return null; } if (datatypeURI.startsWith(XsdType.URI) || datatypeURI.startsWith(LdDatatypeManager.XPATH_DATATYPES_URI)) { return LdDatatypeManager.getXsdTypeByURI(datatypeURI); } LdTerm term = getTerm(datatypeURI); return term == null ? null : term.getDatatype(); } /** * Return the term with the specified uri, and create it if * it does not already exist. */ public LdTerm ensureTerm(String uri) { LdTerm term = getTerm(uri); if (term == null) { term = new LdTerm(); term.setRawIRI(uri); String shortName = makeUnique(localName(uri)); term.setShortName(shortName); termList.add(term); if (termMap != null) { termMap.put(shortName, term); termMap.put(uri, term); } } return term; } /** * Ensure that the specified class is defined in this context, and return * the requested LdClass instance. If the specified LdClass is not found in the context, * it will be created. Likewise an LdTerm for the class will be created as * a side-effect if necessary. */ public LdClass ensureClass(String classURI) { LdTerm term = ensureTerm(classURI); LdClass result = term.getRdfClass(); if (result == null) { result = new LdClass(classURI); term.setRdfClass(result); } return result; } private String makeUnique(String name) { int max = 100; String result = name; int i = 1; for (; i<max && getTerm(result)!=null; i++) { result = name + i; } if (i == max) { result = "x" + (max + random.nextLong()); } return result; } private String localName(String uri) { int mark = uri.lastIndexOf('#'); if (mark < 0) { mark = uri.lastIndexOf('/'); } if (mark < 0) { return "x"; } return uri.substring(mark+1); } }