diff --git a/.gitignore b/.gitignore index c31559ecd..84fbd5508 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ target/ .classpath .project .settings/ + +# IntelliJ # +.idea/ +.iml diff --git a/pom.xml b/pom.xml index 1d4b09417..a929f3c37 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,7 @@ imagej/imagej-ops2 imagej/imagej-testutil + scijava/scijava-persist scijava/scijava-ops scijava/scijava-testutil scijava/scijava-types diff --git a/scijava/scijava-persist/.gitignore b/scijava/scijava-persist/.gitignore new file mode 100644 index 000000000..af9b8e492 --- /dev/null +++ b/scijava/scijava-persist/.gitignore @@ -0,0 +1,6 @@ +/.classpath +/.project +/.settings/ +/.idea/ +/target/ +*.iml diff --git a/scijava/scijava-persist/README.md b/scijava/scijava-persist/README.md new file mode 100644 index 000000000..76d7a8086 --- /dev/null +++ b/scijava/scijava-persist/README.md @@ -0,0 +1,10 @@ +#Scijava mechanism for object serialization + +This incubator contains a mechanism that uses Scijava extensibility mechanism in order to register adapters that can be dispatched in multiple repositories. + +Internally the Gson library is used with `RunTimeAdapters`, which allow to serialize interfaces. + +TODO: +* simple examples +* tests +* think about safety, or at least debugging ease (put a UUID) \ No newline at end of file diff --git a/scijava/scijava-persist/pom.xml b/scijava/scijava-persist/pom.xml new file mode 100644 index 000000000..619500a71 --- /dev/null +++ b/scijava/scijava-persist/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + + org.scijava + scijava-incubator + 0-SNAPSHOT + ../.. + + + scijava-persist + + SciJava Persist + Extensible serialization mechanism for persisting objects. + None + 2021 + + SciJava + https://scijava.org/ + + + + Simplified BSD License + repo + + + + + + + nicokiaru + Nicolas Chiaruttini + https://www.epfl.ch/research/facilities/ptbiop/staff/ + + founder + lead + developer + debugger + reviewer + support + maintainer + + + + + + Nicolas Chiaruttini + http://biop.epfl.ch/INFO_Facility.html + founder + NicoKiaru + + + + + + Image.sc Forum + https://forum.image.sc/tag/scijava + + + + + scm:git:git://github.com/scijava/incubator + scm:git:git@github.com:scijava/incubator + HEAD + https://github.com/scijava/incubator + + + + GitHub Issues + https://github.com/scijava/scijava-testutil/issues + + + Travis CI + https://travis-ci.org/scijava/incubator + + + + + org.scijava.persist.Main + org.scijava.persist + + bsd_2 + SciJava developers. + + + + + scijava.public + https://maven.scijava.org/content/groups/public + + + + + + org.scijava + scijava-common + + + + com.google.code.gson + gson + 2.8.6 + + + + + junit + junit + test + + + diff --git a/scijava/scijava-persist/src/main/java/module-info.java b/scijava/scijava-persist/src/main/java/module-info.java new file mode 100644 index 000000000..a6f95048d --- /dev/null +++ b/scijava/scijava-persist/src/main/java/module-info.java @@ -0,0 +1,7 @@ +open module org.scijava.persist { + + exports org.scijava.persist; + + requires transitive com.google.gson; + requires org.scijava; +} diff --git a/scijava/scijava-persist/src/main/java/org/scijava/persist/DefaultScijavaAdapterService.java b/scijava/scijava-persist/src/main/java/org/scijava/persist/DefaultScijavaAdapterService.java new file mode 100644 index 000000000..da6bc09a3 --- /dev/null +++ b/scijava/scijava-persist/src/main/java/org/scijava/persist/DefaultScijavaAdapterService.java @@ -0,0 +1,60 @@ +package org.scijava.persist; + +import org.scijava.Context; +import org.scijava.Priority; +import org.scijava.plugin.AbstractPTService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.plugin.PluginInfo; +import org.scijava.service.Service; + +import java.util.List; + +/** + * Scijava service which provides the different Scijava Adapters available in the current context. + * + * {@link IObjectScijavaAdapter} plugins are automatically discovered and accessible in this service. + * + * In practice, serializer / deserializers are obtained via {@link ScijavaGsonHelper} helper class + * + * @author Nicolas Chiaruttini, EPFL, 2021 + * + */ +@Plugin(type = Service.class) +public class DefaultScijavaAdapterService extends AbstractPTService implements IObjectScijavaAdapterService { + + @Override + public Class getPluginType() { + return IObjectScijavaAdapter.class; + } + + @Parameter + Context ctx; + + @Override + public Context context() { + return ctx; + } + + @Override + public Context getContext() { + return ctx; + } + + double priority = Priority.NORMAL; + + @Override + public double getPriority() { + return priority; + } + + @Override + public void setPriority(double priority) { + this.priority = priority; + } + + @Override + public List> getAdapters(Class adapterClass) { + return pluginService().getPluginsOfType(adapterClass); + } +} diff --git a/scijava/scijava-persist/src/main/java/org/scijava/persist/IClassAdapter.java b/scijava/scijava-persist/src/main/java/org/scijava/persist/IClassAdapter.java new file mode 100644 index 000000000..3d8e0b47a --- /dev/null +++ b/scijava/scijava-persist/src/main/java/org/scijava/persist/IClassAdapter.java @@ -0,0 +1,11 @@ +package org.scijava.persist; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonSerializer; + +public interface IClassAdapter extends IObjectScijavaAdapter, JsonSerializer, + JsonDeserializer { + + Class getAdapterClass(); + +} diff --git a/scijava/scijava-persist/src/main/java/org/scijava/persist/IClassRuntimeAdapter.java b/scijava/scijava-persist/src/main/java/org/scijava/persist/IClassRuntimeAdapter.java new file mode 100644 index 000000000..d52e35f7f --- /dev/null +++ b/scijava/scijava-persist/src/main/java/org/scijava/persist/IClassRuntimeAdapter.java @@ -0,0 +1,33 @@ +package org.scijava.persist; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializer; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonParseException; +import com.google.gson.JsonIOException; + +import java.lang.reflect.Type; + +public interface IClassRuntimeAdapter extends IObjectScijavaAdapter, JsonSerializer, + JsonDeserializer { + + Class getBaseClass(); + + Class getRunTimeClass(); + + default boolean useCustomAdapter() { + return false; + } + + @Override + default T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + throw new JsonParseException("Default deserializer for class "+getBaseClass()+" ("+getRunTimeClass()+") should not be used, return false in method useCustomAdapter instead"); + } + + @Override + default JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) { + throw new JsonIOException("Default serializer for class "+getBaseClass()+" ("+getRunTimeClass()+") should not be used, should not be used, return false in method useCustomAdapter instead"); + } +} diff --git a/scijava/scijava-persist/src/main/java/org/scijava/persist/IObjectScijavaAdapter.java b/scijava/scijava-persist/src/main/java/org/scijava/persist/IObjectScijavaAdapter.java new file mode 100644 index 000000000..390835023 --- /dev/null +++ b/scijava/scijava-persist/src/main/java/org/scijava/persist/IObjectScijavaAdapter.java @@ -0,0 +1,17 @@ +package org.scijava.persist; + +import org.scijava.plugin.SciJavaPlugin; + +/** + * Top level class for plugins which can serialize object using gson and the scijava context. + * + * The scijava context may provide custom adapters {@link IClassAdapter} and also + * runtime adapters, see {@link IClassRuntimeAdapter}) auto-discovered via scijava plugin + * extensibility mechanism. + * + * @author Nicolas Chiaruttini, EPFL, 2021 + * + */ + +public interface IObjectScijavaAdapter extends SciJavaPlugin { +} diff --git a/scijava/scijava-persist/src/main/java/org/scijava/persist/IObjectScijavaAdapterService.java b/scijava/scijava-persist/src/main/java/org/scijava/persist/IObjectScijavaAdapterService.java new file mode 100644 index 000000000..00e0ba71f --- /dev/null +++ b/scijava/scijava-persist/src/main/java/org/scijava/persist/IObjectScijavaAdapterService.java @@ -0,0 +1,11 @@ +package org.scijava.persist; + +import org.scijava.plugin.PTService; +import org.scijava.plugin.PluginInfo; +import org.scijava.service.SciJavaService; + +import java.util.List; + +public interface IObjectScijavaAdapterService extends PTService, SciJavaService { + List> getAdapters(Class adapterClass); +} diff --git a/scijava/scijava-persist/src/main/java/org/scijava/persist/LazilyParsedNumber.java b/scijava/scijava-persist/src/main/java/org/scijava/persist/LazilyParsedNumber.java new file mode 100644 index 000000000..d3db63cce --- /dev/null +++ b/scijava/scijava-persist/src/main/java/org/scijava/persist/LazilyParsedNumber.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * 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.scijava.persist; + +import java.io.ObjectStreamException; +import java.math.BigDecimal; + +/** + * This class holds a number value that is lazily converted to a specific number type + * + * @author Inderjeet Singh + */ +public final class LazilyParsedNumber extends Number { + private final String value; + + /** @param value must not be null */ + public LazilyParsedNumber(String value) { + this.value = value; + } + + @Override + public int intValue() { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + try { + return (int) Long.parseLong(value); + } catch (NumberFormatException nfe) { + return new BigDecimal(value).intValue(); + } + } + } + + @Override + public long longValue() { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return new BigDecimal(value).longValue(); + } + } + + @Override + public float floatValue() { + return Float.parseFloat(value); + } + + @Override + public double doubleValue() { + return Double.parseDouble(value); + } + + @Override + public String toString() { + return value; + } + + /** + * If somebody is unlucky enough to have to serialize one of these, serialize + * it as a BigDecimal so that they won't need Gson on the other side to + * deserialize it. + */ + private Object writeReplace() throws ObjectStreamException { + return new BigDecimal(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof LazilyParsedNumber) { + LazilyParsedNumber other = (LazilyParsedNumber) obj; + return value == other.value || value.equals(other.value); + } + return false; + } +} diff --git a/scijava/scijava-persist/src/main/java/org/scijava/persist/RuntimeTypeAdapterFactory.java b/scijava/scijava-persist/src/main/java/org/scijava/persist/RuntimeTypeAdapterFactory.java new file mode 100644 index 000000000..f053a47ac --- /dev/null +++ b/scijava/scijava-persist/src/main/java/org/scijava/persist/RuntimeTypeAdapterFactory.java @@ -0,0 +1,300 @@ +/*- + * #%L + * BigDataViewer-Playground + * %% + * Copyright (C) 2019 - 2021 Nicolas Chiaruttini, EPFL - Robert Haase, MPI CBG - Christian Tischer, EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.persist; +/* + * Copyright (C) 2011 Google Inc. + * + * 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. + */ + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + *
   {@code
+ *   abstract class Shape {
+ *     int x;
+ *     int y;
+ *   }
+ *   class Circle extends Shape {
+ *     int radius;
+ *   }
+ *   class Rectangle extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Diamond extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Drawing {
+ *     Shape bottomShape;
+ *     Shape topShape;
+ *   }
+ * }
+ *

Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?

   {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + *

Registering Types

+ * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field + * name to the {@link #of} factory method. If you don't supply an explicit type + * field name, {@code "type"} will be used.
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory
+ *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   {@code
+ *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter factory in your application's GSON builder: + *
   {@code
+ *   Gson gson = new GsonBuilder()
+ *       .registerTypeAdapterFactory(shapeAdapterFactory)
+ *       .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining:
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *       .registerSubtype(Rectangle.class)
+ *       .registerSubtype(Circle.class)
+ *       .registerSubtype(Diamond.class);
+ * }
+ * + *

Serialization and deserialization

+ * In order to serialize and deserialize a polymorphic object, + * you must specify the base type explicitly. + *
   {@code
+ *   Diamond diamond = new Diamond();
+ *   String json = gson.toJson(diamond, Shape.class);
+ * }
+ * And then: + *
   {@code
+ *   Shape shape = gson.fromJson(json, Shape.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap<>(); + private final Map, String> subtypeToLabel = new LinkedHashMap<>(); + private final boolean maintainType; + + private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boolean maintainType) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + this.maintainType = maintainType; + } + + /* + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + * {@code maintainType} flag decide if the type will be stored in pojo or not. + * @param todo + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); + } + + /* + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + * @param todo + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false); + } + + /* + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + * @param todo + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory<>(baseType, "type", false); + } + + /* + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /* + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map> labelToDelegate + = new LinkedHashMap<>(); + final Map, TypeAdapter> subtypeToDelegate + = new LinkedHashMap<>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement; + if (maintainType) { + labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + } else { + labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + } + + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + if (maintainType) { + Streams.write(jsonObject, out); + return; + } + + JsonObject clone = new JsonObject(); + + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + clone.add(typeFieldName, new JsonPrimitive(label)); + + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + } + }.nullSafe(); + } +} + diff --git a/scijava/scijava-persist/src/main/java/org/scijava/persist/ScijavaGsonHelper.java b/scijava/scijava-persist/src/main/java/org/scijava/persist/ScijavaGsonHelper.java new file mode 100644 index 000000000..bf912e0cf --- /dev/null +++ b/scijava/scijava-persist/src/main/java/org/scijava/persist/ScijavaGsonHelper.java @@ -0,0 +1,130 @@ +package org.scijava.persist; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.scijava.Context; +import org.scijava.InstantiableException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Get serializers that registers all scijava registered adapters and runtime adapters + * + * Note : some "simple" objects do not require specific adapters with json. + * + * See {@link RuntimeTypeAdapterFactory} + * + */ + +public class ScijavaGsonHelper { + + public static Gson getGson(Context ctx) { + return getGson(ctx, false); + } + + public static Gson getGson(Context ctx, boolean verbose) { + return getGsonBuilder(ctx, new GsonBuilder(), verbose).create(); + } + + public static GsonBuilder getGsonBuilder(Context ctx, boolean verbose) { + return getGsonBuilder(ctx, new GsonBuilder().setPrettyPrinting(), verbose); + } + + public static GsonBuilder getGsonBuilder(Context ctx, GsonBuilder builder, boolean verbose) { + Consumer log; + if (verbose) { + log = System.out::println; + } else { + log = (str) -> {}; + } + + // First, we register all adapters which are directly serializing/deserializing classes, without the need + // of runtime class serialization customisation + log.accept("IClassAdapters : "); + ctx.getService(IObjectScijavaAdapterService.class) + .getAdapters(IClassAdapter.class) // Gets all scijava class adapters (no runtime) + .forEach(pi -> { + try { + IClassAdapter adapter = pi.createInstance(); // Instanciate the adapter (no argument should be present in the constructor, but auto filled scijava parameters are allowed) + log.accept("\t "+adapter.getAdapterClass()); + builder.registerTypeHierarchyAdapter(adapter.getAdapterClass(), adapter); // Register gson adapter + } catch (InstantiableException e) { + e.printStackTrace(); + } + }); + + // Next, we need to get all serializers which require custom adapters. This typically happens + // when serialiazing interfaces or abstract classes (typical scenario : imglib2 RealTransform objects) + // The interface or abstract class is the Base class, and runtime classes are + // implementing the base interface class or extending the abstract base class + // typical scenario : AffineTransform3D, ThinPlateSplineTransform, all implementing imglib2 RealTransform interface + Map, ClassTypesAndSubTypes> runTimeAdapters = new HashMap<>(); + + ctx.getService(IObjectScijavaAdapterService.class) + .getAdapters(IClassRuntimeAdapter.class) // Gets all Runtime adapters + .forEach(pi -> { + try { + IClassRuntimeAdapter adapter = pi.createInstance(); // Creates runtime adapter TODO : how to fiw raw type here ? + if (runTimeAdapters.containsKey(adapter.getBaseClass())) { + ClassTypesAndSubTypes typesAndSubTypes = runTimeAdapters.get(adapter.getBaseClass()); + if (typesAndSubTypes.subClasses.contains(adapter.getRunTimeClass())) { // Presence of two runtime adapters for the same class! + throw new RuntimeException("Presence of conflicting adapters for class "+adapter.getRunTimeClass()); + } else { + runTimeAdapters.get(adapter.getBaseClass()).subClasses.add(adapter.getRunTimeClass()); + } + } else { + ClassTypesAndSubTypes element = new ClassTypesAndSubTypes<>(adapter.getBaseClass()); + element.subClasses.add(adapter.getRunTimeClass()); + runTimeAdapters.put(adapter.getBaseClass(), element); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + ); + + log.accept("IRunTimeClassAdapters : "); + runTimeAdapters.values().forEach(typesAndSubTypes -> builder.registerTypeAdapterFactory(typesAndSubTypes.getRunTimeAdapterFactory(log))); + + ctx.getService(IObjectScijavaAdapterService.class) + .getAdapters(IClassRuntimeAdapter.class) + .forEach(pi -> { + try { + IClassRuntimeAdapter adapter = pi.createInstance(); + if (adapter.useCustomAdapter()) { // Overrides default adapter only if needed + builder.registerTypeHierarchyAdapter(adapter.getRunTimeClass(), adapter); + } + } catch (InstantiableException e) { + e.printStackTrace(); + } + }); + + return builder; + } + + // Inner static class needed for type safety + public static class ClassTypesAndSubTypes { + + Class baseClass; + + public ClassTypesAndSubTypes(Class clazz) { + this.baseClass = clazz; + } + + List> subClasses = new ArrayList<>(); + + public RuntimeTypeAdapterFactory getRunTimeAdapterFactory(Consumer log) { + RuntimeTypeAdapterFactory factory = RuntimeTypeAdapterFactory.of(baseClass); + log.accept("\t "+baseClass); + subClasses.forEach(subClass -> { + log.accept("\t \t "+subClass); + factory.registerSubtype( subClass ); + }); + return factory; + } + } +} diff --git a/scijava/scijava-persist/src/main/java/org/scijava/persist/Streams.java b/scijava/scijava-persist/src/main/java/org/scijava/persist/Streams.java new file mode 100644 index 000000000..b15344a7f --- /dev/null +++ b/scijava/scijava-persist/src/main/java/org/scijava/persist/Streams.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.scijava.persist; + +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonNull; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.google.gson.stream.MalformedJsonException; +import java.io.EOFException; +import java.io.IOException; +import java.io.Writer; + +/** + * Reads and writes GSON parse trees over streams. + */ +public final class Streams { + private Streams() { + throw new UnsupportedOperationException(); + } + + /** + * Takes a reader in any state and returns the next value as a JsonElement. + */ + public static JsonElement parse(JsonReader reader) throws JsonParseException { + boolean isEmpty = true; + try { + reader.peek(); + isEmpty = false; + return TypeAdapters.JSON_ELEMENT.read(reader); + } catch (EOFException e) { + /* + * For compatibility with JSON 1.5 and earlier, we return a JsonNull for + * empty documents instead of throwing. + */ + if (isEmpty) { + return JsonNull.INSTANCE; + } + // The stream ended prematurely so it is likely a syntax error. + throw new JsonSyntaxException(e); + } catch (MalformedJsonException e) { + throw new JsonSyntaxException(e); + } catch (IOException e) { + throw new JsonIOException(e); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + /** + * Writes the JSON element to the writer, recursively. + */ + public static void write(JsonElement element, JsonWriter writer) throws IOException { + TypeAdapters.JSON_ELEMENT.write(writer, element); + } + + public static Writer writerForAppendable(Appendable appendable) { + return appendable instanceof Writer ? (Writer) appendable : new AppendableWriter(appendable); + } + + /** + * Adapts an {@link Appendable} so it can be passed anywhere a {@link Writer} + * is used. + */ + private static final class AppendableWriter extends Writer { + private final Appendable appendable; + private final CurrentWrite currentWrite = new CurrentWrite(); + + AppendableWriter(Appendable appendable) { + this.appendable = appendable; + } + + @Override public void write(char[] chars, int offset, int length) throws IOException { + currentWrite.chars = chars; + appendable.append(currentWrite, offset, offset + length); + } + + @Override public void write(int i) throws IOException { + appendable.append((char) i); + } + + @Override public void flush() {} + @Override public void close() {} + + /** + * A mutable char sequence pointing at a single char[]. + */ + static class CurrentWrite implements CharSequence { + char[] chars; + public int length() { + return chars.length; + } + public char charAt(int i) { + return chars[i]; + } + public CharSequence subSequence(int start, int end) { + return new String(chars, start, end - start); + } + } + } + +} diff --git a/scijava/scijava-persist/src/main/java/org/scijava/persist/TypeAdapters.java b/scijava/scijava-persist/src/main/java/org/scijava/persist/TypeAdapters.java new file mode 100644 index 000000000..0468b2326 --- /dev/null +++ b/scijava/scijava-persist/src/main/java/org/scijava/persist/TypeAdapters.java @@ -0,0 +1,907 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * 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.scijava.persist; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Calendar; +import java.util.Currency; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Type adapters for basic types. + */ +public final class TypeAdapters { + private TypeAdapters() { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("rawtypes") + public static final TypeAdapter CLASS = new TypeAdapter() { + @Override + public void write(JsonWriter out, Class value) throws IOException { + throw new UnsupportedOperationException("Attempted to serialize java.lang.Class: " + + value.getName() + ". Forgot to register a type adapter?"); + } + @Override + public Class read(JsonReader in) throws IOException { + throw new UnsupportedOperationException( + "Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?"); + } + }.nullSafe(); + + public static final TypeAdapterFactory CLASS_FACTORY = newFactory(Class.class, CLASS); + + public static final TypeAdapter BIT_SET = new TypeAdapter() { + @Override public BitSet read(JsonReader in) throws IOException { + BitSet bitset = new BitSet(); + in.beginArray(); + int i = 0; + JsonToken tokenType = in.peek(); + while (tokenType != JsonToken.END_ARRAY) { + boolean set; + switch (tokenType) { + case NUMBER: + set = in.nextInt() != 0; + break; + case BOOLEAN: + set = in.nextBoolean(); + break; + case STRING: + String stringValue = in.nextString(); + try { + set = Integer.parseInt(stringValue) != 0; + } catch (NumberFormatException e) { + throw new JsonSyntaxException( + "Error: Expecting: bitset number value (1, 0), Found: " + stringValue); + } + break; + default: + throw new JsonSyntaxException("Invalid bitset value type: " + tokenType); + } + if (set) { + bitset.set(i); + } + ++i; + tokenType = in.peek(); + } + in.endArray(); + return bitset; + } + + @Override public void write(JsonWriter out, BitSet src) throws IOException { + out.beginArray(); + for (int i = 0, length = src.length(); i < length; i++) { + int value = (src.get(i)) ? 1 : 0; + out.value(value); + } + out.endArray(); + } + }.nullSafe(); + + public static final TypeAdapterFactory BIT_SET_FACTORY = newFactory(BitSet.class, BIT_SET); + + public static final TypeAdapter BOOLEAN = new TypeAdapter() { + @Override + public Boolean read(JsonReader in) throws IOException { + JsonToken peek = in.peek(); + if (peek == JsonToken.NULL) { + in.nextNull(); + return null; + } else if (peek == JsonToken.STRING) { + // support strings for compatibility with GSON 1.7 + return Boolean.parseBoolean(in.nextString()); + } + return in.nextBoolean(); + } + @Override + public void write(JsonWriter out, Boolean value) throws IOException { + out.value(value); + } + }; + + /** + * Writes a boolean as a string. Useful for map keys, where booleans aren't + * otherwise permitted. + */ + public static final TypeAdapter BOOLEAN_AS_STRING = new TypeAdapter() { + @Override public Boolean read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return Boolean.valueOf(in.nextString()); + } + + @Override public void write(JsonWriter out, Boolean value) throws IOException { + out.value(value == null ? "null" : value.toString()); + } + }; + + public static final TypeAdapterFactory BOOLEAN_FACTORY + = newFactory(boolean.class, Boolean.class, BOOLEAN); + + public static final TypeAdapter BYTE = new TypeAdapter() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + int intValue = in.nextInt(); + return (byte) intValue; + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapterFactory BYTE_FACTORY + = newFactory(byte.class, Byte.class, BYTE); + + public static final TypeAdapter SHORT = new TypeAdapter() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + return (short) in.nextInt(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapterFactory SHORT_FACTORY + = newFactory(short.class, Short.class, SHORT); + + public static final TypeAdapter INTEGER = new TypeAdapter() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + return in.nextInt(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + public static final TypeAdapterFactory INTEGER_FACTORY + = newFactory(int.class, Integer.class, INTEGER); + + public static final TypeAdapter ATOMIC_INTEGER = new TypeAdapter() { + @Override public AtomicInteger read(JsonReader in) throws IOException { + try { + return new AtomicInteger(in.nextInt()); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + @Override public void write(JsonWriter out, AtomicInteger value) throws IOException { + out.value(value.get()); + } + }.nullSafe(); + public static final TypeAdapterFactory ATOMIC_INTEGER_FACTORY = + newFactory(AtomicInteger.class, TypeAdapters.ATOMIC_INTEGER); + + public static final TypeAdapter ATOMIC_BOOLEAN = new TypeAdapter() { + @Override public AtomicBoolean read(JsonReader in) throws IOException { + return new AtomicBoolean(in.nextBoolean()); + } + @Override public void write(JsonWriter out, AtomicBoolean value) throws IOException { + out.value(value.get()); + } + }.nullSafe(); + public static final TypeAdapterFactory ATOMIC_BOOLEAN_FACTORY = + newFactory(AtomicBoolean.class, TypeAdapters.ATOMIC_BOOLEAN); + + public static final TypeAdapter ATOMIC_INTEGER_ARRAY = new TypeAdapter() { + @Override public AtomicIntegerArray read(JsonReader in) throws IOException { + List list = new ArrayList(); + in.beginArray(); + while (in.hasNext()) { + try { + int integer = in.nextInt(); + list.add(integer); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + in.endArray(); + int length = list.size(); + AtomicIntegerArray array = new AtomicIntegerArray(length); + for (int i = 0; i < length; ++i) { + array.set(i, list.get(i)); + } + return array; + } + @Override public void write(JsonWriter out, AtomicIntegerArray value) throws IOException { + out.beginArray(); + for (int i = 0, length = value.length(); i < length; i++) { + out.value(value.get(i)); + } + out.endArray(); + } + }.nullSafe(); + public static final TypeAdapterFactory ATOMIC_INTEGER_ARRAY_FACTORY = + newFactory(AtomicIntegerArray.class, TypeAdapters.ATOMIC_INTEGER_ARRAY); + + public static final TypeAdapter LONG = new TypeAdapter() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + return in.nextLong(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapter FLOAT = new TypeAdapter() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return (float) in.nextDouble(); + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapter DOUBLE = new TypeAdapter() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return in.nextDouble(); + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapter NUMBER = new TypeAdapter() { + @Override + public Number read(JsonReader in) throws IOException { + JsonToken jsonToken = in.peek(); + switch (jsonToken) { + case NULL: + in.nextNull(); + return null; + case NUMBER: + case STRING: + return new LazilyParsedNumber(in.nextString()); + default: + throw new JsonSyntaxException("Expecting number, got: " + jsonToken); + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapterFactory NUMBER_FACTORY = newFactory(Number.class, NUMBER); + + public static final TypeAdapter CHARACTER = new TypeAdapter() { + @Override + public Character read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + String str = in.nextString(); + if (str.length() != 1) { + throw new JsonSyntaxException("Expecting character, got: " + str); + } + return str.charAt(0); + } + @Override + public void write(JsonWriter out, Character value) throws IOException { + out.value(value == null ? null : String.valueOf(value)); + } + }; + + public static final TypeAdapterFactory CHARACTER_FACTORY + = newFactory(char.class, Character.class, CHARACTER); + + public static final TypeAdapter STRING = new TypeAdapter() { + @Override + public String read(JsonReader in) throws IOException { + JsonToken peek = in.peek(); + if (peek == JsonToken.NULL) { + in.nextNull(); + return null; + } + /* coerce booleans to strings for backwards compatibility */ + if (peek == JsonToken.BOOLEAN) { + return Boolean.toString(in.nextBoolean()); + } + return in.nextString(); + } + @Override + public void write(JsonWriter out, String value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapter BIG_DECIMAL = new TypeAdapter() { + @Override public BigDecimal read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + return new BigDecimal(in.nextString()); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + @Override public void write(JsonWriter out, BigDecimal value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapter BIG_INTEGER = new TypeAdapter() { + @Override public BigInteger read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + return new BigInteger(in.nextString()); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + @Override public void write(JsonWriter out, BigInteger value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapterFactory STRING_FACTORY = newFactory(String.class, STRING); + + public static final TypeAdapter STRING_BUILDER = new TypeAdapter() { + @Override + public StringBuilder read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return new StringBuilder(in.nextString()); + } + @Override + public void write(JsonWriter out, StringBuilder value) throws IOException { + out.value(value == null ? null : value.toString()); + } + }; + + public static final TypeAdapterFactory STRING_BUILDER_FACTORY = + newFactory(StringBuilder.class, STRING_BUILDER); + + public static final TypeAdapter STRING_BUFFER = new TypeAdapter() { + @Override + public StringBuffer read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return new StringBuffer(in.nextString()); + } + @Override + public void write(JsonWriter out, StringBuffer value) throws IOException { + out.value(value == null ? null : value.toString()); + } + }; + + public static final TypeAdapterFactory STRING_BUFFER_FACTORY = + newFactory(StringBuffer.class, STRING_BUFFER); + + public static final TypeAdapter URL = new TypeAdapter() { + @Override + public URL read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + String nextString = in.nextString(); + return "null".equals(nextString) ? null : new URL(nextString); + } + @Override + public void write(JsonWriter out, URL value) throws IOException { + out.value(value == null ? null : value.toExternalForm()); + } + }; + + public static final TypeAdapterFactory URL_FACTORY = newFactory(URL.class, URL); + + public static final TypeAdapter URI = new TypeAdapter() { + @Override + public URI read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + String nextString = in.nextString(); + return "null".equals(nextString) ? null : new URI(nextString); + } catch (URISyntaxException e) { + throw new JsonIOException(e); + } + } + @Override + public void write(JsonWriter out, URI value) throws IOException { + out.value(value == null ? null : value.toASCIIString()); + } + }; + + public static final TypeAdapterFactory URI_FACTORY = newFactory(URI.class, URI); + + public static final TypeAdapter INET_ADDRESS = new TypeAdapter() { + @Override + public InetAddress read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + // regrettably, this should have included both the host name and the host address + return InetAddress.getByName(in.nextString()); + } + @Override + public void write(JsonWriter out, InetAddress value) throws IOException { + out.value(value == null ? null : value.getHostAddress()); + } + }; + + public static final TypeAdapterFactory INET_ADDRESS_FACTORY = + newTypeHierarchyFactory(InetAddress.class, INET_ADDRESS); + + public static final TypeAdapter UUID = new TypeAdapter() { + @Override + public UUID read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return java.util.UUID.fromString(in.nextString()); + } + @Override + public void write(JsonWriter out, UUID value) throws IOException { + out.value(value == null ? null : value.toString()); + } + }; + + public static final TypeAdapterFactory UUID_FACTORY = newFactory(UUID.class, UUID); + + public static final TypeAdapter CURRENCY = new TypeAdapter() { + @Override + public Currency read(JsonReader in) throws IOException { + return Currency.getInstance(in.nextString()); + } + @Override + public void write(JsonWriter out, Currency value) throws IOException { + out.value(value.getCurrencyCode()); + } + }.nullSafe(); + public static final TypeAdapterFactory CURRENCY_FACTORY = newFactory(Currency.class, CURRENCY); + + public static final TypeAdapterFactory TIMESTAMP_FACTORY = new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + @Override public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (typeToken.getRawType() != Timestamp.class) { + return null; + } + + final TypeAdapter dateTypeAdapter = gson.getAdapter(Date.class); + return (TypeAdapter) new TypeAdapter() { + @Override public Timestamp read(JsonReader in) throws IOException { + Date date = dateTypeAdapter.read(in); + return date != null ? new Timestamp(date.getTime()) : null; + } + + @Override public void write(JsonWriter out, Timestamp value) throws IOException { + dateTypeAdapter.write(out, value); + } + }; + } + }; + + public static final TypeAdapter CALENDAR = new TypeAdapter() { + private static final String YEAR = "year"; + private static final String MONTH = "month"; + private static final String DAY_OF_MONTH = "dayOfMonth"; + private static final String HOUR_OF_DAY = "hourOfDay"; + private static final String MINUTE = "minute"; + private static final String SECOND = "second"; + + @Override + public Calendar read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + in.beginObject(); + int year = 0; + int month = 0; + int dayOfMonth = 0; + int hourOfDay = 0; + int minute = 0; + int second = 0; + while (in.peek() != JsonToken.END_OBJECT) { + String name = in.nextName(); + int value = in.nextInt(); + if (YEAR.equals(name)) { + year = value; + } else if (MONTH.equals(name)) { + month = value; + } else if (DAY_OF_MONTH.equals(name)) { + dayOfMonth = value; + } else if (HOUR_OF_DAY.equals(name)) { + hourOfDay = value; + } else if (MINUTE.equals(name)) { + minute = value; + } else if (SECOND.equals(name)) { + second = value; + } + } + in.endObject(); + return new GregorianCalendar(year, month, dayOfMonth, hourOfDay, minute, second); + } + + @Override + public void write(JsonWriter out, Calendar value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + out.beginObject(); + out.name(YEAR); + out.value(value.get(Calendar.YEAR)); + out.name(MONTH); + out.value(value.get(Calendar.MONTH)); + out.name(DAY_OF_MONTH); + out.value(value.get(Calendar.DAY_OF_MONTH)); + out.name(HOUR_OF_DAY); + out.value(value.get(Calendar.HOUR_OF_DAY)); + out.name(MINUTE); + out.value(value.get(Calendar.MINUTE)); + out.name(SECOND); + out.value(value.get(Calendar.SECOND)); + out.endObject(); + } + }; + + public static final TypeAdapterFactory CALENDAR_FACTORY = + newFactoryForMultipleTypes(Calendar.class, GregorianCalendar.class, CALENDAR); + + public static final TypeAdapter LOCALE = new TypeAdapter() { + @Override + public Locale read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + String locale = in.nextString(); + StringTokenizer tokenizer = new StringTokenizer(locale, "_"); + String language = null; + String country = null; + String variant = null; + if (tokenizer.hasMoreElements()) { + language = tokenizer.nextToken(); + } + if (tokenizer.hasMoreElements()) { + country = tokenizer.nextToken(); + } + if (tokenizer.hasMoreElements()) { + variant = tokenizer.nextToken(); + } + if (country == null && variant == null) { + return new Locale(language); + } else if (variant == null) { + return new Locale(language, country); + } else { + return new Locale(language, country, variant); + } + } + @Override + public void write(JsonWriter out, Locale value) throws IOException { + out.value(value == null ? null : value.toString()); + } + }; + + public static final TypeAdapterFactory LOCALE_FACTORY = newFactory(Locale.class, LOCALE); + + public static final TypeAdapter JSON_ELEMENT = new TypeAdapter() { + @Override public JsonElement read(JsonReader in) throws IOException { + switch (in.peek()) { + case STRING: + return new JsonPrimitive(in.nextString()); + case NUMBER: + String number = in.nextString(); + return new JsonPrimitive(new LazilyParsedNumber(number)); + case BOOLEAN: + return new JsonPrimitive(in.nextBoolean()); + case NULL: + in.nextNull(); + return JsonNull.INSTANCE; + case BEGIN_ARRAY: + JsonArray array = new JsonArray(); + in.beginArray(); + while (in.hasNext()) { + array.add(read(in)); + } + in.endArray(); + return array; + case BEGIN_OBJECT: + JsonObject object = new JsonObject(); + in.beginObject(); + while (in.hasNext()) { + object.add(in.nextName(), read(in)); + } + in.endObject(); + return object; + case END_DOCUMENT: + case NAME: + case END_OBJECT: + case END_ARRAY: + default: + throw new IllegalArgumentException(); + } + } + + @Override public void write(JsonWriter out, JsonElement value) throws IOException { + if (value == null || value.isJsonNull()) { + out.nullValue(); + } else if (value.isJsonPrimitive()) { + JsonPrimitive primitive = value.getAsJsonPrimitive(); + if (primitive.isNumber()) { + out.value(primitive.getAsNumber()); + } else if (primitive.isBoolean()) { + out.value(primitive.getAsBoolean()); + } else { + out.value(primitive.getAsString()); + } + + } else if (value.isJsonArray()) { + out.beginArray(); + for (JsonElement e : value.getAsJsonArray()) { + write(out, e); + } + out.endArray(); + + } else if (value.isJsonObject()) { + out.beginObject(); + for (Map.Entry e : value.getAsJsonObject().entrySet()) { + out.name(e.getKey()); + write(out, e.getValue()); + } + out.endObject(); + + } else { + throw new IllegalArgumentException("Couldn't write " + value.getClass()); + } + } + }; + + public static final TypeAdapterFactory JSON_ELEMENT_FACTORY + = newTypeHierarchyFactory(JsonElement.class, JSON_ELEMENT); + + private static final class EnumTypeAdapter> extends TypeAdapter { + private final Map nameToConstant = new HashMap(); + private final Map constantToName = new HashMap(); + + public EnumTypeAdapter(Class classOfT) { + try { + for (T constant : classOfT.getEnumConstants()) { + String name = constant.name(); + SerializedName annotation = classOfT.getField(name).getAnnotation(SerializedName.class); + if (annotation != null) { + name = annotation.value(); + for (String alternate : annotation.alternate()) { + nameToConstant.put(alternate, constant); + } + } + nameToConstant.put(name, constant); + constantToName.put(constant, name); + } + } catch (NoSuchFieldException e) { + throw new AssertionError(e); + } + } + @Override public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return nameToConstant.get(in.nextString()); + } + + @Override public void write(JsonWriter out, T value) throws IOException { + out.value(value == null ? null : constantToName.get(value)); + } + } + + public static final TypeAdapterFactory ENUM_FACTORY = new TypeAdapterFactory() { + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override public TypeAdapter create(Gson gson, TypeToken typeToken) { + Class rawType = typeToken.getRawType(); + if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) { + return null; + } + if (!rawType.isEnum()) { + rawType = rawType.getSuperclass(); // handle anonymous subclasses + } + return (TypeAdapter) new EnumTypeAdapter(rawType); + } + }; + + public static TypeAdapterFactory newFactory( + final TypeToken type, final TypeAdapter typeAdapter) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + @Override public TypeAdapter create(Gson gson, TypeToken typeToken) { + return typeToken.equals(type) ? (TypeAdapter) typeAdapter : null; + } + }; + } + + public static TypeAdapterFactory newFactory( + final Class type, final TypeAdapter typeAdapter) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + @Override public TypeAdapter create(Gson gson, TypeToken typeToken) { + return typeToken.getRawType() == type ? (TypeAdapter) typeAdapter : null; + } + @Override public String toString() { + return "Factory[type=" + type.getName() + ",adapter=" + typeAdapter + "]"; + } + }; + } + + public static TypeAdapterFactory newFactory( + final Class unboxed, final Class boxed, final TypeAdapter typeAdapter) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + @Override public TypeAdapter create(Gson gson, TypeToken typeToken) { + Class rawType = typeToken.getRawType(); + return (rawType == unboxed || rawType == boxed) ? (TypeAdapter) typeAdapter : null; + } + @Override public String toString() { + return "Factory[type=" + boxed.getName() + + "+" + unboxed.getName() + ",adapter=" + typeAdapter + "]"; + } + }; + } + + public static TypeAdapterFactory newFactoryForMultipleTypes(final Class base, + final Class sub, final TypeAdapter typeAdapter) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + @Override public TypeAdapter create(Gson gson, TypeToken typeToken) { + Class rawType = typeToken.getRawType(); + return (rawType == base || rawType == sub) ? (TypeAdapter) typeAdapter : null; + } + @Override public String toString() { + return "Factory[type=" + base.getName() + + "+" + sub.getName() + ",adapter=" + typeAdapter + "]"; + } + }; + } + + /** + * Returns a factory for all subtypes of {@code typeAdapter}. We do a runtime check to confirm + * that the deserialized type matches the type requested. + */ + public static TypeAdapterFactory newTypeHierarchyFactory( + final Class clazz, final TypeAdapter typeAdapter) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") + @Override public TypeAdapter create(Gson gson, TypeToken typeToken) { + final Class requestedType = typeToken.getRawType(); + if (!clazz.isAssignableFrom(requestedType)) { + return null; + } + return (TypeAdapter) new TypeAdapter() { + @Override public void write(JsonWriter out, T1 value) throws IOException { + typeAdapter.write(out, value); + } + + @Override public T1 read(JsonReader in) throws IOException { + T1 result = typeAdapter.read(in); + if (result != null && !requestedType.isInstance(result)) { + throw new JsonSyntaxException("Expected a " + requestedType.getName() + + " but was " + result.getClass().getName()); + } + return result; + } + }; + } + @Override public String toString() { + return "Factory[typeHierarchy=" + clazz.getName() + ",adapter=" + typeAdapter + "]"; + } + }; + } +} \ No newline at end of file diff --git a/scijava/scijava-persist/src/test/java/org/scijava/persist/SerializationTests.java b/scijava/scijava-persist/src/test/java/org/scijava/persist/SerializationTests.java new file mode 100644 index 000000000..1ba17ce6f --- /dev/null +++ b/scijava/scijava-persist/src/test/java/org/scijava/persist/SerializationTests.java @@ -0,0 +1,81 @@ +package org.scijava.persist; + +import com.google.gson.Gson; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.scijava.Context; +import org.scijava.persist.testobjects.Circle; +import org.scijava.persist.testobjects.Shape; +import org.scijava.persist.testobjects.Shapes; + +public class SerializationTests { + static Context context; + static Gson gson; + + public static void main(String... args) { + context = new Context(IObjectScijavaAdapterService.class); + if (context==null) { + System.out.println("Null context"); + } + gson = ScijavaGsonHelper.getGson(context); + } + + @Before + public void openFiji() { + // Initializes static SourceService and Display Service and plugins for serialization + context = new Context(IObjectScijavaAdapterService.class); + if (context==null) { + System.out.println("Null context"); + } + gson = ScijavaGsonHelper.getGson(context, true); + } + + @After + public void closeFiji() throws Exception { + context.dispose(); + context = null; + gson = null; + } + + /** + * Test + * {@link org.scijava.persist.testobjects.CircleAdapter} + */ + @Test + public void testCircleObject() { + Shape circle = new Circle(); + testSerializationDeserialization(gson, circle, Shape.class); + } + + /** + * Test adapters located in + * {@link org.scijava.persist.testobjects.Shapes} + */ + @Test + public void testDrawingObject() { + Shapes.Drawing drawing = new Shapes.Drawing(); + drawing.bottomShape = new Circle(); + drawing.topShape = new Shapes.Diamond(); + drawing.middleShape = new Shapes.Rectangle(); + testSerializationDeserialization(gson, drawing, Shapes.Drawing.class); + } + + /** + * Just makes a loop serialize / deserialize / reserialize and checks + * whether the string representation is identical + * + * @param gson serializer/deserializer + * @param o object to serialize and deserialize + * @param c class of the object + */ + public static void testSerializationDeserialization(Gson gson, Object o, Class c) { + String json = gson.toJson(o, c); + System.out.println(json); + Object oRestored = gson.fromJson(json, c); + String json2 = gson.toJson(oRestored, c); + System.out.println(json2); + Assert.assertEquals(json, json2); + } +} \ No newline at end of file diff --git a/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/Circle.java b/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/Circle.java new file mode 100644 index 000000000..5ccb7851b --- /dev/null +++ b/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/Circle.java @@ -0,0 +1,5 @@ +package org.scijava.persist.testobjects; + +public class Circle implements Shape { + int radius; +} diff --git a/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/CircleAdapter.java b/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/CircleAdapter.java new file mode 100644 index 000000000..90d078c2f --- /dev/null +++ b/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/CircleAdapter.java @@ -0,0 +1,19 @@ +package org.scijava.persist.testobjects; + +import org.scijava.persist.IClassRuntimeAdapter; +import org.scijava.plugin.Plugin; + +@Plugin(type = IClassRuntimeAdapter.class) +public class CircleAdapter implements IClassRuntimeAdapter { + + @Override + public Class getBaseClass() { + return Shape.class; + } + + @Override + public Class getRunTimeClass() { + return Circle.class; + } + +} diff --git a/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/Shape.java b/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/Shape.java new file mode 100644 index 000000000..59e65c6f2 --- /dev/null +++ b/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/Shape.java @@ -0,0 +1,4 @@ +package org.scijava.persist.testobjects; + +public interface Shape { +} diff --git a/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/Shapes.java b/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/Shapes.java new file mode 100644 index 000000000..792bf3e40 --- /dev/null +++ b/scijava/scijava-persist/src/test/java/org/scijava/persist/testobjects/Shapes.java @@ -0,0 +1,58 @@ +package org.scijava.persist.testobjects; + +import org.scijava.persist.IClassRuntimeAdapter; +import org.scijava.plugin.Plugin; + +public class Shapes { + + public static class Rectangle implements Shape { + public int width; + public int height; + } + + public static class Diamond implements Shape { + public int width; + public int height; + } + + public static class Drawing { + public Shape bottomShape; + public Shape middleShape; + public Shape topShape; + } + + /** + * It's not necessarily a good idea to put adapters as inner classes, but if you do, + * make sure the class is static or you will get an {@link InstantiationError}. + */ + @Plugin(type = IClassRuntimeAdapter.class) + public static class RectangleAdapter implements IClassRuntimeAdapter { + + @Override + public Class getBaseClass() { + return Shape.class; + } + + @Override + public Class getRunTimeClass() { + return Rectangle.class; + } + + } + + @Plugin(type = IClassRuntimeAdapter.class) + public static class DiamondAdapter implements IClassRuntimeAdapter { + + @Override + public Class getBaseClass() { + return Shape.class; + } + + @Override + public Class getRunTimeClass() { + return Diamond.class; + } + + } + +}