Java tutorial
/** * Copyright (c) 2011, Jilles van Gurp * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.github.jsonj; import static com.github.jsonj.tools.JsonBuilder.fromObject; import static com.github.jsonj.tools.JsonBuilder.nullValue; import static com.github.jsonj.tools.JsonBuilder.primitive; import com.github.jsonj.exceptions.JsonTypeMismatchException; import com.github.jsonj.tools.JsonBuilder; import com.github.jsonj.tools.JsonParser; import com.github.jsonj.tools.JsonSerializer; import com.jillesvangurp.efficientstring.EfficientString; import java.io.IOException; import java.io.Writer; import java.lang.reflect.Field; import java.nio.charset.Charset; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.stream.Stream; import org.apache.commons.lang.Validate; /** * Representation of json objects. This class extends LinkedHashMap and may be used as such. In addition a lot of * convenience is provided in the form of methods you are likely to need when working with json objects * programmatically. */ public class JsonObject implements Map<String, JsonElement>, JsonElement { private static final Charset UTF8 = Charset.forName("UTF-8"); private static final long serialVersionUID = 497820087656073803L; // use during serialization private static JsonParser parser = null; // private final LinkedHashMap<EfficientString, JsonElement> map = new LinkedHashMap<EfficientString, // JsonElement>(); // private final Map<EfficientString, JsonElement> map = new SimpleMap<>(); private final SimpleIntKeyMap<JsonElement> intMap = new SimpleIntKeyMap<>(); private String idField = null; public JsonObject() { } @SuppressWarnings("rawtypes") public JsonObject(Map existing) { super(); Iterator iterator = existing.entrySet().iterator(); while (iterator.hasNext()) { Entry entry = (Entry) iterator.next(); put(entry.getKey().toString(), fromObject(entry.getValue())); } } @Override public JsonType type() { return JsonType.object; } /** * By default, the hash code is calculated recursively, which can be rather expensive. Calling this method allows * you to specify a special field that will be used for calculating this object's hashcode. In case the field value * is null it will fall back to recursive behavior. * * @param fieldName * name of the field value that should be used for calculating the hash code */ public void useIdHashCodeStrategy(String fieldName) { idField = fieldName.intern(); } @Override public JsonObject asObject() { return this; } @Override public JsonArray asArray() { throw new JsonTypeMismatchException("not an array"); } @Override public JsonSet asSet() { throw new JsonTypeMismatchException("not an array"); } @Override public JsonPrimitive asPrimitive() { throw new JsonTypeMismatchException("not a primitive"); } @Override public float asFloat() { throw new JsonTypeMismatchException("not a primitive"); } @Override public double asDouble() { throw new JsonTypeMismatchException("not a primitive"); } @Override public int asInt() { throw new JsonTypeMismatchException("not a primitive"); } @Override public long asLong() { throw new JsonTypeMismatchException("not a primitive"); } @Override public boolean asBoolean() { throw new JsonTypeMismatchException("not a primitive"); } @Override public String asString() { throw new JsonTypeMismatchException("not a primitive"); } @Override public String toString() { return JsonSerializer.serialize(this, false); } @Override public void serialize(Writer w) throws IOException { w.append(JsonSerializer.OPEN_BRACE); Iterator<Entry<Integer, JsonElement>> iterator = intMap.entrySet().iterator(); while (iterator.hasNext()) { Entry<Integer, JsonElement> entry = iterator.next(); EfficientString key = EfficientString.get(entry.getKey()); JsonElement value = entry.getValue(); w.append(JsonSerializer.QUOTE); w.append(JsonSerializer.jsonEscape(key.toString())); w.append(JsonSerializer.QUOTE); w.append(JsonSerializer.COLON); value.serialize(w); if (iterator.hasNext()) { w.append(JsonSerializer.COMMA); } } w.append(JsonSerializer.CLOSE_BRACE); } @Override public String prettyPrint() { return JsonSerializer.serialize(this, true); } @Override public boolean isObject() { return true; } @Override public boolean isArray() { return false; } @Override public boolean isPrimitive() { return false; } /** * Variant of put that can take a Object instead of a primitive. The normal put inherited from LinkedHashMap only * takes JsonElement instances. * * @param key * label * @param value * any object that is accepted by the JsonPrimitive constructor. * @return the JsonElement that was added. * @throws JsonTypeMismatchException * if the value cannot be turned into a primitive. */ public JsonElement put(String key, Object value) { return put(key, primitive(value)); } @Override public JsonElement put(String key, JsonElement value) { Validate.notNull(key); if (value == null) { value = nullValue(); } return intMap.put(EfficientString.fromString(key).index(), value); } public JsonElement put(String key, JsonBuilder value) { return put(key, value.get()); } @Override public void putAll(Map<? extends String, ? extends JsonElement> m) { for (Entry<? extends String, ? extends JsonElement> e : m.entrySet()) { put(e.getKey(), e.getValue()); } } /** * Add multiple fields to the object. * * @param es * field entries */ public void add(@SuppressWarnings("unchecked") Entry<String, JsonElement>... es) { for (Map.Entry<String, JsonElement> e : es) { put(e.getKey(), e.getValue()); } } /** * Allows you to get the nth entry in the JsonObject. Please note that this method iterates over all the entries * until it finds the nth, so getting the last element is probably going to be somewhat expensive, depending on the * size of the collection. Also note that the entries in JsonObject are ordered by the order of insertion (it is a * LinkedHashMap). * * @param index * index of the entry * @return the nth entry in the JsonObject. */ public Entry<String, JsonElement> get(int index) { if (index >= size()) { throw new IllegalArgumentException("index out of range"); } else { int i = 0; for (Entry<String, JsonElement> e : entrySet()) { if (i++ == index) { return e; } } } return null; } /** * @return the first entry in the object. */ public Entry<String, JsonElement> first() { return get(0); } @Override public JsonElement get(Object key) { if (key != null && key instanceof String) { return intMap.get(EfficientString.fromString(key.toString()).index()); } else { throw new IllegalArgumentException(); } } /** * Get a json element at a particular path in an object structure. * * @param labels * list of field names that describe the location to a particular json node. * @return a json element at a particular path in an object or null if it can't be found. */ public JsonElement get(final String... labels) { JsonElement e = this; int n = 0; for (String label : labels) { e = e.asObject().get(label); if (e == null) { return null; } if (n == labels.length - 1) { return e; } if (!e.isObject()) { break; } n++; } return null; } /** * Get a value at a particular path in an object structure. * * @param labels * one or more text labels * @return value or null if it doesn't exist at the specified path */ public String getString(final String... labels) { JsonElement jsonElement = get(labels); if (jsonElement == null || jsonElement.isNull()) { return null; } else { return jsonElement.asString(); } } /** * Get a value at a particular path in an object structure. * * @param labels * one or more text labels * @return value or null if it doesn't exist at the specified path */ public Boolean getBoolean(final String... labels) { JsonElement jsonElement = get(labels); if (jsonElement == null || jsonElement.isNull()) { return null; } else { if (jsonElement.isBoolean()) { return jsonElement.asBoolean(); } else if (jsonElement.isNumber()) { return jsonElement.asInt() > 0; } else if (jsonElement.isPrimitive()) { return Boolean.valueOf(jsonElement.asString()); } else { throw new JsonTypeMismatchException("expected primitive value but was " + jsonElement.type()); } } } /** * @param field * name of the field * @param defaultValue * default value that is returned if the field has no value * @return value of the field as a boolean */ public boolean get(final String field, boolean defaultValue) { JsonElement e = get(field); if (e == null) { return defaultValue; } else { if (e.isBoolean()) { return e.asBoolean(); } else if (e.isNumber()) { return e.asInt() > 0; } else if (e.isPrimitive()) { return Boolean.valueOf(e.asString()); } else { throw new JsonTypeMismatchException("expected primitive value but was " + e.type()); } } } /** * Get a value at a particular path in an object structure. * * @param labels * one or more text labels * @return value or null if it doesn't exist at the specified path */ public Integer getInt(final String... labels) { JsonElement jsonElement = get(labels); if (jsonElement == null || jsonElement.isNull()) { return null; } else { return jsonElement.asInt(); } } /** * @param field * name of the field * @param defaultValue * default value that is returned if the field has no value * @return value of the field as an int */ public int get(final String field, int defaultValue) { JsonElement e = get(field); if (e == null) { return defaultValue; } else { return e.asInt(); } } /** * Get a value at a particular path in an object structure. * * @param labels * one or more text labels * @return value or null if it doesn't exist at the specified path */ public Long getLong(final String... labels) { JsonElement jsonElement = get(labels); if (jsonElement == null || jsonElement.isNull()) { return null; } else { return jsonElement.asLong(); } } /** * @param field * name of the field * @param defaultValue * default value that is returned if the field has no value * @return value of the field as a long */ public long get(final String field, long defaultValue) { JsonElement e = get(field); if (e == null) { return defaultValue; } else { return e.asLong(); } } /** * Get a value at a particular path in an object structure. * * @param labels * one or more text labels * @return value or null if it doesn't exist at the specified path */ public Float getFloat(final String... labels) { JsonElement jsonElement = get(labels); if (jsonElement == null || jsonElement.isNull()) { return null; } else { return jsonElement.asFloat(); } } /** * @param field * name of the field * @param defaultValue * default value that is returned if the field has no value * @return value of the field as a float */ public float get(final String field, float defaultValue) { JsonElement e = get(field); if (e == null) { return defaultValue; } else { return e.asFloat(); } } /** * Get a value at a particular path in an object structure. * * @param labels * one or more text labels * @return value or null if it doesn't exist at the specified path */ public Double getDouble(final String... labels) { JsonElement jsonElement = get(labels); if (jsonElement == null || jsonElement.isNull()) { return null; } else { return jsonElement.asDouble(); } } /** * @param field * name of the field * @param defaultValue * default value that is returned if the field has no value * @return value of the field as a double */ public double get(final String field, double defaultValue) { JsonElement e = get(field); if (e == null) { return defaultValue; } else { return e.asDouble(); } } /** * Get a JsonObject at a particular path in an object structure. * * @param labels * one or more text labels * @return value or null if it doesn't exist at the specified path */ public JsonObject getObject(final String... labels) { JsonElement jsonElement = get(labels); if (jsonElement == null || jsonElement.isNull()) { return null; } else { return jsonElement.asObject(); } } /** * Get a JsonArray at a particular path in an object structure. * * @param labels * one or more text labels * @return value or null if it doesn't exist at the specified path */ public JsonArray getArray(final String... labels) { JsonElement jsonElement = get(labels); if (jsonElement == null || jsonElement.isNull()) { return null; } else { return jsonElement.asArray(); } } /** * Get or create a JsonArray at a particular path in an object structure. Any object on the path will be created as * well if missing. * * @param labels * one or more text labels * @return the created JsonArray * @throws JsonTypeMismatchException * if an element is present at the path that is not a JsonArray */ public JsonArray getOrCreateArray(final String... labels) { JsonObject parent = this; JsonElement decendent = null; int index = 0; for (String label : labels) { decendent = parent.get(label); if (decendent == null && index < labels.length - 1 && parent.isObject()) { decendent = new JsonObject(); parent.put(label, decendent); } else if (index == labels.length - 1) { if (decendent == null) { decendent = new JsonArray(); parent.put(label, decendent); return decendent.asArray(); } else { return decendent.asArray(); } } if (decendent == null) { throw new IllegalStateException("decendant should not be null here"); } parent = decendent.asObject(); index++; } return null; } /** * Extracts or creates and adds a set at the specied path. Any JsonArrays are converted to sets and updated in the * JsonObject as well. * * @param labels * path to the set in the JsonObject * @return the set * @throws JsonTypeMismatchException * if an element is present at the path that is not a JsonArray or JsonSet */ public JsonSet getOrCreateSet(final String... labels) { JsonObject parent = this; JsonElement decendent; int index = 0; for (String label : labels) { decendent = parent.get(label); if (decendent == null && index < labels.length - 1 && parent.isObject()) { decendent = new JsonObject(); parent.put(label, decendent); } else if (index == labels.length - 1) { if (decendent == null) { decendent = new JsonSet(); parent.put(label, decendent); return decendent.asSet(); } else { JsonSet set = decendent.asSet(); if (!(decendent instanceof JsonSet)) { // if it wasn't a set update it parent.put(label, set); } return set; } } if (decendent == null) { throw new IllegalStateException("decendant should not be null here"); } parent = decendent.asObject(); index++; } return null; } /** * Get or create a JsonObject at a particular path in an object structure. Any object on the path will be created as * well if missing. * * @param labels * one or more text labels * @return the created JsonObject * @throws JsonTypeMismatchException * if an element is present at the path that is not a JsonObject */ public JsonObject getOrCreateObject(final String... labels) { JsonObject parent = this; JsonElement decendent; int index = 0; for (String label : labels) { decendent = parent.get(label); if (decendent == null && index < labels.length - 1 && parent.isObject()) { decendent = new JsonObject(); parent.put(label, decendent); } else if (index == labels.length - 1) { if (decendent == null) { decendent = new JsonObject(); parent.put(label, decendent); return decendent.asObject(); } else { return decendent.asObject(); } } if (decendent == null) { throw new IllegalStateException("decendant should not be null here"); } parent = decendent.asObject(); index++; } return null; } @Override public boolean equals(final Object o) { if (o == null) { return false; } if (!(o instanceof JsonObject)) { return false; } JsonObject object = (JsonObject) o; if (object.entrySet().size() != entrySet().size()) { return false; } Set<Entry<String, JsonElement>> es = entrySet(); for (Entry<String, JsonElement> entry : es) { String key = entry.getKey(); JsonElement value = entry.getValue(); if (!value.equals(object.get(key))) { return false; } } return true; } @Override public int hashCode() { if (idField != null) { JsonElement jsonElement = get(idField); if (jsonElement != null) { return jsonElement.hashCode(); } } int hashCode = 23; Set<Entry<String, JsonElement>> entrySet = entrySet(); for (Entry<String, JsonElement> entry : entrySet) { JsonElement value = entry.getValue(); if (value != null) { // skip null entries hashCode = hashCode * entry.getKey().hashCode() * value.hashCode(); } } return hashCode; } @Override public Object clone() { return deepClone(); } @SuppressWarnings("unchecked") @Override public JsonObject deepClone() { JsonObject object = new JsonObject(); Set<java.util.Map.Entry<String, JsonElement>> es = entrySet(); for (Entry<String, JsonElement> entry : es) { JsonElement e = entry.getValue().deepClone(); object.put(entry.getKey(), e); } return object; } @Override public JsonObject immutableClone() { JsonObject object = new JsonObject(); Set<java.util.Map.Entry<String, JsonElement>> es = entrySet(); for (Entry<String, JsonElement> entry : es) { JsonElement e = entry.getValue().immutableClone(); object.put(entry.getKey(), e); } object.intMap.makeImmutable(); return object; } @Override public boolean isEmpty() { boolean empty = true; if (keySet().size() != 0) { for (java.util.Map.Entry<String, JsonElement> entry : entrySet()) { empty = empty && entry.getValue().isEmpty(); if (!empty) { return false; } } } return empty; } @Override public void removeEmpty() { Iterator<java.util.Map.Entry<String, JsonElement>> iterator = entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, JsonElement> entry = iterator.next(); JsonElement element = entry.getValue(); if (element.isEmpty() && !element.isObject()) { iterator.remove(); } else { element.removeEmpty(); } } } @Override public boolean isNumber() { return false; } @Override public boolean isBoolean() { return false; } @Override public boolean isNull() { return false; } @Override public boolean isString() { return false; } @Override public void clear() { intMap.clear(); } @Override public boolean containsKey(Object key) { return get(key) != null; } @Override public boolean containsValue(Object value) { return intMap.containsValue(value); } @Override public Set<Entry<String, JsonElement>> entrySet() { final Set<Entry<Integer, JsonElement>> entrySet = intMap.entrySet(); return new Set<Map.Entry<String, JsonElement>>() { @Override public boolean add(java.util.Map.Entry<String, JsonElement> e) { throw new UnsupportedOperationException("entry set is immutable"); } @Override public boolean addAll(Collection<? extends java.util.Map.Entry<String, JsonElement>> c) { throw new UnsupportedOperationException("entry set is immutable"); } @Override public void clear() { throw new UnsupportedOperationException("entry set is immutable"); } @Override public boolean contains(Object o) { throw new UnsupportedOperationException("not supported"); } @Override public boolean containsAll(Collection<?> c) { throw new UnsupportedOperationException("not supported"); } @Override public boolean isEmpty() { return entrySet.isEmpty(); } @Override public Iterator<Entry<String, JsonElement>> iterator() { return new Iterator<Entry<String, JsonElement>>() { private final Iterator<Entry<Integer, JsonElement>> it = entrySet.iterator(); @Override public boolean hasNext() { return it.hasNext(); } @Override public Entry<String, JsonElement> next() { final Entry<Integer, JsonElement> next = it.next(); return new Entry<String, JsonElement>() { @Override public String getKey() { EfficientString es = EfficientString.get(next.getKey()); return es.toString(); } @Override public JsonElement getValue() { return next.getValue(); } @Override public JsonElement setValue(JsonElement value) { throw new UnsupportedOperationException("immutable entry"); } }; } @Override public void remove() { it.remove(); } }; } @Override public boolean remove(Object o) { throw new UnsupportedOperationException("entry set is immutable"); } @Override public boolean removeAll(Collection<?> c) { throw new UnsupportedOperationException("entry set is immutable"); } @Override public boolean retainAll(Collection<?> c) { throw new UnsupportedOperationException("entry set is immutable"); } @Override public int size() { return entrySet.size(); } @Override public Object[] toArray() { @SuppressWarnings("unchecked") Entry<String, JsonElement>[] result = new Entry[entrySet.size()]; int i = 0; for (final Entry<Integer, JsonElement> e : entrySet) { result[i] = new Entry<String, JsonElement>() { @Override public String getKey() { EfficientString es = EfficientString.get(e.getKey()); return es.toString(); } @Override public JsonElement getValue() { return e.getValue(); } @Override public JsonElement setValue(JsonElement value) { throw new UnsupportedOperationException("immutable"); } }; i++; } return result; } @SuppressWarnings("unchecked") @Override public <T> T[] toArray(T[] a) { return (T[]) toArray(); } }; } @Override public Set<String> keySet() { Set<Integer> keySet = intMap.keySet(); Set<String> keys = new HashSet<String>(); for (Integer idx : keySet) { keys.add(EfficientString.get(idx).toString()); } return keys; } @Override public JsonElement remove(Object key) { if (key != null && key instanceof String) { return intMap.remove(EfficientString.fromString(key.toString()).index()); } else { throw new IllegalArgumentException(); } } @Override public int size() { return intMap.size(); } @Override public Collection<JsonElement> values() { return intMap.values(); } public Stream<JsonElement> map(BiFunction<String, JsonElement, JsonElement> f) { return entrySet().stream().map(e -> f.apply(e.getKey(), e.getValue())); } public void forEachString(BiConsumer<String, String> f) { forEach((k, v) -> { f.accept(k, v.asString()); }); } void writeObject(java.io.ObjectOutputStream out) throws IOException { // when using object serialization, write the json bytes byte[] bytes = toString().getBytes(UTF8); out.writeInt(bytes.length); out.write(bytes); } void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { // when deserializing, parse the json string try { int length = in.readInt(); byte[] buf = new byte[length]; in.readFully(buf); if (parser == null) { // create it lazily, static so won't increase object size parser = new JsonParser(); } JsonElement o = parser.parse(new String(buf, UTF8)); Field f = getClass().getDeclaredField("intMap"); f.setAccessible(true); f.set(this, new SimpleIntKeyMap<>()); for (Entry<String, JsonElement> e : o.asObject().entrySet()) { put(e.getKey(), e.getValue()); } } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { throw new IllegalStateException(e); } } }