add JSON reader and writer to allow duplicate keys, closes #8

This commit is contained in:
Jörg Prante 2019-11-07 11:10:31 +01:00
parent f17e9c97b4
commit 414abce68f
35 changed files with 6823 additions and 96 deletions

View file

@ -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
-----------------

View file

@ -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 {

View file

@ -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

View file

@ -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.
* <p>
* To <strong>parse</strong> a given JSON input, use the <code>parse()</code> methods like in this
* example:
* </p>
* <pre>
* JsonObject object = Json.parse(string).asObject();
* </pre>
* <p>
* To <strong>create</strong> a JSON data structure to be serialized, use the methods
* <code>value()</code>, <code>array()</code>, and <code>object()</code>. For example, the following
* snippet will produce the JSON string <em>{"foo": 23, "bar": true}</em>:
* </p>
* <pre>
* String string = Json.object().add("foo", 23).add("bar", true).toString();
* </pre>
* <p>
* To create a JSON array from a given Java array, you can use one of the <code>array()</code>
* methods with varargs parameters:
* </p>
* <pre>
* String[] names = ...
* JsonArray array = Json.array(names);
* </pre>
*/
public final class Json {
private Json() {
// not meant to be instantiated
}
/**
* Returns a JsonValue instance that represents the given <code>int</code> 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 <code>long</code> 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 <code>float</code> 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 <code>double</code> 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 <code>boolean</code> 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 <code>int</code>
* 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 <code>long</code>
* 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 <code>float</code>
* 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 <code>double</code>
* 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
* <code>boolean</code> 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.
* <p>
* Characters are read in chunks into an input buffer. Hence, wrapping a reader in an additional
* <code>BufferedReader</code> likely won't improve reading performance.
* </p>
*
* @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;
}
}

View file

@ -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.
* <p>
* Elements can be added using the <code>add(...)</code> methods which accept instances of
* {@link JsonValue}, strings, primitive numbers, and boolean values. To replace an element of an
* array, use the <code>set(int, ...)</code> methods.
* </p>
* <p>
* 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:
* </p>
* <pre>
* for (JsonValue value : jsonArray) {
* ...
* }
* </pre>
* <p>
* An equivalent {@link List} can be obtained from the method {@link #values()}.
* </p>
* <p>
* Note that this class is <strong>not thread-safe</strong>. If multiple threads access a
* <code>JsonArray</code> 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.
* </p>
*/
public class JsonArray extends JsonValue implements Iterable<JsonValue> {
private final List<JsonValue> 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 <code>null</code>
*/
public JsonArray(JsonArray array) {
Objects.requireNonNull(array);
values = new ArrayList<>(array.values);
}
/**
* Appends the JSON representation of the specified <code>int</code> 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 <code>long</code> 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 <code>float</code> 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 <code>double</code> 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 <code>boolean</code> 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 <code>null</code>
* @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 <code>int</code> 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. <code>index &lt; 0</code> or
* <code>index &gt;= size</code>
*/
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 <code>long</code> 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. <code>index &lt; 0</code> or
* <code>index &gt;= size</code>
*/
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 <code>float</code> 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. <code>index &lt; 0</code> or
* <code>index &gt;= size</code>
*/
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 <code>double</code> 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. <code>index &lt; 0</code> or
* <code>index &gt;= size</code>
*/
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 <code>boolean</code> 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. <code>index &lt; 0</code> or
* <code>index &gt;= size</code>
*/
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. <code>index &lt; 0</code> or
* <code>index &gt;= size</code>
*/
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 <code>null</code>
* @return the array itself, to enable method chaining
* @throws IndexOutOfBoundsException if the index is out of range, i.e. <code>index &lt; 0</code> or
* <code>index &gt;= size</code>
*/
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. <code>index &lt; 0</code> or
* <code>index &gt;= size</code>
*/
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 <code>true</code> if this array contains no elements.
*
* @return <code>true</code> 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. <code>index &lt; 0</code> or
* <code>index &gt;= size</code>
*/
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<JsonValue> 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<JsonValue> iterator() {
final Iterator<JsonValue> 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<JsonValue> 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 <code>JsonArray</code> and both arrays contain the same list of values.
* <p>
* If two JsonArrays are equal, they will also produce the same JSON output.
* </p>
*
* @param object the object to be compared with this JsonArray
* @return <tt>true</tt> if the specified object is equal to this JsonArray, <code>false</code>
* 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);
}
}

View file

@ -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<JsonArray, JsonObject> {
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);
}
}

View file

@ -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);
}
}

View file

@ -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.
* <p>
* 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.
* </p>
*
* @param <A> The type of handlers used for JSON arrays
* @param <O> The type of handlers used for JSON objects
* @see JsonReader
*/
public interface JsonHandler<A, O> {
/**
* Indicates the beginning of a <code>null</code> 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 <code>null</code> 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 (<code>true</code> or <code>false</code>) 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 (<code>true</code> or <code>false</code>) 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 (<code>'&quot;'</code>).
*/
void startString();
/**
* Indicates the end of a string in the JSON input. This method will be called after reading the
* closing double quote character (<code>'&quot;'</code>).
*
* @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 (<code>'['</code>).
* <p>
* 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.
* </p>
*
* @return a handler for this array, or <code>null</code> 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 (<code>']'</code>).
*
* @param array the array handler returned from {@link #startArray()}, or <code>null</code> 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 <code>start</code>
* method for the specific element type ({@link #startString()}, {@link #startNumber()}, etc.).
*
* @param array the array handler returned from {@link #startArray()}, or <code>null</code> 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 <code>end</code> 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 <code>null</code> 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 (<code>'{'</code>).
* <p>
* 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.
* </p>
*
* @return a handler for this object, or <code>null</code> 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 (<code>'}'</code>).
*
* @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 ('&quot;') of the member name.
*
* @param object the object handler returned from {@link #startObject()}, or <code>null</code> 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 (<code>'"'</code>) 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 ('&quot;') of the member name.
*
* @param object the object handler returned from {@link #startObject()}, or <code>null</code> 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 <code>end</code> 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);
}

View file

@ -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);
}
}

View file

@ -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<Object> asList(JsonArray array) {
List<Object> list = new ArrayList<>(array.size());
for (JsonValue element : array) {
list.add(asObject(element));
}
return list;
}
public static Map<String, Object> asMap(JsonObject object) {
Map<String, Object> 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;
}
}

View file

@ -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);
}
}

View file

@ -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.
* <p>
* Members can be added using the <code>add(String, ...)</code> methods which accept instances of
* {@link JsonValue}, strings, primitive numbers, and boolean values. To modify certain values of an
* object, use the <code>set(String, ...)</code> methods. Please note that the <code>add</code>
* methods are faster than <code>set</code> as they do not search for existing members. On the other
* hand, the <code>add</code> methods do not prevent adding multiple members with the same name.
* Duplicate names are discouraged but not prohibited by JSON.
* </p>
* <p>
* 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:
* </p>
* <pre>
* for (Member member : jsonObject) {
* String name = member.getName();
* JsonValue value = member.getValue();
* ...
* }
* </pre>
* <p>
* 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.
* </p>
* <p>
* Note that this class is <strong>not thread-safe</strong>. If multiple threads access a
* <code>JsonObject</code> 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.
* </p>
*/
class JsonObject extends JsonValue implements Iterable<JsonObject.Member> {
private final List<String> names;
private final List<JsonValue> 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 <code>null</code>
*/
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 <code>int</code> value.
* <p>
* This method <strong>does not prevent duplicate names</strong>. 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 <code>set(name, value)</code> instead. However,
* <strong> <em>add</em> is much faster than <em>set</em></strong> (because it does not need to
* search for existing members). Therefore <em>add</em> should be preferred when constructing new
* objects.
* </p>
*
* @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 <code>long</code> value.
* <p>
* This method <strong>does not prevent duplicate names</strong>. 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 <code>set(name, value)</code> instead. However,
* <strong> <em>add</em> is much faster than <em>set</em></strong> (because it does not need to
* search for existing members). Therefore <em>add</em> should be preferred when constructing new
* objects.
* </p>
*
* @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 <code>float</code> value.
* <p>
* This method <strong>does not prevent duplicate names</strong>. 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 <code>set(name, value)</code> instead. However,
* <strong> <em>add</em> is much faster than <em>set</em></strong> (because it does not need to
* search for existing members). Therefore <em>add</em> should be preferred when constructing new
* objects.
* </p>
*
* @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 <code>double</code> value.
* <p>
* This method <strong>does not prevent duplicate names</strong>. 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 <code>set(name, value)</code> instead. However,
* <strong> <em>add</em> is much faster than <em>set</em></strong> (because it does not need to
* search for existing members). Therefore <em>add</em> should be preferred when constructing new
* objects.
* </p>
*
* @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 <code>boolean</code> value.
* <p>
* This method <strong>does not prevent duplicate names</strong>. 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 <code>set(name, value)</code> instead. However,
* <strong> <em>add</em> is much faster than <em>set</em></strong> (because it does not need to
* search for existing members). Therefore <em>add</em> should be preferred when constructing new
* objects.
* </p>
*
* @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.
* <p>
* This method <strong>does not prevent duplicate names</strong>. 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 <code>set(name, value)</code> instead. However,
* <strong> <em>add</em> is much faster than <em>set</em></strong> (because it does not need to
* search for existing members). Therefore <em>add</em> should be preferred when constructing new
* objects.
* </p>
*
* @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.
* <p>
* This method <strong>does not prevent duplicate names</strong>. 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 <code>set(name, value)</code> instead. However,
* <strong> <em>add</em> is much faster than <em>set</em></strong> (because it does not need to
* search for existing members). Therefore <em>add</em> should be preferred when constructing new
* objects.
* </p>
*
* @param name the name of the member to add
* @param value the value of the member to add, must not be <code>null</code>
* @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 <code>int</code> 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.
* <p>
* This method should <strong>only be used to modify existing objects</strong>. To fill a new
* object with members, the method <code>add(name, value)</code> should be preferred which is much
* faster (as it does not need to search for existing members).
* </p>
*
* @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 <code>long</code> 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.
* <p>
* This method should <strong>only be used to modify existing objects</strong>. To fill a new
* object with members, the method <code>add(name, value)</code> should be preferred which is much
* faster (as it does not need to search for existing members).
* </p>
*
* @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 <code>float</code> 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.
* <p>
* This method should <strong>only be used to modify existing objects</strong>. To fill a new
* object with members, the method <code>add(name, value)</code> should be preferred which is much
* faster (as it does not need to search for existing members).
* </p>
*
* @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 <code>double</code> 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.
* <p>
* This method should <strong>only be used to modify existing objects</strong>. To fill a new
* object with members, the method <code>add(name, value)</code> should be preferred which is much
* faster (as it does not need to search for existing members).
* </p>
*
* @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 <code>boolean</code> 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.
* <p>
* This method should <strong>only be used to modify existing objects</strong>. To fill a new
* object with members, the method <code>add(name, value)</code> should be preferred which is much
* faster (as it does not need to search for existing members).
* </p>
*
* @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.
* <p>
* This method should <strong>only be used to modify existing objects</strong>. To fill a new
* object with members, the method <code>add(name, value)</code> should be preferred which is much
* faster (as it does not need to search for existing members).
* </p>
*
* @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.
* <p>
* This method should <strong>only be used to modify existing objects</strong>. To fill a new
* object with members, the method <code>add(name, value)</code> should be preferred which is much
* faster (as it does not need to search for existing members).
* </p>
*
* @param name the name of the member to add
* @param value the value of the member to add, must not be <code>null</code>
* @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 <code>null</code> 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 <code>int</code> 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
* <code>int</code>, 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 <code>long</code> 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
* <code>long</code>, 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 <code>float</code> 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
* <code>float</code>, 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 <code>double</code> 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
* <code>double</code>, 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 <code>boolean</code> 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 <code>true</code> or <code>false</code> 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 <code>String</code> 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 <code>true</code> if this object contains no members.
*
* @return <code>true</code> 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<String> 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<Member> iterator() {
final Iterator<String> namesIterator = names.iterator();
final Iterator<JsonValue> valuesIterator = values.iterator();
return new Iterator<Member>() {
@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<String> namesIterator = names.iterator();
Iterator<JsonValue> 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 <code>null</code>
*/
public String getName() {
return name;
}
/**
* Returns the value of this member.
*
* @return the value of this member, never <code>null</code>
*/
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 <code>JsonObject</code> and both objects contain the same members <em>in
* the same order</em>.
* <p>
* If two JsonObjects are equal, they will also produce the same JSON output.
* </p>
*
* @param object the object to be compared with this JsonObject
* @return <tt>true</tt> if the specified object is equal to this JsonObject, <code>false</code>
* 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;
}
}
}

View file

@ -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 <A> the JSON array type
* @param <O> the JSON object type
*/
public class JsonReader<A, O> {
private static final int MAX_NESTING_LEVEL = 1000;
private static final int DEFAULT_BUFFER_SIZE = 1024;
private final Reader reader;
private final JsonHandler<A, O> 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<A, O> 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.
* <p>
* Characters are read in chunks into a default-sized input buffer. Hence, wrapping a reader in an
* additional <code>BufferedReader</code> likely won't improve reading performance.
* </p>
*
* @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.
* <p>
* Characters are read in chunks into an input buffer of the given size. Hence, wrapping a reader
* in an additional <code>BufferedReader</code> likely won't improve reading performance.
* </p>
*
* @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;
}
}

View file

@ -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);
}
}

View file

