diff --git a/CREDITS.txt b/CREDITS.txt index e036d93..1f6e6d6 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -46,3 +46,12 @@ https://github.com/marc4j/marc4j/tree/master/test/resources and were donated by libraries for testing purpose. +----------------- + +The JSON reader/writer classes are derived work from + +https://github.com/ralfstx/minimal-json + +The original work is based on the MIT License + +----------------- diff --git a/build.gradle b/build.gradle index 50160ab..1c775a0 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,9 @@ dependencies { testImplementation("com.github.stefanbirkner:system-rules:${project.property('system-rules.version')}") { exclude module: 'junit' } + testImplementation("org.mockito:mockito-core:${project.property('mockito.version')}") { + exclude group: 'org.hamcrest' + } } compileJava { diff --git a/gradle.properties b/gradle.properties index dced48b..5914d1c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,3 +14,4 @@ junit4.version = 4.12 xalan.version = 2.7.2 xmlunit-matchers.version = 2.6.3 system-rules.version = 1.19.0 +mockito.version = 3.1.0 diff --git a/src/main/java/org/xbib/marc/json/Json.java b/src/main/java/org/xbib/marc/json/Json.java new file mode 100755 index 0000000..3058ef1 --- /dev/null +++ b/src/main/java/org/xbib/marc/json/Json.java @@ -0,0 +1,280 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.Objects; + +/** + * This class serves as the entry point to the JSON API. + *

+ * To parse a given JSON input, use the parse() methods like in this + * example: + *

+ *
+ * JsonObject object = Json.parse(string).asObject();
+ * 
+ *

+ * To create a JSON data structure to be serialized, use the methods + * value(), array(), and object(). For example, the following + * snippet will produce the JSON string {"foo": 23, "bar": true}: + *

+ *
+ * String string = Json.object().add("foo", 23).add("bar", true).toString();
+ * 
+ *

+ * To create a JSON array from a given Java array, you can use one of the array() + * methods with varargs parameters: + *

