/*
 * 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;

/**
 *
 * <p>
 * Represents a JSON (JavaScript Object Notation) entity. For more information about JSON, please see
 * <a href="http://www.json.org" target="_">http://www.json.org</a>.
 * </p>
 *
 * <p>
 * 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 <code>Json</code>
 * 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: <code>Json</code>.
 * </p>
 *
 * <p>
 * One can examine the concrete type of a <code>Json</code> with one of the <code>isXXX</code> methods:
 * {@link #isObject()}, {@link #isArray()},{@link #isNumber()},{@link #isBoolean()},{@link #isString()},
 * {@link #isNull()}.
 * </p>
 *
 * <p>
 * The underlying representation of a given <code>Json</code> instance can be obtained by calling
 * the generic {@link #getValue()} method or one of the <code>asXXX</code> 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 <code>asXXX</code> 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()}.
 * </p>
 *
 * <h3>Constructing and Modifying JSON Structures</h3>
 *
 * <p>
 * There are several static factory methods in this class that allow you to create new
 * <code>Json</code> instances:
 * </p>
 *
 * <table>
 * <tr><td>{@link #read(String)}</td>
 * <td>Parse a JSON string and return the resulting <code>Json</code> instance. The syntax
 * recognized is as defined in <a href="http://www.json.org">http://www.json.org</a>.
 * </td>
 * </tr>
 * <tr><td>{@link #make(Object)}</td>
 * <td>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 <code>Json</code> itself.</td>
 * </tr>
 * <tr><td>{@link #nil()}</td>
 * <td>Return a <code>Json</code> instance representing JSON <code>null</code>.</td>
 * </tr>
 * <tr><td>{@link #object()}</td>
 * <td>Create and return an empty JSON object.</td>
 * </tr>
 * <tr><td>{@link #object(Object...)}</td>
 * <td>Create and return a JSON object populated with the key/value pairs
 * passed as an argument sequence. Each even parameter becomes a key (via
 * <code>toString</code>) and each odd parameter is converted to a <code>Json</code>
 * value.</td>
 * </tr>
 * <tr><td>{@link #array()}</td>
 * <td>Create and return an empty JSON array.</td>
 * </tr>
 * <tr><td>{@link #array(Object...)}</td>
 * <td>Create and return a JSON array from the list of arguments.</td>
 * </tr>
 * </table>
 *
 * <p>
 * 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.
 * </p>
 *
 * <p>
 * If a <code>Json</code> 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 <code>Json</code> 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
 * <code>Json</code> 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.
 * </p>
 *
 * <p>
 * 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.
 * </p>
 *
 * <p>
 * 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.
 * </p>
 *
 * <h3>Navigating JSON Structures</h3>
 *
 * <p>
 * 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.
 * </p>
 *
 * <p>
 * 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) &amp;&amp; at(name).equals(make(value))'.
 * </p>
 *
 * <p>
 * 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.
 * </p>
 *
 * <p>
 * The combination of method chaining when modifying <code>Json</code> 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:
 * </p>
 *
 * <pre><code>
 * {"menu": {
 * "id": "file",
 * "value": "File",
 * "popup": {
 *   "menuitem": [
 *     {"value": "New", "onclick": "CreateNewDoc()"},
 *     {"value": "Open", "onclick": "OpenDoc()"},
 *     {"value": "Close", "onclick": "CloseDoc()"}
 *   ]
 * }
 * "position": 0
 * }}
 * </code></pre>
 *
 * <pre><code>
 * 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();
 * ...
 * </code></pre>
 *
 * <p>
 * If there's no danger of naming conflicts, a static import of the factory methods (<code>
 * import static json.Json.*;</code>) would reduce typing even further and make the code more
 * readable.
 * </p>
 *
 * <h3>Converting to String</h3>
 *
 * <p>
 * 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.
 * </p>
 *
 * <h3>Validating with JSON Schema</h3>
 *
 * <p>
 * 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 <code>Json.Schema</code>
 * using the factory method {@link mjson.Json.Schema} and then call its <code>validate</code> method
 * on a JSON instance:
 * </p>
 *
 * <pre><code>
 * 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);
 * </code></pre>
 * @author Borislav Iordanov
 * @version 2.0.0
 */