@ -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 <strong>object</strong>, an <strong> array</strong>,
* a <strong>number</strong>, a <strong>string</strong>, or one of the literals
* <strong>true</strong>, <strong>false</strong>, and <strong>null</strong>.
* <p>
* The literals <strong>true</strong>, <strong>false</strong>, and <strong>null</strong> are
* represented by the constants
* {@code JsonLiteral.NULL}, {@code JsonLiteral.FALSE}, and {@code JsonLiteral.NULL}.
* </p>
* <p>
* JSON <strong>objects</strong> and <strong>arrays</strong> are represented by the subtypes
* {@link JsonObject} and {@link JsonArray}. Instances of these types can be created using the
* public constructors of these classes.
* </p>
* <p>
* Instances that represent JSON <strong>numbers</strong>, <strong>strings</strong> and
* <strong>boolean</strong> values can be created using the static factory methods
* {@code JSON.parse(String)}, {@code valueOf(long)}, {@code valueOf(double)}, etc.
* </p>
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*/
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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> if this value represents a JSON string
*/
public boolean isString() {
return false;
}
/**
* Detects whether this value represents a boolean value.
*
* @return <code>true</code> if this value represents either the JSON literal <code>true</code> or
* <code>false</code>
*/
public boolean isBoolean() {
return false;
}
/**
* Detects whether this value represents the JSON literal <code>true</code>.
*
* @return <code>true</code> if this value represents the JSON literal <code>true</code>
*/
public boolean isTrue() {
return false;
}
/**
* Detects whether this value represents the JSON literal <code>false</code>.
*
* @return <code>true</code> if this value represents the JSON literal <code>false</code>
*/
public boolean isFalse() {
return false;
}
/**
* Detects whether this value represents the JSON literal <code>null</code>.
*
* @return <code>true</code> if this value represents the JSON literal <code>null</code>
*/
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 <code>int</code> value, assuming that this value represents a
* JSON number that can be interpreted as Java <code>int</code>. If this is not the case, an
* exception is thrown.
* <p>
* To be interpreted as Java <code>int</code>, the JSON number must neither contain an exponent
* nor a fraction part. Moreover, the number must be in the <code>Integer</code> range.
* </p>
*
* @return this value as <code>int</code>
* @throws UnsupportedOperationException if this value is not a JSON number
* @throws NumberFormatException if this JSON number can not be interpreted as <code>int</code> value
*/
public int asInt() {
throw new UnsupportedOperationException("Not a number: " + toString());
}
/**
* Returns this JSON value as a <code>long</code> value, assuming that this value represents a
* JSON number that can be interpreted as Java <code>long</code>. If this is not the case, an
* exception is thrown.
* <p>
* To be interpreted as Java <code>long</code>, the JSON number must neither contain an exponent
* nor a fraction part. Moreover, the number must be in the <code>Long</code> range.
* </p>
*
* @return this value as <code>long</code>
* @throws UnsupportedOperationException if this value is not a JSON number
* @throws NumberFormatException if this JSON number can not be interpreted as <code>long</code> value
*/
public long asLong() {
throw new UnsupportedOperationException();
}
/**
* Returns this JSON value as a <code>float</code> value, assuming that this value represents a
* JSON number. If this is not the case, an exception is thrown.
* <p>
* If the JSON number is out of the <code>Float</code> range, {@link Float#POSITIVE_INFINITY} or
* {@link Float#NEGATIVE_INFINITY} is returned.
* </p>
*
* @return this value as <code>float</code>
* @throws UnsupportedOperationException if this value is not a JSON number
*/
public float asFloat() {
throw new UnsupportedOperationException();
}
/**
* Returns this JSON value as a <code>double</code> value, assuming that this value represents a
* JSON number. If this is not the case, an exception is thrown.
* <p>
* If the JSON number is out of the <code>Double</code> range, {@link Double#POSITIVE_INFINITY} or
* {@link Double#NEGATIVE_INFINITY} is returned.
* </p>
*
* @return this value as <code>double</code>
* @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 <code>boolean</code> value, assuming that this value is either
* <code>true</code> or <code>false</code>. If this is not the case, an exception is thrown.
*
* @return this value as <code>boolean</code>
* @throws UnsupportedOperationException if this value is neither <code>true</code> or <code>false</code>
*/
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 <code>null</code> 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;
}

View file

@ -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]};
}
}

View file

@ -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:
*
* <pre>
* jsonValue.writeTo(writer, WriterConfig.prettyPrint());
* </pre>
*/
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;
}
}
}

View file

