diff --git a/src/main/java/blue/endless/jankson/api/Jankson.java b/src/main/java/blue/endless/jankson/api/Jankson.java index 37ebd2d..6a2a868 100644 --- a/src/main/java/blue/endless/jankson/api/Jankson.java +++ b/src/main/java/blue/endless/jankson/api/Jankson.java @@ -40,10 +40,10 @@ import blue.endless.jankson.api.io.JsonReaderOptions; import blue.endless.jankson.api.io.JsonWriter; import blue.endless.jankson.api.io.JsonWriterOptions; +import blue.endless.jankson.api.io.ObjectReaderFactory; import blue.endless.jankson.api.io.ObjectWriter; import blue.endless.jankson.api.io.StructuredDataReader; import blue.endless.jankson.api.io.ValueElementWriter; -import blue.endless.jankson.impl.io.objectreader.ObjectStructuredDataReader; public class Jankson { @@ -229,7 +229,7 @@ public static T readJson(Reader r, JsonReaderOptions opts, Type type) throws * @throws SyntaxError if there was a problem with the json data, or if there was a problem creating the object * * @see #readJsonObject(Reader, JsonReaderOptions) - * @see #writeJson(Object, Writer, JsonWriterOptions) + * @see #writeJson(Object, Writer) */ public static T readJson(Reader r, JsonReaderOptions opts, Class clazz) throws IOException, SyntaxError { JsonReader reader = new JsonReader(r); @@ -238,9 +238,13 @@ public static T readJson(Reader r, JsonReaderOptions opts, Class clazz) t return writer.toObject(); } - public static void writeJson(Object obj, Writer writer, JsonWriterOptions options) throws IOException { + public static void writeJson(Object obj, Writer writer) throws IOException { + writeJson(obj, new ObjectReaderFactory(), writer, JsonWriterOptions.STRICT); + } + + public static void writeJson(Object obj, ObjectReaderFactory factory, Writer writer, JsonWriterOptions options) throws IOException { try { - StructuredDataReader r = ObjectStructuredDataReader.of(obj); + StructuredDataReader r = factory.getReader(obj); JsonWriter w = new JsonWriter(writer, options); r.transferTo(w); writer.flush(); @@ -250,9 +254,9 @@ public static void writeJson(Object obj, Writer writer, JsonWriterOptions option } } - public static String writeJsonString(Object obj, JsonWriterOptions options) throws IOException { + public static String writeJsonString(Object obj, ObjectReaderFactory factory, JsonWriterOptions options) throws IOException { try(StringWriter sw = new StringWriter()) { - StructuredDataReader r = ObjectStructuredDataReader.of(obj); + StructuredDataReader r = factory.getReader(obj); JsonWriter w = new JsonWriter(sw, options); r.transferTo(w); sw.flush(); diff --git a/src/main/java/blue/endless/jankson/api/io/ElementType.java b/src/main/java/blue/endless/jankson/api/io/ElementType.java deleted file mode 100644 index 0a33e0a..0000000 --- a/src/main/java/blue/endless/jankson/api/io/ElementType.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2018-2024 Falkreon (Isaac Ellingson) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package blue.endless.jankson.api.io; - -/** - * Represents states that the parser can find itself in. Returned by {@link blue.endless.jankson.api.io.JsonReader#next() JsonReader.next()}. - */ -public enum ElementType { - - /** - * The parser has encountered a PrimitiveElement in a valid location. - */ - PRIMITIVE(true, true), - - /** - * The parser encountered an opening bracket ('[') in a valid location. Any elements that occur before the matching - * ARRAY_END are within the array. - */ - ARRAY_START(true, false), - - /** - * The parser encountered a closing bracket (']') in a valid location. Any elements that follow are not part of the - * immediately preceding array. - */ - ARRAY_END(true, false), - - /** - * The parser encountered an opening brace ('{') in a valid location. Any elements that occur before the matching - * OBJECT_END are within the object. - */ - OBJECT_START(true, false), - - /** - * The parser encountered a closing brace ('}') in a valid location. Any elements that follow are not part of the - * immediately preceding object. - */ - OBJECT_END(true, false), - - /** - * The parser encountered a key-value pair in a valid location, and is ready to report the entire key. The next - * semantic object will be the delimiter between this key and its associated value. - */ - OBJECT_KEY(true, true), - - /** - * The parser encountered the separator between a key and its associated value - for json, a colon. - */ - //OBJECT_KEY_VALUE_SEPARATOR(true, false), - - /** - * The parser encountered a comment in a valid location, and is ready to report its full contents. - */ - COMMENT(false, true), - - /** - * The parser encountered a run of whitespace and consumed it all. The immediately following element will not be - * whitespace. - */ - WHITESPACE(false, true), - - /** - * The parser encountered a newline. - */ - NEWLINE(false, true), - - /** - * The parser encountered the end of the file. - */ - EOF(true, false); - - private final boolean semantic; - private final boolean hasValue; - ElementType(boolean semantic, boolean hasValue) { - this.semantic = semantic; - this.hasValue = hasValue; - } - - public boolean isSemantic() { return this.semantic; } - /** - * When we encounter this value from {@link StructuredDataReader#next()}, can we call - * {@link StructuredDataReader#getLatestValue() getLatestValue()} and get meaningful information? - * @return - */ - public boolean hasValue() { return this.hasValue; } - -} diff --git a/src/main/java/blue/endless/jankson/api/io/ObjectReaderFactory.java b/src/main/java/blue/endless/jankson/api/io/ObjectReaderFactory.java index 6d0086f..b363dbd 100644 --- a/src/main/java/blue/endless/jankson/api/io/ObjectReaderFactory.java +++ b/src/main/java/blue/endless/jankson/api/io/ObjectReaderFactory.java @@ -22,7 +22,7 @@ * SOFTWARE. */ -package blue.endless.jankson.api.serializer; +package blue.endless.jankson.api.io; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Type; @@ -31,10 +31,13 @@ import java.util.function.Function; import blue.endless.jankson.api.document.ValueElement; -import blue.endless.jankson.api.io.StructuredDataReader; -import blue.endless.jankson.api.io.ValueElementReader; import blue.endless.jankson.impl.io.objectreader.ObjectStructuredDataReader; +/** + * This class manages reading arbitrary Java objects as StructuredData. Because not all classes can + * be recognized or annotated from within, factories may be registered to produce appropriate + * readers externally. + */ public class ObjectReaderFactory { private Map> functionMap = new HashMap<>(); @@ -46,8 +49,8 @@ public class ObjectReaderFactory { * ValueElement representing it. */ @SuppressWarnings("unchecked") - public void register(final Class type, final Function function) { - register((Type) type, (Function) function); + public void registerSerializer(final Class type, final Function function) { + registerSerializer((Type) type, (Function) function); } /** @@ -56,15 +59,35 @@ public void register(final Class type, final Function fu * @param function A function which will receive an object of the specified type, and produce a * ValueElement representing it. */ - public void register(final Type type, final Function function) { + public void registerSerializer(final Type type, final Function function) { Function supplier = (Object obj) -> ValueElementReader.of(function.apply(obj)); functionMap.put(type, supplier); } + /** + * Registers a reader factory function for the specified type + * @param The type the factory will apply to + * @param type The class the factory will apply to + * @param function A reader factory function which will receive an object of the specified type, + * and return a StructuredDataReader representation of it. + */ + @SuppressWarnings("unchecked") + public void register(final Class type, final Function function) { + register((Type) type, (Function) function); + } + /** + * Registers a reader factory function for the specified type + * @param type The type the factory will apply to + * @param function A reader factory function which will receive an object of the specified type, + * and return a StructuredDataReader representation of it. + */ + public void register(final Type type, final Function function) { + functionMap.put(type, function); + } /** - * Gets a reader which will provide a StructuredData representation of the provided object + * Gets a reader which will provide a StructuredData representation of the provided Object * @param The type of the object being serialized / read * @param type The class of the object being serialized / read * @param objectOfType The object being serialized / read @@ -75,7 +98,7 @@ public StructuredDataReader getReader(final Class type, final T objectOfT } /** - * Gets a reader which will provide a StructuredData representation of the provided object + * Gets a reader which will provide a StructuredData representation of the provided Object * @param type The type of the object being serialized / read * @param objectOfType The object being serialized / read * @return A StructuredDataReader which will provide data representing the object @@ -88,7 +111,17 @@ public StructuredDataReader getReader(Type type, final Object objectOfType) { Function function = functionMap.get(type); return (function == null) ? - ObjectStructuredDataReader.of(objectOfType) : + ObjectStructuredDataReader.of(objectOfType, this) : function.apply(objectOfType); } + + /** + * Quick form of {@link #getReader(Type, Object)}. Gets a reader which will provide a + * StructuredData representation of the provided Object. + * @param object The object being serialized / read + * @return A StructuredDataReader which will provide data representing the object + */ + public StructuredDataReader getReader(Object object) { + return getReader(object.getClass(), object); + } } diff --git a/src/main/java/blue/endless/jankson/api/serializer/package-info.java b/src/main/java/blue/endless/jankson/api/serializer/package-info.java deleted file mode 100644 index 880b65a..0000000 --- a/src/main/java/blue/endless/jankson/api/serializer/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2018-2024 Falkreon (Isaac Ellingson) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package blue.endless.jankson.api.serializer; diff --git a/src/main/java/blue/endless/jankson/impl/ObjectToStructuredDataPipe.java b/src/main/java/blue/endless/jankson/impl/ObjectToStructuredDataPipe.java deleted file mode 100644 index aee4594..0000000 --- a/src/main/java/blue/endless/jankson/impl/ObjectToStructuredDataPipe.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2018-2024 Falkreon (Isaac Ellingson) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package blue.endless.jankson.impl; - -import java.io.IOException; -import java.lang.reflect.AccessFlag; -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import blue.endless.jankson.api.MarshallerException; -import blue.endless.jankson.api.annotation.SerializedName; -import blue.endless.jankson.api.document.PrimitiveElement; -import blue.endless.jankson.api.io.StructuredData; -import blue.endless.jankson.api.io.StructuredDataWriter; - -/** - * Writes arbitrary java objects as structured data to a StructuredDataWriter. - */ -public class ObjectToStructuredDataPipe { - - private final StructuredDataWriter underlying; - - /** - * Creates an ObjectStructuredDataWriter that will write to the specified StructuredDataWriter - * @param writer the writer to send strucutred data to - */ - public ObjectToStructuredDataPipe(StructuredDataWriter writer) { - this.underlying = writer; - } - - /** - * Writes an object to the underlying StructuredDataWriter - * @param object the object to write as structured data - * @throws MarshallerException if data cannot be retrieved or converted due to access or internal errors. - * @throws IOException if the underlying StructuredDataWriter encounters problems writing the data. - */ - public void write(Object object) throws MarshallerException, IOException{ - write(object, underlying); - } - - /** - * Writes an arbitrary java object into the specified StructuredDataWriter. - * @param object the object to write as structured data - * @param writer the StructuredDataWriter to write the object to - * @throws MarshallerException if data cannot be retrieved or converted due to access or internal errors. - * @throws IOException if the StructuredDataWriter encounters problems writing the data. - */ - @SuppressWarnings("unchecked") - public static void write(Object object, StructuredDataWriter writer) throws MarshallerException, IOException { - try { - PrimitiveElement.box(object).write(writer); - return; - } catch (Throwable t) {} //don't care - - if (object instanceof Map m) { - packMap(m, writer); - return; - } - - if (object instanceof Collection c) { - packCollection(c, writer); - return; - } else if (object.getClass().isArray()) { - packArray(object, writer); - return; - } - - packPojo(object, writer); - } - - /** - * Writes an arbitrary java object into the specified StructuredDataWriter. - * Only produces Object elements (map-like structures). If this can't be done it throws an error. - * @param object the object to write as structured data - * @param writer the StructuredDataWriter to write the object to - * @throws MarshallerException if data cannot be retrieved or converted due to access or internal errors. - * @throws IOException if the StructuredDataWriter encounters problems writing the data. - */ - @SuppressWarnings("unchecked") - public static void writeObjectElement(Object object, StructuredDataWriter writer) throws MarshallerException, IOException { - if (object instanceof Map m) { - packMap(m, writer); - } else { - packPojo(object, writer); - } - } - - /** - * Writes the specified field into a StructuredDataWriter. Does not write any commas between map or array elements. - * @param f the field to write - * @param o the object instance that holds the field value - * @param writer the StructuredDataWriter to write the data to - * @throws MarshallerException if data cannot be retrieved or converted due to access or internal errors. - * @throws IOException if the StructuredDataWriter encounters problems writing the data. - */ - private static void writeField(Field f, Object o, StructuredDataWriter writer) throws MarshallerException, IOException { - String fieldName = f.getName(); - - String serializedName = - Optional.ofNullable(f.getAnnotation(SerializedName.class)) - .map(SerializedName::value) - .orElse(fieldName); - - if (f.accessFlags().contains(AccessFlag.STATIC) || f.accessFlags().contains(AccessFlag.TRANSIENT)) throw new MarshallerException("Cannot convert static or transient fields."); - - try { - Object v = TypeMagic.getFieldValue(f,o); - writer.write(StructuredData.objectKey(serializedName)); - write(v, writer); - } catch (Throwable t) { - if (serializedName.equals(fieldName)) { - throw new MarshallerException("Error retrieving required field '"+serializedName+"'.", t); - } else { - throw new MarshallerException("Error retrieving required field '"+fieldName+"' (serializedName: '"+serializedName+"').", t); - } - } - } - - private static void packMap(Map m, StructuredDataWriter writer) throws MarshallerException, IOException { - writer.write(StructuredData.OBJECT_START); - //boolean first = true; - for(Map.Entry entry : m.entrySet()) { - //if (!first) writer.nextValue(); - //first = false; - - writer.write(StructuredData.objectKey(entry.getKey().toString())); - //writer.writeKeyValueDelimiter(); - write(entry.getValue(), writer); // Note: This is vulnerable to reference loops! - } - - writer.write(StructuredData.OBJECT_END); - } - - private static void packCollection(Collection c, StructuredDataWriter writer) throws MarshallerException, IOException { - writer.write(StructuredData.ARRAY_START); - //boolean first = true; - for(Object elem : c) { - //if (!first) writer.nextValue(); - //first = false; - - write(elem, writer); // Note: This is vulnerable to reference loops! - } - - writer.write(StructuredData.ARRAY_END); - } - - private static void packArray(Object a, StructuredDataWriter writer) throws MarshallerException, IOException { - if (!a.getClass().isArray()) throw new MarshallerException("Passed-in object is not an array"); - - writer.write(StructuredData.ARRAY_START); - //boolean first = true; - for(int i=0; i consumedFields = new HashSet<>(); - //boolean first = true; - - //Visit all fields, public and private, on the terminal class - for(Field f : o.getClass().getDeclaredFields()) { - // Ignore static and transient fields - if (f.accessFlags().contains(AccessFlag.STATIC) || f.accessFlags().contains(AccessFlag.TRANSIENT)) continue; - - // Ignore this same field next time if we re-visit it - consumedFields.add(f.getName()); - - //if (!first) writer.nextValue(); - //first = false; - - writeField(f, o, writer); - } - - //Visit public super fields - for(Field f : o.getClass().getFields()) { - // Ignore static and transient fields - if (f.accessFlags().contains(AccessFlag.STATIC) || f.accessFlags().contains(AccessFlag.TRANSIENT)) continue; - - // Ignore this field if we're revisiting it or if it's shadowed - if (consumedFields.contains(f.getName())) continue; - - //if (!first) writer.nextValue(); - //first = false; - - writeField(f, o, writer); - } - - writer.write(StructuredData.OBJECT_END); - } -} diff --git a/src/main/java/blue/endless/jankson/impl/io/context/ParserContext.java b/src/main/java/blue/endless/jankson/impl/io/context/ParserContext.java index e10681e..445e2dd 100644 --- a/src/main/java/blue/endless/jankson/impl/io/context/ParserContext.java +++ b/src/main/java/blue/endless/jankson/impl/io/context/ParserContext.java @@ -25,7 +25,6 @@ package blue.endless.jankson.impl.io.context; import java.io.IOException; -import java.util.function.BiConsumer; import java.util.function.Consumer; import blue.endless.jankson.api.SyntaxError; @@ -46,7 +45,7 @@ public interface ParserContext { /** * Returns true if the parser has assembled a complete result. This method may trigger lookahead but MUST NOT read. - * After this method returns true, the Reader state will change, and {@link #parse(LookaheadCodePointReader, BiConsumer, Consumer)} will + * After this method returns true, the Reader state will change, and {@link #parse(LookaheadCodePointReader, Consumer, Consumer)} will * no longer be called. */ public boolean isComplete(LookaheadCodePointReader reader); diff --git a/src/main/java/blue/endless/jankson/impl/io/context/ValueParserContext.java b/src/main/java/blue/endless/jankson/impl/io/context/ValueParserContext.java deleted file mode 100644 index e03e22f..0000000 --- a/src/main/java/blue/endless/jankson/impl/io/context/ValueParserContext.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2018-2024 Falkreon (Isaac Ellingson) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package blue.endless.jankson.impl.io.context; - -import java.io.IOException; - -import blue.endless.jankson.api.SyntaxError; -import blue.endless.jankson.api.io.ElementType; -import blue.endless.jankson.api.io.JsonReaderOptions; -import blue.endless.jankson.api.io.JsonReaderOptions.Hint; -import blue.endless.jankson.impl.io.LookaheadCodePointReader; - -public class ValueParserContext { - private final JsonReaderOptions opts; - private final boolean isRoot; - private ParserContext delegate; - - public ValueParserContext(JsonReaderOptions opts) { - this.opts = opts; - this.isRoot = false; - } - - protected ValueParserContext(JsonReaderOptions opts, boolean isRoot) { - this.opts = opts; - this.isRoot = isRoot; - } - - public ElementType parse(LookaheadCodePointReader reader) throws IOException, SyntaxError { - int ch = reader.peek(); - - switch(ch) { - case '/': - case '#': - //Comment - return ElementType.COMMENT; - //break; - - case '\'': - case '"': - /* - * if ALLOW_BARE_ROOT_OBJECT && isRoot, instead of treating this as a root String object, we're going to - * treat it as a bare-root with a String key in it, so kick us down one level into an Object context. In - * any other circumstance, this will be a String value. - */ - - if (opts.hasHint(Hint.ALLOW_BARE_ROOT_OBJECT) && isRoot) { - //Start an object and start parsing the key - return ElementType.OBJECT_START; - } else { - //String - return ElementType.PRIMITIVE; - } - //break; - - case '{': - //Object - return ElementType.OBJECT_START; - //break; - - case '[': - //Array - return ElementType.ARRAY_START; - //break; - - case '}': - case ']': - //Explicitly Forbidden - throw new SyntaxError("Unmatched symbol"); - - default: - /* - * If we reached this point we've arrived at a bare identifier. What we do will depend on the - * JsonReaderOptions and our own state. If ALLOW_BARE_ROOT_OBJECT && ALLOW_UNQUOTED_KEYS && isRoot, we're - * "looking for" a bare key identifier, and will want to interpret `true` as a key with the name "true" - * rather than the boolean value. Otherwise, we're going to parse this as a token value (like "true" or - * "NaN"), or a number. - */ - - if (opts.hasHint(Hint.ALLOW_BARE_ROOT_OBJECT) && opts.hasHint(Hint.ALLOW_UNQUOTED_KEYS) && isRoot) { - //start an object context - return ElementType.OBJECT_KEY; - } else { - //start a token context - return ElementType.PRIMITIVE; - } - //break; - } - } -} diff --git a/src/main/java/blue/endless/jankson/impl/io/objectreader/ArrayStructuredDataReader.java b/src/main/java/blue/endless/jankson/impl/io/objectreader/ArrayStructuredDataReader.java index 6a6a662..64fd53f 100644 --- a/src/main/java/blue/endless/jankson/impl/io/objectreader/ArrayStructuredDataReader.java +++ b/src/main/java/blue/endless/jankson/impl/io/objectreader/ArrayStructuredDataReader.java @@ -26,17 +26,20 @@ import java.lang.reflect.Array; +import blue.endless.jankson.api.io.ObjectReaderFactory; import blue.endless.jankson.api.io.StructuredData; class ArrayStructuredDataReader extends DelegatingStructuredDataReader { private final Object arr; + private final ObjectReaderFactory factory; private int index = 0; - public ArrayStructuredDataReader(Object array) { + public ArrayStructuredDataReader(Object array, ObjectReaderFactory factory) { if (!array.getClass().isArray()) throw new IllegalArgumentException("This class can only be used with arrays."); this.arr = array; + this.factory = (factory == null) ? new ObjectReaderFactory() : factory; buffer(StructuredData.ARRAY_START); } @@ -48,7 +51,7 @@ protected void onDelegateEmpty() { } else { Object o = Array.get(arr, index); index++; - setDelegate(ObjectStructuredDataReader.of(o)); + setDelegate(factory.getReader(o)); } } } \ No newline at end of file diff --git a/src/main/java/blue/endless/jankson/impl/io/objectreader/CollectionStructuredDataReader.java b/src/main/java/blue/endless/jankson/impl/io/objectreader/CollectionStructuredDataReader.java index 9f12264..0973179 100644 --- a/src/main/java/blue/endless/jankson/impl/io/objectreader/CollectionStructuredDataReader.java +++ b/src/main/java/blue/endless/jankson/impl/io/objectreader/CollectionStructuredDataReader.java @@ -27,13 +27,16 @@ import java.util.Collection; import java.util.Iterator; +import blue.endless.jankson.api.io.ObjectReaderFactory; import blue.endless.jankson.api.io.StructuredData; public class CollectionStructuredDataReader extends DelegatingStructuredDataReader { - private Iterator iter; + private final Iterator iter; + private final ObjectReaderFactory factory; - public CollectionStructuredDataReader(Collection collection) { + public CollectionStructuredDataReader(Collection collection, ObjectReaderFactory factory) { iter = collection.iterator(); + this.factory = factory; buffer(StructuredData.ARRAY_START); } @@ -43,7 +46,7 @@ protected void onDelegateEmpty() { buffer(StructuredData.ARRAY_END); buffer(StructuredData.EOF); } else { - this.setDelegate(ObjectStructuredDataReader.of(iter.next())); + this.setDelegate(factory.getReader(iter.next())); } } } diff --git a/src/main/java/blue/endless/jankson/impl/io/objectreader/MapStructuredDataReader.java b/src/main/java/blue/endless/jankson/impl/io/objectreader/MapStructuredDataReader.java index 3ce434d..25a8843 100644 --- a/src/main/java/blue/endless/jankson/impl/io/objectreader/MapStructuredDataReader.java +++ b/src/main/java/blue/endless/jankson/impl/io/objectreader/MapStructuredDataReader.java @@ -29,16 +29,19 @@ import java.util.Map; import java.util.Objects; +import blue.endless.jankson.api.io.ObjectReaderFactory; import blue.endless.jankson.api.io.StructuredData; public class MapStructuredDataReader extends DelegatingStructuredDataReader { private final Map map; private final Iterator> iterator; + private final ObjectReaderFactory factory; @SuppressWarnings("unchecked") - public MapStructuredDataReader(Map map) { + public MapStructuredDataReader(Map map, ObjectReaderFactory factory) { this.map = (Map) map; this.iterator = this.map.entrySet().iterator(); + this.factory = factory; this.buffer(StructuredData.OBJECT_START); } @@ -55,7 +58,7 @@ protected void onDelegateEmpty() throws IOException { buffer(StructuredData.objectKey( Objects.toString(entry.getKey()) )); - setDelegate(ObjectStructuredDataReader.of(entry.getValue())); + setDelegate(factory.getReader(entry.getValue())); } } diff --git a/src/main/java/blue/endless/jankson/impl/io/objectreader/ObjectStructuredDataReader.java b/src/main/java/blue/endless/jankson/impl/io/objectreader/ObjectStructuredDataReader.java index cf3d180..f7b6760 100644 --- a/src/main/java/blue/endless/jankson/impl/io/objectreader/ObjectStructuredDataReader.java +++ b/src/main/java/blue/endless/jankson/impl/io/objectreader/ObjectStructuredDataReader.java @@ -34,6 +34,7 @@ import blue.endless.jankson.api.annotation.SerializedName; import blue.endless.jankson.api.document.PrimitiveElement; +import blue.endless.jankson.api.io.ObjectReaderFactory; import blue.endless.jankson.api.io.StructuredData; import blue.endless.jankson.api.io.StructuredDataReader; import blue.endless.jankson.impl.TypeMagic; @@ -41,16 +42,20 @@ /** * StructuredDataReader which reads data directly from an arbitrary Java object. * + *

Instances of this object can be created indirectly through ObjectReaderFactory. + * *

This class is not threadsafe! No effort is made to detect mutations during * object access. */ public class ObjectStructuredDataReader extends DelegatingStructuredDataReader { private final Object obj; + private final ObjectReaderFactory factory; private ArrayDeque pendingFields = new ArrayDeque<>(); - private ObjectStructuredDataReader(Object object) { + private ObjectStructuredDataReader(Object object, ObjectReaderFactory factory) { this.obj = object; this.buffer(StructuredData.OBJECT_START); + this.factory = (factory == null) ? new ObjectReaderFactory() : factory; Set alreadyTaken = new HashSet<>(); for(Field f : obj.getClass().getDeclaredFields()) { @@ -83,18 +88,31 @@ protected void onDelegateEmpty() throws IOException { if (value == null) { buffer(StructuredData.NULL); } else { - setDelegate(ObjectStructuredDataReader.of(value)); + setDelegate(factory.getReader(value)); } } catch (Throwable t) { throw new IOException("Could not access field data for field \""+fieldName+"\" ("+cur.getName()+").", t); } } - public static StructuredDataReader of(Object o) { - if (o.getClass().isArray()) return new ArrayStructuredDataReader(o); - if (o instanceof Collection val) return new CollectionStructuredDataReader(val); - if (o instanceof Map val) return new MapStructuredDataReader(val); + /* + * Control flow note: + * This method *is* the fallback behavior of ObjectReaderFactory. + * + * The contents of this method MUST NOT delegate directly to the provided ORF, because ORF + * delegates directly to this method to provide readers for non-overridden types. + */ + + /** + * Do not use this method directly. Obtain an ObjectReaderFactory and ask it for an appropriate + * StructuredDataReader for the object in question. + * @see ObjectReaderFactory + */ + public static StructuredDataReader of(Object o, ObjectReaderFactory factory) { + if (o.getClass().isArray()) return new ArrayStructuredDataReader(o, factory); + if (o instanceof Collection val) return new CollectionStructuredDataReader(val, factory); + if (o instanceof Map val) return new MapStructuredDataReader(val, factory); if (PrimitiveElement.canBox(o)) return new PrimitiveStructuredDataReader(o); - return new ObjectStructuredDataReader(o); + return new ObjectStructuredDataReader(o, factory); } } diff --git a/src/test/java/blue/endless/jankson/TestObjectWriter.java b/src/test/java/blue/endless/jankson/TestObjectWriter.java index 69bca68..3e65181 100644 --- a/src/test/java/blue/endless/jankson/TestObjectWriter.java +++ b/src/test/java/blue/endless/jankson/TestObjectWriter.java @@ -39,6 +39,7 @@ import blue.endless.jankson.api.annotation.SerializedName; import blue.endless.jankson.api.io.JsonReader; import blue.endless.jankson.api.io.JsonWriterOptions; +import blue.endless.jankson.api.io.ObjectReaderFactory; import blue.endless.jankson.api.io.ObjectWriter; import blue.endless.jankson.impl.magic.ClassHierarchy; @@ -339,7 +340,7 @@ public void testFull() throws IOException { PojoConfig config = new PojoConfig(); config.hostName = "localhost"; config.portNumber = 65535; - Jankson.writeJson(config, writer, JsonWriterOptions.DEFAULTS); + Jankson.writeJson(config, new ObjectReaderFactory(), writer, JsonWriterOptions.DEFAULTS); String actual = writer.toString(); Assertions.assertEquals(expected, actual); diff --git a/src/test/java/blue/endless/jankson/TestSerializer.java b/src/test/java/blue/endless/jankson/TestSerializer.java index fb06d1c..dc5e268 100644 --- a/src/test/java/blue/endless/jankson/TestSerializer.java +++ b/src/test/java/blue/endless/jankson/TestSerializer.java @@ -28,9 +28,9 @@ import blue.endless.jankson.api.document.ValueElement; import blue.endless.jankson.api.io.JsonWriter; import blue.endless.jankson.api.io.JsonWriterOptions; +import blue.endless.jankson.api.io.ObjectReaderFactory; import blue.endless.jankson.api.io.StructuredDataReader; import blue.endless.jankson.api.io.ValueElementWriter; -import blue.endless.jankson.impl.ObjectToStructuredDataPipe; import blue.endless.jankson.impl.io.objectreader.ObjectStructuredDataReader; import java.io.IOException; @@ -51,26 +51,26 @@ public class TestSerializer { @Test public void testPrimitiveSerialization() throws IOException { - String actual = Jankson.writeJsonString(2, JsonWriterOptions.ONE_LINE); + String actual = Jankson.writeJsonString(2, new ObjectReaderFactory(), JsonWriterOptions.ONE_LINE); Assertions.assertEquals("2", actual); } @Test public void testArraySerialization() throws IOException { - String actual = Jankson.writeJsonString(new String[] { "foo", "bar" }, JsonWriterOptions.ONE_LINE); + String actual = Jankson.writeJsonString(new String[] { "foo", "bar" }, new ObjectReaderFactory(), JsonWriterOptions.ONE_LINE); Assertions.assertEquals("[ \"foo\", \"bar\" ]", actual); } @Test public void testCollectionSerialization() throws IOException { - String listActual = Jankson.writeJsonString(List.of(1, 2, 3), JsonWriterOptions.ONE_LINE); + String listActual = Jankson.writeJsonString(List.of(1, 2, 3), new ObjectReaderFactory(), JsonWriterOptions.ONE_LINE); Assertions.assertEquals("[ 1, 2, 3 ]", listActual); // Testing Sets is harder because of undetermined or purposefully randomized iteration order // LinkedHashSet fixes this by forcing insertion order LinkedHashSet testSet = new LinkedHashSet<>(); testSet.add(1); testSet.add(2); testSet.add(3); - String setActual = Jankson.writeJsonString(testSet, JsonWriterOptions.ONE_LINE); + String setActual = Jankson.writeJsonString(testSet, new ObjectReaderFactory(), JsonWriterOptions.ONE_LINE); Assertions.assertEquals("[ 1, 2, 3 ]", setActual); }