public class Json implements java.io.Serializable, Iterable<Json>
{
   private static final long serialVersionUID = 1L;

   /**
    * <p>
    * This interface defines how <code>Json</code> instances are constructed. There is a
    * default implementation for each kind of <code>Json</code> value, but you can provide
    * your own implementation. For example, you might want a different representation of
    * an object than a regular <code>HashMap</code>. Or you might want string comparison to be
    * case insensitive.
    * </p>
    *
    * <p>
    * In addition, the {@link #make(Object)} method allows you plug-in your own mapping
    * of arbitrary Java objects to <code>Json</code> instances. You might want to implement
    * a Java Beans to JSON mapping or any other JSON serialization that makes sense in your
    * project.
    * </p>
    *
    * <p>
    * 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.
    * </p>
    *
    * <p>
    * The factory implementation used by the <code>Json</code> 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.
    * </p>
    *
    * @author Borislav Iordanov
    *
    */
   public static interface Factory
   {
      /**
       * Construct and return an object representing JSON <code>null</code>. Implementations are
       * free to cache a return the same instance. The resulting value must return
       * <code>true</code> from <code>isNull()</code> and <code>null</code> from
       * <code>getValue()</code>.
       *
       * @return The representation of a JSON <code>null</code> value.
       */
      Json nil();

      /**
       * Construct and return a JSON boolean. The resulting value must return
       * <code>true</code> from <code>isBoolean()</code> and the passed
       * in parameter from <code>getValue()</code>.
       * @param value The boolean value.
       * @return A JSON with <code>isBoolean() == true</code>. 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
       * <code>true</code> from <code>isString()</code> and the passed
       * in parameter from <code>getValue()</code>.
       * @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
       * <code>true</code> from <code>isNumber()</code> and the passed
       * in parameter from <code>getValue()</code>.
       *
       * @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
       * <code>true</code> from <code>isObject()</code> and an implementation
       * of <code>java.util.Map</code> from <code>getValue()</code>.
       *
       * @return An empty JSON object.
       */
      Json object();

      /**
       * Construct and return a JSON object. The resulting value must return
       * <code>true</code> from <code>isArray()</code> and an implementation
       * of <code>java.util.List</code> from <code>getValue()</code>.
       *
       * @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 <code>Json</code>
       * instance.
       *
       * @param anything An arbitray Java object from which to construct a <code>Json</code>
       * element.
       * @return The newly constructed <code>Json</code> instance.
       */
      Json make(Object anything);
   }

   public static interface Function<T, R> {

      /**
       * Applies this function to the given argument.
       *
       * @param t the function argument
       * @return the function result
       */
      R apply(T t);
   }

   /**
    * <p>
    * 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.
    * </p>
    *
    *  <p>
    *  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:
    *  <ul>
    *  <li>http://jsonary.com/documentation/json-schema/ -
    *  a very well-written tutorial covering the whole standard</li>
    *  <li>http://spacetelescope.github.io/understanding-json-schema/ -
    *  online book, tutorial (Python/Ruby based)</li>
    *  </ul>
    *  </p>
    * @author Borislav Iordanov
    *
    */
   public static interface Schema
   {
      /**
       * <p>
       * 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 <code>Json.object</code>
       * containing the boolean property <code>ok</code>. When <code>ok</code> is <code>true</code>,
       * the return object contains nothing else. When it is <code>false</code>, the return object
       * contains a property <code>errors</code> which is an array of error messages for all
       * detected schema violations.
       * </p>
       *
       * @param document The input document.
       * @return <code>{"ok":true}</code> or <code>{"ok":false, errors:["msg1", "msg2", ...]}</code>
       */
      Json validate(Json document);

      /**
       * <p>Return the JSON representation of the schema.</p>
       */
      Json toJson();

      /**
       * <p>Possible options are: <code>ignoreDefaults:true|false</code>.
       * </p>
       * @return A newly created <code>Json</code> conforming to this schema.
       */
      //Json generate(Json options);
   }