@ -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<Style> style;
private Exception exception;
@ -96,47 +97,46 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
private boolean top;
public MarcJsonWriter(OutputStream out) {
this(out, Style.ARRAY);
this(out, EnumSet.of(Style.ARRAY));
}
public MarcJsonWriter(OutputStream out, Style style) {
public MarcJsonWriter(OutputStream out, EnumSet<Style> style) {
this(out, DEFAULT_BUFFER_SIZE, style);
}
public MarcJsonWriter(OutputStream out, int bufferSize, Style style) {
public MarcJsonWriter(OutputStream out, int bufferSize, EnumSet<Style> style) {
this(new OutputStreamWriter(out, StandardCharsets.UTF_8), style, bufferSize);
}
public MarcJsonWriter(Writer writer) {
this(writer, Style.ARRAY, DEFAULT_BUFFER_SIZE);
this(writer, EnumSet.of(Style.ARRAY), DEFAULT_BUFFER_SIZE);
}
public MarcJsonWriter(Writer writer, Style style, int bufferSize) {
public MarcJsonWriter(Writer writer, EnumSet<Style> style, int bufferSize) {
this.writer = new BufferedWriter(writer, bufferSize);
this.jsonWriter = new JsonWriter(this.writer);
this.bufferSize = bufferSize;
this.style = style;
this.lock = new ReentrantLock();
this.sb = new StringBuilder();
this.builder = Marc.builder();
this.top = true;
}
public MarcJsonWriter(String fileNamePattern, int splitlimit) throws IOException {
this(fileNamePattern, splitlimit, Style.LINES, DEFAULT_BUFFER_SIZE, false);
this(fileNamePattern, splitlimit, EnumSet.of(Style.LINES), DEFAULT_BUFFER_SIZE, false);
}
public MarcJsonWriter(String fileNamePattern, int splitlimit, Style style) throws IOException {
public MarcJsonWriter(String fileNamePattern, int splitlimit, EnumSet<Style> style) throws IOException {
this(fileNamePattern, splitlimit, style, DEFAULT_BUFFER_SIZE, false);
}
public MarcJsonWriter(String fileNamePattern, int splitlimit, Style style, int bufferSize, boolean compress)
public MarcJsonWriter(String fileNamePattern, int splitlimit, EnumSet<Style> style, int bufferSize, boolean compress)
throws IOException {
this.fileNameCounter = new AtomicInteger(0);
this.fileNamePattern = fileNamePattern;
this.splitlimit = splitlimit;
this.bufferSize = bufferSize;
this.lock = new ReentrantLock();
this.sb = new StringBuilder();
this.builder = Marc.builder();
this.top = true;
this.style = style;
@ -226,8 +226,12 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
@Override
public void beginCollection() {
if (style == Style.ARRAY) {
sb.append("[");
if (style.contains(Style.ARRAY)) {
try {
jsonWriter.writeArrayOpen();
} catch (IOException e) {
handleException(e);
}
}
}
@ -262,9 +266,11 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
// would confuse us. Plus, we have our own locking here on record level.
lock.lock();
try {
toJson(marcRecord, sb);
writer.write(sb.toString());
sb.setLength(0);
if (style.contains(Style.ALLOW_DUPLICATES)) {
writeWithDuplicateKeys(marcRecord);
} else {
writeUnderlyingMap(marcRecord);
}
recordCounter.incrementAndGet();
afterRecord();
} catch (Exception e) {
@ -288,12 +294,20 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
@Override
public void endCollection() {
if (style == Style.ARRAY) {
sb.append("]");
if (style.contains(Style.ARRAY)) {
try {
jsonWriter.writeArrayClose();
} catch (IOException e) {
handleException(e);
}
if (style == Style.ELASTICSEARCH_BULK) {
}
if (style.contains(Style.ELASTICSEARCH_BULK)) {
// finish with line-feed "\n", not with System.lineSeparator()
sb.append("\n");
try {
writer.write("\n");
} catch (IOException e) {
handleException(e);
}
}
try {
flush();
@ -312,36 +326,105 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
}
/**
* Format MARC record as key-oriented JSON.
*
* @param sb a string builder to append JSON to
* Write MARC record using fields, indicators, and subfield structures,
* therefore allowing duplicate keys in the output.
* @param marcRecord the MARC record
* @throws IOException if writing fails
*/
@SuppressWarnings("unchecked")
private void toJson(MarcRecord marcRecord, StringBuilder sb) {
private void writeWithDuplicateKeys(MarcRecord marcRecord) throws IOException {
if (marcRecord.isEmpty()) {
return;
}
if (top) {
top = false;
if (style == Style.ELASTICSEARCH_BULK) {
if (style.contains(Style.ELASTICSEARCH_BULK)) {
writeMetaDataLine(marcRecord);
}
} else {
switch (style) {
case ARRAY:
sb.append(",");
break;
case LINES:
sb.append("\n");
break;
case ELASTICSEARCH_BULK:
sb.append("\n");
if (style.contains(Style.ARRAY)) {
jsonWriter.writeArraySeparator();
} else if (style.contains(Style.LINES)) {
jsonWriter.writeLiteral("\n");
} else if (style.contains(Style.ELASTICSEARCH_BULK)) {
jsonWriter.writeLiteral("\n");
writeMetaDataLine(marcRecord);
break;
default:
break;
}
}
jsonWriter.writeObjectOpen();
jsonWriter.writeMemberName(FORMAT_TAG);
jsonWriter.writeMemberSeparator();
jsonWriter.writeString(marcRecord.getFormat());
jsonWriter.writeObjectSeparator();
jsonWriter.writeMemberName(TYPE_TAG);
jsonWriter.writeMemberSeparator();
jsonWriter.writeString(marcRecord.getType());
jsonWriter.writeObjectSeparator();
jsonWriter.writeMemberName(LEADER_TAG);
jsonWriter.writeMemberSeparator();
jsonWriter.writeString(marcRecord.getRecordLabel().toString());
jsonWriter.writeObjectSeparator();
boolean fieldseparator = false;
for (MarcField marcField : marcRecord.getFields()) {
if (fieldseparator) {
jsonWriter.writeObjectSeparator();
}
jsonWriter.writeMemberName(marcField.getTag());
jsonWriter.writeMemberSeparator();
if (marcField.isControl()) {
jsonWriter.writeArrayOpen();
jsonWriter.writeString(marcField.getValue());
jsonWriter.writeArrayClose();
} else {
jsonWriter.writeObjectOpen();
jsonWriter.writeMemberName(marcField.getIndicator().replace(' ', '_'));
jsonWriter.writeMemberSeparator();
jsonWriter.writeArrayOpen();
boolean subfieldseparator = false;
for (MarcField.Subfield subfield : marcField.getSubfields()) {
if (subfieldseparator) {
jsonWriter.writeObjectSeparator();
}
jsonWriter.writeObjectOpen();
jsonWriter.writeMemberName(subfield.getId());
jsonWriter.writeMemberSeparator();
jsonWriter.writeString(subfield.getValue());
jsonWriter.writeObjectClose();
subfieldseparator = true;
}
jsonWriter.writeArrayClose();
jsonWriter.writeObjectClose();
}
fieldseparator = true;
}
jsonWriter.writeObjectClose();
}
/**
* Write MARC record from underlying map as key-oriented JSON.
* @param marcRecord the MARC record
* @throws IOException if writing fails
*/
@SuppressWarnings("unchecked")
private void writeUnderlyingMap(MarcRecord marcRecord) throws IOException {
if (marcRecord.isEmpty()) {
return;
}
if (top) {
top = false;
if (style.contains(Style.ELASTICSEARCH_BULK)) {
writeMetaDataLine(marcRecord);
}
} else {
if (style.contains(Style.ARRAY)) {
writer.write(",");
} else if (style.contains(Style.LINES)) {
writer.write("\n");
} else if (style.contains(Style.ELASTICSEARCH_BULK)) {
writer.write("\n");
writeMetaDataLine(marcRecord);
}
}
StringBuilder sb = new StringBuilder();
sb.append("{");
int c0 = 0;
for (Map.Entry<String, Object> tags : marcRecord.entrySet()) {
@ -452,6 +535,7 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
c0++;
}
sb.append('}');
writer.write(sb.toString());
}
public Exception getException() {
@ -462,13 +546,6 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
writer.write(System.lineSeparator());
}
private void handleException(IOException e) {
exception = e;
if (fatalErrors) {
throw new UncheckedIOException(e);
}
}
@Override
public void close() throws IOException {
writer.close();
@ -476,14 +553,6 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
@Override
public void flush() throws IOException {
if (sb.length() > 0) {
try {
writer.write(sb.toString());
} catch (IOException e) {
handleException(e);
}
sb.setLength(0);
}
writer.flush();
}
@ -512,6 +581,7 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
writer = new OutputStreamWriter(compress ?
new CompressedOutputStream(out, bufferSize) :
new BufferedOutputStream(out, bufferSize), StandardCharsets.UTF_8);
jsonWriter = new JsonWriter(writer);
}
private void writeMetaDataLine(MarcRecord marcRecord) {
@ -526,11 +596,22 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
}
id = object.toString();
if (index != null && indexType != null && id != null) {
sb.append("{\"index\":{")
.append("\"_index\":\"").append(index).append("\",")
.append("\"_type\":\"").append(indexType).append("\",")
.append("\"_id\":\"").append(id).append("\"}}")
.append("\n");
try {
writer.write("{\"index\":{" +
"\"_index\":\"" + index + "\"," +
"\"_type\":\"" + indexType + "\"," +
"\"_id\":\"" + id + "\"}}" +
"\n");
} catch (IOException e) {
handleException(e);
}
}
}
private void handleException(IOException e) {
exception = e;
if (fatalErrors) {
throw new UncheckedIOException(e);
}
}
@ -538,7 +619,7 @@ public class MarcJsonWriter extends MarcContentHandler implements Flushable, Clo
*
*/
public enum Style {
ARRAY, LINES, ELASTICSEARCH_BULK
ARRAY, LINES, ELASTICSEARCH_BULK, ALLOW_DUPLICATES
}
/**

View file

@ -26,6 +26,7 @@ import org.xbib.marc.xml.MarcXchangeWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.EnumSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@ -129,7 +130,7 @@ public class ConcurrencyTest {
File file = File.createTempFile(s + ".", ".jsonlines");
file.deleteOnExit();
FileOutputStream out = new FileOutputStream(file);
try (MarcJsonWriter writer = new MarcJsonWriter(out, MarcJsonWriter.Style.LINES)
try (MarcJsonWriter writer = new MarcJsonWriter(out, EnumSet.of(MarcJsonWriter.Style.LINES))
.setFormat(MarcXchangeConstants.MARCXCHANGE_FORMAT)
.setType(MarcXchangeConstants.BIBLIOGRAPHIC_TYPE)
) {

View file

@ -36,6 +36,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.EnumSet;
/**
*
@ -126,7 +127,7 @@ public class ZDBTest {
OutputStream out = new FileOutputStream(file);
MarcValueTransformers marcValueTransformers = new MarcValueTransformers();
marcValueTransformers.setMarcValueTransformer(value -> Normalizer.normalize(value, Normalizer.Form.NFC));
try (MarcJsonWriter writer = new MarcJsonWriter(out, MarcJsonWriter.Style.LINES)
try (MarcJsonWriter writer = new MarcJsonWriter(out, EnumSet.of(MarcJsonWriter.Style.LINES))
.setFormat(MarcXchangeConstants.MARCXCHANGE_FORMAT)
.setType(MarcXchangeConstants.BIBLIOGRAPHIC_TYPE)
.setMarcValueTransformers(marcValueTransformers)) {

View file

@ -18,6 +18,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
/**
*
@ -57,7 +58,7 @@ public class MabXmlTest {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InputStream in = getClass().getResourceAsStream(s);
MarcJsonWriter writer = new MarcJsonWriter(out,
10, MarcJsonWriter.Style.ELASTICSEARCH_BULK)
10, EnumSet.of(MarcJsonWriter.Style.ELASTICSEARCH_BULK))
.setIndex("testindex", "testtype")) {
MarcContentHandler contentHandler = new MabXMLContentHandler()
.addNamespace("http://www.ddb.de/professionell/mabxml/mabxml-1.xsd")

View file

@ -0,0 +1,520 @@
/*
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import java.io.IOException;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
/**
*
*/
public class JsonArrayTest extends TestUtil {
private JsonArray array;
private static JsonArray array(String... values) {
JsonArray array = new JsonArray();
for (String value : values) {
array.add(value);
}
return array;
}
@Before
public void setUp() {
array = new JsonArray();
}
@Test
public void copyConstructorfailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> new JsonArray(null));
}
@Test
public void copyConstructorhasSameValues() {
array.add(23);
JsonArray copy = new JsonArray(array);
assertEquals(array.values(), copy.values());
}
@Test
public void copyConstructorworksOnSafeCopy() {
JsonArray copy = new JsonArray(array);
array.add(23);
assertTrue(copy.isEmpty());
}
@Test
public void isEmptyisTrueAfterCreation() {
assertTrue(array.isEmpty());
}
@Test
public void isEmptyisFalseAfterAdd() {
array.add(true);
assertFalse(array.isEmpty());
}
@Test
public void sizeisZeroAfterCreation() {
assertEquals(0, array.size());
}
@Test
public void sizeisOneAfterAdd() {
array.add(true);
assertEquals(1, array.size());
}
@Test
public void iteratorisEmptyAfterCreation() {
assertFalse(array.iterator().hasNext());
}
@Test
public void iteratorhasNextAfterAdd() {
array.add(true);
Iterator<JsonValue> iterator = array.iterator();
assertTrue(iterator.hasNext());
assertEquals(JsonLiteral.TRUE, iterator.next());
assertFalse(iterator.hasNext());
}
@Test(expected = UnsupportedOperationException.class)
public void iteratordoesNotAllowModification() {
array.add(23);
Iterator<JsonValue> iterator = array.iterator();
iterator.next();
iterator.remove();
}
@Test(expected = ConcurrentModificationException.class)
public void iteratordetectsConcurrentModification() {
Iterator<JsonValue> iterator = array.iterator();
array.add(23);
iterator.next();
}
@Test
public void valuesisEmptyAfterCreation() {
assertTrue(array.values().isEmpty());
}
@Test
public void valuescontainsValueAfterAdd() {
array.add(true);
assertEquals(1, array.values().size());
assertEquals(JsonLiteral.TRUE, array.values().get(0));
}
@Test
public void valuesReflectsChanges() {
List<JsonValue> values = array.values();
array.add(true);
assertEquals(array.values(), values);
}
@Test
public void valuesPreventsModification() {
List<JsonValue> values = array.values();
values.add(JsonLiteral.TRUE);
}
@Test
public void getreturnsValue() {
array.add(23);
JsonValue value = array.get(0);
assertEquals(Json.of(23), value);
}
@Test(expected = IndexOutOfBoundsException.class)
public void getfailsWithInvalidIndex() {
array.get(0);
}
@Test
public void addint() {
array.add(23);
assertEquals("[23]", array.toString());
}
@Test
public void addintenablesChaining() {
assertSame(array, array.add(23));
}
@Test
public void addlong() {
array.add(23L);
assertEquals("[23]", array.toString());
}
@Test
public void addlongenablesChaining() {
assertSame(array, array.add(23L));
}
@Test
public void addfloat() {
array.add(3.14f);
assertEquals("[3.14]", array.toString());
}
@Test
public void addfloatenablesChaining() {
assertSame(array, array.add(3.14f));
}
@Test
public void adddouble() {
array.add(3.14d);
assertEquals("[3.14]", array.toString());
}
@Test
public void adddoubleenablesChaining() {
assertSame(array, array.add(3.14d));
}
@Test
public void addboolean() {
array.add(true);
assertEquals("[true]", array.toString());
}
@Test
public void addbooleanenablesChaining() {
assertSame(array, array.add(true));
}
@Test
public void addstring() {
array.add("foo");
assertEquals("[\"foo\"]", array.toString());
}
@Test
public void addstringenablesChaining() {
assertSame(array, array.add("foo"));
}
@Test
public void addstringtoleratesNull() {
array.add((String) null);
assertEquals("[null]", array.toString());
}
@Test
public void addjsonNull() {
array.add(JsonLiteral.NULL);
assertEquals("[null]", array.toString());
}
@Test
public void addjsonArray() {
array.add(new JsonArray());
assertEquals("[[]]", array.toString());
}
@Test
public void addjsonObject() {
array.add(new JsonObject());
assertEquals("[{}]", array.toString());
}
@Test
public void addjsonenablesChaining() {
assertSame(array, array.add(JsonLiteral.NULL));
}
@Test
public void addjsonfailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> array.add((JsonValue) null));
}
@Test
public void addjsonnestedArray() {
JsonArray innerArray = new JsonArray();
innerArray.add(23);
array.add(innerArray);
assertEquals("[[23]]", array.toString());
}
@Test
public void addjsonnestedArraymodifiedAfterAdd() {
JsonArray innerArray = new JsonArray();
array.add(innerArray);
innerArray.add(23);
assertEquals("[[23]]", array.toString());
}
@Test
public void addjsonnestedObject() {
JsonObject innerObject = new JsonObject();
innerObject.add("a", 23);
array.add(innerObject);
assertEquals("[{\"a\":23}]", array.toString());
}
@Test
public void addjsonnestedObjectmodifiedAfterAdd() {
JsonObject innerObject = new JsonObject();
array.add(innerObject);
innerObject.add("a", 23);
assertEquals("[{\"a\":23}]", array.toString());
}
@Test
public void setint() {
array.add(false);
array.set(0, 23);
assertEquals("[23]", array.toString());
}
@Test
public void setintenablesChaining() {
array.add(false);
assertSame(array, array.set(0, 23));
}
@Test
public void setlong() {
array.add(false);
array.set(0, 23L);
assertEquals("[23]", array.toString());
}
@Test
public void setlongenablesChaining() {
array.add(false);
assertSame(array, array.set(0, 23L));
}
@Test
public void setfloat() {
array.add(false);
array.set(0, 3.14f);
assertEquals("[3.14]", array.toString());
}
@Test
public void setfloatenablesChaining() {
array.add(false);
assertSame(array, array.set(0, 3.14f));
}
@Test
public void setdouble() {
array.add(false);
array.set(0, 3.14d);
assertEquals("[3.14]", array.toString());
}
@Test
public void setdoubleenablesChaining() {
array.add(false);
assertSame(array, array.set(0, 3.14d));
}
@Test
public void setboolean() {
array.add(false);
array.set(0, true);
assertEquals("[true]", array.toString());
}
@Test
public void setbooleanenablesChaining() {
array.add(false);
assertSame(array, array.set(0, true));
}
@Test
public void setstring() {
array.add(false);
array.set(0, "foo");
assertEquals("[\"foo\"]", array.toString());
}
@Test
public void setstringenablesChaining() {
array.add(false);
assertSame(array, array.set(0, "foo"));
}
@Test
public void setjsonNull() {
array.add(false);
array.set(0, JsonLiteral.NULL);
assertEquals("[null]", array.toString());
}
@Test
public void setjsonArray() {
array.add(false);
array.set(0, new JsonArray());
assertEquals("[[]]", array.toString());
}
@Test
public void setjsonObject() {
array.add(false);
array.set(0, new JsonObject());
assertEquals("[{}]", array.toString());
}
@Test
public void setJsonFailsWithNull() {
array.add(false);
assertException(NullPointerException.class, null, (Runnable) () -> array.set(0, (JsonValue) null));
}
@Test(expected = IndexOutOfBoundsException.class)
public void setjsonfailsWithInvalidIndex() {
array.set(0, JsonLiteral.NULL);
}
@Test
public void setjsonenablesChaining() {
array.add(false);
assertSame(array, array.set(0, JsonLiteral.NULL));
}
@Test
public void setjsonreplacesDifferntArrayElements() {
array.add(3).add(6).add(9);
array.set(1, 4).set(2, 5);
assertEquals("[3,4,5]", array.toString());
}
@Test(expected = IndexOutOfBoundsException.class)
public void removefailsWithInvalidIndex() {
array.remove(0);
}
@Test
public void removeremovesElement() {
array.add(23);
array.remove(0);
assertEquals("[]", array.toString());
}
@Test
public void removekeepsOtherElements() {
array.add("a").add("b").add("c");
array.remove(1);
assertEquals("[\"a\",\"c\"]", array.toString());
}
@Test
public void writeempty() throws IOException {
JsonWriter writer = mock(JsonWriter.class);
array.write(writer);
InOrder inOrder = inOrder(writer);
inOrder.verify(writer).writeArrayOpen();
inOrder.verify(writer).writeArrayClose();
inOrder.verifyNoMoreInteractions();
}
@Test
public void writewithSingleValue() throws IOException {
JsonWriter writer = mock(JsonWriter.class);
array.add(23);
array.write(writer);
InOrder inOrder = inOrder(writer);
inOrder.verify(writer).writeArrayOpen();
inOrder.verify(writer).writeNumber("23");
inOrder.verify(writer).writeArrayClose();
inOrder.verifyNoMoreInteractions();
}
@Test
public void writewithMultipleValues() throws IOException {
JsonWriter writer = mock(JsonWriter.class);
array.add(23).add("foo").add(false);
array.write(writer);
InOrder inOrder = inOrder(writer);
inOrder.verify(writer).writeArrayOpen();
inOrder.verify(writer).writeNumber("23");
inOrder.verify(writer).writeArraySeparator();
inOrder.verify(writer).writeString("foo");
inOrder.verify(writer).writeArraySeparator();
inOrder.verify(writer).writeLiteral("false");
inOrder.verify(writer).writeArrayClose();
inOrder.verifyNoMoreInteractions();
}
@Test
public void isArray() {
assertTrue(array.isArray());
}
@Test
public void asArray() {
assertSame(array, array.asArray());
}
@Test
public void equalstrueForEqualArrays() {
assertEquals(array(), array());
assertEquals(array("foo", "bar"), array("foo", "bar"));
}
@Test
public void equalsfalseForDifferentArrays() {
assertNotEquals(array("foo", "bar"), array("foo", "bar", "baz"));
assertNotEquals(array("foo", "bar"), array("bar", "foo"));
}
@Test
public void equalsfalseForNull() {
assertNotEquals(null, array);
}
@Test
public void equalsfalseForSubclass() {
assertNotEquals(array, new JsonArray(array) {
});
}
@Test
public void hashCodeequalsForEqualArrays() {
assertEquals(array().hashCode(), array().hashCode());
assertEquals(array("foo").hashCode(), array("foo").hashCode());
}
@Test
public void hashCodediffersForDifferentArrays() {
assertNotEquals(array().hashCode(), array("bar").hashCode());
assertNotEquals(array("foo").hashCode(), array("bar").hashCode());
}
}

View file

@ -0,0 +1,133 @@
/*
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.xbib.marc.json.JsonLiteral.FALSE;
import static org.xbib.marc.json.JsonLiteral.NULL;
import static org.xbib.marc.json.JsonLiteral.TRUE;
import org.junit.Test;
import java.io.IOException;
/**
*
*/
public class JsonLiteralTest {
@Test
public void isNull() {
assertTrue(NULL.isNull());
assertFalse(TRUE.isNull());
assertFalse(FALSE.isNull());
}
@Test
public void isTrue() {
assertTrue(TRUE.isTrue());
assertFalse(NULL.isTrue());
assertFalse(FALSE.isTrue());
}
@Test
public void isFalse() {
assertTrue(FALSE.isFalse());
assertFalse(NULL.isFalse());
assertFalse(TRUE.isFalse());
}
@Test
public void isBoolean() {
assertTrue(TRUE.isBoolean());
assertTrue(FALSE.isBoolean());
assertFalse(NULL.isBoolean());
}
@Test
public void nullwrite() throws IOException {
JsonWriter writer = mock(JsonWriter.class);
NULL.write(writer);
verify(writer).writeLiteral("null");
verifyNoMoreInteractions(writer);
}
@Test
public void truewrite() throws IOException {
JsonWriter writer = mock(JsonWriter.class);
TRUE.write(writer);
verify(writer).writeLiteral("true");
verifyNoMoreInteractions(writer);
}
@Test
public void falsewrite() throws IOException {
JsonWriter writer = mock(JsonWriter.class);
FALSE.write(writer);
verify(writer).writeLiteral("false");
verifyNoMoreInteractions(writer);
}
@Test
public void nulltoString() {
assertEquals("null", NULL.toString());
}
@Test
public void truetoString() {
assertEquals("true", TRUE.toString());
}
@Test
public void falsetoString() {
assertEquals("false", FALSE.toString());
}
@Test
public void nullequals() {
assertEquals(NULL, NULL);
assertNotEquals(null, NULL);
assertNotEquals(NULL, TRUE);
assertNotEquals(NULL, FALSE);
assertNotEquals(NULL, Json.of("null"));
}
@Test
public void trueequals() {
assertEquals(TRUE, TRUE);
assertNotEquals(null, TRUE);
assertNotEquals(TRUE, FALSE);
assertNotEquals(TRUE, Boolean.TRUE);
assertNotEquals(NULL, Json.of("true"));
}
@Test
public void falseequals() {
assertEquals(FALSE, FALSE);
assertNotEquals(null, FALSE);
assertNotEquals(FALSE, TRUE);
assertNotEquals(FALSE, Boolean.FALSE);
assertNotEquals(NULL, Json.of("false"));
}
}

View file

@ -0,0 +1,77 @@
/*
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 static org.junit.Assert.assertEquals;
import org.junit.Test;
import java.io.IOException;
import java.util.Objects;
/**
*/
public class JsonMapperTest {
@Test
public void mapperMapTest() throws IOException {
String json = "{\"Hello\":\"World\"}";
JsonValue jsonValue = Json.parse(json);
Object object = JsonMapper.asObject(jsonValue);
assertEquals("{Hello=World}", Objects.requireNonNull(object).toString());
}
@Test
public void mapperNumericMapTest() throws IOException {
String json = "{\"Hello\":123}";
JsonValue jsonValue = Json.parse(json);
Object object = JsonMapper.asObject(jsonValue);
assertEquals("{Hello=123}", Objects.requireNonNull(object).toString());
}
@Test
public void mapperArrayTest() throws IOException {
String json = "[\"Hello\",\"World\"]";
JsonValue jsonValue = Json.parse(json);
Object object = JsonMapper.asObject(jsonValue);
assertEquals("[Hello, World]", Objects.requireNonNull(object).toString());
}
@Test
public void mapperBooleanAndNullArrayTest() throws IOException {
String json = "[true, false, null]";
JsonValue jsonValue = Json.parse(json);
Object object = JsonMapper.asObject(jsonValue);
assertEquals("[true, false, null]", Objects.requireNonNull(object).toString());
}
@Test
public void mapperFloatArrayTest() throws IOException {
String json = "[1.23, 4.56]";
JsonValue jsonValue = Json.parse(json);
Object object = JsonMapper.asObject(jsonValue);
assertEquals("[1.23, 4.56]", Objects.requireNonNull(object).toString());
}
@Test
public void mapperIntArrayTest() throws IOException {
String json = "[123, 456]";
JsonValue jsonValue = Json.parse(json);
Object object = JsonMapper.asObject(jsonValue);
assertEquals("[123, 456]", Objects.requireNonNull(object).toString());
}
}

View file

@ -0,0 +1,164 @@
/*
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.StringWriter;
/**
*
*/
public class JsonNumberTest extends TestUtil {
private StringWriter output;
private JsonWriter writer;
@Before
public void setUp() {
output = new StringWriter();
writer = new JsonWriter(output);
}
@Test
public void constructorfailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> new JsonNumber(null));
}
@Test
public void write() throws IOException {
new JsonNumber("23").write(writer);
assertEquals("23", output.toString());
}
@Test
public void toStringreturnsInputString() {
assertEquals("foo", new JsonNumber("foo").toString());
}
@Test
public void isInt() {
assertTrue(new JsonNumber("23").isInt());
}
@Test
public void asInt() {
assertEquals(23, new JsonNumber("23").asInt());
}
@Test(expected = NumberFormatException.class)
public void asIntfailsWithExceedingValues() {
new JsonNumber("10000000000").asInt();
}
@Test(expected = NumberFormatException.class)
public void asIntfailsWithExponent() {
new JsonNumber("1e5").asInt();
}
@Test(expected = NumberFormatException.class)
public void asIntfailsWithFractional() {
new JsonNumber("23.5").asInt();
}
@Test
public void asLong() {
assertEquals(23L, new JsonNumber("23").asLong());
}
@Test(expected = NumberFormatException.class)
public void asLongfailsWithExceedingValues() {
new JsonNumber("10000000000000000000").asLong();
}
@Test(expected = NumberFormatException.class)
public void asLongfailsWithExponent() {
new JsonNumber("1e5").asLong();
}
@Test(expected = NumberFormatException.class)
public void asLongfailsWithFractional() {
new JsonNumber("23.5").asLong();
}
@Test
public void asFloat() {
assertEquals(23.05f, new JsonNumber("23.05").asFloat(), 0);
}
@Test
public void asFloatreturnsInfinityForExceedingValues() {
assertEquals(Float.POSITIVE_INFINITY, new JsonNumber("1e50").asFloat(), 0);
assertEquals(Float.NEGATIVE_INFINITY, new JsonNumber("-1e50").asFloat(), 0);
}
@Test
public void asDouble() {
double result = new JsonNumber("23.05").asDouble();
assertEquals(23.05, result, 0);
}
@Test
public void asDoublereturnsInfinityForExceedingValues() {
assertEquals(Double.POSITIVE_INFINITY, new JsonNumber("1e500").asDouble(), 0);
assertEquals(Double.NEGATIVE_INFINITY, new JsonNumber("-1e500").asDouble(), 0);
}
@Test
public void equalstrueForSameInstance() {
JsonNumber number = new JsonNumber("23");
assertEquals(number, number);
}
@Test
public void equalstrueForEqualNumberStrings() {
assertEquals(new JsonNumber("23"), new JsonNumber("23"));
}
@Test
public void equalsfalseForDifferentNumberStrings() {
assertNotEquals(new JsonNumber("23"), new JsonNumber("42"));
assertNotEquals(new JsonNumber("1e+5"), new JsonNumber("1e5"));
}
@Test
public void equalsfalseForNull() {
assertNotEquals(null, new JsonNumber("23"));
}
@Test
public void equalsfalseForSubclass() {
assertNotEquals(new JsonNumber("23"), new JsonNumber("23") {
});
}
@Test
public void hashCodeequalsForEqualStrings() {
assertEquals(new JsonNumber("23").hashCode(), new JsonNumber("23").hashCode());
}
@Test
public void hashCodediffersForDifferentStrings() {
assertNotEquals(new JsonNumber("23").hashCode(), new JsonNumber("42").hashCode());
}
}

View file

@ -0,0 +1,952 @@
/*
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import java.io.IOException;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
/**
*
*/
public class JsonObjectTest extends TestUtil {
private JsonObject object;
private static JsonObject object(String... namesAndValues) {
JsonObject object = new JsonObject();
for (int i = 0; i < namesAndValues.length; i += 2) {
object.add(namesAndValues[i], namesAndValues[i + 1]);
}
return object;
}
@Before
public void setUp() {
object = new JsonObject();
}
@Test
public void copyConstructorfailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> new JsonObject(null));
}
@Test
public void copyConstructorhasSameValues() {
object.add("foo", 23);
JsonObject copy = new JsonObject(object);
assertEquals(object.names(), copy.names());
assertSame(object.get("foo"), copy.get("foo"));
}
@Test
public void copyConstructorworksOnSafeCopy() {
JsonObject copy = new JsonObject(object);
object.add("foo", 23);
assertTrue(copy.isEmpty());
}
@Test
public void isEmptytrueAfterCreation() {
assertTrue(object.isEmpty());
}
@Test
public void isEmptyfalseAfterAdd() {
object.add("a", true);
assertFalse(object.isEmpty());
}
@Test
public void sizezeroAfterCreation() {
assertEquals(0, object.size());
}
@Test
public void sizeoneAfterAdd() {
object.add("a", true);
assertEquals(1, object.size());
}
@Test
public void keyRepetitionallowsMultipleEntries() {
object.add("a", true);
object.add("a", "value");
assertEquals(2, object.size());
}
@Test
public void keyRepetitiongetsLastEntry() {
object.add("a", true);
object.add("a", "value");
assertEquals("value", object.getString("a", "missing"));
}
@Test
public void keyRepetitionequalityConsidersRepetitions() {
object.add("a", true);
object.add("a", "value");
JsonObject onlyFirstProperty = new JsonObject();
onlyFirstProperty.add("a", true);
assertNotEquals(onlyFirstProperty, object);
JsonObject bothProperties = new JsonObject();
bothProperties.add("a", true);
bothProperties.add("a", "value");
assertEquals(bothProperties, object);
}
@Test
public void namesemptyAfterCreation() {
assertTrue(object.names().isEmpty());
}
@Test
public void namescontainsNameAfterAdd() {
object.add("foo", true);
List<String> names = object.names();
assertEquals(1, names.size());
assertEquals("foo", names.get(0));
}
@Test
public void namesreflectsChanges() {
List<String> names = object.names();
object.add("foo", true);
assertEquals(1, names.size());
assertEquals("foo", names.get(0));
}
@Test(expected = UnsupportedOperationException.class)
public void namespreventsModification() {
List<String> names = object.names();
names.add("foo");
}
@Test
public void iteratorisEmptyAfterCreation() {
assertFalse(object.iterator().hasNext());
}
@Test
public void iteratorhasNextAfterAdd() {
object.add("a", true);
Iterator<JsonObject.Member> iterator = object.iterator();
assertTrue(iterator.hasNext());
}
@Test
public void iteratornextReturnsActualValue() {
object.add("a", true);
Iterator<JsonObject.Member> iterator = object.iterator();
assertEquals(new JsonObject.Member("a", JsonLiteral.TRUE), iterator.next());
}
@Test
public void iteratornextProgressesToNextValue() {
object.add("a", true);
object.add("b", false);
Iterator<JsonObject.Member> iterator = object.iterator();
iterator.next();
assertTrue(iterator.hasNext());
assertEquals(new JsonObject.Member("b", JsonLiteral.FALSE), iterator.next());
}
@Test(expected = NoSuchElementException.class)
public void iteratornextFailsAtEnd() {
Iterator<JsonObject.Member> iterator = object.iterator();
iterator.next();
}
@Test(expected = UnsupportedOperationException.class)
public void iteratordoesNotAllowModification() {
object.add("a", 23);
Iterator<JsonObject.Member> iterator = object.iterator();
iterator.next();
iterator.remove();
}
@Test(expected = ConcurrentModificationException.class)
public void iteratordetectsConcurrentModification() {
Iterator<JsonObject.Member> iterator = object.iterator();
object.add("a", 23);
iterator.next();
}
@Test
public void getfailsWithNullName() {
assertException(NullPointerException.class, null, (Runnable) () -> object.get(null));
}
@Test
public void getreturnsNullForNonExistingMember() {
assertNull(object.get("foo"));
}
@Test
public void getreturnsValueForName() {
object.add("foo", true);
assertEquals(JsonLiteral.TRUE, object.get("foo"));
}
@Test
public void getreturnsLastValueForName() {
object.add("foo", false).add("foo", true);
assertEquals(JsonLiteral.TRUE, object.get("foo"));
}
@Test
public void getintreturnsValueFromMember() {
object.add("foo", 23);
assertEquals(23, object.getInt("foo", 42));
}
@Test
public void getintreturnsDefaultForMissingMember() {
assertEquals(23, object.getInt("foo", 23));
}
@Test
public void getlongreturnsValueFromMember() {
object.add("foo", 23L);
assertEquals(23L, object.getLong("foo", 42L));
}
@Test
public void getlongreturnsDefaultForMissingMember() {
assertEquals(23L, object.getLong("foo", 23L));
}
@Test
public void getfloatreturnsValueFromMember() {
object.add("foo", 3.14f);
assertEquals(3.14f, object.getFloat("foo", 1.41f), 0);
}
@Test
public void getfloatreturnsDefaultForMissingMember() {
assertEquals(3.14f, object.getFloat("foo", 3.14f), 0);
}
@Test
public void getdoublereturnsValueFromMember() {
object.add("foo", 3.14);
assertEquals(3.14, object.getDouble("foo", 1.41), 0);
}
@Test
public void getdoublereturnsDefaultForMissingMember() {
assertEquals(3.14, object.getDouble("foo", 3.14), 0);
}
@Test
public void getbooleanreturnsValueFromMember() {
object.add("foo", true);
assertTrue(object.getBoolean("foo", false));
}
@Test
public void getbooleanreturnsDefaultForMissingMember() {
assertFalse(object.getBoolean("foo", false));
}
@Test
public void getstringreturnsValueFromMember() {
object.add("foo", "bar");
assertEquals("bar", object.getString("foo", "default"));
}
@Test
public void getstringreturnsDefaultForMissingMember() {
assertEquals("default", object.getString("foo", "default"));
}
@Test
public void addfailsWithNullName() {
assertException(NullPointerException.class, "name is null", (Runnable) () -> object.add(null, 23));
}
@Test
public void addint() {
object.add("a", 23);
assertEquals("{\"a\":23}", object.toString());
}
@Test
public void addintenablesChaining() {
assertSame(object, object.add("a", 23));
}
@Test
public void addlong() {
object.add("a", 23L);
assertEquals("{\"a\":23}", object.toString());
}
@Test
public void addlongenablesChaining() {
assertSame(object, object.add("a", 23L));
}
@Test
public void addfloat() {
object.add("a", 3.14f);
assertEquals("{\"a\":3.14}", object.toString());
}
@Test
public void addfloatenablesChaining() {
assertSame(object, object.add("a", 3.14f));
}
@Test
public void adddouble() {
object.add("a", 3.14d);
assertEquals("{\"a\":3.14}", object.toString());
}
@Test
public void adddoubleenablesChaining() {
assertSame(object, object.add("a", 3.14d));
}
@Test
public void addboolean() {
object.add("a", true);
assertEquals("{\"a\":true}", object.toString());
}
@Test
public void addbooleanenablesChaining() {
assertSame(object, object.add("a", true));
}
@Test
public void addstring() {
object.add("a", "foo");
assertEquals("{\"a\":\"foo\"}", object.toString());
}
@Test
public void addstringtoleratesNull() {
object.add("a", (String) null);
assertEquals("{\"a\":null}", object.toString());
}
@Test
public void addstringenablesChaining() {
assertSame(object, object.add("a", "foo"));
}
@Test
public void addjsonNull() {
object.add("a", JsonLiteral.NULL);
assertEquals("{\"a\":null}", object.toString());
}
@Test
public void addjsonArray() {
object.add("a", new JsonArray());
assertEquals("{\"a\":[]}", object.toString());
}
@Test
public void addjsonObject() {
object.add("a", new JsonObject());
assertEquals("{\"a\":{}}", object.toString());
}
@Test
public void addjsonenablesChaining() {
assertSame(object, object.add("a", JsonLiteral.NULL));
}
@Test
public void addjsonfailsWithNull() {
assertException(NullPointerException.class, "value is null", (Runnable) () -> object.add("a", (JsonValue) null));
}
@Test
public void addjsonnestedArray() {
JsonArray innerArray = new JsonArray();
innerArray.add(23);
object.add("a", innerArray);
assertEquals("{\"a\":[23]}", object.toString());
}
@Test
public void addjsonnestedArraymodifiedAfterAdd() {
JsonArray innerArray = new JsonArray();
object.add("a", innerArray);
innerArray.add(23);
assertEquals("{\"a\":[23]}", object.toString());
}
@Test
public void addjsonnestedObject() {
JsonObject innerObject = new JsonObject();
innerObject.add("a", 23);
object.add("a", innerObject);
assertEquals("{\"a\":{\"a\":23}}", object.toString());
}
@Test
public void addjsonnestedObjectmodifiedAfterAdd() {
JsonObject innerObject = new JsonObject();
object.add("a", innerObject);
innerObject.add("a", 23);
assertEquals("{\"a\":{\"a\":23}}", object.toString());
}
@Test
public void setint() {
object.set("a", 23);
assertEquals("{\"a\":23}", object.toString());
}
@Test
public void setintenablesChaining() {
assertSame(object, object.set("a", 23));
}
@Test
public void setlong() {
object.set("a", 23L);
assertEquals("{\"a\":23}", object.toString());
}
@Test
public void setlongenablesChaining() {
assertSame(object, object.set("a", 23L));
}
@Test
public void setfloat() {
object.set("a", 3.14f);
assertEquals("{\"a\":3.14}", object.toString());
}
@Test
public void setfloatenablesChaining() {
assertSame(object, object.set("a", 3.14f));
}
@Test
public void setdouble() {
object.set("a", 3.14d);
assertEquals("{\"a\":3.14}", object.toString());
}
@Test
public void setdoubleenablesChaining() {
assertSame(object, object.set("a", 3.14d));
}
@Test
public void setboolean() {
object.set("a", true);
assertEquals("{\"a\":true}", object.toString());
}
@Test
public void setbooleanenablesChaining() {
assertSame(object, object.set("a", true));
}
@Test
public void setstring() {
object.set("a", "foo");
assertEquals("{\"a\":\"foo\"}", object.toString());
}
@Test
public void setstringenablesChaining() {
assertSame(object, object.set("a", "foo"));
}
@Test
public void setjsonNull() {
object.set("a", JsonLiteral.NULL);
assertEquals("{\"a\":null}", object.toString());
}
@Test
public void setjsonArray() {
object.set("a", new JsonArray());
assertEquals("{\"a\":[]}", object.toString());
}
@Test
public void setjsonObject() {
object.set("a", new JsonObject());
assertEquals("{\"a\":{}}", object.toString());
}
@Test
public void setjsonenablesChaining() {
assertSame(object, object.set("a", JsonLiteral.NULL));
}
@Test
public void setaddsElementIfMissing() {
object.set("a", JsonLiteral.TRUE);
assertEquals("{\"a\":true}", object.toString());
}
@Test
public void setmodifiesElementIfExisting() {
object.add("a", JsonLiteral.TRUE);
object.set("a", JsonLiteral.FALSE);
assertEquals("{\"a\":false}", object.toString());
}
@Test
public void setmodifiesLastElementIfMultipleExisting() {
object.add("a", 1);
object.add("a", 2);
object.set("a", JsonLiteral.TRUE);
assertEquals("{\"a\":1,\"a\":true}", object.toString());
}
@Test
public void removefailsWithNullName() {
assertException(NullPointerException.class, null, (Runnable) () -> object.remove(null));
}
@Test
public void removeremovesMatchingMember() {
object.add("a", 23);
object.remove("a");
assertEquals("{}", object.toString());
}
@Test
public void removeremovesOnlyMatchingMember() {
object.add("a", 23);
object.add("b", 42);
object.add("c", true);
object.remove("b");
assertEquals("{\"a\":23,\"c\":true}", object.toString());
}
@Test
public void removeremovesOnlyLastMatchingMember() {
object.add("a", 23);
object.add("a", 42);
object.remove("a");
assertEquals("{\"a\":23}", object.toString());
}
@Test
public void removeremovesOnlyLastMatchingMemberafterRemove() {
object.add("a", 23);
object.remove("a");
object.add("a", 42);
object.add("a", 47);
object.remove("a");
assertEquals("{\"a\":42}", object.toString());
}
@Test
public void removedoesNotModifyObjectWithoutMatchingMember() {
object.add("a", 23);
object.remove("b");
assertEquals("{\"a\":23}", object.toString());
}
@Test
public void mergefailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> object.merge(null));
}
@Test
public void mergeappendsMembers() {
object.add("a", 1).add("b", 1);
object.merge(Json.object().add("c", 2).add("d", 2));
assertEquals(Json.object().add("a", 1).add("b", 1).add("c", 2).add("d", 2), object);
}
@Test
public void mergereplacesMembers() {
object.add("a", 1).add("b", 1).add("c", 1);
object.merge(Json.object().add("b", 2).add("d", 2));
assertEquals(Json.object().add("a", 1).add("b", 2).add("c", 1).add("d", 2), object);
}
@Test
public void writeempty() throws IOException {
JsonWriter writer = mock(JsonWriter.class);
object.write(writer);
InOrder inOrder = inOrder(writer);
inOrder.verify(writer).writeObjectOpen();
inOrder.verify(writer).writeObjectClose();
inOrder.verifyNoMoreInteractions();
}
@Test
public void writewithSingleValue() throws IOException {
JsonWriter writer = mock(JsonWriter.class);
object.add("a", 23);
object.write(writer);
InOrder inOrder = inOrder(writer);
inOrder.verify(writer).writeObjectOpen();
inOrder.verify(writer).writeMemberName("a");
inOrder.verify(writer).writeMemberSeparator();
inOrder.verify(writer).writeNumber("23");
inOrder.verify(writer).writeObjectClose();
inOrder.verifyNoMoreInteractions();
}
@Test
public void writewithMultipleValues() throws IOException {
JsonWriter writer = mock(JsonWriter.class);
object.add("a", 23);
object.add("b", 3.14f);
object.add("c", "foo");
object.add("d", true);
object.add("e", (String) null);
object.write(writer);
InOrder inOrder = inOrder(writer);
inOrder.verify(writer).writeObjectOpen();
inOrder.verify(writer).writeMemberName("a");
inOrder.verify(writer).writeMemberSeparator();
inOrder.verify(writer).writeNumber("23");
inOrder.verify(writer).writeObjectSeparator();
inOrder.verify(writer).writeMemberName("b");
inOrder.verify(writer).writeMemberSeparator();
inOrder.verify(writer).writeNumber("3.14");
inOrder.verify(writer).writeObjectSeparator();
inOrder.verify(writer).writeMemberName("c");
inOrder.verify(writer).writeMemberSeparator();
inOrder.verify(writer).writeString("foo");
inOrder.verify(writer).writeObjectSeparator();
inOrder.verify(writer).writeMemberName("d");
inOrder.verify(writer).writeMemberSeparator();
inOrder.verify(writer).writeLiteral("true");
inOrder.verify(writer).writeObjectSeparator();
inOrder.verify(writer).writeMemberName("e");
inOrder.verify(writer).writeMemberSeparator();
inOrder.verify(writer).writeLiteral("null");
inOrder.verify(writer).writeObjectClose();
inOrder.verifyNoMoreInteractions();
}
@Test
public void isObject() {
assertTrue(object.isObject());
}
@Test
public void asObject() {
assertSame(object, object.asObject());
}
@Test
public void equalstrueForSameInstance() {
assertEquals(object, object);
}
@Test
public void equalstrueForEqualObjects() {
assertEquals(object(), object());
assertEquals(object("a", "1", "b", "2"), object("a", "1", "b", "2"));
}
@Test
public void equalsfalseForDifferentObjects() {
assertNotEquals(object("a", "1"), object("a", "2"));
assertNotEquals(object("a", "1"), object("b", "1"));
assertNotEquals(object("a", "1", "b", "2"), object("b", "2", "a", "1"));
}
@Test
public void equalsfalseForNull() {
assertNotEquals(null, new JsonObject());
}
@Test
public void equalsfalseForSubclass() {
JsonObject jsonObject = new JsonObject();
assertNotEquals(jsonObject, new JsonObject(jsonObject) {
});
}
@Test
public void hashCodeequalsForEqualObjects() {
assertEquals(object().hashCode(), object().hashCode());
assertEquals(object("a", "1").hashCode(), object("a", "1").hashCode());
}
@Test
public void hashCodediffersForDifferentObjects() {
assertNotEquals(object().hashCode(), object("a", "1").hashCode());
assertNotEquals(object("a", "1").hashCode(), object("a", "2").hashCode());
assertNotEquals(object("a", "1").hashCode(), object("b", "1").hashCode());
}
@Test
public void indexOfreturnsNoIndexIfEmpty() {
assertEquals(-1, object.indexOf("a"));
}
@Test
public void indexOfreturnsIndexOfMember() {
object.add("a", true);
assertEquals(0, object.indexOf("a"));
}
@Test
public void indexOfreturnsIndexOfLastMember() {
object.add("a", true);
object.add("a", true);
assertEquals(1, object.indexOf("a"));
}
@Test
public void indexOfreturnsIndexOfLastMemberafterRemove() {
object.add("a", true);
object.add("a", true);
object.remove("a");
assertEquals(0, object.indexOf("a"));
}
@Test
public void indexOfreturnsUpdatedIndexAfterRemove() {
// See issue #16
object.add("a", true);
object.add("b", true);
object.remove("a");
assertEquals(0, object.indexOf("b"));
}
@Test
public void indexOfreturnsIndexOfLastMemberforBigObject() {
object.add("a", true);
// for indexes above 255, the hash index table does not return a value
for (int i = 0; i < 256; i++) {
object.add("x-" + i, 0);
}
object.add("a", true);
assertEquals(257, object.indexOf("a"));
}
@Test
public void hashIndexTablecopyConstructor() {
JsonObject.HashIndexTable original = new JsonObject.HashIndexTable();
original.add("name", 23);
JsonObject.HashIndexTable copy = new JsonObject.HashIndexTable(original);
assertEquals(23, copy.get("name"));
}
@Test
public void hashIndexTableadd() {
JsonObject.HashIndexTable indexTable = new JsonObject.HashIndexTable();
indexTable.add("name-0", 0);
indexTable.add("name-1", 1);
indexTable.add("name-fe", 0xfe);
indexTable.add("name-ff", 0xff);
assertEquals(0, indexTable.get("name-0"));
assertEquals(1, indexTable.get("name-1"));
assertEquals(0xfe, indexTable.get("name-fe"));
assertEquals(-1, indexTable.get("name-ff"));
}
@Test
public void hashIndexTableaddoverwritesPreviousValue() {
JsonObject.HashIndexTable indexTable = new JsonObject.HashIndexTable();
indexTable.add("name", 23);
indexTable.add("name", 42);
assertEquals(42, indexTable.get("name"));
}
@Test
public void hashIndexTableaddclearsPreviousValueIfIndexExceeds0xff() {
JsonObject.HashIndexTable indexTable = new JsonObject.HashIndexTable();
indexTable.add("name", 23);
indexTable.add("name", 300);
assertEquals(-1, indexTable.get("name"));
}
@Test
public void hashIndexTableremove() {
JsonObject.HashIndexTable indexTable = new JsonObject.HashIndexTable();
indexTable.add("name", 23);
indexTable.remove(23);
assertEquals(-1, indexTable.get("name"));
}
@Test
public void hashIndexTableremoveupdatesSubsequentElements() {
JsonObject.HashIndexTable indexTable = new JsonObject.HashIndexTable();
indexTable.add("foo", 23);
indexTable.add("bar", 42);
indexTable.remove(23);
assertEquals(41, indexTable.get("bar"));
}
@Test
public void hashIndexTableremovedoesNotChangePrecedingElements() {
JsonObject.HashIndexTable indexTable = new JsonObject.HashIndexTable();
indexTable.add("foo", 23);
indexTable.add("bar", 42);
indexTable.remove(42);
assertEquals(23, indexTable.get("foo"));
}
@Test
public void memberreturnsNameAndValue() {
JsonObject.Member member = new JsonObject.Member("a", JsonLiteral.TRUE);
assertEquals("a", member.getName());
assertEquals(JsonLiteral.TRUE, member.getValue());
}
@Test
public void memberequalstrueForSameInstance() {
JsonObject.Member member = new JsonObject.Member("a", JsonLiteral.TRUE);
assertEquals(member, member);
}
@Test
public void memberequalstrueForEqualObjects() {
JsonObject.Member member = new JsonObject.Member("a", JsonLiteral.TRUE);
assertEquals(member, new JsonObject.Member("a", JsonLiteral.TRUE));
}
@Test
public void memberequalsfalseForDifferingObjects() {
JsonObject.Member member = new JsonObject.Member("a", JsonLiteral.TRUE);
assertNotEquals(member, new JsonObject.Member("b", JsonLiteral.TRUE));
assertNotEquals(member, new JsonObject.Member("a", JsonLiteral.FALSE));
}
@Test
public void memberequalsfalseForNull() {
JsonObject.Member member = new JsonObject.Member("a", JsonLiteral.TRUE);
assertNotNull(member);
}
@Test
public void memberequalsfalseForSubclass() {
JsonObject.Member member = new JsonObject.Member("a", JsonLiteral.TRUE);
assertNotEquals(member, new JsonObject.Member("a", JsonLiteral.TRUE) {
});
}
@Test
public void memberhashCodeequalsForEqualObjects() {
JsonObject.Member member = new JsonObject.Member("a", JsonLiteral.TRUE);
assertEquals(member.hashCode(), new JsonObject.Member("a", JsonLiteral.TRUE).hashCode());
}
@Test
public void memberhashCodediffersForDifferingobjects() {
JsonObject.Member member = new JsonObject.Member("a", JsonLiteral.TRUE);
assertNotEquals(member.hashCode(), new JsonObject.Member("b", JsonLiteral.TRUE).hashCode());
assertNotEquals(member.hashCode(), new JsonObject.Member("a", JsonLiteral.FALSE).hashCode());
}
}

View file

@ -0,0 +1,810 @@
/*
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 static org.hamcrest.core.StringStartsWith.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.xbib.marc.json.Json.parse;
import org.junit.Test;
import java.io.IOException;
import java.io.StringReader;
/**
*
*/
public class JsonReaderTest extends TestUtil {
private static String join(String... strings) {
StringBuilder builder = new StringBuilder();
for (String string : strings) {
builder.append(string).append('\n');
}
return builder.toString();
}
@Test(expected = NullPointerException.class)
public void constructorRejectsNullHandler() {
new JsonReader<>(null, null);
}
@Test(expected = NullPointerException.class)
public void parseStringrRjectsNull() throws IOException {
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader(null), new TestHandler());
reader.parse();
}
@Test(expected = NullPointerException.class)
public void parseReaderRejectsNull() throws IOException {
JsonReader<Object, Object> reader = new JsonReader<>(null, new TestHandler());
reader.parse();
}
@Test(expected = IllegalArgumentException.class)
public void parseReaderRejectsNegativeBufferSize() throws IOException {
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("[]"), new TestHandler());
reader.parse(-1);
}
@Test
public void parseStringRejectsEmpty() {
assertParseException(0, "Unexpected end of input", "");
}
@Test
public void parseReaderRejectsEmpty() {
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader(""), new TestHandler());
JsonException exception = assertException(JsonException.class, (RunnableEx) reader::parse);
assertThat(exception.getMessage(), startsWith("Unexpected end of input"));
}
@Test
public void parseNull() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("null"), handler);
reader.parse();
assertEquals(join("startNull ",
"endNull "),
handler.getLog());
}
@Test
public void parseTrue() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("true"), handler);
reader.parse();
assertEquals(join("startBoolean ",
"endBoolean true "),
handler.getLog());
}
@Test
public void parseFalse() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("false"), handler);
reader.parse();
assertEquals(join("startBoolean ",
"endBoolean false "),
handler.getLog());
}
@Test
public void parseString() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("\"foo\""), handler);
reader.parse();
assertEquals(join("startString ",
"endString foo "),
handler.getLog());
}
@Test
public void parseStringEmpty() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("\"\""), handler);
reader.parse();
assertEquals(join("startString ",
"endString "),
handler.getLog());
}
@Test
public void parseNumber() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("23"), handler);
reader.parse();
assertEquals(join("startNumber ",
"endNumber 23 "),
handler.getLog());
}
@Test
public void parseNumberNegative() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("-23"), handler);
reader.parse();
assertEquals(join("startNumber ",
"endNumber -23 "),
handler.getLog());
}
@Test
public void parsenumbernegativeexponent() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("-2.3e-12"), handler);
reader.parse();
assertEquals(join("startNumber ",
"endNumber -2.3e-12 "),
handler.getLog());
}
@Test
public void parsearray() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("[23]"), handler);
reader.parse();
assertEquals(join("startArray ",
"startArrayValue a1 ",
"startNumber ",
"endNumber 23 ",
"endArrayValue a1 ",
"endArray a1 "),
handler.getLog());
}
@Test
public void parsearrayempty() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("[]"), handler);
reader.parse();
assertEquals(join("startArray ",
"endArray a1 "),
handler.getLog());
}
@Test
public void parseobject() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("{\"foo\": 23}"), handler);
reader.parse();
assertEquals(join("startObject ",
"startObjectName o1 ",
"endObjectName o1 foo ",
"startObjectValue o1 foo ",
"startNumber ",
"endNumber 23 ",
"endObjectValue o1 foo ",
"endObject o1 "),
handler.getLog());
}
@Test
public void parseobjectempty() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("{}"), handler);
reader.parse();
assertEquals(join("startObject ",
"endObject o1 "),
handler.getLog());
}
@Test
public void parsestripsPadding() throws IOException {
assertEquals(new JsonArray(), parse(" [ ] "));
}
@Test
public void parseignoresAllWhiteSpace() throws IOException {
assertEquals(new JsonArray(), parse("\t\r\n [\t\r\n ]\t\r\n "));
}
@Test
public void parsefailsWithUnterminatedString() {
assertParseException(5, "Unexpected end of input", "[\"foo");
}
@Test
public void parsehandlesInputsThatExceedBufferSize() throws IOException {
String input = "[ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47 ]";
JsonDefaultHandler defHandler = new JsonDefaultHandler();
JsonReader<JsonArray, JsonObject> reader = new JsonReader<>(new StringReader(input), defHandler);
reader.parse(3);
assertEquals("[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47]", defHandler.getValue().toString());
}
@Test
public void parsehandlesStringsThatExceedBufferSize() throws IOException {
String input = "[ \"lorem ipsum dolor sit amet\" ]";
JsonDefaultHandler defHandler = new JsonDefaultHandler();
JsonReader<JsonArray, JsonObject> reader = new JsonReader<>(new StringReader(input), defHandler);
reader.parse(3);
assertEquals("[\"lorem ipsum dolor sit amet\"]", defHandler.getValue().toString());
}
@Test
public void parsehandlesNumbersThatExceedBufferSize() throws IOException {
String input = "[ 3.141592653589 ]";
JsonDefaultHandler defHandler = new JsonDefaultHandler();
JsonReader<JsonArray, JsonObject> reader = new JsonReader<>(new StringReader(input), defHandler);
reader.parse(3);
assertEquals("[3.141592653589]", defHandler.getValue().toString());
}
@Test
public void parsehandlesPositionsCorrectlyWhenInputExceedsBufferSize() {
final String input = "{\n \"a\": 23,\n \"b\": 42,\n}";
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader(input), handler);
assertException(JsonException.class, (RunnableEx) () -> reader.parse(3));
}
@Test
public void parsefailsOnTooDeeplyNestedArray() {
JsonArray array = new JsonArray();
for (int i = 0; i < 1001; i++) {
array = new JsonArray().add(array);
}
final String input = array.toString();
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader(input), handler);
JsonException exception = assertException(JsonException.class, (RunnableEx) reader::parse);
assertEquals("Nesting too deep", exception.getMessage());
}
@Test
public void parsefailsOnTooDeeplyNestedObject() {
JsonObject object = new JsonObject();
for (int i = 0; i < 1001; i++) {
object = new JsonObject().add("foo", object);
}
final String input = object.toString();
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader(input), handler);
JsonException exception = assertException(JsonException.class, (RunnableEx) reader::parse);
assertEquals("Nesting too deep", exception.getMessage());
}
@Test
public void parsefailsOnTooDeeplyNestedMixedObject() {
JsonValue value = new JsonObject();
for (int i = 0; i < 1001; i++) {
value = i % 2 == 0 ? new JsonArray().add(value) : new JsonObject().add("foo", value);
}
final String input = value.toString();
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader(input), handler);
JsonException exception = assertException(JsonException.class, (RunnableEx) reader::parse);
assertEquals("Nesting too deep", exception.getMessage());
}
@Test
public void parsedoesNotFailWithManyArrays() throws IOException {
JsonArray array = new JsonArray();
for (int i = 0; i < 1001; i++) {
array.add(new JsonArray().add(7));
}
final String input = array.toString();
JsonValue result = parse(input);
assertTrue(result.isArray());
}
@Test
public void parsedoesNotFailWithManyEmptyArrays() throws IOException {
JsonArray array = new JsonArray();
for (int i = 0; i < 1001; i++) {
array.add(new JsonArray());
}
final String input = array.toString();
JsonValue result = parse(input);
assertTrue(result.isArray());
}
@Test
public void parsedoesNotFailWithManyObjects() throws IOException {
JsonArray array = new JsonArray();
for (int i = 0; i < 1001; i++) {
array.add(new JsonObject().add("a", 7));
}
final String input = array.toString();
JsonValue result = parse(input);
assertTrue(result.isArray());
}
@Test
public void parsedoesNotFailWithManyEmptyObjects() throws IOException {
JsonArray array = new JsonArray();
for (int i = 0; i < 1001; i++) {
array.add(new JsonObject());
}
final String input = array.toString();
JsonValue result = parse(input);
assertTrue(result.isArray());
}
@Test
public void parseCanBeCalledOnce() throws IOException {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader("[23]"), handler);
reader.parse();
assertEquals(join(
"startArray ",
"startArrayValue a1 ",
"startNumber ",
"endNumber 23 ",
"endArrayValue a1 ",
"endArray a1 "),
handler.getLog());
handler = new TestHandler();
reader = new JsonReader<>(new StringReader("[42]"), handler);
reader.parse();
assertEquals(join(
"startArray ",
"startArrayValue a1 ",
"startNumber ",
"endNumber 42 ",
"endArrayValue a1 ",
"endArray a1 "),
handler.getLog());
}
@Test
public void arraysempty() throws IOException {
assertEquals("[]", parse("[]").toString());
}
@Test
public void arrayssingleValue() throws IOException {
assertEquals("[23]", parse("[23]").toString());
}
@Test
public void arraysmultipleValues() throws IOException {
assertEquals("[23,42]", parse("[23,42]").toString());
}
@Test
public void arrayswithWhitespaces() throws IOException {
assertEquals("[23,42]", parse("[ 23 , 42 ]").toString());
}
@Test
public void arraysnested() throws IOException {
assertEquals("[[23]]", parse("[[23]]").toString());
assertEquals("[[[]]]", parse("[[[]]]").toString());
assertEquals("[[23],42]", parse("[[23],42]").toString());
assertEquals("[[23],[42]]", parse("[[23],[42]]").toString());
assertEquals("[[23],[42]]", parse("[[23],[42]]").toString());
assertEquals("[{\"foo\":[23]},{\"bar\":[42]}]",
parse("[{\"foo\":[23]},{\"bar\":[42]}]").toString());
}
@Test
public void arraysillegalSyntax() {
assertParseException(1, "Expected value", "[,]");
assertParseException(4, "Expected ',' or ']'", "[23 42]");
assertParseException(4, "Expected value", "[23,]");
}
@Test
public void arraysincomplete() {
assertParseException(1, "Unexpected end of input", "[");
assertParseException(2, "Unexpected end of input", "[ ");
assertParseException(3, "Unexpected end of input", "[23");
assertParseException(4, "Unexpected end of input", "[23 ");
assertParseException(4, "Unexpected end of input", "[23,");
assertParseException(5, "Unexpected end of input", "[23, ");
}
@Test
public void objectsempty() throws IOException {
assertEquals("{}", parse("{}").toString());
}
@Test
public void objectssingleValue() throws IOException {
assertEquals("{\"foo\":23}", parse("{\"foo\":23}").toString());
}
@Test
public void objectsmultipleValues() throws IOException {
assertEquals("{\"foo\":23,\"bar\":42}", parse("{\"foo\":23,\"bar\":42}").toString());
}
@Test
public void objectswhitespace() throws IOException {
assertEquals("{\"foo\":23,\"bar\":42}", parse("{ \"foo\" : 23, \"bar\" : 42 }").toString());
}
@Test
public void objectsnested() throws IOException {
assertEquals("{\"foo\":{}}", parse("{\"foo\":{}}").toString());
assertEquals("{\"foo\":{\"bar\":42}}", parse("{\"foo\":{\"bar\": 42}}").toString());
assertEquals("{\"foo\":{\"bar\":{\"baz\":42}}}",
parse("{\"foo\":{\"bar\": {\"baz\": 42}}}").toString());
assertEquals("{\"foo\":[{\"bar\":{\"baz\":[[42]]}}]}",
parse("{\"foo\":[{\"bar\": {\"baz\": [[42]]}}]}").toString());
}
@Test
public void objectsillegalSyntax() {
assertParseException(1, "Expected name", "{,}");
assertParseException(1, "Expected name", "{:}");
assertParseException(1, "Expected name", "{23}");
assertParseException(4, "Expected ':'", "{\"a\"}");
assertParseException(5, "Expected ':'", "{\"a\" \"b\"}");
assertParseException(5, "Expected value", "{\"a\":}");
assertParseException(8, "Expected name", "{\"a\":23,}");
assertParseException(8, "Expected name", "{\"a\":23,42");
}
@Test
public void objectsincomplete() {
assertParseException(1, "Unexpected end of input", "{");
assertParseException(2, "Unexpected end of input", "{ ");
assertParseException(2, "Unexpected end of input", "{\"");
assertParseException(4, "Unexpected end of input", "{\"a\"");
assertParseException(5, "Unexpected end of input", "{\"a\" ");
assertParseException(5, "Unexpected end of input", "{\"a\":");
assertParseException(6, "Unexpected end of input", "{\"a\": ");
assertParseException(7, "Unexpected end of input", "{\"a\":23");
assertParseException(8, "Unexpected end of input", "{\"a\":23 ");
assertParseException(8, "Unexpected end of input", "{\"a\":23,");
assertParseException(9, "Unexpected end of input", "{\"a\":23, ");
}
@Test
public void stringsemptyStringisAccepted() throws IOException {
assertEquals("", parse("\"\"").asString());
}
@Test
public void stringsasciiCharactersareAccepted() throws IOException {
assertEquals(" ", parse("\" \"").asString());
assertEquals("a", parse("\"a\"").asString());
assertEquals("foo", parse("\"foo\"").asString());
assertEquals("A2-D2", parse("\"A2-D2\"").asString());
assertEquals("\u007f", parse("\"\u007f\"").asString());
}
@Test
public void stringsnonAsciiCharactersareAccepted() throws IOException {
assertEquals("Русский", parse("\"Русский\"").asString());
assertEquals("العربية", parse("\"العربية\"").asString());
assertEquals("日本語", parse("\"日本語\"").asString());
}
@Test
public void stringscontrolCharactersareRejected() {
// JSON string must not contain characters < 0x20
assertParseException(3, "Expected valid string character", "\"--\n--\"");
assertParseException(3, "Expected valid string character", "\"--\r\n--\"");
assertParseException(3, "Expected valid string character", "\"--\t--\"");
assertParseException(3, "Expected valid string character", "\"--\u0000--\"");
assertParseException(3, "Expected valid string character", "\"--\u001f--\"");
}
@Test
public void stringsvalidEscapesareAccepted() throws IOException {
// valid escapes are \" \\ \/ \b \f \n \r \t and unicode escapes
assertEquals(" \" ", parse("\" \\\" \"").asString());
assertEquals(" \\ ", parse("\" \\\\ \"").asString());
assertEquals(" / ", parse("\" \\/ \"").asString());
assertEquals(" \u0008 ", parse("\" \\b \"").asString());
assertEquals(" \u000c ", parse("\" \\f \"").asString());
assertEquals(" \r ", parse("\" \\r \"").asString());
assertEquals(" \n ", parse("\" \\n \"").asString());
assertEquals(" \t ", parse("\" \\t \"").asString());
}
@Test
public void stringsescapeatStart() throws IOException {
assertEquals("\\x", parse("\"\\\\x\"").asString());
}
@Test
public void stringsescapeatEnd() throws IOException {
assertEquals("x\\", parse("\"x\\\\\"").asString());
}
@Test
public void stringsillegalEscapesareRejected() {
assertParseException(2, "Expected valid escape sequence", "\"\\a\"");
assertParseException(2, "Expected valid escape sequence", "\"\\x\"");
assertParseException(2, "Expected valid escape sequence", "\"\\000\"");
}
@Test
public void stringsvalidUnicodeEscapesareAccepted() throws IOException {
assertEquals("\u0021", parse("\"\\u0021\"").asString());
assertEquals("\u4711", parse("\"\\u4711\"").asString());
assertEquals("\uffff", parse("\"\\uffff\"").asString());
assertEquals("\uabcdx", parse("\"\\uabcdx\"").asString());
}
@Test
public void stringsillegalUnicodeEscapesareRejected() {
assertParseException(3, "Expected hexadecimal digit", "\"\\u \"");
assertParseException(3, "Expected hexadecimal digit", "\"\\ux\"");
assertParseException(5, "Expected hexadecimal digit", "\"\\u20 \"");
assertParseException(6, "Expected hexadecimal digit", "\"\\u000x\"");
}
@Test
public void stringsincompleteStringsareRejected() {
assertParseException(1, "Unexpected end of input", "\"");
assertParseException(4, "Unexpected end of input", "\"foo");
assertParseException(5, "Unexpected end of input", "\"foo\\");
assertParseException(6, "Unexpected end of input", "\"foo\\n");
assertParseException(6, "Unexpected end of input", "\"foo\\u");
assertParseException(7, "Unexpected end of input", "\"foo\\u0");
assertParseException(9, "Unexpected end of input", "\"foo\\u000");
assertParseException(10, "Unexpected end of input", "\"foo\\u0000");
}
@Test
public void numbersinteger() throws IOException {
assertEquals(new JsonNumber("0"), parse("0"));
assertEquals(new JsonNumber("-0"), parse("-0"));
assertEquals(new JsonNumber("1"), parse("1"));
assertEquals(new JsonNumber("-1"), parse("-1"));
assertEquals(new JsonNumber("23"), parse("23"));
assertEquals(new JsonNumber("-23"), parse("-23"));
assertEquals(new JsonNumber("1234567890"), parse("1234567890"));
assertEquals(new JsonNumber("123456789012345678901234567890"),
parse("123456789012345678901234567890"));
}
@Test
public void numbersminusZero() throws IOException {
// allowed by JSON, allowed by Java
JsonValue value = parse("-0");
assertEquals(0, value.asInt());
assertEquals(0L, value.asLong());
assertEquals(0f, value.asFloat(), 0);
assertEquals(0d, value.asDouble(), 0);
}
@Test
public void numbersdecimal() throws IOException {
assertEquals(new JsonNumber("0.23"), parse("0.23"));
assertEquals(new JsonNumber("-0.23"), parse("-0.23"));
assertEquals(new JsonNumber("1234567890.12345678901234567890"),
parse("1234567890.12345678901234567890"));
}
@Test
public void numberswithExponent() throws IOException {
assertEquals(new JsonNumber("0.1e9"), parse("0.1e9"));
assertEquals(new JsonNumber("0.1e9"), parse("0.1e9"));
assertEquals(new JsonNumber("0.1E9"), parse("0.1E9"));
assertEquals(new JsonNumber("-0.23e9"), parse("-0.23e9"));
assertEquals(new JsonNumber("0.23e9"), parse("0.23e9"));
assertEquals(new JsonNumber("0.23e+9"), parse("0.23e+9"));
assertEquals(new JsonNumber("0.23e-9"), parse("0.23e-9"));
}
@Test
public void numberswithInvalidFormat() {
assertParseException(0, "Expected value", "+1");
assertParseException(0, "Expected value", ".1");
assertParseException(1, "Unexpected character", "02");
assertParseException(2, "Unexpected character", "-02");
assertParseException(1, "Expected digit", "-x");
assertParseException(2, "Expected digit", "1.x");
assertParseException(2, "Expected digit", "1ex");
assertParseException(3, "Unexpected character", "1e1x");
}
@Test
public void numbersincomplete() {
assertParseException(1, "Unexpected end of input", "-");
assertParseException(2, "Unexpected end of input", "1.");
assertParseException(4, "Unexpected end of input", "1.0e");
assertParseException(5, "Unexpected end of input", "1.0e-");
}
@Test
public void nullcomplete() throws IOException {
assertEquals(JsonLiteral.NULL, parse("null"));
}
@Test
public void nullincomplete() {
assertParseException(1, "Unexpected end of input", "n");
assertParseException(2, "Unexpected end of input", "nu");
assertParseException(3, "Unexpected end of input", "nul");
}
@Test
public void nullwithIllegalCharacter() {
assertParseException(1, "Expected 'u'", "nx");
assertParseException(2, "Expected 'l'", "nux");
assertParseException(3, "Expected 'l'", "nulx");
assertParseException(4, "Unexpected character", "nullx");
}
@Test
public void truecomplete() throws IOException {
assertSame(JsonLiteral.TRUE, parse("true"));
}
@Test
public void trueincomplete() {
assertParseException(1, "Unexpected end of input", "t");
assertParseException(2, "Unexpected end of input", "tr");
assertParseException(3, "Unexpected end of input", "tru");
}
@Test
public void truewithIllegalCharacter() {
assertParseException(1, "Expected 'r'", "tx");
assertParseException(2, "Expected 'u'", "trx");
assertParseException(3, "Expected 'e'", "trux");
assertParseException(4, "Unexpected character", "truex");
}
@Test
public void falsecomplete() throws IOException {
assertSame(JsonLiteral.FALSE, parse("false"));
}
@Test
public void falseincomplete() {
assertParseException(1, "Unexpected end of input", "f");
assertParseException(2, "Unexpected end of input", "fa");
assertParseException(3, "Unexpected end of input", "fal");
assertParseException(4, "Unexpected end of input", "fals");
}
@Test
public void falsewithIllegalCharacter() {
assertParseException(1, "Expected 'a'", "fx");
assertParseException(2, "Expected 'l'", "fax");
assertParseException(3, "Expected 's'", "falx");
assertParseException(4, "Expected 'e'", "falsx");
assertParseException(5, "Unexpected character", "falsex");
}
private void assertParseException(int offset, String message, final String json) {
TestHandler handler = new TestHandler();
JsonReader<Object, Object> reader = new JsonReader<>(new StringReader(json), handler);
JsonException exception = assertException(JsonException.class, (Runnable) () -> {
try {
reader.parse();
} catch (IOException e) {
// ignore
}
});
assertThat(exception.getMessage(), startsWith(message));
}
private static class TestHandler implements JsonHandler<Object, Object> {
StringBuilder log = new StringBuilder();
int sequence = 0;
@Override
public void startNull() {
record("startNull");
}
@Override
public void endNull() {
record("endNull");
}
@Override
public void startBoolean() {
record("startBoolean");
}
@Override
public void endBoolean(boolean value) {
record("endBoolean", value);
}
@Override
public void startString() {
record("startString");
}
@Override
public void endString(String string) {
record("endString", string);
}
@Override
public void startNumber() {
record("startNumber");
}
@Override
public void endNumber(String string) {
record("endNumber", string);
}
@Override
public Object startArray() {
record("startArray");
return "a" + ++sequence;
}
@Override
public void endArray(Object array) {
record("endArray", array);
}
@Override
public void startArrayValue(Object array) {
record("startArrayValue", array);
}
@Override
public void endArrayValue(Object array) {
record("endArrayValue", array);
}
@Override
public Object startObject() {
record("startObject");
return "o" + ++sequence;
}
@Override
public void endObject(Object object) {
record("endObject", object);
}
@Override
public void startObjectName(Object object) {
record("startObjectName", object);
}
@Override
public void endObjectName(Object object, String name) {
record("endObjectName", object, name);
}
@Override
public void startObjectValue(Object object, String name) {
record("startObjectValue", object, name);
}
@Override
public void endObjectValue(Object object, String name) {
record("endObjectValue", object, name);
}
private void record(String event, Object... args) {
log.append(event);
for (Object arg : args) {
log.append(' ').append(arg);
}
log.append(' ').append('\n');
}
String getLog() {
return log.toString();
}
}
}

