JDILParser.java
/*
* Copyright (c) 2007-2017 MetaSolutions AB
*
* 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.entrystore.rest.util.jdil;
import java.io.*;
import java.net.*;
import java.util.*;
import org.json.*;
import org.restlet.*;
import org.restlet.data.*;
/**
* A class providing a few static methods for parsing org.json datastructures as JDIL.
*
* @author Carl Leonardsson
*
*/
public class JDILParser {
/**
* An interface meant to detect whether or not it is appropriate to interpret string values
* in JDIL objects as URIs. Usage: expandJDIL*.
*
* @author Carl Leonardsson
*
*/
public interface URIDetector{
/**
* Detects whether the value of the key <code>key</code> in the JDIL object
* <code>jdil</code> should be interpretted as a URI provided it is a String.
*
* @param key a JDIL key value
* @param jdil a JDIL object
* @return true if the value of the key <code>key</code> in the JDIL object
* <code>jdil</code> should be interpretted as a URI provided it is a String,
* false otherwise
*/
Boolean hasURIValue(String key, JSONObject jdil);
}
/**
* Creates a new namespace by merging <code>namespace</code> and the namespace specified in
* <code>json</code> by jdil notation ("@namespaces").
* Has no side-effects (save exception throwing).
*
* @param json any JSON object
* @param namespaces any <code>Map<String,String></code>
* @return A copy of <code>namespace</code> updated such that every name/value pair a:b in the
* "@namespaces" object of <code>json</code> is represented in the returned map by a mapping
* a |-> b. In cases where there is already a mapping a |-> c in <code>namespace</code> c is
* overridden by b. Note that only the "@namespaces" value of <code>json</code> is considered -
* possible "@namespaces" values of child objects of <code>json</code> are ignored.
* @throws JDILException if json has a "@namespaces" value and that value is faulty according
* to jdil syntax.
*/
public static Map<String,String> extractNamespaces(JSONObject json,Map<String,String> namespaces) throws JDILException{
Map<String,String> newNamespaces = new HashMap<String,String>();
/* Deep copy namespace into newNamespace */
Iterator<String> nsIt = namespaces.keySet().iterator();
while(nsIt.hasNext()){
String key = nsIt.next();
newNamespaces.put(key,namespaces.get(key));
}
/* Does json have a @namespaces value? */
if(json.has("@namespaces")){
Object jsonNamespaces = json.opt("@namespaces");
if(jsonNamespaces instanceof String){
/* The string jsonNamespaces is supposed to hold an URI
* which holds the namespaces definitions. (ref. http://jdil.org)
* Read the URI and set jsonNamespaces to a corresponding JSONObject. */
String nsURI = (String)jsonNamespaces;
String nsObj;
try{
nsObj = getURIContentAsText(nsURI);
}catch(Exception exc){
throw new JDILException("Failed to GET namespaces from URI "+nsURI+": "+exc.getMessage());
}
try{
jsonNamespaces = new JSONObject(nsObj);
}catch(JSONException exc){
throw new JDILException("Content of URI "+nsURI+" supposed to contain namespace definitions is an invalid JSON object: "+exc.getMessage());
}
}
if(jsonNamespaces instanceof JSONObject){
Iterator jsonNsIt = ((JSONObject)jsonNamespaces).keys();
while(jsonNsIt.hasNext()){
String key = (String)jsonNsIt.next();
Object o = ((JSONObject)jsonNamespaces).opt(key);
if(o instanceof java.lang.String){
newNamespaces.put(key,(String)o);
}else{
throw new JDILException("Expected JSON string as value of namespace "+key+", got "+((JSONObject)jsonNamespaces).opt(key).toString());
}
}
}else{
throw new JDILException("Expected JSON object or URI for @namespaces, got "+jsonNamespaces.toString());
}
}/* else -> it is quite alright not to have a @namespaces value */
return newNamespaces;
}
/**
* Replaces all JDIL star notations "*"^a:id with a:{"@id":id} in <code>json</code>. This is
* carried out not only for key/value pairs in <code>json</code> but also recursively in all objects
* contained in <code>json</code>. The replacement is stupid in the aspect that any "*"^a:val will
* be replaced by a:{"@id":val} regardless of the values or types of a and val.
*
* @param json the JDIL object to be rewritten
*/
public static void removeJDILStar(JSONObject json){
Iterator keyIt;
Set<String> modifiedKeys = new HashSet<String>();
Boolean noStars = false;
/* Do replacement for key/value pairs of this object */
while(!noStars){
noStars = true;
keyIt = json.keys();
while(keyIt.hasNext()){
//System.out.print("key: ");
String key = (String)keyIt.next();
//System.out.println(key);
if((!modifiedKeys.contains(key)) && key.startsWith("*")){
noStars = false;
Object id = json.opt(key);
json.remove(key);
JSONObject newObj = new JSONObject();
key = key.substring(1);
modifiedKeys.add(key);
try{
newObj.put("@id",id);
json.put(key,newObj);
}catch(JSONException exc){
/* cannot happen */
/* Note that "*":val would render "":{"@id":val}
* which actually is acceptable JSON. */
}
break;
}
}
}
/* Recurse */
keyIt = json.keys();
while(keyIt.hasNext()){
String key = (String)keyIt.next();
Object val = json.opt(key);
if(val instanceof JSONObject){
removeJDILStar((JSONObject)val);
}else if(val instanceof JSONArray){
removeJDILStar((JSONArray)val);
}
}
}
/**
* Replaces all JDIL star notations "*"^a:id with a:{"@id":id} in <code>json</code>. This is
* carried recursively for all objects and arrays contained in <code>json</code>. The replacement
* is stupid in the aspect that any "*"^a:val will be replaced by a:{"@id":val} regardless of
* the values or types of a and val.
*
* @param json the JDIL array to be rewritten
*/
public static void removeJDILStar(JSONArray json){
int i;
for(i = 0; i < json.length(); i++){
Object val = json.opt(i);
if(val instanceof JSONObject){
removeJDILStar((JSONObject)val);
}else if(val instanceof JSONArray){
removeJDILStar((JSONArray)val);
}
}
}
/**
* Expands the namespace of <code>uri</code> according to the mapping provided by <code>namespaces</code>.
*
* @param uri any String
* @param namespaces any Map<String,String>
* @return namespace^id if <code>uri</code> is on the form ns^":"^id and there is a mapping
* ns |-> namespace in <code>namespaces</code>, <code>uri</code> otherwise.
*/
public static String expandURIString(String uri, Map<String,String> namespaces){
if(uri.indexOf(":") != -1){
String shortns = uri.substring(0,uri.indexOf(":"));
if(namespaces.keySet().contains(shortns)){
String longns = new String(namespaces.get(shortns));
return longns.concat(uri.substring(uri.indexOf(":")+1));
}
}
return uri;
}
/**
* Attempts to GET <code>URIString</code> and return the result as text.
*
* @param URIString a String representing an URI with scheme specified
* (e.g. "file://path" rather than just "path")
* @return text representing the content of <code>URIString</code>
* @throws JDILException if unable to GET content of <code>URIString</code>
* @throws IOException if underlying restlet fails (ask restlet documentation for details *haha*)
* @throws URISyntaxException if <code>URIString</code> is an URI with invalid syntax
*/
private static String getURIContentAsText(String URIString) throws JDILException, IOException, URISyntaxException{
String scheme = new URI(URIString).getScheme();
if(scheme == null){
throw new JDILException("No scheme given in URI "+URIString);
}else{
Client client = new Client(new Protocol(scheme));
Request request = new Request(Method.GET, URIString);
Response response = client.handle(request);
if(response.getStatus().equals(Status.SUCCESS_OK) && response.isEntityAvailable()){ /* did the request succeed and yield data? */
return response.getEntity().getText();
}else{
throw new JDILException("Unable to GET URI "+URIString);
}
}
}
/**
* Creates and returns a copy of <code>jdil</code> normalised with respect to JDIL "@id" value and
* with every URI value specified with a namespace expanded by the namespace definition. Call the
* returned object <code>jdilCopy</code>.
* <p>In <code>jdilCopy</code> all objects with the same JDIL "@id" value are the same (not copies)
* object and that object has every key/value pair occuring in any of the objects in <code>jdil</code>
* with that "@id" value.
* <p>Note that this means that loops can be created by certain input JDIL constructs. Such loops will
* not be detected by this method.
* <p>In <code>jdilCopy</code> every URI of the form ns^":"^id where a namespace declaration ns:namespace
* is visible to the URI is exchanged for namespace^id. Here as URIs are considered precisely the values
* (keys are not URIs) val such that the key/value pair key/val is in an object obj which is in
* <code>jdil</code> and uriDetector.hasURIValue(key,obj) is true. Namespace declarations considered are
* those declared in <code>jdil</code> by the JDIL "@namespaces" construct.
* <p><code>jdilCopy</code> will contain no JDIL "@namespaces" constructs.
* <p>This method recurses over JSONObjects and JSONArrays thus not only <code>jdil</code> but also
* descendants of <code>jdil</code> will be expanded when creating <code>jdilCopy</code>.
*
* @param jdil any JDIL object
* @param uriDetector a URIDetector used to determine whether a value is a URI.
* @return <code>jdilCopy</code>
* @throws JDILException if two objects with the same "@id" value has key/value pairs with the same
* key but differing values or if an object has a "@id" value which is not a string.
*/
public static JSONObject expandJDILObject(JSONObject jdil, URIDetector uriDetector) throws JDILException{
JSONObject jdilCopy = null; /* initialisation avoids complaints, see below */
try{
jdilCopy = new JSONObject(jdil.toString());
}catch(JSONException exc){ /* cannot happen */ }
removeJDILStar(jdilCopy);
return expandJDILObjectWithoutStar(jdilCopy, new HashMap<String,String>(), new HashMap<String,JSONObject>(), uriDetector);
}
/**
* Maps expandJDILObject over all JSONObjects and JSONArrays in <code>jdil</code>.
*
* @param jdil any JDIL array
* @param uriDetector see expandJDILObject
* @return a deep copy of <code>jdil</code> where expandJDILObject has been mapped over all JDILObjects
* in <code>jdil</code>
* @throws JDILException if any of the calls to expandJDILObject does
*/
public static JSONArray expandJDILArray(JSONArray jdil, URIDetector uriDetector) throws JDILException{
JSONArray jdilCopy = null; /* initialisation avoids complaints, see below */
try{
jdilCopy = new JSONArray(jdil.toString());
}catch(JSONException exc){ /* cannot happen */ }
removeJDILStar(jdilCopy);
return expandJDILArrayWithoutStar(jdilCopy,new HashMap<String,String>(),new HashMap<String,JSONObject>(),uriDetector);
}
/**
* Creates and returns a copy of <code>jdil</code> normalised with respect to JDIL "@id" value and
* with every URI value specified with a namespace expanded by the namespace definition. Call the
* returned object <code>jdilCopy</code>.
* <p>In <code>jdilCopy</code> all objects with the same JDIL "@id" value are the same (not copies)
* object and that object has every key/value pair occuring in any of the objects in <code>jdil</code>
* with that "@id" value. If there is a JDIL object in <code>namedJDILObjects</code> with the same
* "@id" value then the objects in <code>jdilCopy</code> with that "@id" value will be the same
* object as that object. After this method returns all objects in <code>jdilCopy</code> with a "@id"
* value will be represented in <code>namedJdilObjects</code>, the ones in <code>namedJdilObjects</code>
* from the beginning will remain there but might be extended with additional key/value pairs as
* described above.
* <p>Note that this means that loops can be created by certain input JDIL constructs. Such loops will
* not be detected by this method.
* <p>In <code>jdilCopy</code> every URI of the form ns^":"^id where a namespace declaration ns:namespace
* is visible to the URI is exchanged for namespace^id. Here as URIs are considered precisely the values
* (keys are not URIs) val such that the key/value pair key/val is in an object obj which is in
* <code>jdil</code> and uriDetector(key,obj) is true. Namespace declarations considered are those in
* <code>namespaces</code> and those declared in <code>jdil</code> by the JDIL "@namespaces"
* construct.
* <p><code>jdilCopy</code> will contain no JDIL "@namespaces" constructs.
* <p>This method recurses over JSONObjects and JSONArrays thus not only <code>jdil</code> but also
* descendants of <code>jdil</code> will be expanded when creating <code>jdilCopy</code>.
*
* @param jdil any JDIL object in which there is no occurence of the JDIL "*" construct.
* @param namespaces a mapping where every a |-> b is regarded as a namespace declaration a:b
* outside of but visible to <code>jdil</code>. This map should probably be empty in most cases.
* @param namedJdilObjects a mapping from JDIL "@id" values to JDIL objects with those JDIL
* "@id" values. This map should probably be empty in most cases.
* @param uriDetector a URIDetector used to determine whether a value is a URI.
* @return <code>jdilCopy</code>
* @throws JDILException if two objects with the same "@id" value has key/value pairs with the same
* key but differing values or if an object has a "@id" value which is not a string.
*/
private static JSONObject expandJDILObjectWithoutStar(JSONObject jdil, Map<String,String> namespaces, Map<String,JSONObject> namedJdilObjects, URIDetector uriDetector) throws JDILException{
JSONObject jdilCopy;
Map<String,String> newNamespaces = extractNamespaces(jdil,namespaces);
try{
jdilCopy = new JSONObject(jdil.toString()); /* copy jdil */
}catch(JSONException exc){
/* cannot happen */
jdilCopy = new JSONObject(); /* avoids complaints */
}
/* URI expansion and recursive calls */
Iterator keyIt = jdilCopy.keys();
while(keyIt.hasNext()){
String key = (String)keyIt.next();
Object val = jdilCopy.opt(key); /* safe since we know that jdilCopy.has(key) */
try{
if(val instanceof JSONObject){
/* Recurse */
jdilCopy.put(key, expandJDILObjectWithoutStar((JSONObject)val,newNamespaces,namedJdilObjects,uriDetector));
}else if(val instanceof JSONArray){
/* Recurse */
jdilCopy.put(key, expandJDILArrayWithoutStar((JSONArray)val,newNamespaces,namedJdilObjects,uriDetector));
}else if(val instanceof String &&
uriDetector.hasURIValue(key,jdilCopy)){
/* Expand URI */
jdilCopy.put(key,expandURIString((String)val,newNamespaces));
}
}catch(JSONException exc){ /* cannot happen */ }
}
if(jdilCopy.has("@id")){
/* "@id" namespace */
Object id = jdilCopy.opt("@id"); /* safe since we know that sirffCopy.has("@id")*/
if(id instanceof java.lang.String){
try{
jdilCopy.put("@id",expandURIString((String)id,newNamespaces));
}catch(JSONException exc){
/* cannot happen */
}
/* "@id" value collision? */
if(namedJdilObjects.keySet().contains(jdilCopy.opt("@id"))){
/* collision */
/* replace sirffCopy by the other object with the same id */
JSONObject tmp = jdilCopy;
jdilCopy = namedJdilObjects.get(jdilCopy.opt("@id"));
/* merge key/value pairs */
keyIt = tmp.keys();
while(keyIt.hasNext()){
String key = (String)keyIt.next();
if(jdilCopy.has(key)){
/* possible to merge? */
if(!jdilCopy.opt(key).equals(tmp.opt(key))){
throw new JDILException("Colliding definitions of "+key+" value in object "+(String)id+". Both "+jdilCopy.opt(key).toString()+" and "+tmp.opt(key)+".");
}
}else{
try{
jdilCopy.put(key,tmp.get(key));
}catch(JSONException exc){ /* cannot happen */ }
}
}
}else{
/* insert a new mapping to namedJdilObjects */
namedJdilObjects.put(jdilCopy.opt("@id").toString(),jdilCopy);
}
}else{
throw new JDILException("Expected URI for @id, got "+id.toString());
}
}
jdilCopy.remove("@namespaces");
return jdilCopy;
}
/**
* Maps expandJDILObjectWithoutStar over all JSONObjects and JSONArrays in <code>jdil</code>.
*
* @param jdil any JDIL array which does not contain any occurence of the JDIL "*" construct.
* @param namespaces see expandJDILObjectWithoutStar
* @param namedJdilObjects see expandJDILObjectWithoutStar
* @param uriDetector see expandJDILObjectWithoutStar
* @return a deep copy of <code>jdil</code> where expandJDILObjectWithoutStar has been mapped over
* all JDILObjects in <code>jdil</code>
* @throws JDILException if any of the calls to expandJDILObject does
*/
private static JSONArray expandJDILArrayWithoutStar(JSONArray jdil, Map<String,String> namespaces, Map<String,JSONObject> namedJdilObjects, URIDetector uriDetector) throws JDILException{
JSONArray copy = new JSONArray();
int i;
for(i = 0; i < jdil.length(); i++){
try{
Object obj = jdil.get(i);
if(obj instanceof JSONObject){
copy.put(i,expandJDILObjectWithoutStar((JSONObject)obj,namespaces,namedJdilObjects,uriDetector));
}else if(obj instanceof JSONArray){
copy.put(i,expandJDILArrayWithoutStar((JSONArray)obj,namespaces,namedJdilObjects,uriDetector));
}else{
copy.put(i,obj);
}
}catch(JSONException exc){ /* cannot happen */ }
}
return copy;
}
/**
* Checks whether a JDIL object <code>jdil</code> contains loops.
*
* @param jdil any JSONObject which is expanded in the way described in expandJDILObject.
* @return true if any object in <code>jdil</code> has a descendant object with the same "@id"
* value as itself, false otherwise.
*/
public static Boolean hasLoops(JSONObject jdil){
return hasLoops(jdil, new HashSet<String>());
}
/**
* Checks whether a JDIL object <code>jdil</code> contains loops or any "@id" value which also
* occurs in <code>ancestors</code>.
*
* @param jdil any JSONObject which is expanded in the way described in expandJDILObject
* @param ancestors any string set
* @return true if any object in <code>jdil</code> has a descendant object with the same "@id"
* value as itself or if any object in <code>jdil</code> has an "@id" value which occurs in
* <code>ancestors</code>, false otherwise.
*/
private static Boolean hasLoops(JSONObject jdil, HashSet<String>ancestors){
Object tmpObj = null;
String id = null;
Boolean hasLoop = false;
if(jdil.has("@id")){
tmpObj = jdil.opt("@id");
if(tmpObj instanceof String){
id = (String)tmpObj;
if(ancestors.contains(id)){
return true;
}else{
ancestors.add(id);
}
}
}
/* Recurse */
Iterator keyIt = jdil.keys();
while(keyIt.hasNext()){
String key = (String)keyIt.next();
tmpObj = jdil.opt(key);
if(tmpObj instanceof JSONObject){
if(hasLoops((JSONObject)tmpObj,ancestors)){
hasLoop = true;
break;
}
}else if(tmpObj instanceof JSONArray){
if(hasLoops((JSONArray)tmpObj,ancestors)){
hasLoop = true;
break;
}
}
}
/* If we added the @id of this object - remove it now */
/* We know that this @id was not in ancestors before so we can safely remove it */
if(id != null){
ancestors.remove(id);
}
return hasLoop;
}
/**
* Checks whether a JDIL array <code>jdil</code> contains loops or any "@id" value which also
* occurs in <code>ancestors</code>.
*
* @param jdil any JSONArray which is expanded in the way described in expandJDILArray
* @param ancestors any string set
* @return true if any object in <code>jdil</code> has a descendant object with the same "@id"
* value as itself or if any object in <code>jdil</code> has an "@id" value which occurs in
* <code>ancestors</code>, false otherwise.
*/
private static Boolean hasLoops(JSONArray jdil, HashSet<String> ancestors){
Boolean hasLoop = false;
for(int i = 0; i < jdil.length(); i++){
Object tmpObj = jdil.opt(i);
if(tmpObj instanceof JSONObject){
if(hasLoops((JSONObject)tmpObj,ancestors)){
hasLoop = true;
break;
}
}else if(tmpObj instanceof JSONArray){
if(hasLoops((JSONArray)tmpObj,ancestors)){
hasLoop = true;
break;
}
}
}
return hasLoop;
}
}