   @Override
   public Iterator<Json> iterator()
   {
      return new Iterator<Json>()
      {
         @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<String, Json> resolved,
                          Map<Json, Json> expanded,
                          Function<URI, Json> 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);
   }

   /**
    * <p>
    * Replace all JSON references, as per the http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03
    * specification, by their referants.
    * </p>
    * @param json
    * @param duplicate
    * @param done
    * @return
    */
   static Json expandReferences(Json json,
                                Json topdoc,
                                URI base,
                                Map<String, Json> resolved,
                                Map<Json, Json> expanded,
                                Function<URI, Json> 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<String, Json> 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<Json, Json>{}

      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<Instruction> 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<CheckProperty> props = new ArrayList<CheckProperty>();
         ArrayList<CheckPatternProperty> patternProps = new ArrayList<CheckPatternProperty>();

         // 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<String> found)
            {
               Json errors = null;
               for (Map.Entry<String, Json> 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<String> checked = new HashSet<String>();
            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<String, Json> 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<Instruction> seq = new ArrayList<Instruction>();
         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<Instruction> alternates = new ArrayList<Instruction>();
         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<Instruction> alternates = new ArrayList<Instruction>();
         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<Json, Instruction> 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<String, Json> 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<String, Json> 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<Instruction>();
               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<String, Json> 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<URI, Json> relativeReferenceResolver)
      {
         try
         {
            this.uri = uri == null ? new URI("") : uri;
            if (relativeReferenceResolver == null)
               relativeReferenceResolver = new Function<URI, Json>() { 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<String, Json>(),
                  new IdentityHashMap<Json, Json>(),
                  relativeReferenceResolver);
         }
         catch (Exception ex)  { throw new RuntimeException(ex); }
         this.start = compile(this.theschema, new IdentityHashMap<Json, Instruction>());
      }

      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<URI, Json> 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<Factory> threadFactory = new ThreadLocal<Factory>();

   /**
    * <p>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. </p>
    *
    * @return the factory
    */
   public static Factory factory()
   {
      Factory f = threadFactory.get();
      return f != null ? f : globalFactory;
   }

   /**
    * <p>
    * Specify a global Json {@link Factory} to be used by all threads that don't have a
    * specific thread-local factory attached to them.
    * </p>
    *
    * @param factory The new global factory
    */
   public static void setGlobalFactory(Factory factory) { globalFactory = factory; }

   /**
    * <p>
    * 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).
    * </p>
    *
    * @param factory the new thread local factory
    */
   public static void attachFactory(Factory factory) { threadFactory.set(factory); }

   /**
    * <p>
    * 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.
    * </p>
    */
   public static void detachFactory() { threadFactory.remove(); }

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

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

   /**
    * <p>
    * Parse a JSON entity from a {@link CharacterIterator}.
    * </p>
    * @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 <code>null Json</code> instance.
    */
   public static Json nil() { return factory().nil(); }
   /**
    * @return a newly constructed, empty JSON object.
    */
   public static Json object()	{ return factory().object();	}
   /**
    * <p>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
    * <code>toString</code> method. Each value is first converted
    * to a <code>Json</code> instance using the {@link #make(Object)} method.
    * </p>
    * @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(); }

   /**
    * <p>Return a new JSON array filled up with the list of arguments.</p>
    *
    * @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;
   }

   /**
    * <p>
    * Exposes some internal methods that are useful for {@link org.sharegov.mjson.Json.Factory} implementations
    * or other extension/layers of the library.
    * </p>
    *
    * @author Borislav Iordanov
    *
    */
   public static class help
   {
      /**
       * <p>
       * 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.
       * </p>
       */
      public static String escape(String string) { return escaper.escapeJsonString(string); }

      /**
       * <p>
       * Given a JSON Pointer, as per RFC 6901, return the nested JSON value within
       * the <code>element</code> parameter.
       * </p>
       */
      public static Json resolvePointer(String pointer, Json element) { return Json.resolvePointer(pointer, element); }
   }

   static class JsonSingleValueIterator implements Iterator<Json> {
      private boolean retrieved = false;
      @Override
      public boolean hasNext() {
         return !retrieved;
      }

      @Override
      public Json next() {
         retrieved = true;
         return null;
      }

      @Override
      public void remove() {
      }
   }


   /**
    * <p>
    * Convert an arbitrary Java instance to a {@link Json} instance.
    * </p>
    *
    * <p>
    * Maps, Collections and arrays are recursively copied where each of
    * their elements concerted into <code>Json</code> instances as well. The keys
    * of a {@link Map} parameter are normally strings, but anything with a meaningful
    * <code>toString</code> implementation will work as well.
    * </p>
    *
    * @param anything Any Java object that the current JSON factory in effect is capable of handling.
    * @return The <code>Json</code>. This method will never return <code>null</code>. It will
    * throw an {@link IllegalArgumentException} if it doesn't know how to convert the argument
    * to a <code>Json</code> 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; }

   /**
    * <p>Return a string representation of <code>this</code> 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. </p>
    * @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(); }

   /**
    * <p>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.
    * </p>
    *
    * @param enclosing The parent element.
    */
   public void attachTo(Json enclosing) { this.enclosing = enclosing; }

   /**
    * @return the <code>Json</code> entity, if any, enclosing this
    * <code>Json</code>. The returned value can be <code>null</code> or
    * a <code>Json</code> object or list, but not one of the primitive types.
    */
   public final Json up() { return enclosing; }

   /**
    * @return a clone (a duplicate) of this <code>Json</code> 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; }

   /**
    * <p>Return the <code>Json</code> element at the specified index of this
    * <code>Json</code> array. This method applies only to Json arrays.
    * </p>
    *
    * @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(); }

   /**
    * <p>
    * Return the specified property of a <code>Json</code> object or <code>null</code>
    * if there's no such property. This method applies only to Json objects.
    * </p>
    * @param The property name.
    * @return The JSON element that is the value of that property.
    */
   public Json at(String property)	{ throw new UnsupportedOperationException(); }

   /**
    * <p>
    * Return the specified property of a <code>Json</code> object if it exists.
    * If it doesn't, then create a new property with value the <code>def</code>
    * parameter and return that parameter.
    * </p>
    *
    * @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;
   }

   /**
    * <p>
    * Return the specified property of a <code>Json</code> object if it exists.
    * If it doesn't, then create a new property with value the <code>def</code>
    * parameter and return that parameter.
    * </p>
    *
    * @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));
   }

   /**
    * <p>
    * Return true if this <code>Json</code> object has the specified property
    * and false otherwise.
    * </p>
    *
    * @param property The name of the property.
    */
   public boolean has(String property)	{ throw new UnsupportedOperationException(); }

   /**
    * <p>
    * Return <code>true</code> if and only if this <code>Json</code> object has a property with
    * the specified value. In particular, if the object has no such property <code>false</code> is returned.
    * </p>
    *
    * @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 <code>Json</code>, it is first converted to
    * such an instance.
    * @return
    */
   public boolean is(String property, Object value) { throw new UnsupportedOperationException(); }

   /**
    * <p>
    * Return <code>true</code> if and only if this <code>Json</code> array has an element with
    * the specified value at the specified index. In particular, if the array has no element at
    * this index, <code>false</code> is returned.
    * </p>
    *
    * @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 <code>Json</code>, it is first converted to
    * such an instance.
    * @return
    */
   public boolean is(int index, Object value) { throw new UnsupportedOperationException(); }

   /**
    * <p>
    * Add the specified <code>Json</code> element to this array.
    * </p>
    *
    * @return this
    */
   public Json add(Json el) { throw new UnsupportedOperationException(); }

   /**
    * <p>
    * Add an arbitrary Java object to this <code>Json</code> array. The object
    * is first converted to a <code>Json</code> instance by calling the static
    * {@link #make} method.
    * </p>
    *
    * @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)); }

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

   /**
    * <p>
    * Remove the element at the specified index from a <code>Json</code> array and return
    * that element.
    * </p>
    *
    * @param index The index of the element to delete.
    * @return The element value.
    */
   public Json atDel(int index)	{ throw new UnsupportedOperationException(); }

   /**
    * <p>
    * Delete the specified property from a <code>Json</code> object.
    * </p>
    *
    * @param property The property to be removed.
    * @return this
    */
   public Json delAt(String property)	{ throw new UnsupportedOperationException(); }

   /**
    * <p>
    * Remove the element at the specified index from a <code>Json</code> array.
    * </p>
    *
    * @param index The index of the element to delete.
    * @return this
    */
   public Json delAt(int index) { throw new UnsupportedOperationException(); }

   /**
    * <p>
    * Remove the specified element from a <code>Json</code> array.
    * </p>
    *
    * @param el The element to delete.
    * @return this
    */
   public Json remove(Json el)	{ throw new UnsupportedOperationException(); }

   /**
    * <p>
    * Remove the specified Java object (converted to a Json instance)
    * from a <code>Json</code> array. This is equivalent to
    * <code>remove({@link #make(Object)})</code>.
    * </p>
    *
    * @param anything The object to delete.
    * @return this
    */
   public final Json remove(Object anything) { return remove(make(anything)); }

   /**
    * <p>
    * Set a <code>Json</code> objects's property.
    * </p>
    *
    * @param property The property name.
    * @param value The value of the property.
    * @return this
    */
   public Json set(String property, Json value) { throw new UnsupportedOperationException();	}

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

   /**
    * <p>
    * Change the value of a JSON array element. This must be an array.
    * </p>
    * @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(); }

   /**
    * <p>
    * Combine this object or array with the passed in object or array. The types of
    * <code>this</code> and the <code>object</code> argument must match. If both are
    * <code>Json</code> objects, all properties of the parameter are added to <code>this</code>.
    * If both are arrays, all elements of the parameter are appended to <code>this</code>
    * </p>
    * @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 <code>{}@link #with(Json,Json...options)}</code> with each option
    * argument converted to <code>Json</code> 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 <code>Json</code> 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 <code>Json</code> 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 <code>Json</code> 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 <code>Json</code> 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 <code>Json</code> 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 <code>Json</code> 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 <code>Json</code> 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 <code>Json</code> 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 <code>Json</code> 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 <code>Json</code> 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 <code>Json</code> 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
    * <code>Json</code> object.
    */
   public Map<String, Object> asMap() { throw new UnsupportedOperationException(); }

   /**
    * @return the underlying map of properties of a <code>Json</code> object. The returned
    * map is the actual object representation so any modifications to it are modifications
    * of the <code>Json</code> object itself. Call
    * {@link #isObject()} first if you're not sure this instance is indeed a
    * <code>Json</code> object.
    */
   public Map<String, Json> asJsonMap() { throw new UnsupportedOperationException(); }

   /**
    * @return a list of the elements of a <code>Json</code> 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
    * <code>Json</code> array.
    */
   public List<Object> asList()  { throw new UnsupportedOperationException(); }

   /**
    * @return the underlying {@link List} representation of a <code>Json</code> array.
    * The returned list is the actual array representation so any modifications to it
    * are modifications of the <code>Json</code> array itself. Call
    * {@link #isArray()} first if you're not sure this instance is indeed a
    * <code>Json</code> array.
    */
   public List<Json> asJsonList() { throw new UnsupportedOperationException(); }

   /**
    * @return <code>true</code> if this is a <code>Json</code> null entity
    * and <code>false</code> otherwise.
    */
   public boolean isNull() { return false; }
   /**
    * @return <code>true</code> if this is a <code>Json</code> string entity
    * and <code>false</code> otherwise.
    */
   public boolean isString() { return false; }
   /**
    * @return <code>true</code> if this is a <code>Json</code> number entity
    * and <code>false</code> otherwise.
    */
   public boolean isNumber() { return false; }
   /**
    * @return <code>true</code> if this is a <code>Json</code> boolean entity
    * and <code>false</code> otherwise.
    */
   public boolean isBoolean() { return false;	}
   /**
    * @return <code>true</code> if this is a <code>Json</code> array (i.e. list) entity
    * and <code>false</code> otherwise.
    */
   public boolean isArray() { return false; }
   /**
    * @return <code>true</code> if this is a <code>Json</code> object entity
    * and <code>false</code> otherwise.
    */
   public boolean isObject(){ return false; }
   /**
    * @return <code>true</code> if this is a <code>Json</code> primitive entity
    * (one of string, number or boolean) and <code>false</code> otherwise.
    *
    */
   public boolean isPrimitive() { return isString() || isNumber() || isBoolean(); }

   /**
    * <p>
    * Json-pad this object as an argument to a callback function.
    * </p>
    *
    * @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 <code>callback</code>
    * 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<Object> asList() { return (List<Object>)Collections.singletonList(null); }

      public int hashCode() { return 0; }
      public boolean equals(Object x)
      {
         return x instanceof NullJson;
      }

      @Override
      public Iterator<Json> iterator() {
         return new JsonSingleValueIterator() {
            @Override
            public Json next() {
               super.next();
               return NullJson.this;
            }
         };
      }

   }

   /**
    * <p>
    * Set the parent (i.e. enclosing element) of Json element.
    * </p>
    *
    * @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;
      }
   }

   /**
    * <p>
    * Remove/unset the parent (i.e. enclosing element) of Json element.
    * </p>
    *
    * @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<Object> asList() { return (List<Object>)(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<Json> 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<Object> asList() { return (List<Object>)(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<Json> 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<Object> asList() { return (List<Object>)(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<Json> 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<Json> L = new ArrayList<Json>();

      ArrayJson() { }
      ArrayJson(Json e) { super(e); }

      @Override
      public Iterator<Json> 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<Json> asJsonList() { return L; }
      public List<Object> asList()
      {
         ArrayList<Object> A = new ArrayList<Object>();
         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<Json, Json>());
      }

      String toStringImpl(int maxCharacters, Map<Json, Json> done)
      {
         StringBuilder sb = new StringBuilder("[");
         for (Iterator<Json> 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<String, Json> object = new HashMap<String, Json>();

      @Override
      public Iterator<Json> iterator() {
         return object.values().iterator();
      }

      ObjectJson() { }
      ObjectJson(Json e) { super(e); }

      public Json dup()
      {
         ObjectJson j = new ObjectJson();
         for (Map.Entry<String, Json> 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<String, Json> 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<String, Json> e : other.asJsonMap().entrySet())
               set(e.getKey(), e.getValue().dup());
         else
            for (Map.Entry<String, Json> 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<String, Json> 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<String, Object> asMap()
      {
         HashMap<String, Object> m = new HashMap<String, Object>();
         for (Map.Entry<String, Json> e : object.entrySet())
            m.put(e.getKey(), e.getValue().getValue());
         return m;
      }
      @Override
      public Map<String, Json> asJsonMap() { return object; }

      public String toString()
      {
         return toString(Integer.MAX_VALUE);
      }

      public String toString(int maxCharacters)
      {
         return toStringImpl(maxCharacters, new IdentityHashMap<Json, Json>());
      }

      String toStringImpl(int maxCharacters, Map<Json, Json> done)
      {
         StringBuilder sb = new StringBuilder("{");
         if (done.containsKey(this))
            return sb.append("...}").toString();
         done.put(this, this);
         for (Iterator<Map.Entry<String, Json>> i = object.entrySet().iterator(); i.hasNext(); )
         {
            Map.Entry<String, Json> 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.
    *
    * <p>This class contains a single method to escape a passed in string value:
    * <pre>
    *   String jsonStringValue = "beforeQuote\"afterQuote";
    *   String escapedValue = Escaper.escapeJsonString(jsonStringValue);
    * </pre></p>
    *
    * @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<Character> JS_ESCAPE_CHARS;
      private static final Set<Character> HTML_ESCAPE_CHARS;

      static {
         Set<Character> mandatoryEscapeSet = new HashSet<Character>();
         mandatoryEscapeSet.add('"');
         mandatoryEscapeSet.add('\\');
         JS_ESCAPE_CHARS = Collections.unmodifiableSet(mandatoryEscapeSet);

         Set<Character> htmlEscapeSet = new HashSet<Character>();
         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<Object> PUNCTUATION = new HashSet<Object>(
            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<Character, Character> escapes = new HashMap<Character, Character>();
      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> 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();
      }
   }
}