View file

@ -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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.StringWriter;
/**
*
*/
public class JsonStringTest extends TestUtil {
private StringWriter stringWriter;
private JsonWriter jsonWriter;
@Before
public void setUp() {
stringWriter = new StringWriter();
jsonWriter = new JsonWriter(stringWriter);
}
@Test
public void constructor_failsWithNull() {
assertException(NullPointerException.class, null, new Runnable() {
public void run() {
new JsonString(null);
}
});
}
@Test
public void write() throws IOException {
new JsonString("foo").write(jsonWriter);
assertEquals("\"foo\"", stringWriter.toString());
}
@Test
public void write_escapesStrings() throws IOException {
new JsonString("foo\\bar").write(jsonWriter);
assertEquals("\"foo\\\\bar\"", stringWriter.toString());
}
@Test
public void isString() {
assertTrue(new JsonString("foo").isString());
}
@Test
public void asString() {
assertEquals("foo", new JsonString("foo").asString());
}
@Test
public void equals_trueForSameInstance() {
JsonString string = new JsonString("foo");
assertEquals(string, string);
}
@Test
public void equals_trueForEqualStrings() {
assertEquals(new JsonString("foo"), new JsonString("foo"));
}
@Test
public void equals_falseForDifferentStrings() {
assertNotEquals(new JsonString(""), new JsonString("foo"));
assertNotEquals(new JsonString("foo"), new JsonString("bar"));
}
@Test
public void equals_falseForNull() {
assertNotEquals(null, new JsonString("foo"));
}
@Test
public void equals_falseForSubclass() {
assertNotEquals(new JsonString("foo"), new JsonString("foo") {
});
}
@Test
public void hashCode_equalsForEqualStrings() {
assertEquals(new JsonString("foo").hashCode(), new JsonString("foo").hashCode());
}
@Test
public void hashCode_differsForDifferentStrings() {
assertNotEquals(new JsonString("").hashCode(), new JsonString("foo").hashCode());
assertNotEquals(new JsonString("foo").hashCode(), new JsonString("bar").hashCode());
}
}

