From d752af85d79ab5aed4ad28d5ed61387947fb1fc3 Mon Sep 17 00:00:00 2001 From: Andrea Cavalli Date: Fri, 7 May 2021 12:06:07 +0200 Subject: [PATCH] Add moshi --- pom.xml | 7 +- .../commonutils/moshi/MoshiPolymorphic.java | 274 ++++++++++++++++++ 2 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/warp/commonutils/moshi/MoshiPolymorphic.java diff --git a/pom.xml b/pom.xml index cc7b311..fbc8309 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ common-utils org.warp - 1.1.2 + 1.1.3 UTF-8 @@ -81,6 +81,11 @@ guava 30.1.1-jre + + com.squareup.moshi + moshi + 1.12.0 + diff --git a/src/main/java/org/warp/commonutils/moshi/MoshiPolymorphic.java b/src/main/java/org/warp/commonutils/moshi/MoshiPolymorphic.java new file mode 100644 index 0000000..827bf2f --- /dev/null +++ b/src/main/java/org/warp/commonutils/moshi/MoshiPolymorphic.java @@ -0,0 +1,274 @@ +package org.warp.commonutils.moshi; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonReader.Options; +import com.squareup.moshi.JsonWriter; +import com.squareup.moshi.Moshi; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class MoshiPolymorphic { + + private final boolean instantiateUsingStaticOf; + private final boolean useGetters; + private boolean initialized = false; + private Moshi abstractMoshi; + private final Map, JsonAdapter> abstractClassesSerializers = new ConcurrentHashMap<>(); + private final Map, JsonAdapter> concreteClassesSerializers = new ConcurrentHashMap<>(); + private final Map> customAdapters = new ConcurrentHashMap<>(); + + public MoshiPolymorphic() { + this(false, false); + } + + public MoshiPolymorphic(boolean instantiateUsingStaticOf, boolean useGetters) { + this.instantiateUsingStaticOf = instantiateUsingStaticOf; + this.useGetters = useGetters; + } + + private synchronized void initialize() { + if (!this.initialized) { + this.initialized = true; + var abstractMoshiBuilder = new Moshi.Builder(); + var abstractClasses = getAbstractClasses(); + var concreteClasses = getConcreteClasses(); + + for (Class declaredClass : abstractClasses) { + var name = fixType(declaredClass.getSimpleName()); + JsonAdapter adapter = new PolymorphicAdapter<>(name); + abstractClassesSerializers.put(declaredClass, adapter); + customAdapters.put(name, adapter); + abstractMoshiBuilder.addLast(declaredClass, adapter); + } + + for (Class declaredClass : concreteClasses) { + var modifiers = declaredClass.getModifiers(); + var name = fixType(declaredClass.getSimpleName()); + JsonAdapter adapter = new NormalValueAdapter<>(name, declaredClass); + concreteClassesSerializers.put(declaredClass, adapter); + customAdapters.put(name, adapter); + abstractMoshiBuilder.add(declaredClass, adapter); + } + + abstractMoshi = abstractMoshiBuilder.build(); + } + } + + protected abstract Set> getAbstractClasses(); + + protected abstract Set> getConcreteClasses(); + + protected abstract boolean shouldIgnoreField(String fieldName); + + public Moshi.Builder registerAdapters(Moshi.Builder moshiBuilder) { + initialize(); + abstractClassesSerializers.forEach(moshiBuilder::add); + concreteClassesSerializers.forEach(moshiBuilder::add); + return moshiBuilder; + } + + private class PolymorphicAdapter extends JsonAdapter { + + private final String adapterName; + + private PolymorphicAdapter(String adapterName) { + this.adapterName = adapterName; + } + + private final Options NAMES = Options.of("type", "properties"); + + @Nullable + @Override + public T fromJson(@NotNull JsonReader jsonReader) throws IOException { + String type = null; + + jsonReader.beginObject(); + iterate: while (jsonReader.hasNext()) { + switch (jsonReader.selectName(NAMES)) { + case 0: + type = fixType(jsonReader.nextString()); + break; + case 1: + if (type == null) { + throw new JsonDataException("Type must be defined before properties"); + } + break iterate; + default: + String name = jsonReader.nextName(); + throw new JsonDataException("Key \"" + name + "\" is invalid"); + } + } + + JsonAdapter propertiesAdapter = customAdapters.get(type); + if (propertiesAdapter == null) { + throw new JsonDataException("Type \"" + type + "\" is unknown"); + } + //noinspection unchecked + var result = (T) propertiesAdapter.fromJson(jsonReader); + + jsonReader.endObject(); + + return result; + } + + @Override + public void toJson(@NotNull JsonWriter jsonWriter, @Nullable T t) throws IOException { + if (t == null) { + jsonWriter.nullValue(); + } else { + String type = fixType(t.getClass().getSimpleName()); + + JsonAdapter propertiesAdapter = customAdapters.get(type); + if (propertiesAdapter == null) { + abstractMoshi.adapter(java.lang.Object.class).toJson(jsonWriter, t); + } else { + jsonWriter.beginObject(); + jsonWriter.name("type").value(type); + jsonWriter.name("properties"); + //noinspection unchecked + propertiesAdapter.toJson(jsonWriter, (OBJ) t); + jsonWriter.endObject(); + } + } + } + } + + private class NormalValueAdapter extends JsonAdapter { + + private final String adapterName; + private final Options names; + private final Class declaredClass; + private final Field[] declaredFields; + private final Function[] fieldGetters; + + private NormalValueAdapter(String adapterName, Class declaredClass) { + try { + this.adapterName = adapterName; + this.declaredClass = declaredClass; + var declaredFields = new ArrayList<>(List.of(declaredClass.getDeclaredFields())); + declaredFields.removeIf(field -> shouldIgnoreField(field.getName())); + this.declaredFields = declaredFields.toArray(Field[]::new); + String[] fieldNames = new String[this.declaredFields.length]; + //noinspection unchecked + this.fieldGetters = new Function[this.declaredFields.length]; + int i = 0; + for (Field declaredField : this.declaredFields) { + fieldNames[i] = declaredField.getName(); + + if (useGetters) { + var getterMethod = declaredField + .getDeclaringClass() + .getMethod("get" + StringUtils.capitalize(declaredField.getName())); + fieldGetters[i] = obj -> { + try { + return getterMethod.invoke(obj); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + }; + } else { + fieldGetters[i] = t -> { + try { + return declaredField.get(t); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }; + } + + i++; + } this.names = Options.of(fieldNames); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + @Nullable + @Override + public T fromJson(@NotNull JsonReader jsonReader) throws IOException { + try { + Object instance; + Object[] fields; + if (instantiateUsingStaticOf) { + fields = new Object[declaredFields.length]; + instance = null; + } else { + fields = null; + instance = declaredClass.getConstructor().newInstance(); + } + + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + var nameId = jsonReader.selectName(names); + if (nameId >= 0 && nameId < this.declaredFields.length) { + var fieldValue = abstractMoshi.adapter(declaredFields[nameId].getType()).fromJson(jsonReader); + if (instantiateUsingStaticOf) { + fields[nameId] = fieldValue; + } else { + declaredFields[nameId].set(instance, fieldValue); + } + } else { + String keyName = jsonReader.nextName(); + throw new JsonDataException("Key \"" + keyName + "\" is invalid"); + } + } + jsonReader.endObject(); + + if (instantiateUsingStaticOf) { + Class[] params = new Class[declaredFields.length]; + for (int i = 0; i < declaredFields.length; i++) { + params[i] = declaredFields[i].getType(); + } + instance = declaredClass.getMethod("of", params).invoke(null, fields); + } + + //noinspection unchecked + return (T) instance; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new JsonDataException(e); + } + } + + @Override + public void toJson(@NotNull JsonWriter jsonWriter, @Nullable T t) throws IOException { + if (t == null) { + jsonWriter.nullValue(); + } else { + jsonWriter.beginObject(); + int i = 0; + for (Field declaredField : declaredFields) { + jsonWriter.name(declaredField.getName()); + Class fieldType = declaredField.getType(); + if (abstractClassesSerializers.containsKey(fieldType)) { + //noinspection unchecked + abstractClassesSerializers.get(fieldType).toJson(jsonWriter, (OBJ) fieldGetters[i].apply(t)); + } else if (concreteClassesSerializers.containsKey(fieldType)) { + //noinspection unchecked + concreteClassesSerializers.get(fieldType).toJson(jsonWriter, (OBJ) fieldGetters[i].apply(t)); + } else { + abstractMoshi.adapter(fieldType).toJson(jsonWriter, fieldGetters[i].apply(t)); + } + i++; + } + jsonWriter.endObject(); + } + } + } + + private static String fixType(String nextString) { + return nextString.replaceAll("[^a-zA-Z0-9]", ""); + } +} \ No newline at end of file