Java tutorial
/* * Copyright (c) 2007-2014 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; } }