View file

@ -0,0 +1,234 @@
/*
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
/**
*
*/
public class JsonTest extends TestUtil {
@Test
public void literalConstants() {
assertTrue(JsonLiteral.NULL.isNull());
assertTrue(JsonLiteral.TRUE.isTrue());
assertTrue(JsonLiteral.FALSE.isFalse());
}
@Test
public void valueInt() {
assertEquals("0", Json.of(0).toString());
assertEquals("23", Json.of(23).toString());
assertEquals("-1", Json.of(-1).toString());
assertEquals("2147483647", Json.of(Integer.MAX_VALUE).toString());
assertEquals("-2147483648", Json.of(Integer.MIN_VALUE).toString());
}
@Test
public void valueLong() {
assertEquals("0", Json.of(0L).toString());
assertEquals("9223372036854775807", Json.of(Long.MAX_VALUE).toString());
assertEquals("-9223372036854775808", Json.of(Long.MIN_VALUE).toString());
}
@Test
public void valueFloat() {
assertEquals("23.5", Json.of(23.5f).toString());
assertEquals("-3.1416", Json.of(-3.1416f).toString());
assertEquals("1.23E-6", Json.of(0.00000123f).toString());
assertEquals("-1.23E7", Json.of(-12300000f).toString());
}
@Test
public void valueFloatCutsOffPointZero() {
assertEquals("0", Json.of(0f).toString());
assertEquals("-1", Json.of(-1f).toString());
assertEquals("10", Json.of(10f).toString());
}
@Test
public void valueFloatFailsWithInfinity() {
String message = "Infinite and NaN values not permitted in JSON";
assertException(IllegalArgumentException.class, message,
(Runnable) () -> Json.of(Float.POSITIVE_INFINITY));
}
@Test
public void valuefloatfailsWithNaN() {
String message = "Infinite and NaN values not permitted in JSON";
assertException(IllegalArgumentException.class, message, (Runnable) () -> Json.of(Float.NaN));
}
@Test
public void valueDouble() {
assertEquals("23.5", Json.of(23.5d).toString());
assertEquals("3.1416", Json.of(3.1416d).toString());
assertEquals("1.23E-6", Json.of(0.00000123d).toString());
assertEquals("1.7976931348623157E308", Json.of(1.7976931348623157E308d).toString());
}
@Test
public void valueDoublecutsOffPointZero() {
assertEquals("0", Json.of(0d).toString());
assertEquals("-1", Json.of(-1d).toString());
assertEquals("10", Json.of(10d).toString());
}
@Test
public void valuedoublefailsWithInfinity() {
String message = "Infinite and NaN values not permitted in JSON";
assertException(IllegalArgumentException.class, message, (Runnable) () -> Json.of(Double.POSITIVE_INFINITY));
}
@Test
public void valuedoublefailsWithNaN() {
String message = "Infinite and NaN values not permitted in JSON";
assertException(IllegalArgumentException.class, message, (Runnable) () -> Json.of(Double.NaN));
}
@Test
public void valueboolean() {
assertSame(JsonLiteral.TRUE, Json.of(true));
assertSame(JsonLiteral.FALSE, Json.of(false));
}
@Test
public void valuestring() {
assertEquals("", Json.of("").asString());
assertEquals("Hello", Json.of("Hello").asString());
assertEquals("\"Hello\"", Json.of("\"Hello\"").asString());
}
@Test
public void valuestringtoleratesNull() {
assertSame(JsonLiteral.NULL, Json.of(null));
}
@Test
public void array() {
assertEquals(new JsonArray(), Json.array());
}
@Test
public void arrayint() {
assertEquals(new JsonArray().add(23), Json.array(23));
assertEquals(new JsonArray().add(23).add(42), Json.array(23, 42));
}
@Test
public void arrayintfailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> Json.array((int[]) null));
}
@Test
public void arraylong() {
assertEquals(new JsonArray().add(23L), Json.array(23L));
assertEquals(new JsonArray().add(23L).add(42L), Json.array(23L, 42L));
}
@Test
public void arraylongfailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> Json.array((long[]) null));
}
@Test
public void arrayfloat() {
assertEquals(new JsonArray().add(3.14f), Json.array(3.14f));
assertEquals(new JsonArray().add(3.14f).add(1.41f), Json.array(3.14f, 1.41f));
}
@Test
public void arrayfloatfailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> Json.array((float[]) null));
}
@Test
public void arraydouble() {
assertEquals(new JsonArray().add(3.14d), Json.array(3.14d));
assertEquals(new JsonArray().add(3.14d).add(1.41d), Json.array(3.14d, 1.41d));
}
@Test
public void arraydoublefailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> Json.array((double[]) null));
}
@Test
public void arrayboolean() {
assertEquals(new JsonArray().add(true), Json.array(true));
assertEquals(new JsonArray().add(true).add(false), Json.array(true, false));
}
@Test
public void arraybooleanfailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> Json.array((boolean[]) null));
}
@Test
public void arraystring() {
assertEquals(new JsonArray().add("foo"), Json.array("foo"));
assertEquals(new JsonArray().add("foo").add("bar"), Json.array("foo", "bar"));
}
@Test
public void arraystringfailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> Json.array((String[]) null));
}
@Test
public void object() {
assertEquals(new JsonObject(), Json.object());
}
@Test
public void parsestring() throws IOException {
assertEquals(Json.of(23), Json.parse("23"));
}
@Test
public void parsestringfailsWithNull() {
assertException(NullPointerException.class, null, (Runnable) () -> {
try {
Json.parse((String) null);
} catch (IOException e) {
//
}
});
}
@Test
public void parsereader() throws IOException {
Reader reader = new StringReader("23");
assertEquals(Json.of(23), Json.parse(reader));
}
@Test
public void parsereaderfailsWithNull() {
assertException(NullPointerException.class, null, (RunnableEx) () -> Json.parse((Reader) null));
}
}