+ *
+ * String[] names = ...
+ * JsonArray array = Json.array(names);
+ * 
+ */ +public final class Json { + + private Json() { + // not meant to be instantiated + } + + /** + * Returns a JsonValue instance that represents the given int value. + * + * @param value the value to get a JSON representation for + * @return a JSON value that represents the given value + */ + public static JsonValue of(int value) { + return new JsonNumber(Integer.toString(value, 10)); + } + + /** + * Returns a JsonValue instance that represents the given long value. + * + * @param value the value to get a JSON representation for + * @return a JSON value that represents the given value + */ + public static JsonValue of(long value) { + return new JsonNumber(Long.toString(value, 10)); + } + + /** + * Returns a JsonValue instance that represents the given float value. + * + * @param value the value to get a JSON representation for + * @return a JSON value that represents the given value + */ + public static JsonValue of(float value) { + if (Float.isInfinite(value) || Float.isNaN(value)) { + throw new IllegalArgumentException("Infinite and NaN values not permitted in JSON"); + } + return new JsonNumber(cutOffPointZero(Float.toString(value))); + } + + /** + * Returns a JsonValue instance that represents the given double value. + * + * @param value the value to get a JSON representation for + * @return a JSON value that represents the given value + */ + public static JsonValue of(double value) { + if (Double.isInfinite(value) || Double.isNaN(value)) { + throw new IllegalArgumentException("Infinite and NaN values not permitted in JSON"); + } + return new JsonNumber(cutOffPointZero(Double.toString(value))); + } + + /** + * Returns a JsonValue instance that represents the given string. + * + * @param string the string to get a JSON representation for + * @return a JSON value that represents the given string + */ + public static JsonValue of(String string) { + return string == null ? JsonLiteral.NULL : new JsonString(string); + } + + /** + * Returns a JsonValue instance that represents the given boolean value. + * + * @param value the value to get a JSON representation for + * @return a JSON value that represents the given value + */ + public static JsonValue of(boolean value) { + return value ? JsonLiteral.TRUE : JsonLiteral.FALSE; + } + + /** + * Creates a new empty JsonArray. This is equivalent to creating a new JsonArray using the + * constructor. + * + * @return a new empty JSON array + */ + public static JsonArray array() { + return new JsonArray(); + } + + /** + * Creates a new JsonArray that contains the JSON representations of the given int + * values. + * + * @param values the values to be included in the new JSON array + * @return a new JSON array that contains the given values + */ + public static JsonArray array(int... values) { + Objects.requireNonNull(values); + JsonArray array = new JsonArray(); + for (int value : values) { + array.add(value); + } + return array; + } + + /** + * Creates a new JsonArray that contains the JSON representations of the given long + * values. + * + * @param values the values to be included in the new JSON array + * @return a new JSON array that contains the given values + */ + public static JsonArray array(long... values) { + Objects.requireNonNull(values); + JsonArray array = new JsonArray(); + for (long value : values) { + array.add(value); + } + return array; + } + + /** + * Creates a new JsonArray that contains the JSON representations of the given float + * values. + * + * @param values the values to be included in the new JSON array + * @return a new JSON array that contains the given values + */ + public static JsonArray array(float... values) { + Objects.requireNonNull(values); + JsonArray array = new JsonArray(); + for (float value : values) { + array.add(value); + } + return array; + } + + /** + * Creates a new JsonArray that contains the JSON representations of the given double + * values. + * + * @param values the values to be included in the new JSON array + * @return a new JSON array that contains the given values + */ + public static JsonArray array(double... values) { + Objects.requireNonNull(values); + JsonArray array = new JsonArray(); + for (double value : values) { + array.add(value); + } + return array; + } + + /** + * Creates a new JsonArray that contains the JSON representations of the given + * boolean values. + * + * @param values the values to be included in the new JSON array + * @return a new JSON array that contains the given values + */ + public static JsonArray array(boolean... values) { + Objects.requireNonNull(values); + JsonArray array = new JsonArray(); + for (boolean value : values) { + array.add(value); + } + return array; + } + + /** + * Creates a new JsonArray that contains the JSON representations of the given strings. + * + * @param strings the strings to be included in the new JSON array + * @return a new JSON array that contains the given strings + */ + public static JsonArray array(String... strings) { + Objects.requireNonNull(strings); + JsonArray array = new JsonArray(); + for (String value : strings) { + array.add(value); + } + return array; + } + + /** + * Creates a new empty JsonObject. This is equivalent to creating a new JsonObject using the + * constructor. + * + * @return a new empty JSON object + */ + public static JsonObject object() { + return new JsonObject(); + } + + /** + * Parses the given input string as JSON. The input must contain a valid JSON value, optionally + * padded with whitespace. + * + * @param string the input string, must be valid JSON + * @return a value that represents the parsed JSON + * @throws IOException if the input is not valid JSON + */ + public static JsonValue parse(String string) throws IOException { + Objects.requireNonNull(string); + JsonDefaultHandler handler = new JsonDefaultHandler(); + new JsonReader<>(new StringReader(string), handler).parse(); + return handler.getValue(); + } + + /** + * Reads the entire input from the given reader and parses it as JSON. The input must contain a + * valid JSON value, optionally padded with whitespace. + *

+ * Characters are read in chunks into an input buffer. Hence, wrapping a reader in an additional + * BufferedReader likely won't improve reading performance. + *

+ * + * @param reader the reader to read the JSON value from + * @return a value that represents the parsed JSON + * @throws IOException if an I/O error occurs in the reader + * @throws JsonException if the input is not valid JSON + */ + public static JsonValue parse(Reader reader) throws IOException { + JsonDefaultHandler handler = new JsonDefaultHandler(); + try (reader) { + new JsonReader<>(reader, handler).parse(); + } + return handler.getValue(); + } + + private static String cutOffPointZero(String string) { + if (string.endsWith(".0")) { + return string.substring(0, string.length() - 2); + } + return string; + } + +} diff --git a/src/main/java/org/xbib/marc/json/JsonArray.java b/src/main/java/org/xbib/marc/json/JsonArray.java new file mode 100755 index 0000000..7b26a3c --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonArray.java @@ -0,0 +1,398 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * Represents a JSON array, an ordered collection of JSON values. + *

+ * Elements can be added using the add(...) methods which accept instances of + * {@link JsonValue}, strings, primitive numbers, and boolean values. To replace an element of an + * array, use the set(int, ...) methods. + *

+ *

+ * Elements can be accessed by their index using {@link #get(int)}. This class also supports + * iterating over the elements in document order using an {@link #iterator()} or an enhanced for + * loop: + *

+ *
+ * for (JsonValue value : jsonArray) {
+ *   ...
+ * }
+ * 
+ *

+ * An equivalent {@link List} can be obtained from the method {@link #values()}. + *

+ *

+ * Note that this class is not thread-safe. If multiple threads access a + * JsonArray instance concurrently, while at least one of these threads modifies the + * contents of this array, access to the instance must be synchronized externally. Failure to do so + * may lead to an inconsistent state. + *

+ */ +public class JsonArray extends JsonValue implements Iterable { + + private final List values; + + /** + * Creates a new empty JsonArray. + */ + public JsonArray() { + values = new ArrayList<>(); + } + + /** + * Creates a new JsonArray with the contents of the specified JSON array. + * + * @param array the JsonArray to get the initial contents from, must not be null + */ + public JsonArray(JsonArray array) { + Objects.requireNonNull(array); + values = new ArrayList<>(array.values); + } + + /** + * Appends the JSON representation of the specified int value to the end of this + * array. + * + * @param value the value to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(int value) { + values.add(Json.of(value)); + return this; + } + + /** + * Appends the JSON representation of the specified long value to the end of this + * array. + * + * @param value the value to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(long value) { + values.add(Json.of(value)); + return this; + } + + /** + * Appends the JSON representation of the specified float value to the end of this + * array. + * + * @param value the value to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(float value) { + values.add(Json.of(value)); + return this; + } + + /** + * Appends the JSON representation of the specified double value to the end of this + * array. + * + * @param value the value to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(double value) { + values.add(Json.of(value)); + return this; + } + + /** + * Appends the JSON representation of the specified boolean value to the end of this + * array. + * + * @param value the value to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(boolean value) { + values.add(Json.of(value)); + return this; + } + + /** + * Appends the JSON representation of the specified string to the end of this array. + * + * @param value the string to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(String value) { + values.add(Json.of(value)); + return this; + } + + /** + * Appends the specified JSON value to the end of this array. + * + * @param value the JsonValue to add to the array, must not be null + * @return the array itself, to enable method chaining + */ + public JsonArray add(JsonValue value) { + Objects.requireNonNull(value); + values.add(value); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified int value. + * + * @param index the index of the array element to replace + * @param value the value to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, int value) { + values.set(index, Json.of(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified long value. + * + * @param index the index of the array element to replace + * @param value the value to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, long value) { + values.set(index, Json.of(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified float value. + * + * @param index the index of the array element to replace + * @param value the value to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, float value) { + values.set(index, Json.of(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified double value. + * + * @param index the index of the array element to replace + * @param value the value to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, double value) { + values.set(index, Json.of(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified boolean value. + * + * @param index the index of the array element to replace + * @param value the value to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, boolean value) { + values.set(index, Json.of(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified string. + * + * @param index the index of the array element to replace + * @param value the string to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, String value) { + values.set(index, Json.of(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the specified JSON value. + * + * @param index the index of the array element to replace + * @param value the value to be stored at the specified array position, must not be null + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, JsonValue value) { + if (value == null) { + throw new NullPointerException(); + } + values.set(index, value); + return this; + } + + /** + * Removes the element at the specified index from this array. + * + * @param index the index of the element to remove + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray remove(int index) { + values.remove(index); + return this; + } + + /** + * Returns the number of elements in this array. + * + * @return the number of elements in this array + */ + public int size() { + return values.size(); + } + + /** + * Returns true if this array contains no elements. + * + * @return true if this array contains no elements + */ + public boolean isEmpty() { + return values.isEmpty(); + } + + /** + * Returns the value of the element at the specified position in this array. + * + * @param index the index of the array element to return + * @return the value of the element at the specified position + * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonValue get(int index) { + return values.get(index); + } + + /** + * Returns a list of the values in this array in document order. + * + * @return a list of the values in this array + */ + public List values() { + return values; + } + + /** + * Returns an iterator over the values of this array in document order. The returned iterator + * cannot be used to modify this array. + * + * @return an iterator over the values of this array + */ + @Override + public Iterator iterator() { + final Iterator iterator = values.iterator(); + return new Iterator<>() { + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public JsonValue next() { + return iterator.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + void write(JsonWriter writer) throws IOException { + writer.writeArrayOpen(); + Iterator iterator = iterator(); + if (iterator.hasNext()) { + iterator.next().write(writer); + while (iterator.hasNext()) { + writer.writeArraySeparator(); + iterator.next().write(writer); + } + } + writer.writeArrayClose(); + } + + @Override + public boolean isArray() { + return true; + } + + @Override + public JsonArray asArray() { + return this; + } + + @Override + public int hashCode() { + return values.hashCode(); + } + + /** + * Indicates whether a given object is "equal to" this JsonArray. An object is considered equal + * if it is also a JsonArray and both arrays contain the same list of values. + *

+ * If two JsonArrays are equal, they will also produce the same JSON output. + *

+ * + * @param object the object to be compared with this JsonArray + * @return true if the specified object is equal to this JsonArray, false + * otherwise + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null) { + return false; + } + if (getClass() != object.getClass()) { + return false; + } + JsonArray other = (JsonArray) object; + return values.equals(other.values); + } + +} diff --git a/src/main/java/org/xbib/marc/json/JsonDefaultHandler.java b/src/main/java/org/xbib/marc/json/JsonDefaultHandler.java new file mode 100644 index 0000000..42e99d8 --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonDefaultHandler.java @@ -0,0 +1,111 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +/** + * + */ +public class JsonDefaultHandler implements JsonHandler { + + protected JsonValue value; + + public JsonValue getValue() { + return value; + } + + @Override + public JsonArray startArray() { + return new JsonArray(); + } + + @Override + public JsonObject startObject() { + return new JsonObject(); + } + + @Override + public void startNull() { + } + + @Override + public void endNull() { + value = JsonLiteral.NULL; + } + + @Override + public void startBoolean() { + } + + @Override + public void endBoolean(boolean bool) { + value = bool ? JsonLiteral.TRUE : JsonLiteral.FALSE; + } + + @Override + public void startString() { + } + + @Override + public void endString(String string) { + value = new JsonString(string); + } + + @Override + public void startNumber() { + } + + @Override + public void endNumber(String string) { + value = new JsonNumber(string); + } + + @Override + public void endArray(JsonArray array) { + value = array; + } + + @Override + public void startArrayValue(JsonArray array) { + } + + @Override + public void endObject(JsonObject object) { + value = object; + } + + @Override + public void startObjectName(JsonObject object) { + } + + @Override + public void endObjectName(JsonObject object, String name) { + } + + @Override + public void startObjectValue(JsonObject object, String name) { + } + + @Override + public void endArrayValue(JsonArray array) { + array.add(value); + } + + @Override + public void endObjectValue(JsonObject object, String name) { + object.add(name, value); + } +} diff --git a/src/main/java/org/xbib/marc/json/JsonException.java b/src/main/java/org/xbib/marc/json/JsonException.java new file mode 100755 index 0000000..da43112 --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonException.java @@ -0,0 +1,30 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +public class JsonException extends RuntimeException { + + private static final long serialVersionUID = -3386151672072419281L; + + JsonException(Throwable throwable) { + super(throwable); + } + + JsonException(String message) { + super(message); + } +} diff --git a/src/main/java/org/xbib/marc/json/JsonHandler.java b/src/main/java/org/xbib/marc/json/JsonHandler.java new file mode 100755 index 0000000..ff6a3a9 --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonHandler.java @@ -0,0 +1,197 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +/** + * An interface for parser events. A {@link JsonHandler} can be given to a {@link JsonReader}. The + * parser will then call the methods of the given handler while reading the input. + + *

+ * Implementations that build an object representation of the parsed JSON can return arbitrary handler + * objects for JSON arrays and JSON objects in {@link #startArray()} and {@link #startObject()}. + * These handler objects will then be provided in all subsequent parser events for this particular + * array or object. They can be used to keep track the elements of a JSON array or object. + *

+ * + * @param The type of handlers used for JSON arrays + * @param The type of handlers used for JSON objects + * @see JsonReader + */ +public interface JsonHandler { + + /** + * Indicates the beginning of a null literal in the JSON input. This method will be + * called when reading the first character of the literal. + */ + void startNull(); + + /** + * Indicates the end of a null literal in the JSON input. This method will be called + * after reading the last character of the literal. + */ + void endNull(); + + /** + * Indicates the beginning of a boolean literal (true or false) in the + * JSON input. This method will be called when reading the first character of the literal. + */ + void startBoolean(); + + /** + * Indicates the end of a boolean literal (true or false) in the JSON + * input. This method will be called after reading the last character of the literal. + * + * @param value the parsed boolean value + */ + void endBoolean(boolean value); + + /** + * Indicates the beginning of a string in the JSON input. This method will be called when reading + * the opening double quote character ('"'). + */ + void startString(); + + /** + * Indicates the end of a string in the JSON input. This method will be called after reading the + * closing double quote character ('"'). + * + * @param string the parsed string + */ + void endString(String string); + + /** + * Indicates the beginning of a number in the JSON input. This method will be called when reading + * the first character of the number. + */ + void startNumber(); + + /** + * Indicates the end of a number in the JSON input. This method will be called after reading the + * last character of the number. + * + * @param string the parsed number string + */ + void endNumber(String string); + + /** + * Indicates the beginning of an array in the JSON input. This method will be called when reading + * the opening square bracket character ('['). + *

+ * This method may return an object to handle subsequent parser events for this array. This array + * handler will then be provided in all calls to {@link #startArrayValue(Object) + * startArrayValue()}, {@link #endArrayValue(Object) endArrayValue()}, and + * {@link #endArray(Object) endArray()} for this array. + *

+ * + * @return a handler for this array, or null if not needed + */ + A startArray(); + + /** + * Indicates the end of an array in the JSON input. This method will be called after reading the + * closing square bracket character (']'). + * + * @param array the array handler returned from {@link #startArray()}, or null if not + * provided + */ + void endArray(A array); + + /** + * Indicates the beginning of an array element in the JSON input. This method will be called when + * reading the first character of the element, just before the call to the start + * method for the specific element type ({@link #startString()}, {@link #startNumber()}, etc.). + * + * @param array the array handler returned from {@link #startArray()}, or null if not + * provided + */ + void startArrayValue(A array); + + /** + * Indicates the end of an array element in the JSON input. This method will be called after + * reading the last character of the element value, just after the end method for the + * specific element type (like {@link #endString(String) endString()}, {@link #endNumber(String) + * endNumber()}, etc.). + * + * @param array the array handler returned from {@link #startArray()}, or null if not + * provided + */ + void endArrayValue(A array); + + /** + * Indicates the beginning of an object in the JSON input. This method will be called when reading + * the opening curly bracket character ('{'). + *

+ * This method may return an object to handle subsequent parser events for this object. This + * object handler will be provided in all calls to {@link #startObjectName(Object) + * startObjectName()}, {@link #endObjectName(Object, String) endObjectName()}, + * {@link #startObjectValue(Object, String) startObjectValue()}, + * {@link #endObjectValue(Object, String) endObjectValue()}, and {@link #endObject(Object) + * endObject()} for this object. + *

+ * + * @return a handler for this object, or null if not needed + */ + O startObject(); + + /** + * Indicates the end of an object in the JSON input. This method will be called after reading the + * closing curly bracket character ('}'). + * + * @param object the object handler returned from {@link #startObject()}, or null if not provided + */ + void endObject(O object); + + /** + * Indicates the beginning of the name of an object member in the JSON input. This method will be + * called when reading the opening quote character ('"') of the member name. + * + * @param object the object handler returned from {@link #startObject()}, or null if not + * provided + */ + void startObjectName(O object); + + /** + * Indicates the end of an object member name in the JSON input. This method will be called after + * reading the closing quote character ('"') of the member name. + * + * @param object the object handler returned from {@link #startObject()}, or null if not provided + * @param name the parsed member name + */ + void endObjectName(O object, String name); + + /** + * Indicates the beginning of the name of an object member in the JSON input. This method will be + * called when reading the opening quote character ('"') of the member name. + * + * @param object the object handler returned from {@link #startObject()}, or null if not + * provided + * @param name the member name + */ + void startObjectValue(O object, String name); + + /** + * Indicates the end of an object member value in the JSON input. This method will be called after + * reading the last character of the member value, just after the end method for the + * specific member type (like {@link #endString(String) endString()}, {@link #endNumber(String) + * endNumber()}, etc.). + * + * @param object the object handler returned from {@link #startObject()}, or null if not provided + * @param name the parsed member name + */ + void endObjectValue(O object, String name); + +} diff --git a/src/main/java/org/xbib/marc/json/JsonLiteral.java b/src/main/java/org/xbib/marc/json/JsonLiteral.java new file mode 100755 index 0000000..73c35fb --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonLiteral.java @@ -0,0 +1,92 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.io.IOException; + +/** + * A JSON literal. + */ +class JsonLiteral extends JsonValue { + + public static final JsonLiteral NULL = new JsonLiteral("null"); + + public static final JsonLiteral TRUE = new JsonLiteral("true"); + + public static final JsonLiteral FALSE = new JsonLiteral("false"); + + private final String value; + + private final boolean isNull; + + private final boolean isTrue; + + private final boolean isFalse; + + JsonLiteral(String value) { + this.value = value; + isNull = "null".equals(value); + isTrue = "true".equals(value); + isFalse = "false".equals(value); + } + + @Override + void write(JsonWriter writer) throws IOException { + writer.writeLiteral(value); + } + + @Override + public String toString() { + return value; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean isNull() { + return isNull; + } + + @Override + public boolean isTrue() { + return isTrue; + } + + @Override + public boolean isFalse() { + return isFalse; + } + + @Override + public boolean isBoolean() { + return isTrue || isFalse; + } + + @Override + public boolean asBoolean() { + return isNull ? super.asBoolean() : isTrue; + } + + @Override + public boolean equals(Object object) { + return this == object || object != null && getClass() == object.getClass() && value.equals(((JsonLiteral) object).value); + } + +} diff --git a/src/main/java/org/xbib/marc/json/JsonMapper.java b/src/main/java/org/xbib/marc/json/JsonMapper.java new file mode 100644 index 0000000..b9aaee4 --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonMapper.java @@ -0,0 +1,111 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + */ +public class JsonMapper { + + private JsonMapper() { + // do not instantiate this class + } + + public static Object asObject(JsonValue value) { + if (value.isBoolean()) { + return value.asBoolean(); + } else if (value.isInt()) { + return value.asInt(); + } else if (value.isLong()) { + return value.asLong(); + } else if (value.isFloat()) { + return value.asFloat(); + } else if (value.isDouble()) { + return value.asDouble(); + } else if (value.isString()) { + return value.asString(); + } else if (value.isArray()) { + return asList(value.asArray()); + } else if (value.isObject()) { + return asMap(value.asObject()); + } else { + return null; + } + } + + public static List asList(JsonArray array) { + List list = new ArrayList<>(array.size()); + for (JsonValue element : array) { + list.add(asObject(element)); + } + return list; + } + + public static Map asMap(JsonObject object) { + Map map = new HashMap<>(object.size(), 1.f); + for (JsonObject.Member member : object) { + map.put(member.getName(), asObject(member.getValue())); + } + return map; + } + + public static JsonValue asJsonValue(Object object) { + if (object == null) { + return JsonLiteral.NULL; + } else if (object instanceof Boolean) { + return Json.of((Boolean) object); + } else if (object instanceof Integer) { + return Json.of((Integer) object); + } else if (object instanceof Long) { + return Json.of((Long) object); + } else if (object instanceof Float) { + return Json.of((Float) object); + } else if (object instanceof Double) { + return Json.of((Double) object); + } else if (object instanceof String) { + return Json.of((String) object); + } else if (object instanceof Collection) { + return asJsonArray((Collection) object); + } else if (object instanceof Map) { + return asJsonObject((Map) object); + } else { + return null; + } + } + + public static JsonArray asJsonArray(Collection collection) { + JsonArray array = new JsonArray(); + for (Object element : collection) { + array.add(asJsonValue(element)); + } + return array; + } + + public static JsonObject asJsonObject(Map map) { + JsonObject object = new JsonObject(); + for (Map.Entry entry : map.entrySet()) { + object.add(String.valueOf(entry.getKey()), + asJsonValue(entry.getValue())); + } + return object; + } +} diff --git a/src/main/java/org/xbib/marc/json/JsonNumber.java b/src/main/java/org/xbib/marc/json/JsonNumber.java new file mode 100755 index 0000000..0a47cae --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonNumber.java @@ -0,0 +1,115 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.io.IOException; +import java.util.Objects; + +/** + * + */ +class JsonNumber extends JsonValue { + + private final String string; + + JsonNumber(String string) { + Objects.requireNonNull(string); + this.string = string; + } + + @Override + public String toString() { + return string; + } + + @Override + void write(JsonWriter writer) throws IOException { + writer.writeNumber(string); + } + + @Override + public boolean isInt() { + try { + asInt(); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + @Override + public boolean isLong() { + try { + asLong(); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + @Override + public boolean isFloat() { + try { + asFloat(); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + @Override + public boolean isDouble() { + try { + asDouble(); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + @Override + public int asInt() { + return Integer.parseInt(string, 10); + } + + @Override + public long asLong() { + return Long.parseLong(string, 10); + } + + @Override + public float asFloat() { + return Float.parseFloat(string); + } + + @Override + public double asDouble() { + return Double.parseDouble(string); + } + + @Override + public int hashCode() { + return string.hashCode(); + } + + @Override + public boolean equals(Object object) { + return this == object || object != null && getClass() == object.getClass() + && string.equals(((JsonNumber) object).string); + } + +} diff --git a/src/main/java/org/xbib/marc/json/JsonObject.java b/src/main/java/org/xbib/marc/json/JsonObject.java new file mode 100755 index 0000000..ca3d6b9 --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonObject.java @@ -0,0 +1,786 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * Represents a JSON object, a set of name/value pairs, where the names are strings and the values + * are JSON values. + *

+ * Members can be added using the add(String, ...) methods which accept instances of + * {@link JsonValue}, strings, primitive numbers, and boolean values. To modify certain values of an + * object, use the set(String, ...) methods. Please note that the add + * methods are faster than set as they do not search for existing members. On the other + * hand, the add methods do not prevent adding multiple members with the same name. + * Duplicate names are discouraged but not prohibited by JSON. + *

+ *

+ * Members can be accessed by their name using {@link #get(String)}. A list of all names can be + * obtained from the method {@link #names()}. This class also supports iterating over the members in + * document order using an {@link #iterator()} or an enhanced for loop: + *

+ *
+ * for (Member member : jsonObject) {
+ *   String name = member.getName();
+ *   JsonValue value = member.getValue();
+ *   ...
+ * }
+ * 
+ *

+ * Even though JSON objects are unordered by definition, instances of this class preserve the order + * of members to allow processing in document order and to guarantee a predictable output. + *

+ *

+ * Note that this class is not thread-safe. If multiple threads access a + * JsonObject instance concurrently, while at least one of these threads modifies the + * contents of this object, access to the instance must be synchronized externally. Failure to do so + * may lead to an inconsistent state. + *

+ */ +class JsonObject extends JsonValue implements Iterable { + + private final List names; + + private final List values; + + private HashIndexTable table; + + /** + * Creates a new empty JsonObject. + */ + JsonObject() { + names = new ArrayList<>(); + values = new ArrayList<>(); + table = new HashIndexTable(); + } + + /** + * Creates a new JsonObject, initialized with the contents of the specified JSON object. + * + * @param object the JSON object to get the initial contents from, must not be null + */ + public JsonObject(JsonObject object) { + Objects.requireNonNull(object); + names = new ArrayList<>(object.names); + values = new ArrayList<>(object.values); + table = new HashIndexTable(); + updateHashIndex(); + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified int value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, int value) { + add(name, Json.of(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified long value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, long value) { + add(name, Json.of(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified float value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, float value) { + add(name, Json.of(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified double value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, double value) { + add(name, Json.of(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified boolean value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, boolean value) { + add(name, Json.of(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified string. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, String value) { + add(name, Json.of(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the specified JSON + * value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add, must not be null + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, JsonValue value) { + if (name == null) { + throw new NullPointerException("name is null"); + } + if (value == null) { + throw new NullPointerException("value is null"); + } + table.add(name, names.size()); + names.add(name); + values.add(value); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified int value. If this object does not contain a member with this name, a + * new member is added at the end of the object. If this object contains multiple members with + * this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name the name of the member to replace + * @param value the value to set to the member + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, int value) { + set(name, Json.of(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified long value. If this object does not contain a member with this name, a + * new member is added at the end of the object. If this object contains multiple members with + * this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name the name of the member to replace + * @param value the value to set to the member + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, long value) { + set(name, Json.of(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified float value. If this object does not contain a member with this name, a + * new member is added at the end of the object. If this object contains multiple members with + * this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, float value) { + set(name, Json.of(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified double value. If this object does not contain a member with this name, a + * new member is added at the end of the object. If this object contains multiple members with + * this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, double value) { + set(name, Json.of(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified boolean value. If this object does not contain a member with this name, + * a new member is added at the end of the object. If this object contains multiple members with + * this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, boolean value) { + set(name, Json.of(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified string. If this object does not contain a member with this name, a new member is + * added at the end of the object. If this object contains multiple members with this name, only + * the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, String value) { + set(name, Json.of(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the specified JSON value. If this + * object does not contain a member with this name, a new member is added at the end of the + * object. If this object contains multiple members with this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name the name of the member to add + * @param value the value of the member to add, must not be null + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, JsonValue value) { + if (name == null) { + throw new NullPointerException("name is null"); + } + if (value == null) { + throw new NullPointerException("value is null"); + } + int index = indexOf(name); + if (index != -1) { + values.set(index, value); + } else { + table.add(name, names.size()); + names.add(name); + values.add(value); + } + return this; + } + + /** + * Removes a member with the specified name from this object. If this object contains multiple + * members with the given name, only the last one is removed. If this object does not contain a + * member with the specified name, the object is not modified. + * + * @param name the name of the member to remove + * @return the object itself, to enable method chaining + */ + public JsonObject remove(String name) { + Objects.requireNonNull(name); + int index = indexOf(name); + if (index != -1) { + table.remove(index); + names.remove(index); + values.remove(index); + } + return this; + } + + /** + * Copies all members of the specified object into this object. When the specified object contains + * members with names that also exist in this object, the existing values in this object will be + * replaced by the corresponding values in the specified object. + * + * @param object the object to merge + * @return the object itself, to enable method chaining + */ + public JsonObject merge(JsonObject object) { + Objects.requireNonNull(object); + for (Member member : object) { + this.set(member.name, member.value); + } + return this; + } + + /** + * Returns the value of the member with the specified name in this object. If this object contains + * multiple members with the given name, this method will return the last one. + * + * @param name the name of the member whose value is to be returned + * @return the value of the last member with the specified name, or null if this + * object does not contain a member with that name + */ + public JsonValue get(String name) { + Objects.requireNonNull(name); + int index = indexOf(name); + return index != -1 ? values.get(index) : null; + } + + /** + * Returns the int value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one will be picked. If this + * member's value does not represent a JSON number or if it cannot be interpreted as Java + * int, an exception is thrown. + * + * @param name the name of the member whose value is to be returned + * @param defaultValue the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public int getInt(String name, int defaultValue) { + JsonValue value = get(name); + return value != null ? value.asInt() : defaultValue; + } + + /** + * Returns the long value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one will be picked. If this + * member's value does not represent a JSON number or if it cannot be interpreted as Java + * long, an exception is thrown. + * + * @param name the name of the member whose value is to be returned + * @param defaultValue the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public long getLong(String name, long defaultValue) { + JsonValue value = get(name); + return value != null ? value.asLong() : defaultValue; + } + + /** + * Returns the float value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one will be picked. If this + * member's value does not represent a JSON number or if it cannot be interpreted as Java + * float, an exception is thrown. + * + * @param name the name of the member whose value is to be returned + * @param defaultValue the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public float getFloat(String name, float defaultValue) { + JsonValue value = get(name); + return value != null ? value.asFloat() : defaultValue; + } + + /** + * Returns the double value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one will be picked. If this + * member's value does not represent a JSON number or if it cannot be interpreted as Java + * double, an exception is thrown. + * + * @param name the name of the member whose value is to be returned + * @param defaultValue the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public double getDouble(String name, double defaultValue) { + JsonValue value = get(name); + return value != null ? value.asDouble() : defaultValue; + } + + /** + * Returns the boolean value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one will be picked. If this + * member's value does not represent a JSON true or false value, an + * exception is thrown. + * + * @param name the name of the member whose value is to be returned + * @param defaultValue the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public boolean getBoolean(String name, boolean defaultValue) { + JsonValue value = get(name); + return value != null ? value.asBoolean() : defaultValue; + } + + /** + * Returns the String value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one is picked. If this + * member's value does not represent a JSON string, an exception is thrown. + * + * @param name the name of the member whose value is to be returned + * @param defaultValue the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public String getString(String name, String defaultValue) { + JsonValue value = get(name); + return value != null ? value.asString() : defaultValue; + } + + /** + * Returns the number of members (name/value pairs) in this object. + * + * @return the number of members in this object + */ + public int size() { + return names.size(); + } + + /** + * Returns true if this object contains no members. + * + * @return true if this object contains no members + */ + public boolean isEmpty() { + return names.isEmpty(); + } + + /** + * Returns a list of the names in this object in document order. The returned list is backed by + * this object and will reflect subsequent changes. It cannot be used to modify this object. + * Attempts to modify the returned list will result in an exception. + * + * @return a list of the names in this object + */ + public List names() { + return Collections.unmodifiableList(names); + } + + /** + * Returns an iterator over the members of this object in document order. The returned iterator + * cannot be used to modify this object. + * + * @return an iterator over the members of this object + */ + @Override + public Iterator iterator() { + final Iterator namesIterator = names.iterator(); + final Iterator valuesIterator = values.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return namesIterator.hasNext(); + } + + @Override + public Member next() { + String name = namesIterator.next(); + JsonValue value = valuesIterator.next(); + return new Member(name, value); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + }; + } + + @Override + void write(JsonWriter writer) throws IOException { + writer.writeObjectOpen(); + Iterator namesIterator = names.iterator(); + Iterator valuesIterator = values.iterator(); + if (namesIterator.hasNext()) { + writer.writeMemberName(namesIterator.next()); + writer.writeMemberSeparator(); + valuesIterator.next().write(writer); + while (namesIterator.hasNext()) { + writer.writeObjectSeparator(); + writer.writeMemberName(namesIterator.next()); + writer.writeMemberSeparator(); + valuesIterator.next().write(writer); + } + } + writer.writeObjectClose(); + } + + @Override + public boolean isObject() { + return true; + } + + @Override + public JsonObject asObject() { + return this; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + names.hashCode(); + result = 31 * result + values.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + JsonObject other = (JsonObject) obj; + return names.equals(other.names) && values.equals(other.values); + } + + int indexOf(String name) { + int index = table.get(name); + if (index != -1 && name.equals(names.get(index))) { + return index; + } + return names.lastIndexOf(name); + } + + private void updateHashIndex() { + int size = names.size(); + for (int i = 0; i < size; i++) { + table.add(names.get(i), i); + } + } + + /** + * Represents a member of a JSON object, a pair of a name and a value. + */ + static class Member { + + private final String name; + private final JsonValue value; + + Member(String name, JsonValue value) { + this.name = name; + this.value = value; + } + + /** + * Returns the name of this member. + * + * @return the name of this member, never null + */ + public String getName() { + return name; + } + + /** + * Returns the value of this member. + * + * @return the value of this member, never null + */ + public JsonValue getValue() { + return value; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + name.hashCode(); + result = 31 * result + value.hashCode(); + return result; + } + + /** + * Indicates whether a given object is "equal to" this JsonObject. An object is considered equal + * if it is also a JsonObject and both objects contain the same members in + * the same order. + *

+ * If two JsonObjects are equal, they will also produce the same JSON output. + *

+ * + * @param object the object to be compared with this JsonObject + * @return true if the specified object is equal to this JsonObject, false + * otherwise + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null) { + return false; + } + if (getClass() != object.getClass()) { + return false; + } + Member other = (Member) object; + return name.equals(other.name) && value.equals(other.value); + } + + } + + /** + * + */ + static class HashIndexTable { + + private final byte[] hashTable = new byte[32]; // must be a power of two + + HashIndexTable() { + } + + HashIndexTable(HashIndexTable original) { + System.arraycopy(original.hashTable, 0, hashTable, 0, hashTable.length); + } + + void add(String name, int index) { + int slot = hashSlotFor(name); + if (index < 0xff) { + // increment by 1, 0 stands for empty + hashTable[slot] = (byte) (index + 1); + } else { + hashTable[slot] = 0; + } + } + + void remove(int index) { + for (int i = 0; i < hashTable.length; i++) { + if (hashTable[i] == index + 1) { + hashTable[i] = 0; + } else if (hashTable[i] > index + 1) { + hashTable[i]--; + } + } + } + + int get(Object name) { + int slot = hashSlotFor(name); + // subtract 1, 0 stands for empty + return (hashTable[slot] & 0xff) - 1; + } + + private int hashSlotFor(Object element) { + return element.hashCode() & hashTable.length - 1; + } + } +} diff --git a/src/main/java/org/xbib/marc/json/JsonReader.java b/src/main/java/org/xbib/marc/json/JsonReader.java new file mode 100755 index 0000000..b137c08 --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonReader.java @@ -0,0 +1,461 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.io.IOException; +import java.io.Reader; +import java.util.Objects; + +/** + * A streaming parser for JSON text. The parser reports all events to a given handler. + * + * @param the JSON array type + * @param the JSON object type + */ +public class JsonReader { + + private static final int MAX_NESTING_LEVEL = 1000; + + private static final int DEFAULT_BUFFER_SIZE = 1024; + + private final Reader reader; + + private final JsonHandler handler; + + private char[] buffer; + + private int index; + + private int fill; + + private int current; + + private StringBuilder captureBuffer; + + private int captureStart; + + private int nestingLevel; + + /** + * Creates a new JsonParser with the given handler. The parser will report all parser events to + * this handler. + * @param reader the reader + * @param handler the handler to process parser events + */ + public JsonReader(Reader reader, JsonHandler handler) { + Objects.requireNonNull(handler); + this.handler = handler; + this.reader = reader; + } + + /** + * Reads the entire input from the given reader and parses it as JSON. The input must contain a + * valid JSON value, optionally padded with whitespace. + *

+ * Characters are read in chunks into a default-sized input buffer. Hence, wrapping a reader in an + * additional BufferedReader likely won't improve reading performance. + *

+ * + * @throws IOException if an I/O error occurs in the reader + * @throws JsonException if the input is not valid JSON + */ + public void parse() throws IOException { + parse(DEFAULT_BUFFER_SIZE); + } + + /** + * Reads the entire input from the given reader and parses it as JSON. The input must contain a + * valid JSON value, optionally padded with whitespace. + *

+ * Characters are read in chunks into an input buffer of the given size. Hence, wrapping a reader + * in an additional BufferedReader likely won't improve reading performance. + *

+ * + * @param buffersize the size of the input buffer in chars + * @throws IOException if an I/O error occurs in the reader + * @throws JsonException if the input is not valid JSON + */ + public void parse(int buffersize) throws IOException { + if (reader == null) { + throw new NullPointerException("reader is null"); + } + if (buffersize <= 0) { + throw new IllegalArgumentException("buffersize is zero or negative"); + } + buffer = new char[buffersize]; + index = 0; + fill = 0; + current = 0; + captureStart = -1; + read(); + skipWhiteSpace(); + readValue(); + skipWhiteSpace(); + if (!isEndOfText()) { + throw error("Unexpected character"); + } + } + + private void readValue() throws IOException { + switch (current) { + case 'n': + readNull(); + break; + case 't': + readTrue(); + break; + case 'f': + readFalse(); + break; + case '"': + readString(); + break; + case '[': + readArray(); + break; + case '{': + readObject(); + break; + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + readNumber(); + break; + default: + throw expected("value"); + } + } + + private void readArray() throws IOException { + A array = handler.startArray(); + read(); + if (++nestingLevel > MAX_NESTING_LEVEL) { + throw error("Nesting too deep"); + } + skipWhiteSpace(); + if (readChar(']')) { + nestingLevel--; + handler.endArray(array); + return; + } + do { + skipWhiteSpace(); + handler.startArrayValue(array); + readValue(); + handler.endArrayValue(array); + skipWhiteSpace(); + } while (readChar(',')); + if (!readChar(']')) { + throw expected("',' or ']'"); + } + nestingLevel--; + handler.endArray(array); + } + + private void readObject() throws IOException { + O object = handler.startObject(); + read(); + if (++nestingLevel > MAX_NESTING_LEVEL) { + throw error("Nesting too deep"); + } + skipWhiteSpace(); + if (readChar('}')) { + nestingLevel--; + handler.endObject(object); + return; + } + do { + skipWhiteSpace(); + handler.startObjectName(object); + String name = readName(); + handler.endObjectName(object, name); + skipWhiteSpace(); + if (!readChar(':')) { + throw expected("':'"); + } + skipWhiteSpace(); + handler.startObjectValue(object, name); + readValue(); + handler.endObjectValue(object, name); + skipWhiteSpace(); + } while (readChar(',')); + if (!readChar('}')) { + throw expected("',' or '}'"); + } + nestingLevel--; + handler.endObject(object); + } + + private String readName() throws IOException { + if (current != '"') { + throw expected("name"); + } + return readStringInternal(); + } + + private void readNull() throws IOException { + handler.startNull(); + read(); + readRequiredChar('u'); + readRequiredChar('l'); + readRequiredChar('l'); + handler.endNull(); + } + + private void readTrue() throws IOException { + handler.startBoolean(); + read(); + readRequiredChar('r'); + readRequiredChar('u'); + readRequiredChar('e'); + handler.endBoolean(true); + } + + private void readFalse() throws IOException { + handler.startBoolean(); + read(); + readRequiredChar('a'); + readRequiredChar('l'); + readRequiredChar('s'); + readRequiredChar('e'); + handler.endBoolean(false); + } + + private void readRequiredChar(char ch) throws IOException { + if (!readChar(ch)) { + throw expected("'" + ch + "'"); + } + } + + private void readString() throws IOException { + handler.startString(); + handler.endString(readStringInternal()); + } + + private String readStringInternal() throws IOException { + read(); + startCapture(); + while (current != '"') { + if (current == '\\') { + pauseCapture(); + readEscape(); + startCapture(); + } else if (current < 0x20) { + throw expected("valid string character"); + } else { + read(); + } + } + String string = endCapture(); + read(); + return string; + } + + private void readEscape() throws IOException { + read(); + switch (current) { + case '"': + case '/': + case '\\': + captureBuffer.append((char) current); + break; + case 'b': + captureBuffer.append('\b'); + break; + case 'f': + captureBuffer.append('\f'); + break; + case 'n': + captureBuffer.append('\n'); + break; + case 'r': + captureBuffer.append('\r'); + break; + case 't': + captureBuffer.append('\t'); + break; + case 'u': + char[] hexChars = new char[4]; + for (int i = 0; i < 4; i++) { + read(); + if (!isHexDigit()) { + throw expected("hexadecimal digit"); + } + hexChars[i] = (char) current; + } + captureBuffer.append((char) Integer.parseInt(new String(hexChars), 16)); + break; + default: + throw expected("valid escape sequence"); + } + read(); + } + + private void readNumber() throws IOException { + handler.startNumber(); + startCapture(); + readChar('-'); + int firstDigit = current; + if (!readDigit()) { + throw expected("digit"); + } + if (firstDigit != '0') { + while (true) { + if (readDigit()) { + break; + } + } + } + readFraction(); + readExponent(); + handler.endNumber(endCapture()); + } + + private void readFraction() throws IOException { + if (!readChar('.')) { + return; + } + if (!readDigit()) { + throw expected("digit"); + } + while (true) { + if (readDigit()) { + break; + } + } + } + + private void readExponent() throws IOException { + if (!readChar('e') && !readChar('E')) { + return; + } + if (!readChar('+')) { + readChar('-'); + } + if (!readDigit()) { + throw expected("digit"); + } + while (true) { + if (readDigit()) { + break; + } + } + } + + private boolean readChar(char ch) throws IOException { + if (current != ch) { + return false; + } + read(); + return true; + } + + private boolean readDigit() throws IOException { + if (!isDigit()) { + return false; + } + read(); + return true; + } + + private void skipWhiteSpace() throws IOException { + while (isWhiteSpace()) { + read(); + } + } + + private void read() throws IOException { + if (index == fill) { + if (captureStart != -1) { + captureBuffer.append(buffer, captureStart, fill - captureStart); + captureStart = 0; + } + fill = reader.read(buffer, 0, buffer.length); + index = 0; + if (fill == -1) { + current = -1; + index++; + return; + } + } + current = buffer[index++]; + } + + private void startCapture() { + if (captureBuffer == null) { + captureBuffer = new StringBuilder(); + } + captureStart = index - 1; + } + + private void pauseCapture() { + int end = current == -1 ? index : index - 1; + captureBuffer.append(buffer, captureStart, end - captureStart); + captureStart = -1; + } + + private String endCapture() { + int start = captureStart; + int end = index - 1; + captureStart = -1; + if (captureBuffer.length() > 0) { + captureBuffer.append(buffer, start, end - start); + String captured = captureBuffer.toString(); + captureBuffer.setLength(0); + return captured; + } + return new String(buffer, start, end - start); + } + + private JsonException expected(String expected) { + if (isEndOfText()) { + return error("Unexpected end of input"); + } + return error("Expected " + expected); + } + + private JsonException error(String message) { + return new JsonException(message); + } + + private boolean isWhiteSpace() { + return current == ' ' || current == '\t' || current == '\n' || current == '\r'; + } + + private boolean isDigit() { + return current >= '0' && current <= '9'; + } + + private boolean isHexDigit() { + return current >= '0' && current <= '9' + || current >= 'a' && current <= 'f' + || current >= 'A' && current <= 'F'; + } + + private boolean isEndOfText() { + return current == -1; + } + +} diff --git a/src/main/java/org/xbib/marc/json/JsonString.java b/src/main/java/org/xbib/marc/json/JsonString.java new file mode 100755 index 0000000..5ea9791 --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonString.java @@ -0,0 +1,60 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.io.IOException; +import java.util.Objects; + +/** + * + */ +class JsonString extends JsonValue { + + private final String string; + + JsonString(String string) { + Objects.requireNonNull(string); + this.string = string; + } + + @Override + void write(JsonWriter writer) throws IOException { + writer.writeString(string); + } + + @Override + public boolean isString() { + return true; + } + + @Override + public String asString() { + return string; + } + + @Override + public int hashCode() { + return string.hashCode(); + } + + @Override + public boolean equals(Object object) { + return this == object || object != null && getClass() == object.getClass() + && string.equals(((JsonString) object).string); + } + +} diff --git a/src/main/java/org/xbib/marc/json/JsonValue.java b/src/main/java/org/xbib/marc/json/JsonValue.java new file mode 100755 index 0000000..bcca77b --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonValue.java @@ -0,0 +1,297 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.io.IOException; +import java.io.StringWriter; + +/** + * Represents a JSON value. This can be a JSON object, an array, + * a number, a string, or one of the literals + * true, false, and null. + *

+ * The literals true, false, and null are + * represented by the constants + * {@code JsonLiteral.NULL}, {@code JsonLiteral.FALSE}, and {@code JsonLiteral.NULL}. + *

+ *

+ * JSON objects and arrays are represented by the subtypes + * {@link JsonObject} and {@link JsonArray}. Instances of these types can be created using the + * public constructors of these classes. + *

+ *

+ * Instances that represent JSON numbers, strings and + * boolean values can be created using the static factory methods + * {@code JSON.parse(String)}, {@code valueOf(long)}, {@code valueOf(double)}, etc. + *

+ *

+ * In order to find out whether an instance of this class is of a certain type, the methods + * {@link #isObject()}, {@link #isArray()}, {@link #isString()}, {@link #isInt()} etc. can be + * used. + *

+ *

+ * If the type of a JSON value is known, the methods {@link #asObject()}, {@link #asArray()}, + * {@link #asString()}, {@link #asInt()}, etc. can be used to get this value directly in the + * appropriate target type. + *

+ */ +abstract class JsonValue { + + JsonValue() { + // prevent subclasses outside of this package + } + + /** + * Detects whether this value represents a JSON object. If this is the case, this value is an + * instance of {@link JsonObject}. + * + * @return true if this value is an instance of JsonObject + */ + public boolean isObject() { + return false; + } + + /** + * Detects whether this value represents a JSON array. If this is the case, this value is an + * instance of {@link JsonArray}. + * + * @return true if this value is an instance of JsonArray + */ + public boolean isArray() { + return false; + } + + /** + * Detects whether this value represents a JSON number that is an integer. + * + * @return true if this value represents a JSON number that is an integer + */ + public boolean isInt() { + return false; + } + + /** + * Detects whether this value represents a JSON number that is an long. + * + * @return true if this value represents a JSON number that is an long + */ + public boolean isLong() { + return false; + } + + /** + * Detects whether this value represents a JSON number that is an float. + * + * @return true if this value represents a JSON number that is an float + */ + public boolean isFloat() { + return false; + } + + /** + * Detects whether this value represents a JSON number that is an double. + * + * @return true if this value represents a JSON number that is an double + */ + public boolean isDouble() { + return false; + } + + /** + * Detects whether this value represents a JSON string. + * + * @return true if this value represents a JSON string + */ + public boolean isString() { + return false; + } + + /** + * Detects whether this value represents a boolean value. + * + * @return true if this value represents either the JSON literal true or + * false + */ + public boolean isBoolean() { + return false; + } + + /** + * Detects whether this value represents the JSON literal true. + * + * @return true if this value represents the JSON literal true + */ + public boolean isTrue() { + return false; + } + + /** + * Detects whether this value represents the JSON literal false. + * + * @return true if this value represents the JSON literal false + */ + public boolean isFalse() { + return false; + } + + /** + * Detects whether this value represents the JSON literal null. + * + * @return true if this value represents the JSON literal null + */ + public boolean isNull() { + return false; + } + + /** + * Returns this JSON value as {@link JsonObject}, assuming that this value represents a JSON + * object. If this is not the case, an exception is thrown. + * + * @return a JSONObject for this value + * @throws UnsupportedOperationException if this value is not a JSON object + */ + public JsonObject asObject() { + throw new UnsupportedOperationException("Not an object: " + toString()); + } + + /** + * Returns this JSON value as {@link JsonArray}, assuming that this value represents a JSON array. + * If this is not the case, an exception is thrown. + * + * @return a JSONArray for this value + * @throws UnsupportedOperationException if this value is not a JSON array + */ + public JsonArray asArray() { + throw new UnsupportedOperationException("Not an array: " + toString()); + } + + /** + * Returns this JSON value as an int value, assuming that this value represents a + * JSON number that can be interpreted as Java int. If this is not the case, an + * exception is thrown. + *

+ * To be interpreted as Java int, the JSON number must neither contain an exponent + * nor a fraction part. Moreover, the number must be in the Integer range. + *

+ * + * @return this value as int + * @throws UnsupportedOperationException if this value is not a JSON number + * @throws NumberFormatException if this JSON number can not be interpreted as int value + */ + public int asInt() { + throw new UnsupportedOperationException("Not a number: " + toString()); + } + + /** + * Returns this JSON value as a long value, assuming that this value represents a + * JSON number that can be interpreted as Java long. If this is not the case, an + * exception is thrown. + *

+ * To be interpreted as Java long, the JSON number must neither contain an exponent + * nor a fraction part. Moreover, the number must be in the Long range. + *

+ * + * @return this value as long + * @throws UnsupportedOperationException if this value is not a JSON number + * @throws NumberFormatException if this JSON number can not be interpreted as long value + */ + public long asLong() { + throw new UnsupportedOperationException(); + } + + /** + * Returns this JSON value as a float value, assuming that this value represents a + * JSON number. If this is not the case, an exception is thrown. + *

+ * If the JSON number is out of the Float range, {@link Float#POSITIVE_INFINITY} or + * {@link Float#NEGATIVE_INFINITY} is returned. + *

+ * + * @return this value as float + * @throws UnsupportedOperationException if this value is not a JSON number + */ + public float asFloat() { + throw new UnsupportedOperationException(); + } + + /** + * Returns this JSON value as a double value, assuming that this value represents a + * JSON number. If this is not the case, an exception is thrown. + *

+ * If the JSON number is out of the Double range, {@link Double#POSITIVE_INFINITY} or + * {@link Double#NEGATIVE_INFINITY} is returned. + *

+ * + * @return this value as double + * @throws UnsupportedOperationException if this value is not a JSON number + */ + public double asDouble() { + throw new UnsupportedOperationException(); + } + + /** + * Returns this JSON value as String, assuming that this value represents a JSON string. If this + * is not the case, an exception is thrown. + * + * @return the string represented by this value + * @throws UnsupportedOperationException if this value is not a JSON string + */ + public String asString() { + throw new UnsupportedOperationException(); + } + + /** + * Returns this JSON value as a boolean value, assuming that this value is either + * true or false. If this is not the case, an exception is thrown. + * + * @return this value as boolean + * @throws UnsupportedOperationException if this value is neither true or false + */ + public boolean asBoolean() { + throw new UnsupportedOperationException(); + } + + /** + * Returns the JSON string for this value in its minimal form, without any additional whitespace. + * + * @return a JSON string that represents this value + */ + @Override + public String toString() { + return toString(JsonWriterConfig.minimal()); + } + + /** + * Returns the JSON string for this value using the given formatting. + * + * @param config a configuration that controls the formatting or null for the minimal form + * @return a JSON string that represents this value + */ + public String toString(JsonWriterConfig config) { + StringWriter writer = new StringWriter(); + try { + write(config.createWriter(writer)); + } catch (IOException exception) { + // StringWriter does not throw IOException, so this is impossible + throw new JsonException(exception); + } + return writer.toString(); + } + + abstract void write(JsonWriter writer) throws IOException; + +} diff --git a/src/main/java/org/xbib/marc/json/JsonWriter.java b/src/main/java/org/xbib/marc/json/JsonWriter.java new file mode 100755 index 0000000..a06cc5f --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonWriter.java @@ -0,0 +1,141 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.io.IOException; +import java.io.Writer; + +/** + * + */ +public class JsonWriter { + + private static final int CONTROL_CHARACTERS_END = 0x001f; + + private static final char[] QUOT_CHARS = {'\\', '"'}; + private static final char[] BS_CHARS = {'\\', '\\'}; + private static final char[] LF_CHARS = {'\\', 'n'}; + private static final char[] CR_CHARS = {'\\', 'r'}; + private static final char[] TAB_CHARS = {'\\', 't'}; + // In JavaScript, U+2028 and U+2029 characters count as line endings and must be encoded. + // http://stackoverflow.com/questions/2965293/javascript-parse-error-on-u2028-unicode-character + private static final char[] UNICODE_2028_CHARS = {'\\', 'u', '2', '0', '2', '8'}; + private static final char[] UNICODE_2029_CHARS = {'\\', 'u', '2', '0', '2', '9'}; + private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f'}; + + protected final Writer writer; + + public JsonWriter(Writer writer) { + this.writer = writer; + } + + protected void writeLiteral(String value) throws IOException { + writer.write(value); + } + + protected void writeNumber(String string) throws IOException { + writer.write(string); + } + + protected void writeString(String string) throws IOException { + writer.write('"'); + writeJsonString(string); + writer.write('"'); + } + + protected void writeArrayOpen() throws IOException { + writer.write('['); + } + + protected void writeArrayClose() throws IOException { + writer.write(']'); + } + + protected void writeArraySeparator() throws IOException { + writer.write(','); + } + + protected void writeObjectOpen() throws IOException { + writer.write('{'); + } + + protected void writeObjectClose() throws IOException { + writer.write('}'); + } + + protected void writeMemberName(String name) throws IOException { + writer.write('"'); + writeJsonString(name); + writer.write('"'); + } + + protected void writeMemberSeparator() throws IOException { + writer.write(':'); + } + + protected void writeObjectSeparator() throws IOException { + writer.write(','); + } + + private void writeJsonString(String string) throws IOException { + int length = string.length(); + int start = 0; + for (int index = 0; index < length; index++) { + char[] replacement = getReplacementChars(string.charAt(index)); + if (replacement != null) { + writer.write(string, start, index - start); + writer.write(replacement); + start = index + 1; + } + } + writer.write(string, start, length - start); + } + + private static char[] getReplacementChars(char ch) { + if (ch > '\\') { + if (ch < '\u2028' || ch > '\u2029') { + // The lower range contains 'a' .. 'z'. Only 2 checks required. + return null; + } + return ch == '\u2028' ? UNICODE_2028_CHARS : UNICODE_2029_CHARS; + } + if (ch == '\\') { + return BS_CHARS; + } + if (ch > '"') { + // This range contains '0' .. '9' and 'A' .. 'Z'. Need 3 checks to get here. + return null; + } + if (ch == '"') { + return QUOT_CHARS; + } + if (ch > CONTROL_CHARACTERS_END) { + return null; + } + if (ch == '\n') { + return LF_CHARS; + } + if (ch == '\r') { + return CR_CHARS; + } + if (ch == '\t') { + return TAB_CHARS; + } + return new char[]{'\\', 'u', '0', '0', HEX_DIGITS[ch >> 4 & 0x000f], HEX_DIGITS[ch & 0x000f]}; + } +} diff --git a/src/main/java/org/xbib/marc/json/JsonWriterConfig.java b/src/main/java/org/xbib/marc/json/JsonWriterConfig.java new file mode 100755 index 0000000..cef9f5c --- /dev/null +++ b/src/main/java/org/xbib/marc/json/JsonWriterConfig.java @@ -0,0 +1,158 @@ +/* + Copyright 2016 Jörg Prante + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.xbib.marc.json; + +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; + +/** + * Controls the formatting of the JSON output. Use one of the available constants. + */ +@FunctionalInterface +interface JsonWriterConfig { + + JsonWriter createWriter(Writer writer); + + /** + * Write JSON in its minimal form, without any additional whitespace. This is the default. + */ + static JsonWriterConfig minimal() { + return JsonWriter::new; + } + + /** + * Write JSON in pretty-print, with each value on a separate line and an indentation of two + * spaces. + */ + static JsonWriterConfig prettyPrint(int n) { + return new PrettyPrint(n); + } + + /** + * Enables human readable JSON output by inserting whitespace between values.after commas and + * colons. Example: + * + *
+     * jsonValue.writeTo(writer, WriterConfig.prettyPrint());
+     * 
+ */ + class PrettyPrint implements JsonWriterConfig { + + private final char[] indentChars; + + PrettyPrint(char[] indentChars) { + this.indentChars = indentChars; + } + + /** + * Print every value on a separate line. Use the given number of spaces for indentation. + * + * @param number the number of spaces to use + */ + PrettyPrint(int number) { + this(fillChars(number)); + } + + private static char[] fillChars(int number) { + if (number < 0) { + throw new IllegalArgumentException("number is negative"); + } + char[] chars = new char[number]; + Arrays.fill(chars, ' '); + return chars; + } + + @Override + public JsonWriter createWriter(Writer writer) { + return new PrettyPrintWriter(writer, indentChars); + } + } + + class PrettyPrintWriter extends JsonWriter { + + private final char[] indentChars; + private int indent; + + private PrettyPrintWriter(Writer writer, char[] indentChars) { + super(writer); + this.indentChars = indentChars; + } + + @Override + protected void writeArrayOpen() throws IOException { + indent++; + writer.write('['); + writeNewLine(); + } + + @Override + protected void writeArrayClose() throws IOException { + indent--; + writeNewLine(); + writer.write(']'); + } + + @Override + protected void writeArraySeparator() throws IOException { + writer.write(','); + if (!writeNewLine()) { + writer.write(' '); + } + } + + @Override + protected void writeObjectOpen() throws IOException { + indent++; + writer.write('{'); + writeNewLine(); + } + + @Override + protected void writeObjectClose() throws IOException { + indent--; + writeNewLine(); + writer.write('}'); + } + + @Override + protected void writeMemberSeparator() throws IOException { + writer.write(':'); + writer.write(' '); + } + + @Override + protected void writeObjectSeparator() throws IOException { + writer.write(','); + if (!writeNewLine()) { + writer.write(' '); + } + } + + private boolean writeNewLine() throws IOException { + if (indentChars == null) { + return false; + } + writer.write('\n'); + for (int i = 0; i < indent; i++) { + writer.write(indentChars); + } + return true; + } + + } +} diff --git a/src/main/java/org/xbib/marc/json/MarcJsonWriter.java b/src/main/java/org/xbib/marc/json/MarcJsonWriter.java index 441fa9c..cf515d3 100644 --- a/src/main/java/org/xbib/marc/json/MarcJsonWriter.java +++ b/src/main/java/org/xbib/marc/json/MarcJsonWriter.java @@ -38,6 +38,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -65,15 +66,15 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo private final Lock lock; - private final StringBuilder sb; - private Writer writer; + private JsonWriter jsonWriter; + private Marc.Builder builder; private boolean fatalErrors; - private Style style; + private EnumSet