All Downloads are FREE. Search and download functionalities are using the official Maven repository.

mjson.Json Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2011 Miami-Dade County.
 *
 * 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.
 *
 * Note: this file incorporates source code from 3d party entities. Such code
 * is copyrighted by those entities as indicated below.
 */
package mjson;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URL;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
//import java.util.function.Function;
import java.util.regex.Pattern;

/**
 *
 * 

* Represents a JSON (JavaScript Object Notation) entity. For more information about JSON, please see * http://www.json.org. *

* *

* A JSON entity can be one of several things: an object (set of name/Json entity pairs), an array (a list of * other JSON entities), a string, a number, a boolean or null. All of those are represented as Json * instances. Each of the different types of entities supports a different set of operations. However, this class * unifies all operations into a single interface so in Java one is always dealing with a single object type: this class. * The approach effectively amounts to dynamic typing where using an unsupported operation won't be detected at * compile time, but will throw a runtime {@link UnsupportedOperationException}. It simplifies working with JSON * structures considerably and it leads to shorter at cleaner Java code. It makes much easier to work * with JSON structure without the need to convert to "proper" Java representation in the form of * POJOs and the like. When traversing a JSON, there's no need to type-cast at each step because there's * only one type: Json. *

* *

* One can examine the concrete type of a Json with one of the isXXX methods: * {@link #isObject()}, {@link #isArray()},{@link #isNumber()},{@link #isBoolean()},{@link #isString()}, * {@link #isNull()}. *

* *

* The underlying representation of a given Json instance can be obtained by calling * the generic {@link #getValue()} method or one of the asXXX methods such * as {@link #asBoolean()} or {@link #asString()} etc. * JSON objects are represented as Java {@link Map}s while JSON arrays are represented as Java * {@link List}s. Because those are mutable aggregate structures, there are two versions of the * corresponding asXXX methods: {@link #asMap()} which performs a deep copy of the underlying * map, unwrapping every nested Json entity to its Java representation and {@link #asJsonMap()} which * simply return the map reference. Similarly there are {@link #asList()} and {@link #asJsonList()}. *

* *

Constructing and Modifying JSON Structures

* *

* There are several static factory methods in this class that allow you to create new * Json instances: *

* * * * * * * * * * * * * * * * * * * * * * * *
{@link #read(String)}Parse a JSON string and return the resulting Json instance. The syntax * recognized is as defined in http://www.json.org. *
{@link #make(Object)}Creates a Json instance based on the concrete type of the parameter. The types * recognized are null, numbers, primitives, String, Map, Collection, Java arrays * and Json itself.
{@link #nil()}Return a Json instance representing JSON null.
{@link #object()}Create and return an empty JSON object.
{@link #object(Object...)}Create and return a JSON object populated with the key/value pairs * passed as an argument sequence. Each even parameter becomes a key (via * toString) and each odd parameter is converted to a Json * value.
{@link #array()}Create and return an empty JSON array.
{@link #array(Object...)}Create and return a JSON array from the list of arguments.
* *

* To customize how Json elements are represented and to provide your own version of the * {@link #make(Object)} method, you create an implementation of the {@link Factory} interface * and configure it either globally with the {@link #setGlobalFactory(Factory)} method or * on a per-thread basis with the {@link #attachFactory(Factory)}/{@link #detachFactory()} * methods. *

* *

* If a Json instance is an object, you can set its properties by * calling the {@link #set(String, Object)} method which will add a new property or replace an existing one. * Adding elements to an array Json is done with the {@link #add(Object)} method. * Removing elements by their index (or key) is done with the {@link #delAt(int)} (or * {@link #delAt(String)}) method. You can also remove an element from an array without * knowing its index with the {@link #remove(Object)} method. All these methods return the * Json instance being manipulated so that method calls can be chained. * If you want to remove an element from an object or array and return the removed element * as a result of the operation, call {@link #atDel(int)} or {@link #atDel(String)} instead. *

* *

* If you want to add properties to an object in bulk or append a sequence of elements to array, * use the {@link #with(Json, Json...opts)} method. When used on an object, this method expects another * object as its argument and it will copy all properties of that argument into itself. Similarly, * when called on array, the method expects another array and it will append all elements of its * argument to itself. *

* *

* To make a clone of a Json object, use the {@link #dup()} method. This method will create a new * object even for the immutable primitive Json types. Objects and arrays are cloned * (i.e. duplicated) recursively. *

* *

Navigating JSON Structures

* *

* The {@link #at(int)} method returns the array element at the specified index and the * {@link #at(String)} method does the same for a property of an object instance. You can * use the {@link #at(String, Object)} version to create an object property with a default * value if it doesn't exist already. *

* *

* To test just whether a Json object has a given property, use the {@link #has(String)} method. To test * whether a given object property or an array elements is equal to a particular value, use the * {@link #is(String, Object)} and {@link #is(int, Object)} methods respectively. Those methods return * true if the given named property (or indexed element) is equal to the passed in Object as the second * parameter. They return false if an object doesn't have the specified property or an index array is out * of bounds. For example is(name, value) is equivalent to 'has(name) && at(name).equals(make(value))'. *

* *

* To help in navigating JSON structures, instances of this class contain a reference to the * enclosing JSON entity (object or array) if any. The enclosing entity can be accessed * with {@link #up()} method. *

* *

* The combination of method chaining when modifying Json instances and * the ability to navigate "inside" a structure and then go back to the enclosing * element lets one accomplish a lot in a single Java statement, without the need * of intermediary variables. Here for example how the following JSON structure can * be created in one statement using chained calls: *

* *

 * {"menu": {
 * "id": "file",
 * "value": "File",
 * "popup": {
 *   "menuitem": [
 *     {"value": "New", "onclick": "CreateNewDoc()"},
 *     {"value": "Open", "onclick": "OpenDoc()"},
 *     {"value": "Close", "onclick": "CloseDoc()"}
 *   ]
 * }
 * "position": 0
 * }}
 * 
* *

 * import mjson.Json;
 * import static mjson.Json.*;
 * ...
 * Json j = object()
 *  .at("menu", object())
 *    .set("id", "file")
 *    .set("value", "File")
 *    .at("popup", object())
 *      .at("menuitem", array())
 *        .add(object("value", "New", "onclick", "CreateNewDoc()"))
 *        .add(object("value", "Open", "onclick", "OpenDoc()"))
 *        .add(object("value", "Close", "onclick", "CloseDoc()"))
 *        .up()
 *      .up()
 *    .set("position", 0)
 *  .up();
 * ...
 * 
* *

* If there's no danger of naming conflicts, a static import of the factory methods ( * import static json.Json.*;) would reduce typing even further and make the code more * readable. *

* *

Converting to String

* *

* To get a compact string representation, simply use the {@link #toString()} method. If you * want to wrap it in a JavaScript callback (for JSON with padding), use the {@link #pad(String)} * method. *

* *

Validating with JSON Schema

* *

* Since version 1.3, mJson supports JSON Schema, draft 4. A schema is represented by the internal * class {@link mjson.Json.Schema}. To perform a validation, you have a instantiate a Json.Schema * using the factory method {@link mjson.Json.Schema} and then call its validate method * on a JSON instance: *

* *

 * import mjson.Json;
 * import static mjson.Json.*;
 * ...
 * Json inputJson = Json.read(inputString);
 * Json schema = Json.schema(new URI("http://mycompany.com/schemas/model"));
 * Json errors = schema.validate(inputJson);
 * for (Json error : errors.asJsonList())
 * 	   System.out.println("Validation error " + err);
 * 
* @author Borislav Iordanov * @version 2.0.0 */ public class Json implements java.io.Serializable, Iterable { private static final long serialVersionUID = 1L; /** *

* This interface defines how Json instances are constructed. There is a * default implementation for each kind of Json value, but you can provide * your own implementation. For example, you might want a different representation of * an object than a regular HashMap. Or you might want string comparison to be * case insensitive. *

* *

* In addition, the {@link #make(Object)} method allows you plug-in your own mapping * of arbitrary Java objects to Json instances. You might want to implement * a Java Beans to JSON mapping or any other JSON serialization that makes sense in your * project. *

* *

* To avoid implementing all methods in that interface, you can extend the {@link DefaultFactory} * default implementation and simply overwrite the ones you're interested in. *

* *

* The factory implementation used by the Json classes is specified simply by calling * the {@link #setGlobalFactory(Factory)} method. The factory is a static, global variable by default. * If you need different factories in different areas of a single application, you may attach them * to different threads of execution using the {@link #attachFactory(Factory)}. Recall a separate * copy of static variables is made per ClassLoader, so for example in a web application context, that * global factory can be different for each web application (as Java web servers usually use a separate * class loader per application). Thread-local factories are really a provision for special cases. *

* * @author Borislav Iordanov * */ public static interface Factory { /** * Construct and return an object representing JSON null. Implementations are * free to cache a return the same instance. The resulting value must return * true from isNull() and null from * getValue(). * * @return The representation of a JSON null value. */ Json nil(); /** * Construct and return a JSON boolean. The resulting value must return * true from isBoolean() and the passed * in parameter from getValue(). * @param value The boolean value. * @return A JSON with isBoolean() == true. Implementations * are free to cache and return the same instance for true and false. */ Json bool(boolean value); /** * Construct and return a JSON string. The resulting value must return * true from isString() and the passed * in parameter from getValue(). * @param value The string to wrap as a JSON value. * @return A JSON element with the given string as a value. */ Json string(String value); /** * Construct and return a JSON number. The resulting value must return * true from isNumber() and the passed * in parameter from getValue(). * * @param value The numeric value. * @return Json instance representing that value. */ Json number(Number value); /** * Construct and return a JSON object. The resulting value must return * true from isObject() and an implementation * of java.util.Map from getValue(). * * @return An empty JSON object. */ Json object(); /** * Construct and return a JSON object. The resulting value must return * true from isArray() and an implementation * of java.util.List from getValue(). * * @return An empty JSON array. */ Json array(); /** * Construct and return a JSON object. The resulting value can be of any * JSON type. The method is responsible for examining the type of its * argument and performing an appropriate mapping to a Json * instance. * * @param anything An arbitray Java object from which to construct a Json * element. * @return The newly constructed Json instance. */ Json make(Object anything); } public static interface Function { /** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply(T t); } /** *

* Represents JSON schema - a specific data format that a JSON entity must * follow. The idea of a JSON schema is very similar to XML. Its main purpose * is validating input. *

* *

* More information about the various JSON schema specifications can be * found at http://json-schema.org. JSON Schema is an IETF draft (v4 currently) and * our implementation follows this set of specifications. A JSON schema is specified * as a JSON object that contains keywords defined by the specification. Here are * a few introductory materials: *

    *
  • http://jsonary.com/documentation/json-schema/ - * a very well-written tutorial covering the whole standard
  • *
  • http://spacetelescope.github.io/understanding-json-schema/ - * online book, tutorial (Python/Ruby based)
  • *
*

* @author Borislav Iordanov * */ public static interface Schema { /** *

* Validate a JSON document according to this schema. The validations attempts to * proceed even in the face of errors. The return value is always a Json.object * containing the boolean property ok. When ok is true, * the return object contains nothing else. When it is false, the return object * contains a property errors which is an array of error messages for all * detected schema violations. *

* * @param document The input document. * @return {"ok":true} or {"ok":false, errors:["msg1", "msg2", ...]} */ Json validate(Json document); /** *

Return the JSON representation of the schema.

*/ Json toJson(); /** *

Possible options are: ignoreDefaults:true|false. *

* @return A newly created Json conforming to this schema. */ //Json generate(Json options); } @Override public Iterator iterator() { return new Iterator() { @Override public boolean hasNext() { return false; } @Override public Json next() { return null; } @Override public void remove() { } }; } static String fetchContent(URL url) { java.io.Reader reader = null; try { reader = new java.io.InputStreamReader((java.io.InputStream)url.getContent()); StringBuilder content = new StringBuilder(); char [] buf = new char[1024]; for (int n = reader.read(buf); n > -1; n = reader.read(buf)) content.append(buf, 0, n); return content.toString(); } catch (Exception ex) { throw new RuntimeException(ex); } finally { if (reader != null) try { reader.close(); } catch (Throwable t) { } } } static Json resolvePointer(String pointerRepresentation, Json top) { String [] parts = pointerRepresentation.split("/"); Json result = top; for (String p : parts) { // TODO: unescaping and decoding if (p.length() == 0) continue; p = p.replace("~1", "/").replace("~0", "~"); if (result.isArray()) result = result.at(Integer.parseInt(p)); else if (result.isObject()) result = result.at(p); else throw new RuntimeException("Can't resolve pointer " + pointerRepresentation + " on document " + top.toString(200)); } return result; } static URI makeAbsolute(URI base, String ref) throws Exception { URI refuri; if (base != null && base.getAuthority() != null && !new URI(ref).isAbsolute()) { StringBuilder sb = new StringBuilder(); if (base.getScheme() != null) sb.append(base.getScheme()).append("://"); sb.append(base.getAuthority()); if (!ref.startsWith("/")) { if (ref.startsWith("#")) sb.append(base.getPath()); else { int slashIdx = base.getPath().lastIndexOf('/'); sb.append(slashIdx == -1 ? base.getPath() : base.getPath().substring(0, slashIdx)).append("/"); } } refuri = new URI(sb.append(ref).toString()); } else if (base != null) refuri = base.resolve(ref); else refuri = new URI(ref); return refuri; } static Json resolveRef(URI base, Json refdoc, URI refuri, Map resolved, Map expanded, Function uriResolver) throws Exception { if (refuri.isAbsolute() && (base == null || !base.isAbsolute() || !base.getScheme().equals(refuri.getScheme()) || !Objects.equals(base.getHost(), refuri.getHost()) || base.getPort() != refuri.getPort() || !base.getPath().equals(refuri.getPath()))) { URI docuri = null; refuri = refuri.normalize(); if (refuri.getHost() == null) docuri = new URI(refuri.getScheme() + ":" + refuri.getPath()); else docuri = new URI(refuri.getScheme() + "://" + refuri.getHost() + ((refuri.getPort() > -1) ? ":" + refuri.getPort() : "") + refuri.getPath()); refdoc = uriResolver.apply(docuri); refdoc = expandReferences(refdoc, refdoc, docuri, resolved, expanded, uriResolver); } if (refuri.getFragment() == null) return refdoc; else return resolvePointer(refuri.getFragment(), refdoc); } /** *

* Replace all JSON references, as per the http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 * specification, by their referants. *

* @param json * @param duplicate * @param done * @return */ static Json expandReferences(Json json, Json topdoc, URI base, Map resolved, Map expanded, Function uriResolver) throws Exception { if (expanded.containsKey(json)) return json; if (json.isObject()) { if (json.has("id") && json.at("id").isString()) // change scope of nest references { base = base.resolve(json.at("id").asString()); } if (json.has("$ref")) { URI refuri = makeAbsolute(base, json.at("$ref").asString()); // base.resolve(json.at("$ref").asString()); Json ref = resolved.get(refuri.toString()); if (ref == null) { ref = Json.object(); resolved.put(refuri.toString(), ref); ref.with(resolveRef(base, topdoc, refuri, resolved, expanded, uriResolver)); } json = ref; } else { for (Map.Entry e : json.asJsonMap().entrySet()) json.set(e.getKey(), expandReferences(e.getValue(), topdoc, base, resolved, expanded, uriResolver)); } } else if (json.isArray()) { for (int i = 0; i < json.asJsonList().size(); i++) json.set(i, expandReferences(json.at(i), topdoc, base, resolved, expanded, uriResolver)); } expanded.put(json, json); return json; } static class DefaultSchema implements Schema { static interface Instruction extends Function{} static Json maybeError(Json errors, Json E) { return E == null ? errors : (errors == null ? Json.array() : errors).with(E, new Json[0]); } // Anything is valid schema static Instruction any = new Instruction() { public Json apply(Json param) { return null; } }; // Type validation class IsObject implements Instruction { public Json apply(Json param) { return param.isObject() ? null : Json.make(param.toString(maxchars)); } } class IsArray implements Instruction { public Json apply(Json param) { return param.isArray() ? null : Json.make(param.toString(maxchars)); } } class IsString implements Instruction { public Json apply(Json param) { return param.isString() ? null : Json.make(param.toString(maxchars)); } } class IsBoolean implements Instruction { public Json apply(Json param) { return param.isBoolean() ? null : Json.make(param.toString(maxchars)); } } class IsNull implements Instruction { public Json apply(Json param) { return param.isNull() ? null : Json.make(param.toString(maxchars)); } } class IsNumber implements Instruction { public Json apply(Json param) { return param.isNumber() ? null : Json.make(param.toString(maxchars)); } } class IsInteger implements Instruction { public Json apply(Json param) { return param.isNumber() && ((Number)param.getValue()) instanceof Integer ? null : Json.make(param.toString(maxchars)); } } class CheckString implements Instruction { int min = 0, max = Integer.MAX_VALUE; Pattern pattern; public Json apply(Json param) { Json errors = null; if (!param.isString()) return errors; String s = param.asString(); final int size = s.codePointCount(0, s.length()); if (size < min || size > max) errors = maybeError(errors,Json.make("String " + param.toString(maxchars) + " has length outside of the permitted range [" + min + "," + max + "].")); if (pattern != null && !pattern.matcher(s).matches()) errors = maybeError(errors,Json.make("String " + param.toString(maxchars) + " does not match regex " + pattern.toString())); return errors; } } class CheckNumber implements Instruction { double min = Double.NaN, max = Double.NaN, multipleOf = Double.NaN; boolean exclusiveMin = false, exclusiveMax = false; public Json apply(Json param) { Json errors = null; if (!param.isNumber()) return errors; double value = param.asDouble(); if (!Double.isNaN(min) && (value < min || exclusiveMin && value == min)) errors = maybeError(errors,Json.make("Number " + param + " is below allowed minimum " + min)); if (!Double.isNaN(max) && (value > max || exclusiveMax && value == max)) errors = maybeError(errors,Json.make("Number " + param + " is above allowed maximum " + max)); if (!Double.isNaN(multipleOf) && (value / multipleOf) % 1 != 0) errors = maybeError(errors,Json.make("Number " + param + " is not a multiple of " + multipleOf)); return errors; } } class CheckArray implements Instruction { int min = 0, max = Integer.MAX_VALUE; Boolean uniqueitems = null; Instruction additionalSchema = any; Instruction schema; ArrayList schemas; public Json apply(Json param) { Json errors = null; if (!param.isArray()) return errors; if (schema == null && schemas == null && additionalSchema == null) // no schema specified return errors; int size = param.asJsonList().size(); for (int i = 0; i < size; i++) { Instruction S = schema != null ? schema : (schemas != null && i < schemas.size()) ? schemas.get(i) : additionalSchema; if (S == null) errors = maybeError(errors,Json.make("Additional items are not permitted: " + param.at(i) + " in " + param.toString(maxchars))); else errors = maybeError(errors, S.apply(param.at(i))); if (uniqueitems != null && uniqueitems && param.asJsonList().lastIndexOf(param.at(i)) > i) errors = maybeError(errors,Json.make("Element " + param.at(i) + " is duplicate in array.")); if (errors != null && !errors.asJsonList().isEmpty()) break; } if (size < min || size > max) errors = maybeError(errors,Json.make("Array " + param.toString(maxchars) + " has number of elements outside of the permitted range [" + min + "," + max + "].")); return errors; } } class CheckPropertyPresent implements Instruction { String propname; public CheckPropertyPresent(String propname) { this.propname = propname; } public Json apply(Json param) { if (!param.isObject()) return null; if (param.has(propname)) return null; else return Json.array().add(Json.make("Required property " + propname + " missing from object " + param.toString(maxchars))); } } class CheckObject implements Instruction { int min = 0, max = Integer.MAX_VALUE; Instruction additionalSchema = any; ArrayList props = new ArrayList(); ArrayList patternProps = new ArrayList(); // Object validation class CheckProperty implements Instruction { String name; Instruction schema; public CheckProperty(String name, Instruction schema) { this.name = name; this.schema = schema; } public Json apply(Json param) { Json value = param.at(name); if (value == null) return null; else return schema.apply(param.at(name)); } } class CheckPatternProperty // implements Instruction { Pattern pattern; Instruction schema; public CheckPatternProperty(String pattern, Instruction schema) { this.pattern = Pattern.compile(pattern); this.schema = schema; } public Json apply(Json param, Set found) { Json errors = null; for (Map.Entry e : param.asJsonMap().entrySet()) if (pattern.matcher(e.getKey()).find()) { found.add(e.getKey()); errors = maybeError(errors, schema.apply(e.getValue())); } return errors; } } public Json apply(Json param) { Json errors = null; if (!param.isObject()) return errors; HashSet checked = new HashSet(); for (CheckProperty I : props) { if (param.has(I.name)) checked.add(I.name); errors = maybeError(errors, I.apply(param)); } for (CheckPatternProperty I : patternProps) { errors = maybeError(errors, I.apply(param, checked)); } if (additionalSchema != any) for (Map.Entry e : param.asJsonMap().entrySet()) if (!checked.contains(e.getKey())) errors = maybeError(errors, additionalSchema == null ? Json.make("Extra property '" + e.getKey() + "', schema doesn't allow any properties not explicitly defined:" + param.toString(maxchars)) : additionalSchema.apply(e.getValue())); if (param.asJsonMap().size() < min) errors = maybeError(errors, Json.make("Object " + param.toString(maxchars) + " has fewer than the permitted " + min + " number of properties.")); if (param.asJsonMap().size() > max) errors = maybeError(errors, Json.make("Object " + param.toString(maxchars) + " has more than the permitted " + min + " number of properties.")); return errors; } } class Sequence implements Instruction { ArrayList seq = new ArrayList(); public Json apply(Json param) { Json errors = null; for (Instruction I : seq) errors = maybeError(errors, I.apply(param)); return errors; } public Sequence add(Instruction I) { seq.add(I); return this; } } class CheckType implements Instruction { Json types; public CheckType(Json types) { this.types = types; } public Json apply(Json param) { String ptype = param.isString() ? "string" : param.isObject() ? "object" : param.isArray() ? "array" : param.isNumber() ? "number" : param.isNull() ? "null" : "boolean"; for (Json type : types.asJsonList()) if (type.asString().equals(ptype)) return null; else if (type.asString().equals("integer") && param.isNumber() && param.asDouble() % 1 == 0) return null; return Json.array().add(Json.make("Type mistmatch for " + param.toString(maxchars) + ", allowed types: " + types)); } } class CheckEnum implements Instruction { Json theenum; public CheckEnum(Json theenum) { this.theenum = theenum; } public Json apply(Json param) { for (Json option : theenum.asJsonList()) if (param.equals(option)) return null; return Json.array().add("Element " + param.toString(maxchars) + " doesn't match any of enumerated possibilities " + theenum); } } class CheckAny implements Instruction { ArrayList alternates = new ArrayList(); Json schema; public Json apply(Json param) { for (Instruction I : alternates) if (I.apply(param) == null) return null; return Json.array().add("Element " + param.toString(maxchars) + " must conform to at least one of available sub-schemas " + schema.toString(maxchars)); } } class CheckOne implements Instruction { ArrayList alternates = new ArrayList(); Json schema; public Json apply(Json param) { int matches = 0; Json errors = Json.array(); for (Instruction I : alternates) { Json result = I.apply(param); if (result == null) matches++; else errors.add(result); } if (matches != 1) { return Json.array().add("Element " + param.toString(maxchars) + " must conform to exactly one of available sub-schemas, but not more " + schema.toString(maxchars)).add(errors); } else return null; } } class CheckNot implements Instruction { Instruction I; Json schema; public CheckNot(Instruction I, Json schema) { this.I = I; this.schema = schema; } public Json apply(Json param) { if (I.apply(param) != null) return null; else return Json.array().add("Element " + param.toString(maxchars) + " must NOT conform to the schema " + schema.toString(maxchars)); } } class CheckSchemaDependency implements Instruction { Instruction schema; String property; public CheckSchemaDependency(String property, Instruction schema) { this.property = property; this.schema = schema; } public Json apply(Json param) { if (!param.isObject()) return null; else if (!param.has(property)) return null; else return (schema.apply(param)); } } class CheckPropertyDependency implements Instruction { Json required; String property; public CheckPropertyDependency(String property, Json required) { this.property = property; this.required = required; } public Json apply(Json param) { if (!param.isObject()) return null; if (!param.has(property)) return null; else { Json errors = null; for (Json p : required.asJsonList()) if (!param.has(p.asString())) errors = maybeError(errors, Json.make("Conditionally required property " + p + " missing from object " + param.toString(maxchars))); return errors; } } } Instruction compile(Json S, Map compiled) { Instruction result = compiled.get(S); if (result != null) return result; Sequence seq = new Sequence(); compiled.put(S, seq); if (S.has("type") && !S.is("type", "any")) seq.add(new CheckType(S.at("type").isString() ? Json.array().add(S.at("type")) : S.at("type"))); if (S.has("enum")) seq.add(new CheckEnum(S.at("enum"))); if (S.has("allOf")) { Sequence sub = new Sequence(); for (Json x : S.at("allOf").asJsonList()) sub.add(compile(x, compiled)); seq.add(sub); } if (S.has("anyOf")) { CheckAny any = new CheckAny(); any.schema = S.at("anyOf"); for (Json x : any.schema.asJsonList()) any.alternates.add(compile(x, compiled)); seq.add(any); } if (S.has("oneOf")) { CheckOne any = new CheckOne(); any.schema = S.at("oneOf"); for (Json x : any.schema.asJsonList()) any.alternates.add(compile(x, compiled)); seq.add(any); } if (S.has("not")) seq.add(new CheckNot(compile(S.at("not"), compiled), S.at("not"))); if (S.has("required") && S.at("required").isArray()) { for (Json p : S.at("required").asJsonList()) seq.add(new CheckPropertyPresent(p.asString())); } CheckObject objectCheck = new CheckObject(); if (S.has("properties")) for (Map.Entry p : S.at("properties").asJsonMap().entrySet()) objectCheck.props.add(objectCheck.new CheckProperty( p.getKey(), compile(p.getValue(), compiled))); if (S.has("patternProperties")) for (Map.Entry p : S.at("patternProperties").asJsonMap().entrySet()) objectCheck.patternProps.add(objectCheck.new CheckPatternProperty(p.getKey(), compile(p.getValue(), compiled))); if (S.has("additionalProperties")) { if (S.at("additionalProperties").isObject()) objectCheck.additionalSchema = compile(S.at("additionalProperties"), compiled); else if (!S.at("additionalProperties").asBoolean()) objectCheck.additionalSchema = null; // means no additional properties allowed } if (S.has("minProperties")) objectCheck.min = S.at("minProperties").asInteger(); if (S.has("maxProperties")) objectCheck.max = S.at("maxProperties").asInteger(); if (!objectCheck.props.isEmpty() || !objectCheck.patternProps.isEmpty() || objectCheck.additionalSchema != any || objectCheck.min > 0 || objectCheck.max < Integer.MAX_VALUE) seq.add(objectCheck); CheckArray arrayCheck = new CheckArray(); if (S.has("items")) if (S.at("items").isObject()) arrayCheck.schema = compile(S.at("items"), compiled); else { arrayCheck.schemas = new ArrayList(); for (Json s : S.at("items").asJsonList()) arrayCheck.schemas.add(compile(s, compiled)); } if (S.has("additionalItems")) if (S.at("additionalItems").isObject()) arrayCheck.additionalSchema = compile(S.at("additionalItems"), compiled); else if (!S.at("additionalItems").asBoolean()) arrayCheck.additionalSchema = null; if (S.has("uniqueItems")) arrayCheck.uniqueitems = S.at("uniqueItems").asBoolean(); if (S.has("minItems")) arrayCheck.min = S.at("minItems").asInteger(); if (S.has("maxItems")) arrayCheck.max = S.at("maxItems").asInteger(); if (arrayCheck.schema != null || arrayCheck.schemas != null || arrayCheck.additionalSchema != any || arrayCheck.uniqueitems != null || arrayCheck.max < Integer.MAX_VALUE || arrayCheck.min > 0) seq.add(arrayCheck); CheckNumber numberCheck = new CheckNumber(); if (S.has("minimum")) numberCheck.min = S.at("minimum").asDouble(); if (S.has("maximum")) numberCheck.max = S.at("maximum").asDouble(); if (S.has("multipleOf")) numberCheck.multipleOf = S.at("multipleOf").asDouble(); if (S.has("exclusiveMinimum")) numberCheck.exclusiveMin = S.at("exclusiveMinimum").asBoolean(); if (S.has("exclusiveMaximum")) numberCheck.exclusiveMax = S.at("exclusiveMaximum").asBoolean(); if (!Double.isNaN(numberCheck.min) || !Double.isNaN(numberCheck.max) || !Double.isNaN(numberCheck.multipleOf)) seq.add(numberCheck); CheckString stringCheck = new CheckString(); if (S.has("minLength")) stringCheck.min = S.at("minLength").asInteger(); if (S.has("maxLength")) stringCheck.max = S.at("maxLength").asInteger(); if (S.has("pattern")) stringCheck.pattern = Pattern.compile(S.at("pattern").asString()); if (stringCheck.min > 0 || stringCheck.max < Integer.MAX_VALUE || stringCheck.pattern != null) seq.add(stringCheck); if (S.has("dependencies")) for (Map.Entry e : S.at("dependencies").asJsonMap().entrySet()) if (e.getValue().isObject()) seq.add(new CheckSchemaDependency(e.getKey(), compile(e.getValue(), compiled))); else if (e.getValue().isArray()) seq.add(new CheckPropertyDependency(e.getKey(), e.getValue())); else seq.add(new CheckPropertyDependency(e.getKey(), Json.array(e.getValue()))); result = seq.seq.size() == 1 ? seq.seq.get(0) : seq; compiled.put(S, result); return result; } int maxchars = 50; URI uri; Json theschema; Instruction start; DefaultSchema(URI uri, Json theschema, Function relativeReferenceResolver) { try { this.uri = uri == null ? new URI("") : uri; if (relativeReferenceResolver == null) relativeReferenceResolver = new Function() { public Json apply(URI docuri) { try { return Json.read(fetchContent(docuri.toURL())); } catch(Exception ex) { throw new RuntimeException(ex); } }}; this.theschema = theschema.dup(); this.theschema = expandReferences(this.theschema, this.theschema, this.uri, new HashMap(), new IdentityHashMap(), relativeReferenceResolver); } catch (Exception ex) { throw new RuntimeException(ex); } this.start = compile(this.theschema, new IdentityHashMap()); } public Json validate(Json document) { Json result = Json.object("ok", true); Json errors = start.apply(document); return errors == null ? result : result.set("errors", errors).set("ok", false); } public Json toJson() { return theschema; } public Json generate(Json options) { // TODO... return Json.nil(); } } public static Schema schema(Json S) { return new DefaultSchema(null, S, null); } public static Schema schema(URI uri) { return schema(uri, null); } public static Schema schema(URI uri, Function relativeReferenceResolver) { try { return new DefaultSchema(uri, Json.read(Json.fetchContent(uri.toURL())), relativeReferenceResolver); } catch (Exception ex) { throw new RuntimeException(ex); } } public static Schema schema(Json S, URI uri) { return new DefaultSchema(uri, S, null); } public static class DefaultFactory implements Factory { public Json nil() { return new NullJson(); } public Json bool(boolean x) { return new BooleanJson(x ? Boolean.TRUE : Boolean.FALSE, null); } public Json string(String x) { return new StringJson(x, null); } public Json number(Number x) { return new NumberJson(x, null); } public Json array() { return new ArrayJson(); } public Json object() { return new ObjectJson(); } public Json make(Object anything) { if (anything == null) return nil(); else if (anything instanceof Json) return (Json)anything; else if (anything instanceof String) return factory().string((String)anything); else if (anything instanceof Collection) { Json L = array(); for (Object x : (Collection)anything) L.add(factory().make(x)); return L; } else if (anything instanceof Map) { Json O = object(); for (Map.Entry x : ((Map)anything).entrySet()) O.set(x.getKey().toString(), factory().make(x.getValue())); return O; } else if (anything instanceof Boolean) return factory().bool((Boolean)anything); else if (anything instanceof Number) return factory().number((Number)anything); else if (anything.getClass().isArray()) { Class comp = anything.getClass().getComponentType(); if (!comp.isPrimitive()) return Json.array((Object[])anything); Json A = array(); if (boolean.class == comp) for (boolean b : (boolean[])anything) A.add(b); else if (byte.class == comp) for (byte b : (byte[])anything) A.add(b); else if (char.class == comp) for (char b : (char[])anything) A.add(b); else if (short.class == comp) for (short b : (short[])anything) A.add(b); else if (int.class == comp) for (int b : (int[])anything) A.add(b); else if (long.class == comp) for (long b : (long[])anything) A.add(b); else if (float.class == comp) for (float b : (float[])anything) A.add(b); else if (double.class == comp) for (double b : (double[])anything) A.add(b); return A; } else throw new IllegalArgumentException("Don't know how to convert to Json : " + anything); } } public static final Factory defaultFactory = new DefaultFactory(); private static Factory globalFactory = defaultFactory; // TODO: maybe use initialValue thread-local method to attach global factory by default here... private static ThreadLocal threadFactory = new ThreadLocal(); /** *

Return the {@link Factory} currently in effect. This is the factory that the {@link #make(Object)} method * will dispatch on upon determining the type of its argument. If you already know the type * of element to construct, you can avoid the type introspection implicit to the make method * and call the factory directly. This will result in an optimization.

* * @return the factory */ public static Factory factory() { Factory f = threadFactory.get(); return f != null ? f : globalFactory; } /** *

* Specify a global Json {@link Factory} to be used by all threads that don't have a * specific thread-local factory attached to them. *

* * @param factory The new global factory */ public static void setGlobalFactory(Factory factory) { globalFactory = factory; } /** *

* Attach a thread-local Json {@link Factory} to be used specifically by this thread. Thread-local * Json factories are the only means to have different {@link Factory} implementations used simultaneously * in the same application (well, more accurately, the same ClassLoader). *

* * @param factory the new thread local factory */ public static void attachFactory(Factory factory) { threadFactory.set(factory); } /** *

* Clear the thread-local factory previously attached to this thread via the * {@link #attachFactory(Factory)} method. The global factory takes effect after * a call to this method. *

*/ public static void detachFactory() { threadFactory.remove(); } /** *

* Parse a JSON entity from its string representation. *

* * @param jsonAsString A valid JSON representation as per the json.org * grammar. Cannot be null. * @return The JSON entity parsed: an object, array, string, number or boolean, or null. Note that * this method will never return the actual Java null. */ public static Json read(String jsonAsString) { return (Json)new Reader().read(jsonAsString); } /** *

* Parse a JSON entity from a URL. *

* * @param location A valid URL where to load a JSON document from. Cannot be null. * @return The JSON entity parsed: an object, array, string, number or boolean, or null. Note that * this method will never return the actual Java null. */ public static Json read(URL location) { return (Json)new Reader().read(fetchContent(location)); } /** *

* Parse a JSON entity from a {@link CharacterIterator}. *

* @param it A character iterator. * @return the parsed JSON element * @see #read(String) */ public static Json read(CharacterIterator it) { return (Json)new Reader().read(it); } /** * @return the null Json instance. */ public static Json nil() { return factory().nil(); } /** * @return a newly constructed, empty JSON object. */ public static Json object() { return factory().object(); } /** *

Return a new JSON object initialized from the passed list of * name/value pairs. The number of arguments must * be even. Each argument at an even position is taken to be a name * for the following value. The name arguments are normally of type * Java String, but they can be of any other type having an appropriate * toString method. Each value is first converted * to a Json instance using the {@link #make(Object)} method. *

* @param args A sequence of name value pairs. * @return the new JSON object. */ public static Json object(Object...args) { Json j = object(); if (args.length % 2 != 0) throw new IllegalArgumentException("An even number of arguments is expected."); for (int i = 0; i < args.length; i++) j.set(args[i].toString(), factory().make(args[++i])); return j; } /** * @return a new constructed, empty JSON array. */ public static Json array() { return factory().array(); } /** *

Return a new JSON array filled up with the list of arguments.

* * @param args The initial content of the array. * @return the new JSON array */ public static Json array(Object...args) { Json A = array(); for (Object x : args) A.add(factory().make(x)); return A; } /** *

* Exposes some internal methods that are useful for {@link org.sharegov.mjson.Json.Factory} implementations * or other extension/layers of the library. *

* * @author Borislav Iordanov * */ public static class help { /** *

* Perform JSON escaping so that ", <, >, etc. characters are properly encoded in the * JSON string representation before returning to the client code. This is useful when * serializing property names or string values. *

*/ public static String escape(String string) { return escaper.escapeJsonString(string); } /** *

* Given a JSON Pointer, as per RFC 6901, return the nested JSON value within * the element parameter. *

*/ public static Json resolvePointer(String pointer, Json element) { return Json.resolvePointer(pointer, element); } } static class JsonSingleValueIterator implements Iterator { private boolean retrieved = false; @Override public boolean hasNext() { return !retrieved; } @Override public Json next() { retrieved = true; return null; } @Override public void remove() { } } /** *

* Convert an arbitrary Java instance to a {@link Json} instance. *

* *

* Maps, Collections and arrays are recursively copied where each of * their elements concerted into Json instances as well. The keys * of a {@link Map} parameter are normally strings, but anything with a meaningful * toString implementation will work as well. *

* * @param anything Any Java object that the current JSON factory in effect is capable of handling. * @return The Json. This method will never return null. It will * throw an {@link IllegalArgumentException} if it doesn't know how to convert the argument * to a Json instance. * @throws IllegalArgumentException when the concrete type of the parameter is * unknown. */ public static Json make(Object anything) { return factory().make(anything); } // end of static utility method section Json enclosing = null; protected Json() { } protected Json(Json enclosing) { this.enclosing = enclosing; } /** *

Return a string representation of this that does * not exceed a certain maximum length. This is useful in constructing * error messages or any other place where only a "preview" of the * JSON element should be displayed. Some JSON structures can get * very large and this method will help avoid string serializing * the whole of them.

* @param maxCharacters The maximum number of characters for * the string representation. * @return The string representation of this object. */ public String toString(int maxCharacters) { return toString(); } /** *

Explicitly set the parent of this element. The parent is presumably an array * or an object. Normally, there's no need to call this method as the parent is * automatically set by the framework. You may need to call it however, if you implement * your own {@link Factory} with your own implementations of the Json types. *

* * @param enclosing The parent element. */ public void attachTo(Json enclosing) { this.enclosing = enclosing; } /** * @return the Json entity, if any, enclosing this * Json. The returned value can be null or * a Json object or list, but not one of the primitive types. */ public final Json up() { return enclosing; } /** * @return a clone (a duplicate) of this Json entity. Note that cloning * is deep if array and objects. Primitives are also cloned, even though their values are immutable * because the new enclosing entity (the result of the {@link #up()} method) may be different. * since they are immutable. */ public Json dup() { return this; } /** *

Return the Json element at the specified index of this * Json array. This method applies only to Json arrays. *

* * @param index The index of the desired element. * @return The JSON element at the specified index in this array. */ public Json at(int index) { throw new UnsupportedOperationException(); } /** *

* Return the specified property of a Json object or null * if there's no such property. This method applies only to Json objects. *

* @param The property name. * @return The JSON element that is the value of that property. */ public Json at(String property) { throw new UnsupportedOperationException(); } /** *

* Return the specified property of a Json object if it exists. * If it doesn't, then create a new property with value the def * parameter and return that parameter. *

* * @param property The property to return. * @param def The default value to set and return in case the property doesn't exist. */ public final Json at(String property, Json def) { Json x = at(property); if (x == null) { // set(property, def); return def; } else return x; } /** *

* Return the specified property of a Json object if it exists. * If it doesn't, then create a new property with value the def * parameter and return that parameter. *

* * @param property The property to return. * @param def The default value to set and return in case the property doesn't exist. */ public final Json at(String property, Object def) { return at(property, make(def)); } /** *

* Return true if this Json object has the specified property * and false otherwise. *

* * @param property The name of the property. */ public boolean has(String property) { throw new UnsupportedOperationException(); } /** *

* Return true if and only if this Json object has a property with * the specified value. In particular, if the object has no such property false is returned. *

* * @param property The property name. * @param value The value to compare with. Comparison is done via the equals method. * If the value is not an instance of Json, it is first converted to * such an instance. * @return */ public boolean is(String property, Object value) { throw new UnsupportedOperationException(); } /** *

* Return true if and only if this Json array has an element with * the specified value at the specified index. In particular, if the array has no element at * this index, false is returned. *

* * @param index The 0-based index of the element in a JSON array. * @param value The value to compare with. Comparison is done via the equals method. * If the value is not an instance of Json, it is first converted to * such an instance. * @return */ public boolean is(int index, Object value) { throw new UnsupportedOperationException(); } /** *

* Add the specified Json element to this array. *

* * @return this */ public Json add(Json el) { throw new UnsupportedOperationException(); } /** *

* Add an arbitrary Java object to this Json array. The object * is first converted to a Json instance by calling the static * {@link #make} method. *

* * @param anything Any Java object that can be converted to a Json instance. * @return this */ public final Json add(Object anything) { return add(make(anything)); } /** *

* Remove the specified property from a Json object and return * that property. *

* * @param property The property to be removed. * @return The property value or null if the object didn't have such * a property to begin with. */ public Json atDel(String property) { throw new UnsupportedOperationException(); } /** *

* Remove the element at the specified index from a Json array and return * that element. *

* * @param index The index of the element to delete. * @return The element value. */ public Json atDel(int index) { throw new UnsupportedOperationException(); } /** *

* Delete the specified property from a Json object. *

* * @param property The property to be removed. * @return this */ public Json delAt(String property) { throw new UnsupportedOperationException(); } /** *

* Remove the element at the specified index from a Json array. *

* * @param index The index of the element to delete. * @return this */ public Json delAt(int index) { throw new UnsupportedOperationException(); } /** *

* Remove the specified element from a Json array. *

* * @param el The element to delete. * @return this */ public Json remove(Json el) { throw new UnsupportedOperationException(); } /** *

* Remove the specified Java object (converted to a Json instance) * from a Json array. This is equivalent to * remove({@link #make(Object)}). *

* * @param anything The object to delete. * @return this */ public final Json remove(Object anything) { return remove(make(anything)); } /** *

* Set a Json objects's property. *

* * @param property The property name. * @param value The value of the property. * @return this */ public Json set(String property, Json value) { throw new UnsupportedOperationException(); } /** *

* Set a Json objects's property. *

* * @param property The property name. * @param value The value of the property, converted to a Json representation * with {@link #make}. * @return this */ public final Json set(String property, Object value) { return set(property, make(value)); } /** *

* Change the value of a JSON array element. This must be an array. *

* @param index 0-based index of the element in the array. * @param value the new value of the element * @return this */ public Json set(int index, Object value) { throw new UnsupportedOperationException(); } /** *

* Combine this object or array with the passed in object or array. The types of * this and the object argument must match. If both are * Json objects, all properties of the parameter are added to this. * If both are arrays, all elements of the parameter are appended to this *

* @param object The object or array whose properties or elements must be added to this * Json object or array. * @param options A sequence of options that governs the merging process. * @return this */ public Json with(Json object, Json[]options) { throw new UnsupportedOperationException(); } /** * Same as {}@link #with(Json,Json...options)} with each option * argument converted to Json first. */ public Json with(Json object, Object...options) { Json [] jopts = new Json[options.length]; for (int i = 0; i < jopts.length; i++) jopts[i] = make(options[i]); return with(object, jopts); } /** * @return the underlying value of this Json entity. The actual value will * be a Java Boolean, String, Number, Map, List or null. For complex entities (objects * or arrays), the method will perform a deep copy and extra underlying values recursively * for all nested elements. */ public Object getValue() { throw new UnsupportedOperationException(); } /** * @return the boolean value of a boolean Json instance. Call * {@link #isBoolean()} first if you're not sure this instance is indeed a * boolean. */ public boolean asBoolean() { throw new UnsupportedOperationException(); } /** * @return the string value of a string Json instance. Call * {@link #isString()} first if you're not sure this instance is indeed a * string. */ public String asString() { throw new UnsupportedOperationException(); } /** * @return the integer value of a number Json instance. Call * {@link #isNumber()} first if you're not sure this instance is indeed a * number. */ public int asInteger() { throw new UnsupportedOperationException(); } /** * @return the float value of a float Json instance. Call * {@link #isNumber()} first if you're not sure this instance is indeed a * number. */ public float asFloat() { throw new UnsupportedOperationException(); } /** * @return the double value of a number Json instance. Call * {@link #isNumber()} first if you're not sure this instance is indeed a * number. */ public double asDouble() { throw new UnsupportedOperationException(); } /** * @return the long value of a number Json instance. Call * {@link #isNumber()} first if you're not sure this instance is indeed a * number. */ public long asLong() { throw new UnsupportedOperationException(); } /** * @return the short value of a number Json instance. Call * {@link #isNumber()} first if you're not sure this instance is indeed a * number. */ public short asShort() { throw new UnsupportedOperationException(); } /** * @return the byte value of a number Json instance. Call * {@link #isNumber()} first if you're not sure this instance is indeed a * number. */ public byte asByte() { throw new UnsupportedOperationException(); } /** * @return the first character of a string Json instance. Call * {@link #isString()} first if you're not sure this instance is indeed a * string. */ public char asChar() { throw new UnsupportedOperationException(); } /** * @return a map of the properties of an object Json instance. The map * is a clone of the object and can be modified safely without affecting it. Call * {@link #isObject()} first if you're not sure this instance is indeed a * Json object. */ public Map asMap() { throw new UnsupportedOperationException(); } /** * @return the underlying map of properties of a Json object. The returned * map is the actual object representation so any modifications to it are modifications * of the Json object itself. Call * {@link #isObject()} first if you're not sure this instance is indeed a * Json object. */ public Map asJsonMap() { throw new UnsupportedOperationException(); } /** * @return a list of the elements of a Json array. The list is a clone * of the array and can be modified safely without affecting it. Call * {@link #isArray()} first if you're not sure this instance is indeed a * Json array. */ public List asList() { throw new UnsupportedOperationException(); } /** * @return the underlying {@link List} representation of a Json array. * The returned list is the actual array representation so any modifications to it * are modifications of the Json array itself. Call * {@link #isArray()} first if you're not sure this instance is indeed a * Json array. */ public List asJsonList() { throw new UnsupportedOperationException(); } /** * @return true if this is a Json null entity * and false otherwise. */ public boolean isNull() { return false; } /** * @return true if this is a Json string entity * and false otherwise. */ public boolean isString() { return false; } /** * @return true if this is a Json number entity * and false otherwise. */ public boolean isNumber() { return false; } /** * @return true if this is a Json boolean entity * and false otherwise. */ public boolean isBoolean() { return false; } /** * @return true if this is a Json array (i.e. list) entity * and false otherwise. */ public boolean isArray() { return false; } /** * @return true if this is a Json object entity * and false otherwise. */ public boolean isObject(){ return false; } /** * @return true if this is a Json primitive entity * (one of string, number or boolean) and false otherwise. * */ public boolean isPrimitive() { return isString() || isNumber() || isBoolean(); } /** *

* Json-pad this object as an argument to a callback function. *

* * @param callback The name of the callback function. Can be null or empty, * in which case no padding is done. * @return The jsonpadded, stringified version of this object if the callback * is not null or empty, or just the stringified version of the object. */ public String pad(String callback) { return (callback != null && callback.length() > 0) ? callback + "(" + toString() + ");" : toString(); } //------------------------------------------------------------------------- // END OF PUBLIC INTERFACE //------------------------------------------------------------------------- /** * Return an object representing the complete configuration * of a merge. The properties of the object represent paths * of the JSON structure being merged and the values represent * the set of options that apply to each path. * @param options the configuration options * @return the configuration object */ protected Json collectWithOptions(Json...options) { Json result = object(); for (Json opt : options) { if (opt.isString()) { if (!result.has("")) result.set("", object()); result.at("").set(opt.asString(), true); } else { if (!opt.has("for")) opt.set("for", array("")); Json forPaths = opt.at("for"); if (!forPaths.isArray()) forPaths = array(forPaths); for (Json path : forPaths.asJsonList()) { if (!result.has(path.asString())) result.set(path.asString(), object()); Json at_path = result.at(path.asString()); at_path.set("merge", opt.is("merge", true)); at_path.set("dup", opt.is("dup", true)); at_path.set("sort", opt.is("sort", true)); at_path.set("compareBy", opt.at("compareBy", nil())); } } } return result; } static class NullJson extends Json { private static final long serialVersionUID = 1L; NullJson() {} NullJson(Json e) {super(e);} public Object getValue() { return null; } public Json dup() { return new NullJson(); } public boolean isNull() { return true; } public String toString() { return "null"; } public List asList() { return (List)Collections.singletonList(null); } public int hashCode() { return 0; } public boolean equals(Object x) { return x instanceof NullJson; } @Override public Iterator iterator() { return new JsonSingleValueIterator() { @Override public Json next() { super.next(); return NullJson.this; } }; } } /** *

* Set the parent (i.e. enclosing element) of Json element. *

* * @param el * @param parent */ static void setParent(Json el, Json parent) { if (el.enclosing == null) el.enclosing = parent; else if (el.enclosing instanceof ParentArrayJson) ((ParentArrayJson)el.enclosing).L.add(parent); else { ParentArrayJson A = new ParentArrayJson(); A.L.add(el.enclosing); A.L.add(parent); el.enclosing = A; } } /** *

* Remove/unset the parent (i.e. enclosing element) of Json element. *

* * @param el * @param parent */ static void removeParent(Json el, Json parent) { if (el.enclosing == parent) el.enclosing = null; else if (el.enclosing.isArray()) { ArrayJson A = (ArrayJson)el.enclosing; int idx = 0; while (A.L.get(idx) != parent && idx < A.L.size()) idx++; if (idx < A.L.size()) A.L.remove(idx); } } static class BooleanJson extends Json { private static final long serialVersionUID = 1L; boolean val; BooleanJson() {} BooleanJson(Json e) {super(e);} BooleanJson(Boolean val, Json e) { super(e); this.val = val; } public Object getValue() { return val; } public Json dup() { return new BooleanJson(val, null); } public boolean asBoolean() { return val; } public boolean isBoolean() { return true; } public String toString() { return val ? "true" : "false"; } @SuppressWarnings("unchecked") public List asList() { return (List)(List)Collections.singletonList(val); } public int hashCode() { return val ? 1 : 0; } public boolean equals(Object x) { return x instanceof BooleanJson && ((BooleanJson)x).val == val; } @Override public Iterator iterator() { return new JsonSingleValueIterator() { @Override public Json next() { super.next(); return BooleanJson.this; } }; } } static class StringJson extends Json { private static final long serialVersionUID = 1L; String val; StringJson() {} StringJson(Json e) {super(e);} StringJson(String val, Json e) { super(e); this.val = val; } public Json dup() { return new StringJson(val, null); } public boolean isString() { return true; } public Object getValue() { return val; } public String asString() { return val; } public int asInteger() { return Integer.parseInt(val); } public float asFloat() { return Float.parseFloat(val); } public double asDouble() { return Double.parseDouble(val); } public long asLong() { return Long.parseLong(val); } public short asShort() { return Short.parseShort(val); } public byte asByte() { return Byte.parseByte(val); } public char asChar() { return val.charAt(0); } @SuppressWarnings("unchecked") public List asList() { return (List)(List)Collections.singletonList(val); } public String toString() { return '"' + escaper.escapeJsonString(val) + '"'; } public String toString(int maxCharacters) { if (val.length() <= maxCharacters) return toString(); else return '"' + escaper.escapeJsonString(val.subSequence(0, maxCharacters)) + "...\""; } public int hashCode() { return val.hashCode(); } public boolean equals(Object x) { return x instanceof StringJson && ((StringJson)x).val.equals(val); } @Override public Iterator iterator() { return new JsonSingleValueIterator() { @Override public Json next() { super.next(); return StringJson.this; } }; } } static class NumberJson extends Json { private static final long serialVersionUID = 1L; Number val; NumberJson() {} NumberJson(Json e) {super(e);} NumberJson(Number val, Json e) { super(e); this.val = val; } public Json dup() { return new NumberJson(val, null); } public boolean isNumber() { return true; } public Object getValue() { return val; } public String asString() { return val.toString(); } public int asInteger() { return val.intValue(); } public float asFloat() { return val.floatValue(); } public double asDouble() { return val.doubleValue(); } public long asLong() { return val.longValue(); } public short asShort() { return val.shortValue(); } public byte asByte() { return val.byteValue(); } @SuppressWarnings("unchecked") public List asList() { return (List)(List)Collections.singletonList(val); } public String toString() { return val.toString(); } public int hashCode() { return val.hashCode(); } public boolean equals(Object x) { return x instanceof NumberJson && val.doubleValue() == ((NumberJson)x).val.doubleValue(); } @Override public Iterator iterator() { return new JsonSingleValueIterator() { @Override public Json next() { super.next(); return NumberJson.this; } }; } } static class ArrayJson extends Json { private static final long serialVersionUID = 1L; List L = new ArrayList(); ArrayJson() { } ArrayJson(Json e) { super(e); } @Override public Iterator iterator() { return L.iterator(); } public Json dup() { ArrayJson j = new ArrayJson(); for (Json e : L) { Json v = e.dup(); v.enclosing = j; j.L.add(v); } return j; } public Json set(int index, Object value) { Json jvalue = make(value); L.set(index, jvalue); setParent(jvalue, this); return this; } public List asJsonList() { return L; } public List asList() { ArrayList A = new ArrayList(); for (Json x: L) A.add(x.getValue()); return A; } public boolean is(int index, Object value) { if (index < 0 || index >= L.size()) return false; else return L.get(index).equals(make(value)); } public Object getValue() { return asList(); } public boolean isArray() { return true; } public Json at(int index) { return L.get(index); } public Json add(Json el) { L.add(el); setParent(el, this); return this; } public Json remove(Json el) { L.remove(el); el.enclosing = null; return this; } boolean isEqualJson(Json left, Json right) { if (left == null) return right == null; else return left.equals(right); } boolean isEqualJson(Json left, Json right, Json fields) { if (fields.isNull()) return left.equals(right); else if (fields.isString()) return isEqualJson(resolvePointer(fields.asString(), left), resolvePointer(fields.asString(), right)); else if (fields.isArray()) { for (Json field : fields.asJsonList()) if (!isEqualJson(resolvePointer(field.asString(), left), resolvePointer(field.asString(), right))) return false; return true; } else throw new IllegalArgumentException("Compare by options should be either a property name or an array of property names: " + fields); } @SuppressWarnings({ "unchecked", "rawtypes" }) int compareJson(Json left, Json right, Json fields) { if (fields.isNull()) return ((Comparable)left.getValue()).compareTo(right.getValue()); else if (fields.isString()) { Json leftProperty = resolvePointer(fields.asString(), left); Json rightProperty = resolvePointer(fields.asString(), right); return ((Comparable)leftProperty).compareTo(rightProperty); } else if (fields.isArray()) { for (Json field : fields.asJsonList()) { Json leftProperty = resolvePointer(field.asString(), left); Json rightProperty = resolvePointer(field.asString(), right); int result = ((Comparable) leftProperty).compareTo(rightProperty); if (result != 0) return result; } return 0; } else throw new IllegalArgumentException("Compare by options should be either a property name or an array of property names: " + fields); } Json withOptions(Json array, Json allOptions, String path) { Json opts = allOptions.at(path, object()); boolean dup = opts.is("dup", true); Json compareBy = opts.at("compareBy", nil()); if (opts.is("sort", true)) { int thisIndex = 0, thatIndex = 0; while (thatIndex < array.asJsonList().size()) { Json thatElement = array.at(thatIndex); if (thisIndex == L.size()) { L.add(dup ? thatElement.dup() : thatElement); thisIndex++; thatIndex++; continue; } int compared = compareJson(at(thisIndex), thatElement, compareBy); if (compared < 0) // this < that thisIndex++; else if (compared > 0) // this > that { L.add(thisIndex, dup ? thatElement.dup() : thatElement); thatIndex++; } else { // equal, ignore thatIndex++; } } } else { for (Json thatElement : array.asJsonList()) { boolean present = false; for (Json thisElement : L) if (isEqualJson(thisElement, thatElement, compareBy)) { present = true; break; } if (!present) L.add(dup ? thatElement.dup() : thatElement); } } return this; } public Json with(Json object, Json...options) { if (object == null) return this; if (!object.isArray()) add(object); else if (options.length > 0) { Json O = collectWithOptions(options); return withOptions(object, O, ""); } else // what about "enclosing" here? we don't have a provision where a Json // element belongs to more than one enclosing elements... L.addAll(((ArrayJson)object).L); return this; } public Json atDel(int index) { Json el = L.remove(index); if (el != null) el.enclosing = null; return el; } public Json delAt(int index) { Json el = L.remove(index); if (el != null) el.enclosing = null; return this; } public String toString() { return toString(Integer.MAX_VALUE); } public String toString(int maxCharacters) { return toStringImpl(maxCharacters, new IdentityHashMap()); } String toStringImpl(int maxCharacters, Map done) { StringBuilder sb = new StringBuilder("["); for (Iterator i = L.iterator(); i.hasNext(); ) { Json value = i.next(); String s = value.isObject() ? ((ObjectJson)value).toStringImpl(maxCharacters, done) : value.isArray() ? ((ArrayJson)value).toStringImpl(maxCharacters, done) : value.toString(maxCharacters); if (sb.length() + s.length() > maxCharacters) s = s.substring(0, Math.max(0, maxCharacters - sb.length())); else sb.append(s); if (i.hasNext()) sb.append(","); if (sb.length() >= maxCharacters) { sb.append("..."); break; } } sb.append("]"); return sb.toString(); } public int hashCode() { return L.hashCode(); } public boolean equals(Object x) { return x instanceof ArrayJson && ((ArrayJson)x).L.equals(L); } } static class ParentArrayJson extends ArrayJson { /** * */ private static final long serialVersionUID = 1L; } static class ObjectJson extends Json { private static final long serialVersionUID = 1L; Map object = new HashMap(); @Override public Iterator iterator() { return object.values().iterator(); } ObjectJson() { } ObjectJson(Json e) { super(e); } public Json dup() { ObjectJson j = new ObjectJson(); for (Map.Entry e : object.entrySet()) { Json v = e.getValue().dup(); v.enclosing = j; j.object.put(e.getKey(), v); } return j; } public boolean has(String property) { return object.containsKey(property); } public boolean is(String property, Object value) { Json p = object.get(property); if (p == null) return false; else return p.equals(make(value)); } public Json at(String property) { return object.get(property); } protected Json withOptions(Json other, Json allOptions, String path) { if (!allOptions.has(path)) allOptions.set(path, object()); Json options = allOptions.at(path, object()); boolean duplicate = options.is("dup", true); if (options.is("merge", true)) { for (Map.Entry e : other.asJsonMap().entrySet()) { Json local = object.get(e.getKey()); if (local instanceof ObjectJson) ((ObjectJson)local).withOptions(e.getValue(), allOptions, path + "/" + e.getKey()); else if (local instanceof ArrayJson) ((ArrayJson)local).withOptions(e.getValue(), allOptions, path + "/" + e.getKey()); else set(e.getKey(), duplicate ? e.getValue().dup() : e.getValue()); } } else if (duplicate) for (Map.Entry e : other.asJsonMap().entrySet()) set(e.getKey(), e.getValue().dup()); else for (Map.Entry e : other.asJsonMap().entrySet()) set(e.getKey(), e.getValue()); return this; } public Json with(Json x, Json...options) { if (x == null) return this; if (!x.isObject()) throw new UnsupportedOperationException(); if (options.length > 0) { Json O = collectWithOptions(options); return withOptions(x, O, ""); } else for (Map.Entry e : x.asJsonMap().entrySet()) set(e.getKey(), e.getValue()); return this; } public Json set(String property, Json el) { if (property == null) throw new IllegalArgumentException("Null property names are not allowed, value is " + el); if (el == null) el = nil(); setParent(el, this); object.put(property, el); return this; } public Json atDel(String property) { Json el = object.remove(property); removeParent(el, this); return el; } public Json delAt(String property) { Json el = object.remove(property); removeParent(el, this); return this; } public Object getValue() { return asMap(); } public boolean isObject() { return true; } public Map asMap() { HashMap m = new HashMap(); for (Map.Entry e : object.entrySet()) m.put(e.getKey(), e.getValue().getValue()); return m; } @Override public Map asJsonMap() { return object; } public String toString() { return toString(Integer.MAX_VALUE); } public String toString(int maxCharacters) { return toStringImpl(maxCharacters, new IdentityHashMap()); } String toStringImpl(int maxCharacters, Map done) { StringBuilder sb = new StringBuilder("{"); if (done.containsKey(this)) return sb.append("...}").toString(); done.put(this, this); for (Iterator> i = object.entrySet().iterator(); i.hasNext(); ) { Map.Entry x = i.next(); sb.append('"'); sb.append(escaper.escapeJsonString(x.getKey())); sb.append('"'); sb.append(":"); String s = x.getValue().isObject() ? ((ObjectJson)x.getValue()).toStringImpl(maxCharacters, done) : x.getValue().isArray() ? ((ArrayJson)x.getValue()).toStringImpl(maxCharacters, done) : x.getValue().toString(maxCharacters); if (sb.length() + s.length() > maxCharacters) s = s.substring(0, Math.max(0, maxCharacters - sb.length())); sb.append(s); if (i.hasNext()) sb.append(","); if (sb.length() >= maxCharacters) { sb.append("..."); break; } } sb.append("}"); return sb.toString(); } public int hashCode() { return object.hashCode(); } public boolean equals(Object x) { return x instanceof ObjectJson && ((ObjectJson)x).object.equals(object); } } // ------------------------------------------------------------------------ // Extra utilities, taken from around the internet: // ------------------------------------------------------------------------ /* * Copyright (C) 2008 Google Inc. * * 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. */ /** * A utility class that is used to perform JSON escaping so that ", <, >, etc. characters are * properly encoded in the JSON string representation before returning to the client code. * *

This class contains a single method to escape a passed in string value: *

    *   String jsonStringValue = "beforeQuote\"afterQuote";
    *   String escapedValue = Escaper.escapeJsonString(jsonStringValue);
    * 

* * @author Inderjeet Singh * @author Joel Leitch */ static Escaper escaper = new Escaper(false); final static class Escaper { private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; private static final Set JS_ESCAPE_CHARS; private static final Set HTML_ESCAPE_CHARS; static { Set mandatoryEscapeSet = new HashSet(); mandatoryEscapeSet.add('"'); mandatoryEscapeSet.add('\\'); JS_ESCAPE_CHARS = Collections.unmodifiableSet(mandatoryEscapeSet); Set htmlEscapeSet = new HashSet(); htmlEscapeSet.add('<'); htmlEscapeSet.add('>'); htmlEscapeSet.add('&'); htmlEscapeSet.add('='); htmlEscapeSet.add('\''); // htmlEscapeSet.add('/'); -- Removing slash for now since it causes some incompatibilities HTML_ESCAPE_CHARS = Collections.unmodifiableSet(htmlEscapeSet); } private final boolean escapeHtmlCharacters; Escaper(boolean escapeHtmlCharacters) { this.escapeHtmlCharacters = escapeHtmlCharacters; } public String escapeJsonString(CharSequence plainText) { StringBuilder escapedString = new StringBuilder(plainText.length() + 20); try { escapeJsonString(plainText, escapedString); } catch (IOException e) { throw new RuntimeException(e); } return escapedString.toString(); } private void escapeJsonString(CharSequence plainText, StringBuilder out) throws IOException { int pos = 0; // Index just past the last char in plainText written to out. int len = plainText.length(); for (int charCount, i = 0; i < len; i += charCount) { int codePoint = Character.codePointAt(plainText, i); charCount = Character.charCount(codePoint); if (!isControlCharacter(codePoint) && !mustEscapeCharInJsString(codePoint)) { continue; } out.append(plainText, pos, i); pos = i + charCount; switch (codePoint) { case '\b': out.append("\\b"); break; case '\t': out.append("\\t"); break; case '\n': out.append("\\n"); break; case '\f': out.append("\\f"); break; case '\r': out.append("\\r"); break; case '\\': out.append("\\\\"); break; case '/': out.append("\\/"); break; case '"': out.append("\\\""); break; default: appendHexJavaScriptRepresentation(codePoint, out); break; } } out.append(plainText, pos, len); } private boolean mustEscapeCharInJsString(int codepoint) { if (!Character.isSupplementaryCodePoint(codepoint)) { char c = (char) codepoint; return JS_ESCAPE_CHARS.contains(c) || (escapeHtmlCharacters && HTML_ESCAPE_CHARS.contains(c)); } return false; } private static boolean isControlCharacter(int codePoint) { // JSON spec defines these code points as control characters, so they must be escaped return codePoint < 0x20 || codePoint == 0x2028 // Line separator || codePoint == 0x2029 // Paragraph separator || (codePoint >= 0x7f && codePoint <= 0x9f); } private static void appendHexJavaScriptRepresentation(int codePoint, Appendable out) throws IOException { if (Character.isSupplementaryCodePoint(codePoint)) { // Handle supplementary unicode values which are not representable in // javascript. We deal with these by escaping them as two 4B sequences // so that they will round-trip properly when sent from java to javascript // and back. char[] surrogates = Character.toChars(codePoint); appendHexJavaScriptRepresentation(surrogates[0], out); appendHexJavaScriptRepresentation(surrogates[1], out); return; } out.append("\\u") .append(HEX_CHARS[(codePoint >>> 12) & 0xf]) .append(HEX_CHARS[(codePoint >>> 8) & 0xf]) .append(HEX_CHARS[(codePoint >>> 4) & 0xf]) .append(HEX_CHARS[codePoint & 0xf]); } } public static class MalformedJsonException extends RuntimeException { private static final long serialVersionUID = 1L; public MalformedJsonException(String msg) { super(msg); } } private static class Reader { private static final Object OBJECT_END = new String("}"); private static final Object ARRAY_END = new String("]"); private static final Object OBJECT_START = new String("{"); private static final Object ARRAY_START = new String("["); private static final Object COLON = new String(":"); private static final Object COMMA = new String(","); private static final HashSet PUNCTUATION = new HashSet( Arrays.asList(OBJECT_END, OBJECT_START, ARRAY_END, ARRAY_START, COLON, COMMA)); public static final int FIRST = 0; public static final int CURRENT = 1; public static final int NEXT = 2; private static Map escapes = new HashMap(); static { escapes.put(new Character('"'), new Character('"')); escapes.put(new Character('\\'), new Character('\\')); escapes.put(new Character('/'), new Character('/')); escapes.put(new Character('b'), new Character('\b')); escapes.put(new Character('f'), new Character('\f')); escapes.put(new Character('n'), new Character('\n')); escapes.put(new Character('r'), new Character('\r')); escapes.put(new Character('t'), new Character('\t')); } private CharacterIterator it; private char c; private Object token; private StringBuffer buf = new StringBuffer(); private char next() { if (it.getIndex() == it.getEndIndex()) throw new MalformedJsonException("Reached end of input at the " + it.getIndex() + "th character."); c = it.next(); return c; } private char previous() { c = it.previous(); return c; } private void skipWhiteSpace() { do { if (Character.isWhitespace(c)) ; else if (c == '/') { next(); if (c == '*') { // skip multiline comments while (c != CharacterIterator.DONE) if (next() == '*' && next() == '/') break; if (c == CharacterIterator.DONE) throw new MalformedJsonException("Unterminated comment while parsing JSON string."); } else if (c == '/') while (c != '\n' && c != CharacterIterator.DONE) next(); else { previous(); break; } } else break; } while (next() != CharacterIterator.DONE); } public Object read(CharacterIterator ci, int start) { it = ci; switch (start) { case FIRST: c = it.first(); break; case CURRENT: c = it.current(); break; case NEXT: c = it.next(); break; } return read(); } public Object read(CharacterIterator it) { return read(it, NEXT); } public Object read(String string) { return read(new StringCharacterIterator(string), FIRST); } private void expected(Object expectedToken, Object actual) { if (expectedToken != actual) throw new MalformedJsonException("Expected " + expectedToken + ", but got " + actual + " instead"); } @SuppressWarnings("unchecked") private T read() { skipWhiteSpace(); char ch = c; next(); switch (ch) { case '"': token = readString(); break; case '[': token = readArray(); break; case ']': token = ARRAY_END; break; case ',': token = COMMA; break; case '{': token = readObject(); break; case '}': token = OBJECT_END; break; case ':': token = COLON; break; case 't': if (c != 'r' || next() != 'u' || next() != 'e') throw new MalformedJsonException("Invalid JSON token: expected 'true' keyword."); next(); token = factory().bool(Boolean.TRUE); break; case'f': if (c != 'a' || next() != 'l' || next() != 's' || next() != 'e') throw new MalformedJsonException("Invalid JSON token: expected 'false' keyword."); next(); token = factory().bool(Boolean.FALSE); break; case 'n': if (c != 'u' || next() != 'l' || next() != 'l') throw new MalformedJsonException("Invalid JSON token: expected 'null' keyword."); next(); token = nil(); break; default: c = it.previous(); if (Character.isDigit(c) || c == '-') { token = readNumber(); } else throw new MalformedJsonException("Invalid JSON near position: " + it.getIndex()); } return (T)token; } private String readObjectKey() { Object key = read(); if (key == null) throw new MalformedJsonException("Missing object key (don't forget to put quotes!)."); else if (key == OBJECT_END) return null; else if (PUNCTUATION.contains(key)) throw new MalformedJsonException("Missing object key, found: " + key); else return ((Json)key).asString(); } private Json readObject() { Json ret = object(); String key = readObjectKey(); while (token != OBJECT_END) { expected(COLON, read()); // should be a colon if (token != OBJECT_END) { Json value = read(); ret.set(key, value); if (read() == COMMA) { key = readObjectKey(); if (key == null || PUNCTUATION.contains(key)) throw new MalformedJsonException("Expected a property name, but found: " + key); } else expected(OBJECT_END, token); } } return ret; } private Json readArray() { Json ret = array(); Object value = read(); while (token != ARRAY_END) { if (PUNCTUATION.contains(value)) throw new MalformedJsonException("Expected array element, but found: " + value); ret.add((Json)value); if (read() == COMMA) { value = read(); if (value == ARRAY_END) throw new MalformedJsonException("Expected array element, but found end of array after command."); } else expected(ARRAY_END, token); } return ret; } private Json readNumber() { int length = 0; boolean isFloatingPoint = false; buf.setLength(0); if (c == '-') { add(); } length += addDigits(); if (c == '.') { add(); length += addDigits(); isFloatingPoint = true; } if (c == 'e' || c == 'E') { add(); if (c == '+' || c == '-') { add(); } addDigits(); isFloatingPoint = true; } String s = buf.toString(); Number n = isFloatingPoint ? (length < 17) ? Double.valueOf(s) : new BigDecimal(s) : (length < 20) ? Long.valueOf(s) : new BigInteger(s); return factory().number(n); } private int addDigits() { int ret; for (ret = 0; Character.isDigit(c); ++ret) { add(); } return ret; } private Json readString() { buf.setLength(0); while (c != '"') { if (c == '\\') { next(); if (c == 'u') { add(unicode()); } else { Object value = escapes.get(new Character(c)); if (value != null) { add(((Character) value).charValue()); } } } else { add(); } } next(); return factory().string(buf.toString()); } private void add(char cc) { buf.append(cc); next(); } private void add() { add(c); } private char unicode() { int value = 0; for (int i = 0; i < 4; ++i) { switch (next()) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': value = (value << 4) + c - '0'; break; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': value = (value << 4) + (c - 'a') + 10; break; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': value = (value << 4) + (c - 'A') + 10; break; } } return (char) value; } } // END Reader public static void main(String []argv) { try { URI assetUri = new URI("https://raw.githubusercontent.com/pudo/aleph/master/aleph/schema/entity/asset.json"); URI schemaRoot = new URI("https://raw.githubusercontent.com/pudo/aleph/master/aleph/schema/"); // This fails Json.schema(assetUri); // And so does this Json asset = Json.read(assetUri.toURL()); Json.schema(asset, schemaRoot); } catch (Throwable t) { t.printStackTrace(); } } }