View file

@ -0,0 +1,163 @@
/*
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 static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.StringWriter;
/**
*
*/
public class JsonWriterTest {
private StringWriter output;
private JsonWriter writer;
private static String string(char... chars) {
return String.valueOf(chars);
}
@Before
public void setUp() {
output = new StringWriter();
writer = new JsonWriter(output);
}
@Test
public void writeLiteral() throws IOException {
writer.writeLiteral("foo");
assertEquals("foo", output.toString());
}
@Test
public void writeNumber() throws IOException {
writer.writeNumber("23");
assertEquals("23", output.toString());
}
@Test
public void writeStringempty() throws IOException {
writer.writeString("");
assertEquals("\"\"", output.toString());
}
@Test
public void writeStingescapesBackslashes() throws IOException {
writer.writeString("foo\\bar");
assertEquals("\"foo\\\\bar\"", output.toString());
}
@Test
public void writeArrayParts() throws IOException {
writer.writeArrayOpen();
writer.writeArraySeparator();
writer.writeArrayClose();
assertEquals("[,]", output.toString());
}
@Test
public void writeObjectParts() throws IOException {
writer.writeObjectOpen();
writer.writeMemberSeparator();
writer.writeObjectSeparator();
writer.writeObjectClose();
assertEquals("{:,}", output.toString());
}
@Test
public void writeMemberNameempty() throws IOException {
writer.writeMemberName("");
assertEquals("\"\"", output.toString());
}
@Test
public void writeMemberNameescapesBackslashes() throws IOException {
writer.writeMemberName("foo\\bar");
assertEquals("\"foo\\\\bar\"", output.toString());
}
@Test
public void escapesQuotes() throws IOException {
writer.writeString("a\"b");
assertEquals("\"a\\\"b\"", output.toString());
}
@Test
public void escapesEscapedQuotes() throws IOException {
writer.writeString("foo\\\"bar");
assertEquals("\"foo\\\\\\\"bar\"", output.toString());
}
@Test
public void escapesNewLine() throws IOException {
writer.writeString("foo\nbar");
assertEquals("\"foo\\nbar\"", output.toString());
}
@Test
public void escapesWindowsNewLine() throws IOException {
writer.writeString("foo\r\nbar");
assertEquals("\"foo\\r\\nbar\"", output.toString());
}
@Test
public void escapesTabs() throws IOException {
writer.writeString("foo\tbar");
assertEquals("\"foo\\tbar\"", output.toString());
}
@Test
public void escapesSpecialCharacters() throws IOException {
writer.writeString("foo\u2028bar\u2029");
assertEquals("\"foo\\u2028bar\\u2029\"", output.toString());
}
@Test
public void escapesZeroCharacter() throws IOException {
writer.writeString(string('f', 'o', 'o', (char) 0, 'b', 'a', 'r'));
assertEquals("\"foo\\u0000bar\"", output.toString());
}
@Test
public void escapesEscapeCharacter() throws IOException {
writer.writeString(string('f', 'o', 'o', (char) 27, 'b', 'a', 'r'));
assertEquals("\"foo\\u001bbar\"", output.toString());
}
@Test
public void escapesControlCharacters() throws IOException {
writer.writeString(string((char) 1, (char) 8, (char) 15, (char) 16, (char) 31));
assertEquals("\"\\u0001\\u0008\\u000f\\u0010\\u001f\"", output.toString());
}
@Test
public void escapesFirstChar() throws IOException {
writer.writeString(string('\\', 'x'));
assertEquals("\"\\\\x\"", output.toString());
}
@Test
public void escapesLastChar() throws IOException {
writer.writeString(string('x', '\\'));
assertEquals("\"x\\\\\"", output.toString());
}
}

