diff --git a/src/main/java/org/joda/beans/ser/json/JodaBeanSimpleJsonWalker.java b/src/main/java/org/joda/beans/ser/json/JodaBeanSimpleJsonWalker.java
deleted file mode 100644
index c559c961..00000000
--- a/src/main/java/org/joda/beans/ser/json/JodaBeanSimpleJsonWalker.java
+++ /dev/null
@@ -1,552 +0,0 @@
-/*
- * Copyright 2001-present Stephen Colebourne
- *
- * 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.joda.beans.ser.json;
-
-import java.io.IOException;
-import java.lang.reflect.Array;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Spliterator;
-import java.util.stream.StreamSupport;
-
-import org.joda.beans.Bean;
-import org.joda.beans.ResolvedType;
-import org.joda.beans.ser.JodaBeanSer;
-import org.joda.collect.grid.Grid;
-import org.joda.collect.grid.ImmutableGrid;
-import org.joda.convert.ToStringConverter;
-
-import com.google.common.collect.BiMap;
-import com.google.common.collect.ImmutableMultiset;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Multiset;
-import com.google.common.collect.Table;
-
-/**
- * Walks a Joda-Bean to write in simple JSON format.
- *
- * This class contains mutable state and cannot be used from multiple threads.
- * A new instance must be created for each message.
- */
-class JodaBeanSimpleJsonWalker {
-
- // why is there an ugly ClassValue setup here?
- // because this is O(1) whereas switch with pattern match which is O(n)
- private static final ClassValue> LOOKUP = new ClassValue<>() {
-
- @SuppressWarnings("rawtypes") // sneaky use of raw type to allow typed value in each method below
- @Override
- protected JsonHandler computeValue(Class> type) {
- if (Bean.class.isAssignableFrom(type)) {
- return (JsonHandler) JodaBeanSimpleJsonWalker::writeBean;
- }
- if (type.isArray()) {
- var componentType = type.getComponentType();
- if (componentType.isPrimitive()) {
- if (componentType != byte.class) {
- return JodaBeanSimpleJsonWalker::writePrimitiveArray;
- }
- } else {
- return (JsonHandler) JodaBeanSimpleJsonWalker::writeArray;
- }
- }
- if (type == String.class) {
- return (writer, declaredType, propName, value) -> writer.output.writeString((String) value);
- }
- if (type == Long.class || type == long.class) {
- return (writer, declaredType, propName, value) -> writer.output.writeLong((Long) value);
- }
- if (type == Integer.class || type == int.class) {
- return (writer, declaredType, propName, value) -> writer.output.writeInt((Integer) value);
- }
- if (type == Short.class || type == short.class) {
- return (writer, declaredType, propName, value) -> writer.output.writeInt((Short) value);
- }
- if (type == Byte.class || type == byte.class) {
- return (writer, declaredType, propName, value) -> writer.output.writeInt((Byte) value);
- }
- if (type == Double.class || type == double.class) {
- return (writer, declaredType, propName, value) -> writer.writeDouble((Double) value);
- }
- if (type == Float.class || type == float.class) {
- return (writer, declaredType, propName, value) -> writer.writeFloat((Float) value);
- }
- if (type == Boolean.class || type == boolean.class) {
- return (writer, declaredType, propName, value) -> writer.output.writeBoolean((Boolean) value);
- }
- if (type == Optional.class) {
- return OptionalJsonHandler.INSTANCE;
- }
- return BaseJsonHandlers.INSTANCE.computeValue(type);
- }
- };
-
- /**
- * The settings to use.
- */
- private final JodaBeanSer settings;
- /**
- * The outputter.
- */
- private JsonOutput output;
-
- /**
- * Creates an instance.
- *
- * @param settings the settings to use, not null
- */
- JodaBeanSimpleJsonWalker(JodaBeanSer settings) {
- this.settings = settings;
- }
-
- //-------------------------------------------------------------------------
- /**
- * Writes the bean to the {@code Appendable}.
- *
- * The type of the bean will be set in the message.
- *
- * @param bean the bean to output, not null
- * @param output the output appendable, not null
- * @throws IOException if an error occurs
- */
- void walk(Bean bean, Appendable output) throws IOException {
- this.output = new JsonOutput(output, settings.getIndent(), settings.getNewLine());
- walkObject(ResolvedType.OBJECT, "", bean);
- output.append(settings.getNewLine());
- }
-
- //-----------------------------------------------------------------------
- // walk an object, by determining the runtime type
- private void walkObject(ResolvedType declaredType, String propertyName, Object value) throws IOException {
- if (value == null) {
- output.writeNull();
- } else {
- var handler = LOOKUP.get(value.getClass());
- handler.handle(this, declaredType, propertyName, value);
- }
- }
-
- //-------------------------------------------------------------------------
- private void writeBean(ResolvedType declaredType, String propertyName, Bean bean) throws IOException {
- // check for Joda-Convert cannot be in ClassValue as it relies on the settings
- if (settings.getConverter().isConvertible(bean.getClass())) {
- writeJodaConvert(declaredType, propertyName, bean);
- } else {
- output.writeObjectStart();
- writeBeanProperties(declaredType, propertyName, bean);
- output.writeObjectEnd();
- }
- }
-
- private void writeBeanProperties(ResolvedType declaredType, String propertyName, Bean bean) throws IOException {
- for (var metaProperty : bean.metaBean().metaPropertyIterable()) {
- if (settings.isSerialized(metaProperty)) {
- var value = metaProperty.get(bean);
- if (value != null) {
- var resolvedType = ResolvedType.from(metaProperty.propertyGenericType(), bean.getClass());
- var handler = LOOKUP.get(value.getClass());
- handler.handleProperty(this, resolvedType, metaProperty.name(), value);
- }
- }
- }
- }
-
- //-------------------------------------------------------------------------
- private void writeArray(ResolvedType declaredType, String propertyName, Object[] array) throws IOException {
- var componentType = declaredType.toComponentType();
- output.writeArrayStart();
- for (var item : array) {
- output.writeArrayItemStart();
- walkObject(componentType, "", item);
- }
- output.writeArrayEnd();
- }
-
- private void writePrimitiveArray(ResolvedType declaredType, String propertyName, Object array) throws IOException {
- var componentType = declaredType.toComponentType();
- var handler = LOOKUP.get(componentType.getRawType());
- var arrayLength = Array.getLength(array);
- output.writeArrayStart();
- for (int i = 0; i < arrayLength; i++) {
- output.writeArrayItemStart();
- handler.handle(this, declaredType, propertyName, Array.get(array, i));
- }
- output.writeArrayEnd();
- }
-
- private void writeDouble(Double val) throws IOException {
- if (Double.isNaN(val)) {
- output.writeNull();
- } else {
- output.writeDouble(val);
- }
- }
-
- private void writeFloat(Float val) throws IOException {
- if (Float.isNaN(val)) {
- output.writeNull();
- } else {
- output.writeFloat(val);
- }
- }
-
- private void writeJodaConvert(ResolvedType declaredType, String propertyName, Object value) throws IOException {
- var realType = value.getClass();
- try {
- var converted = settings.getConverter().convertToString(value);
- if (converted == null) {
- throw invalidNullString(propertyName, value);
- }
- output.writeString(converted);
- } catch (RuntimeException ex) {
- throw new IllegalArgumentException(
- "Unable to write property '" + propertyName + "', type " + realType.getName() + " could not be converted to a String",
- ex);
- }
- }
-
- private static IllegalArgumentException invalidNullString(String propertyName, Object value) {
- return new IllegalArgumentException(
- "Unable to write property '" + propertyName + "' because converter returned a null string: " + value);
- }
-
- //-------------------------------------------------------------------------
- private static interface JsonHandler {
- public abstract void handle(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- T obj) throws IOException;
-
- public default void handleProperty(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- T obj) throws IOException {
-
- walker.output.writeObjectKey(propertyName);
- handle(walker, declaredType, propertyName, obj);
- }
- }
-
- //-------------------------------------------------------------------------
- private static sealed class BaseJsonHandlers {
-
- private static final BaseJsonHandlers INSTANCE = getInstance();
-
- private static final BaseJsonHandlers getInstance() {
- try {
- ImmutableGrid.of(); // check if class is available
- return new CollectJsonHandlers();
- } catch (RuntimeException | LinkageError ex) {
- try {
- ImmutableMultiset.of(); // check if class is available
- return new GuavaJsonHandlers();
- } catch (RuntimeException | LinkageError ex2) {
- return new BaseJsonHandlers();
- }
- }
- }
-
- @SuppressWarnings("rawtypes") // sneaky use of raw type to allow typed value in each method below
- JsonHandler computeValue(Class> type) {
- if (Map.class.isAssignableFrom(type)) {
- return (JsonHandler>) BaseJsonHandlers::writeMap;
- }
- if (Collection.class.isAssignableFrom(type)) {
- return (JsonHandler>) BaseJsonHandlers::writeCollection;
- }
- if (Iterable.class.isAssignableFrom(type)) {
- return (JsonHandler>) BaseJsonHandlers::writeIterable;
- }
- return JodaBeanSimpleJsonWalker::writeJodaConvert;
- }
-
- // writes an iterable
- private static void writeIterable(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- Iterable> iterable) throws IOException {
-
- // convert to a list, which is necessary as there is no size() on Iterable
- // this ensures that the generics of the iterable are retained
- var list = StreamSupport.stream(iterable::spliterator, Spliterator.ORDERED, false).toList();
- var adjustedType = ResolvedType.of(List.class, declaredType.getArguments());
- writeCollection(walker, adjustedType, propertyName, list);
- }
-
- // writes a collection
- private static void writeCollection(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- Collection> coll) throws IOException {
-
- var itemType = declaredType.getArgumentOrDefault(0);
- walker.output.writeArrayStart();
- for (var item : coll) {
- walker.output.writeArrayItemStart();
- walker.walkObject(itemType, "", item);
- }
- walker.output.writeArrayEnd();
- }
-
- // writes a map, with meta type information if necessary
- private static void writeMap(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- Map, ?> map) throws IOException {
-
- writeMapEntries(walker, declaredType, propertyName, map.entrySet());
- }
-
- // writes a map given map entries, code shared with Multimap
- static void writeMapEntries(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- Collection> mapEntries) throws IOException {
-
- var keyType = declaredType.getArgumentOrDefault(0);
- var valueType = declaredType.getArgumentOrDefault(1);
- // converter based on the declared type if possible, else based on the runtime type
- var keyConverterOpt = walker.settings.getConverter().converterFor(keyType.getRawType());
- ToStringConverter keyConverter = keyConverterOpt.isPresent() ?
- keyConverterOpt.get().withoutGenerics() :
- key -> walker.settings.getConverter().convertToString(key);
- walker.output.writeObjectStart();
- for (var entry : mapEntries) {
- var key = entry.getKey();
- if (key == null) {
- throw invalidNullMapKey(propertyName);
- }
- var str = keyConverter.convertToString(key);
- walker.output.writeObjectKey(str);
- walker.walkObject(valueType, "", entry.getValue());
- }
- walker.output.writeObjectEnd();
- }
-
- private static IllegalArgumentException invalidNullMapKey(String propertyName) {
- return new IllegalArgumentException(
- "Unable to write property '" + propertyName + "', map key must not be null");
- }
-
- }
-
- //-------------------------------------------------------------------------
- private static sealed class GuavaJsonHandlers extends BaseJsonHandlers {
-
- @Override
- @SuppressWarnings("rawtypes") // sneaky use of raw type to allow typed value in each method below
- JsonHandler computeValue(Class> type) {
- if (Multimap.class.isAssignableFrom(type)) {
- return (JsonHandler>) GuavaJsonHandlers::writeMultimap;
- }
- if (Multiset.class.isAssignableFrom(type)) {
- return (JsonHandler>) GuavaJsonHandlers::writeMultiset;
- }
- if (Table.class.isAssignableFrom(type)) {
- return (JsonHandler>) GuavaJsonHandlers::writeTable;
- }
- if (BiMap.class.isAssignableFrom(type)) {
- return (JsonHandler>) GuavaJsonHandlers::writeBiMap;
- }
- if (com.google.common.base.Optional.class.isAssignableFrom(type)) {
- return GuavaOptionalJsonHandler.INSTANCE;
- }
- return super.computeValue(type);
- }
-
- // writes a multimap
- private static void writeMultimap(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- Multimap, ?> mmap) throws IOException {
-
- writeMapEntries(walker, declaredType, propertyName, mmap.entries());
- }
-
- // writes a multiset
- private static void writeMultiset(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- Multiset> mset) throws IOException {
-
- // write content, using a map of value to count
- var valueType = declaredType.getArgumentOrDefault(0);
- walker.output.writeArrayStart();
- for (var entry : mset.entrySet()) {
- walker.output.writeArrayItemStart();
- walker.output.writeArrayStart();
- walker.output.writeArrayItemStart();
- walker.walkObject(valueType, "", entry.getElement());
- walker.output.writeArrayItemStart();
- walker.output.writeInt(entry.getCount());
- walker.output.writeArrayEnd();
- }
- walker.output.writeArrayEnd();
- }
-
- // writes a table
- private static void writeTable(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- Table, ?, ?> table) throws IOException {
-
- // write content, using an array of cells
- var rowType = declaredType.getArgumentOrDefault(0);
- var columnType = declaredType.getArgumentOrDefault(1);
- var valueType = declaredType.getArgumentOrDefault(2);
- walker.output.writeArrayStart();
- for (var cell : table.cellSet()) {
- walker.output.writeArrayItemStart();
- walker.output.writeArrayStart();
- walker.output.writeArrayItemStart();
- walker.walkObject(rowType, "", cell.getRowKey());
- walker.output.writeArrayItemStart();
- walker.walkObject(columnType, "", cell.getColumnKey());
- walker.output.writeArrayItemStart();
- walker.walkObject(valueType, "", cell.getValue());
- walker.output.writeArrayEnd();
- }
- walker.output.writeArrayEnd();
- }
-
- // writes a BiMap, with meta type information if necessary
- private static void writeBiMap(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- BiMap, ?> biMap) throws IOException {
-
- writeMapEntries(walker, declaredType, propertyName, biMap.entrySet());
- }
- }
-
- //-------------------------------------------------------------------------
- private static final class CollectJsonHandlers extends GuavaJsonHandlers {
-
- @Override
- @SuppressWarnings("rawtypes") // sneaky use of raw type to allow typed value in each method below
- JsonHandler computeValue(Class> type) {
- if (Grid.class.isAssignableFrom(type)) {
- return (JsonHandler>) CollectJsonHandlers::writeGrid;
- }
- return super.computeValue(type);
- }
-
- private static void writeGrid(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- Grid> grid) throws IOException {
-
- // write grid using sparse approach
- var valueType = declaredType.getArgumentOrDefault(0);
- walker.output.writeArrayStart();
- walker.output.writeArrayItemStart();
- walker.output.writeInt(grid.rowCount());
- walker.output.writeArrayItemStart();
- walker.output.writeInt(grid.columnCount());
- for (var cell : grid.cells()) {
- walker.output.writeArrayItemStart();
- walker.output.writeArrayStart();
- walker.output.writeArrayItemStart();
- walker.output.writeInt(cell.getRow());
- walker.output.writeArrayItemStart();
- walker.output.writeInt(cell.getColumn());
- walker.output.writeArrayItemStart();
- walker.walkObject(valueType, "", cell.getValue());
- walker.output.writeArrayEnd();
- }
- walker.output.writeArrayEnd();
- }
- }
-
- //-------------------------------------------------------------------------
- static final class OptionalJsonHandler implements JsonHandler> {
- private static final OptionalJsonHandler INSTANCE = new OptionalJsonHandler();
-
- // when Optional is not a property, it is processed as a kind of collection
- @Override
- public void handle(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- Optional> opt) throws IOException {
-
- var valueType = declaredType.getArgumentOrDefault(0);
- walker.walkObject(valueType, "", opt.orElse(null));
- }
-
- // when Optional is a property, it is ignored if empty
- @Override
- public void handleProperty(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- Optional> opt) throws IOException {
-
- var value = opt.orElse(null);
- if (value != null) {
- var valueType = declaredType.getArgumentOrDefault(0);
- walker.output.writeObjectKey(propertyName);
- walker.walkObject(valueType, propertyName, value);
- }
- }
- }
-
- //-------------------------------------------------------------------------
- static final class GuavaOptionalJsonHandler implements JsonHandler> {
- private static final GuavaOptionalJsonHandler INSTANCE = new GuavaOptionalJsonHandler();
-
- // when Optional is not a property, it is processed as a kind of collection
- @Override
- public void handle(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- com.google.common.base.Optional> opt) throws IOException {
-
- var valueType = declaredType.getArgumentOrDefault(0);
- walker.walkObject(valueType, "", opt.orNull());
- }
-
- // when Optional is a property, it is ignored if empty
- @Override
- public void handleProperty(
- JodaBeanSimpleJsonWalker walker,
- ResolvedType declaredType,
- String propertyName,
- com.google.common.base.Optional> opt) throws IOException {
-
- var value = opt.orNull();
- if (value != null) {
- var valueType = declaredType.getArgumentOrDefault(0);
- walker.output.writeObjectKey(propertyName);
- walker.walkObject(valueType, propertyName, value);
- }
- }
- }
-}
diff --git a/src/main/java/org/joda/beans/ser/json/JodaBeanSimpleJsonWriter.java b/src/main/java/org/joda/beans/ser/json/JodaBeanSimpleJsonWriter.java
index 24315ae6..35df4a6e 100644
--- a/src/main/java/org/joda/beans/ser/json/JodaBeanSimpleJsonWriter.java
+++ b/src/main/java/org/joda/beans/ser/json/JodaBeanSimpleJsonWriter.java
@@ -16,10 +16,27 @@
package org.joda.beans.ser.json;
import java.io.IOException;
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Spliterator;
+import java.util.stream.StreamSupport;
import org.joda.beans.Bean;
import org.joda.beans.JodaBeanUtils;
+import org.joda.beans.ResolvedType;
import org.joda.beans.ser.JodaBeanSer;
+import org.joda.collect.grid.Grid;
+import org.joda.collect.grid.ImmutableGrid;
+import org.joda.convert.ToStringConverter;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableMultiset;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Table;
/**
* Provides the ability for a Joda-Bean to be written to a simple JSON format.
@@ -43,10 +60,65 @@
*/
public class JodaBeanSimpleJsonWriter {
+ // why is there an ugly ClassValue setup here?
+ // because this is O(1) whereas switch with pattern match which is O(n)
+ private static final ClassValue> LOOKUP = new ClassValue<>() {
+
+ @SuppressWarnings("rawtypes") // sneaky use of raw type to allow typed value in each method below
+ @Override
+ protected JsonHandler computeValue(Class> type) {
+ if (Bean.class.isAssignableFrom(type)) {
+ return (JsonHandler) JodaBeanSimpleJsonWriter::writeBean;
+ }
+ if (type.isArray()) {
+ var componentType = type.getComponentType();
+ if (componentType.isPrimitive()) {
+ if (componentType != byte.class) {
+ return JodaBeanSimpleJsonWriter::writePrimitiveArray;
+ }
+ } else {
+ return (JsonHandler) JodaBeanSimpleJsonWriter::writeArray;
+ }
+ }
+ if (type == String.class) {
+ return (writer, declaredType, propName, value) -> writer.output.writeString((String) value);
+ }
+ if (type == Long.class || type == long.class) {
+ return (writer, declaredType, propName, value) -> writer.output.writeLong((Long) value);
+ }
+ if (type == Integer.class || type == int.class) {
+ return (writer, declaredType, propName, value) -> writer.output.writeInt((Integer) value);
+ }
+ if (type == Short.class || type == short.class) {
+ return (writer, declaredType, propName, value) -> writer.output.writeInt((Short) value);
+ }
+ if (type == Byte.class || type == byte.class) {
+ return (writer, declaredType, propName, value) -> writer.output.writeInt((Byte) value);
+ }
+ if (type == Double.class || type == double.class) {
+ return (writer, declaredType, propName, value) -> writer.writeDouble((Double) value);
+ }
+ if (type == Float.class || type == float.class) {
+ return (writer, declaredType, propName, value) -> writer.writeFloat((Float) value);
+ }
+ if (type == Boolean.class || type == boolean.class) {
+ return (writer, declaredType, propName, value) -> writer.output.writeBoolean((Boolean) value);
+ }
+ if (type == Optional.class) {
+ return OptionalJsonHandler.INSTANCE;
+ }
+ return BaseJsonHandlers.INSTANCE.computeValue(type);
+ }
+ };
+
/**
* The settings to use.
*/
- private final JodaBeanSimpleJsonWalker walker;
+ private final JodaBeanSer settings;
+ /**
+ * The outputter.
+ */
+ private JsonOutput output;
/**
* Creates an instance.
@@ -55,7 +127,7 @@ public class JodaBeanSimpleJsonWriter {
*/
public JodaBeanSimpleJsonWriter(JodaBeanSer settings) {
JodaBeanUtils.notNull(settings, "settings");
- this.walker = new JodaBeanSimpleJsonWalker(settings);
+ this.settings = settings;
}
//-----------------------------------------------------------------------
@@ -87,7 +159,428 @@ public String write(Bean bean) {
public void write(Bean bean, Appendable output) throws IOException {
JodaBeanUtils.notNull(bean, "bean");
JodaBeanUtils.notNull(output, "output");
- walker.walk(bean, output);
+ this.output = new JsonOutput(output, settings.getIndent(), settings.getNewLine());
+ writeObject(ResolvedType.OBJECT, "", bean);
+ output.append(settings.getNewLine());
+ }
+
+ //-----------------------------------------------------------------------
+ // walk an object, by determining the runtime type
+ private void writeObject(ResolvedType declaredType, String propertyName, Object value) throws IOException {
+ if (value == null) {
+ output.writeNull();
+ } else {
+ var handler = LOOKUP.get(value.getClass());
+ handler.handle(this, declaredType, propertyName, value);
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ private void writeBean(ResolvedType declaredType, String propertyName, Bean bean) throws IOException {
+ // check for Joda-Convert cannot be in ClassValue as it relies on the settings
+ if (settings.getConverter().isConvertible(bean.getClass())) {
+ writeJodaConvert(declaredType, propertyName, bean);
+ } else {
+ output.writeObjectStart();
+ writeBeanProperties(declaredType, propertyName, bean);
+ output.writeObjectEnd();
+ }
+ }
+
+ private void writeBeanProperties(ResolvedType declaredType, String propertyName, Bean bean) throws IOException {
+ for (var metaProperty : bean.metaBean().metaPropertyIterable()) {
+ if (settings.isSerialized(metaProperty)) {
+ var value = metaProperty.get(bean);
+ if (value != null) {
+ var resolvedType = ResolvedType.from(metaProperty.propertyGenericType(), bean.getClass());
+ var handler = LOOKUP.get(value.getClass());
+ handler.handleProperty(this, resolvedType, metaProperty.name(), value);
+ }
+ }
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ private void writeArray(ResolvedType declaredType, String propertyName, Object[] array) throws IOException {
+ var componentType = declaredType.toComponentType();
+ output.writeArrayStart();
+ for (var item : array) {
+ output.writeArrayItemStart();
+ writeObject(componentType, "", item);
+ }
+ output.writeArrayEnd();
+ }
+
+ private void writePrimitiveArray(ResolvedType declaredType, String propertyName, Object array) throws IOException {
+ var componentType = declaredType.toComponentType();
+ var handler = LOOKUP.get(componentType.getRawType());
+ var arrayLength = Array.getLength(array);
+ output.writeArrayStart();
+ for (int i = 0; i < arrayLength; i++) {
+ output.writeArrayItemStart();
+ handler.handle(this, declaredType, propertyName, Array.get(array, i));
+ }
+ output.writeArrayEnd();
+ }
+
+ private void writeDouble(Double val) throws IOException {
+ if (Double.isNaN(val)) {
+ output.writeNull();
+ } else {
+ output.writeDouble(val);
+ }
}
+ private void writeFloat(Float val) throws IOException {
+ if (Float.isNaN(val)) {
+ output.writeNull();
+ } else {
+ output.writeFloat(val);
+ }
+ }
+
+ private void writeJodaConvert(ResolvedType declaredType, String propertyName, Object value) throws IOException {
+ var realType = value.getClass();
+ try {
+ var converted = settings.getConverter().convertToString(value);
+ if (converted == null) {
+ throw invalidNullString(propertyName, value);
+ }
+ output.writeString(converted);
+ } catch (RuntimeException ex) {
+ throw new IllegalArgumentException(
+ "Unable to write property '" + propertyName + "', type " + realType.getName() + " could not be converted to a String",
+ ex);
+ }
+ }
+
+ private static IllegalArgumentException invalidNullString(String propertyName, Object value) {
+ return new IllegalArgumentException(
+ "Unable to write property '" + propertyName + "' because converter returned a null string: " + value);
+ }
+
+ //-------------------------------------------------------------------------
+ private static interface JsonHandler {
+ public abstract void handle(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ T obj) throws IOException;
+
+ public default void handleProperty(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ T obj) throws IOException {
+
+ writer.output.writeObjectKey(propertyName);
+ handle(writer, declaredType, propertyName, obj);
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ private static sealed class BaseJsonHandlers {
+
+ private static final BaseJsonHandlers INSTANCE = getInstance();
+
+ private static final BaseJsonHandlers getInstance() {
+ try {
+ ImmutableGrid.of(); // check if class is available
+ return new CollectJsonHandlers();
+ } catch (RuntimeException | LinkageError ex) {
+ try {
+ ImmutableMultiset.of(); // check if class is available
+ return new GuavaJsonHandlers();
+ } catch (RuntimeException | LinkageError ex2) {
+ return new BaseJsonHandlers();
+ }
+ }
+ }
+
+ @SuppressWarnings("rawtypes") // sneaky use of raw type to allow typed value in each method below
+ JsonHandler computeValue(Class> type) {
+ if (Map.class.isAssignableFrom(type)) {
+ return (JsonHandler>) BaseJsonHandlers::writeMap;
+ }
+ if (Collection.class.isAssignableFrom(type)) {
+ return (JsonHandler>) BaseJsonHandlers::writeCollection;
+ }
+ if (Iterable.class.isAssignableFrom(type)) {
+ return (JsonHandler>) BaseJsonHandlers::writeIterable;
+ }
+ return JodaBeanSimpleJsonWriter::writeJodaConvert;
+ }
+
+ // writes an iterable
+ private static void writeIterable(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ Iterable> iterable) throws IOException {
+
+ // convert to a list, which is necessary as there is no size() on Iterable
+ // this ensures that the generics of the iterable are retained
+ var list = StreamSupport.stream(iterable::spliterator, Spliterator.ORDERED, false).toList();
+ var adjustedType = ResolvedType.of(List.class, declaredType.getArguments());
+ writeCollection(writer, adjustedType, propertyName, list);
+ }
+
+ // writes a collection
+ private static void writeCollection(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ Collection> coll) throws IOException {
+
+ var itemType = declaredType.getArgumentOrDefault(0);
+ writer.output.writeArrayStart();
+ for (var item : coll) {
+ writer.output.writeArrayItemStart();
+ writer.writeObject(itemType, "", item);
+ }
+ writer.output.writeArrayEnd();
+ }
+
+ // writes a map, with meta type information if necessary
+ private static void writeMap(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ Map, ?> map) throws IOException {
+
+ writeMapEntries(writer, declaredType, propertyName, map.entrySet());
+ }
+
+ // writes a map given map entries, code shared with Multimap
+ static void writeMapEntries(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ Collection> mapEntries) throws IOException {
+
+ var keyType = declaredType.getArgumentOrDefault(0);
+ var valueType = declaredType.getArgumentOrDefault(1);
+ // converter based on the declared type if possible, else based on the runtime type
+ var keyConverterOpt = writer.settings.getConverter().converterFor(keyType.getRawType());
+ ToStringConverter keyConverter = keyConverterOpt.isPresent() ?
+ keyConverterOpt.get().withoutGenerics() :
+ key -> writer.settings.getConverter().convertToString(key);
+ writer.output.writeObjectStart();
+ for (var entry : mapEntries) {
+ var key = entry.getKey();
+ if (key == null) {
+ throw invalidNullMapKey(propertyName);
+ }
+ var str = keyConverter.convertToString(key);
+ writer.output.writeObjectKey(str);
+ writer.writeObject(valueType, "", entry.getValue());
+ }
+ writer.output.writeObjectEnd();
+ }
+
+ private static IllegalArgumentException invalidNullMapKey(String propertyName) {
+ return new IllegalArgumentException(
+ "Unable to write property '" + propertyName + "', map key must not be null");
+ }
+
+ }
+
+ //-------------------------------------------------------------------------
+ private static sealed class GuavaJsonHandlers extends BaseJsonHandlers {
+
+ @Override
+ @SuppressWarnings("rawtypes") // sneaky use of raw type to allow typed value in each method below
+ JsonHandler computeValue(Class> type) {
+ if (Multimap.class.isAssignableFrom(type)) {
+ return (JsonHandler>) GuavaJsonHandlers::writeMultimap;
+ }
+ if (Multiset.class.isAssignableFrom(type)) {
+ return (JsonHandler>) GuavaJsonHandlers::writeMultiset;
+ }
+ if (Table.class.isAssignableFrom(type)) {
+ return (JsonHandler>) GuavaJsonHandlers::writeTable;
+ }
+ if (BiMap.class.isAssignableFrom(type)) {
+ return (JsonHandler>) GuavaJsonHandlers::writeBiMap;
+ }
+ if (com.google.common.base.Optional.class.isAssignableFrom(type)) {
+ return GuavaOptionalJsonHandler.INSTANCE;
+ }
+ return super.computeValue(type);
+ }
+
+ // writes a multimap
+ private static void writeMultimap(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ Multimap, ?> mmap) throws IOException {
+
+ writeMapEntries(writer, declaredType, propertyName, mmap.entries());
+ }
+
+ // writes a multiset
+ private static void writeMultiset(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ Multiset> mset) throws IOException {
+
+ // write content, using a map of value to count
+ var valueType = declaredType.getArgumentOrDefault(0);
+ writer.output.writeArrayStart();
+ for (var entry : mset.entrySet()) {
+ writer.output.writeArrayItemStart();
+ writer.output.writeArrayStart();
+ writer.output.writeArrayItemStart();
+ writer.writeObject(valueType, "", entry.getElement());
+ writer.output.writeArrayItemStart();
+ writer.output.writeInt(entry.getCount());
+ writer.output.writeArrayEnd();
+ }
+ writer.output.writeArrayEnd();
+ }
+
+ // writes a table
+ private static void writeTable(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ Table, ?, ?> table) throws IOException {
+
+ // write content, using an array of cells
+ var rowType = declaredType.getArgumentOrDefault(0);
+ var columnType = declaredType.getArgumentOrDefault(1);
+ var valueType = declaredType.getArgumentOrDefault(2);
+ writer.output.writeArrayStart();
+ for (var cell : table.cellSet()) {
+ writer.output.writeArrayItemStart();
+ writer.output.writeArrayStart();
+ writer.output.writeArrayItemStart();
+ writer.writeObject(rowType, "", cell.getRowKey());
+ writer.output.writeArrayItemStart();
+ writer.writeObject(columnType, "", cell.getColumnKey());
+ writer.output.writeArrayItemStart();
+ writer.writeObject(valueType, "", cell.getValue());
+ writer.output.writeArrayEnd();
+ }
+ writer.output.writeArrayEnd();
+ }
+
+ // writes a BiMap, with meta type information if necessary
+ private static void writeBiMap(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ BiMap, ?> biMap) throws IOException {
+
+ writeMapEntries(writer, declaredType, propertyName, biMap.entrySet());
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ private static final class CollectJsonHandlers extends GuavaJsonHandlers {
+
+ @Override
+ @SuppressWarnings("rawtypes") // sneaky use of raw type to allow typed value in each method below
+ JsonHandler computeValue(Class> type) {
+ if (Grid.class.isAssignableFrom(type)) {
+ return (JsonHandler>) CollectJsonHandlers::writeGrid;
+ }
+ return super.computeValue(type);
+ }
+
+ private static void writeGrid(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ Grid> grid) throws IOException {
+
+ // write grid using sparse approach
+ var valueType = declaredType.getArgumentOrDefault(0);
+ writer.output.writeArrayStart();
+ writer.output.writeArrayItemStart();
+ writer.output.writeInt(grid.rowCount());
+ writer.output.writeArrayItemStart();
+ writer.output.writeInt(grid.columnCount());
+ for (var cell : grid.cells()) {
+ writer.output.writeArrayItemStart();
+ writer.output.writeArrayStart();
+ writer.output.writeArrayItemStart();
+ writer.output.writeInt(cell.getRow());
+ writer.output.writeArrayItemStart();
+ writer.output.writeInt(cell.getColumn());
+ writer.output.writeArrayItemStart();
+ writer.writeObject(valueType, "", cell.getValue());
+ writer.output.writeArrayEnd();
+ }
+ writer.output.writeArrayEnd();
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ static final class OptionalJsonHandler implements JsonHandler> {
+ private static final OptionalJsonHandler INSTANCE = new OptionalJsonHandler();
+
+ // when Optional is not a property, it is processed as a kind of collection
+ @Override
+ public void handle(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ Optional> opt) throws IOException {
+
+ var valueType = declaredType.getArgumentOrDefault(0);
+ writer.writeObject(valueType, "", opt.orElse(null));
+ }
+
+ // when Optional is a property, it is ignored if empty
+ @Override
+ public void handleProperty(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ Optional> opt) throws IOException {
+
+ var value = opt.orElse(null);
+ if (value != null) {
+ var valueType = declaredType.getArgumentOrDefault(0);
+ writer.output.writeObjectKey(propertyName);
+ writer.writeObject(valueType, propertyName, value);
+ }
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ static final class GuavaOptionalJsonHandler implements JsonHandler> {
+ private static final GuavaOptionalJsonHandler INSTANCE = new GuavaOptionalJsonHandler();
+
+ // when Optional is not a property, it is processed as a kind of collection
+ @Override
+ public void handle(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ com.google.common.base.Optional> opt) throws IOException {
+
+ var valueType = declaredType.getArgumentOrDefault(0);
+ writer.writeObject(valueType, "", opt.orNull());
+ }
+
+ // when Optional is a property, it is ignored if empty
+ @Override
+ public void handleProperty(
+ JodaBeanSimpleJsonWriter writer,
+ ResolvedType declaredType,
+ String propertyName,
+ com.google.common.base.Optional> opt) throws IOException {
+
+ var value = opt.orNull();
+ if (value != null) {
+ var valueType = declaredType.getArgumentOrDefault(0);
+ writer.output.writeObjectKey(propertyName);
+ writer.writeObject(valueType, propertyName, value);
+ }
+ }
+ }
}