View file

@ -35,37 +35,13 @@ import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.text.Normalizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.EnumSet;
/**
*
*/
public class MarcJsonWriterTest {
private static final Pattern quotePattern = Pattern.compile("\"", Pattern.LITERAL);
private static final String escapeQuote = "\\\"";
private static final Pattern backslashPattern = Pattern.compile("\\\\");
private static final String escapeBackslash = "\\\\";
private static String escape(String value) {
String s = backslashPattern.matcher(value).replaceAll(Matcher.quoteReplacement(escapeBackslash));
return quotePattern.matcher(s).replaceAll(Matcher.quoteReplacement(escapeQuote));
}
@Test
public void testEscapeJSON() {
String s = "\"Hello world\"";
String t = escape(s);
assertEquals("\\\"Hello world\\\"", t);
s = "\\P123";
t = escape(s);
assertEquals("\\\\P123", t);
}
/**
* {@code }MarcJsonWriter} can receive MARC fields.
*
@ -169,7 +145,7 @@ public class MarcJsonWriterTest {
File file = File.createTempFile(s + ".", ".json");
file.deleteOnExit();
FileOutputStream out = new FileOutputStream(file);
try (MarcJsonWriter writer = new MarcJsonWriter(out, MarcJsonWriter.Style.LINES)
try (MarcJsonWriter writer = new MarcJsonWriter(out, EnumSet.of(MarcJsonWriter.Style.LINES))
.setFormat(MarcXchangeConstants.MARCXCHANGE_FORMAT)
.setType(MarcXchangeConstants.BIBLIOGRAPHIC_TYPE)
) {
@ -227,7 +203,8 @@ public class MarcJsonWriterTest {
InputStream in = getClass().getResource("/org/xbib/marc//" + s).openStream();
MarcValueTransformers marcValueTransformers = new MarcValueTransformers();
marcValueTransformers.setMarcValueTransformer(value -> Normalizer.normalize(value, Normalizer.Form.NFC));
try (MarcJsonWriter writer = new MarcJsonWriter("build/bulk%d.jsonl", 3, MarcJsonWriter.Style.ELASTICSEARCH_BULK)
try (MarcJsonWriter writer = new MarcJsonWriter("build/bulk%d.jsonl",
3, EnumSet.of(MarcJsonWriter.Style.ELASTICSEARCH_BULK))
.setIndex("testindex", "testtype")) {
writer.setMarcValueTransformers(marcValueTransformers);
Marc.builder()
@ -264,8 +241,8 @@ public class MarcJsonWriterTest {
MarcValueTransformers marcValueTransformers = new MarcValueTransformers();
marcValueTransformers.setMarcValueTransformer(value -> Normalizer.normalize(value, Normalizer.Form.NFC));
// split at 3, Elasticsearch bulk format, buffer size 65536, compress = true
try (MarcJsonWriter writer = new MarcJsonWriter("build/bulk%d.jsonl.gz", 3,
MarcJsonWriter.Style.ELASTICSEARCH_BULK, 65536, true)
try (MarcJsonWriter writer = new MarcJsonWriter("build/bulk%d.jsonl.gz",
3, EnumSet.of(MarcJsonWriter.Style.ELASTICSEARCH_BULK), 65536, true)
.setIndex("testindex", "testtype")) {
writer.setMarcValueTransformers(marcValueTransformers);
Marc.builder()
@ -300,7 +277,7 @@ public class MarcJsonWriterTest {
String s = "bundeskunsthalle.xml";
InputStream in = getClass().getResource("/org/xbib/marc/xml/" + s).openStream();
try (MarcJsonWriter writer = new MarcJsonWriter("build/bk-bulk%d.jsonl", 1,
MarcJsonWriter.Style.ELASTICSEARCH_BULK)
EnumSet.of(MarcJsonWriter.Style.ELASTICSEARCH_BULK))
.setIndex("testindex", "testtype")) {
Marc.builder()
.setFormat(MarcXchangeConstants.MARCXCHANGE_FORMAT)
@ -318,7 +295,7 @@ public class MarcJsonWriterTest {
File file = File.createTempFile("multi.", ".json");
file.deleteOnExit();
FileOutputStream out = new FileOutputStream(file);
try (MarcJsonWriter writer = new MarcJsonWriter(out, MarcJsonWriter.Style.ARRAY)) {
try (MarcJsonWriter writer = new MarcJsonWriter(out, EnumSet.of(MarcJsonWriter.Style.ARRAY))) {
writer.beginCollection();
try (InputStream inputStream = getClass().getResource("/org/xbib/marc/summerland.mrc").openStream()) {
Marc.builder()
@ -376,4 +353,35 @@ public class MarcJsonWriterTest {
new FileInputStream(file));
}
}
/**
* Test JSON format that allows duplicate keys. This allows to format MARC in order,
* as defined by cataloging rules.
*
* @throws Exception if test fails
*/
@Test
public void testMarcRecordJsonWithDuplicateKeys() throws Exception {
for (String s : new String[]{
"test_ubl.mrc"
}) {
InputStream in = getClass().getResource("/org/xbib/marc/" + s).openStream();
File file = File.createTempFile(s + ".", ".json");
file.deleteOnExit();
FileOutputStream out = new FileOutputStream(file);
try (MarcJsonWriter writer = new MarcJsonWriter(out, EnumSet.of(MarcJsonWriter.Style.ALLOW_DUPLICATES))
) {
Marc.builder()
.setFormat(MarcXchangeConstants.MARCXCHANGE_FORMAT)
.setType(MarcXchangeConstants.BIBLIOGRAPHIC_TYPE)
.setInputStream(in)
.setCharset(Charset.forName("ANSEL"))
.setMarcListener(writer)
.build()
.writeCollection();
}
assertStream(s, getClass().getResource("/org/xbib/marc/json/" + s + ".dupkey.json").openStream(),
new FileInputStream(file));
}
}
}

View file

@ -0,0 +1,120 @@
/*
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.xbib.marc.json.JsonWriterConfig.prettyPrint;
import org.junit.Test;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Locale;
/**
*
*/
public class PrettyPrintTest {
@Test
public void testIndentWithSpaces_emptyArray() throws IOException {
StringWriter sw = new StringWriter();
JsonWriter output = JsonWriterConfig.prettyPrint(2).createWriter(sw);
new JsonArray().write(output);
assertEquals("[\n \n]", sw.toString());
}
@Test
public void testIndentWithSpaces_emptyObject() throws IOException {
StringWriter sw = new StringWriter();
JsonWriter output = JsonWriterConfig.prettyPrint(2).createWriter(sw);
new JsonObject().write(output);
assertEquals("{\n \n}", sw.toString());
}
@Test
public void testIndentWithSpaces_array() throws IOException {
StringWriter sw = new StringWriter();
JsonWriter output = JsonWriterConfig.prettyPrint(2).createWriter(sw);
new JsonArray().add(23).add(42).write(output);
assertEquals("[\n 23,\n 42\n]", sw.toString());
}
@Test
public void testIndentWithSpaces_nestedArray() throws IOException {
StringWriter sw = new StringWriter();
JsonWriter output = JsonWriterConfig.prettyPrint(2).createWriter(sw);
new JsonArray().add(23).add(new JsonArray().add(42)).write(output);
assertEquals("[\n 23,\n [\n 42\n ]\n]", sw.toString());
}
@Test
public void testIndentWithSpaces_object() throws IOException {
StringWriter sw = new StringWriter();
JsonWriter output = JsonWriterConfig.prettyPrint(2).createWriter(sw);
new JsonObject().add("a", 23).add("b", 42).write(output);
assertEquals("{\n \"a\": 23,\n \"b\": 42\n}", sw.toString());
}
@Test
public void testIndentWithSpaces_nestedObject() throws IOException {
StringWriter sw = new StringWriter();
JsonWriter output = JsonWriterConfig.prettyPrint(2).createWriter(sw);
new JsonObject().add("a", 23).add("b", new JsonObject().add("c", 42)).write(output);
assertEquals("{\n \"a\": 23,\n \"b\": {\n \"c\": 42\n }\n}", sw.toString());
}
@Test
public void testIndentWithSpaces_zero() throws IOException {
StringWriter sw = new StringWriter();
JsonWriter output = JsonWriterConfig.prettyPrint(0).createWriter(sw);
new JsonArray().add(23).add(42).write(output);
assertEquals("[\n23,\n42\n]", sw.toString());
}
@Test
public void testIndentWithSpaces_one() throws IOException {
StringWriter sw = new StringWriter();
JsonWriter output = JsonWriterConfig.prettyPrint(1).createWriter(sw);
new JsonArray().add(23).add(42).write(output);
assertEquals("[\n 23,\n 42\n]", sw.toString());
}
@Test
public void testIndentWithSpaces_failsWithNegativeValues() {
try {
prettyPrint(-1);
fail();
} catch (IllegalArgumentException ex) {
assertTrue(ex.getMessage().toLowerCase(Locale.US).contains("negative"));
}
}
@Test
public void testIndentWithSpaces_createsIndependentInstances() {
Writer writer = mock(Writer.class);
JsonWriterConfig config = prettyPrint(1);
Object instance1 = config.createWriter(writer);
Object instance2 = config.createWriter(writer);
assertNotSame(instance1, instance2);
}
}

View file

@ -0,0 +1,95 @@
/*
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class TestUtil {
public static <T extends Exception> T assertException(Class<T> type,
String message,
Runnable runnable) {
return assertException(type, message, adapt(runnable));
}
public static <T extends Exception> T assertException(Class<T> type,
String message,
RunnableEx runnable) {
T exception = assertException(type, runnable);
assertEquals("exception message", message, exception.getMessage());
return exception;
}
public static <T extends Exception> T assertException(Class<T> type, Runnable runnable) {
return assertException(type, adapt(runnable));
}
public static <T extends Exception> T assertException(Class<T> type, RunnableEx runnable) {
T exception = catchException(runnable, type);
assertNotNull("Expected exception: " + type.getName(), exception);
return exception;
}
@SuppressWarnings("unchecked")
private static <T extends Exception> T catchException(RunnableEx runnable, Class<T> type) {
try {
runnable.run();
return null;
} catch (Exception exception) {
if (type.isAssignableFrom(exception.getClass())) {
return (T) exception;
}
String message = "Unexpected exception: " + exception.getMessage();
throw new RuntimeException(message, exception);
}
}
@SuppressWarnings("unchecked")
public static <T> T serializeAndDeserialize(T instance) throws Exception {
return (T) deserialize(serialize(instance));
}
public static byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
new ObjectOutputStream(outputStream).writeObject(object);
return outputStream.toByteArray();
}
public static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
return new ObjectInputStream(inputStream).readObject();
}
private static RunnableEx adapt(final Runnable runnable) {
return new RunnableEx() {
public void run() {
runnable.run();
}
};
}
public interface RunnableEx {
void run() throws Exception;
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
04427cam a2201081 c45000010012000000030007000120050017000190070003000360080041000390150016000800160023000960200054001190200061001730240018002340350022002520350023002740350026002970350023003230400029003460410008003750440013003830820013003960840012004091000104004212450468005252500015009932640049010082640012010573000022010693360026010913370046011173380025011636550081011886890092012696890077013616890091014386890013015296890092015426890079016346890013017137000105017267000124018317000064019557730054020198560132020738560120022059820025023259690015023509830019023659690006023849820023023909840011024139830020024249840006024449000026024509000022024769000028024989000030025269000027025569000020025839000020026039000029026239000020026529000027026729000030026999000025027299000036027549500026027909500025028169500013028419500016028549500026028709500042028969500015029389500019029539500017029729500024029899500024030139500022030379500021030599500022030809500011031029500025031139510010031389510010031489510010031589510007031688520032031758520034032078520032032418520032032739800040033050-730849546DE-62720190704100209.0tu121203s2019 xx ||||| 00| ||ger c a12,N482dnb7 a10281626692DE-101 a3579026895cGb.ca. EUR 168.00 (DE)93-579-02689-5 a9783579026893cGb.ca. EUR 168.00 (DE)9978-3-579-02689-33 a9783579026893 a(DE-627)730849546 a(DE-576)9730849544 a(DE-599)DNB1028162669 a(DE-101)1028162669 aDE-627bgercDE-627erda ager cXA-DE-NW04a290qDNB a12ssgn1 aBuber, Martind1878-1965eVerfasserIn0(DE-588)1185164770(DE-627)1333991090(DE-576)2088770884aut10aWerkausgaben13pSchriften zur biblischen Religion / herausgegeben von Christian Wiese unter Mitarbeit von Heike Breitenbach ; eingeleitet von Michael Fishbane ; kommentiert von Christian Wiese und Heike Breitenbach unter Mitarbeit von Andreas Loschn1pTextcMartin Buber ; im Auftrag der Philosophischen Fakultät der Heinrich Heine Universität Düsseldorf und der Israel Acadademy of Sciences and Humanities herausgegeben von Paul Mendes-Flohr und Bernd Witte a1. Auflage 1aGüterslohbGütersloher Verlagshausc[2019] 4c© 2019 a726 Seitenc23 cm aTextbtxt2rdacontent aohne Hilfsmittel zu benutzenbn2rdamedia aBandbnc2rdacarrier 7aQuelle0(DE-588)4135952-50(DE-627)1056612360(DE-576)2096650842gnd-content00Du0(DE-588)4001515-40(DE-627)1046036660(DE-576)208843116aBibelpAltes Testament2gnd01Ds0(DE-588)4015950-40(DE-627)1046753140(DE-576)20891434XaExegese2gnd02Ds0(DE-588)4136677-30(DE-627)1056557400(DE-576)209671181aJüdische Philosophie2gnd0 5(DE-627)10Du0(DE-588)4001515-40(DE-627)1046036660(DE-576)208843116aBibelpAltes Testament2gnd11Ds0(DE-588)4059758-10(DE-627)1041314460(DE-576)209132159aTheologie2gnd1 5(DE-627)1 aWiese, Christiand1961-eHerausgeberIn0(DE-588)1214519170(DE-627)0813170690(DE-576)1812949584edt1 aFishbane, Michael A.d1943-eVerfasserIn einer Einleitung0(DE-588)1313998370(DE-627)50877294X0(DE-576)1787759164win1 aBuber, Martind1878-1965tSchriften zur biblischen Religion18w(DE-627)1165587912w(DE-576)095587918g13,1q13,142uhttp://deposit.d-nb.de/cgi-bin/dokserv?id=4186947&prov=M&dok_var=1&dok_ext=htmnVerlag: MVBqtext/htmlv2013-05-013Inhaltstext42uhttp://swbplus.bsz-bw.de/bsz730849546inh.htmmV:DE-576;B:DE-21qapplication/pdfv201906051551003Inhaltsverzeichnis a(DE-15)0025681716x1 bFHTheolx1 a(DE-15)3717597 cB a(DE-14)34144552x1 bFH1x1 a(DE-14)16659087 cB aPYŠBYYN, Mîkāʿēl aFishbane, Michael aFišbeyn, Mîḵāʾēl aBūber, Mordekaj Marṭin aBuber, Martin Mordehai aBuber, Marṭin aMpumper, Martin aBûber, Mordekay Martîn aBūbā, Marutin aBuber, Mordehai Martin aBuber, Mordekhai Marṭin ובר, מרטין ובר, מרדכי מרטין aChristliche Theologie огословие aJudentum aPhilosophie aÖstliche Philosophie удейская философия aAlter Bund aBibelauslegung aBibelexegese aBiblische Auslegung aBibelinterpretation aBiblische Exegese aSchriftauslegung aBibelwissenschaft aExeget aЭкзегетика bXA-AT bXA-DE bXB-IL bXY aDE-14z2019-05-06T09:35:35Z aDE-L325z2019-05-28T10:02:58Z aDE-16z2019-05-15T10:23:35Z aDE-15z2019-05-09T09:13:04Z a730849546b0k730849546o9730849544