/*-
*******************************************************************************
* Copyright (c) 2011, 2014, 2016 Diamond Light Source Ltd.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Colin Palmer - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.dawnsci.json;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;
import org.eclipse.dawnsci.analysis.api.persistence.IClassRegistry;
import org.eclipse.dawnsci.analysis.api.persistence.IMarshaller;
import org.eclipse.dawnsci.analysis.api.persistence.IMarshallerService;
import org.eclipse.dawnsci.analysis.api.roi.IOrientableROI;
import org.eclipse.dawnsci.analysis.api.roi.IROI;
import org.eclipse.dawnsci.analysis.api.roi.IRectangularROI;
import org.eclipse.dawnsci.analysis.dataset.roi.CircularFitROI;
import org.eclipse.dawnsci.analysis.dataset.roi.CircularROI;
import org.eclipse.dawnsci.analysis.dataset.roi.EllipticalFitROI;
import org.eclipse.dawnsci.analysis.dataset.roi.EllipticalROI;
import org.eclipse.dawnsci.analysis.dataset.roi.FreeDrawROI;
import org.eclipse.dawnsci.analysis.dataset.roi.GridROI;
import org.eclipse.dawnsci.analysis.dataset.roi.HyperbolicROI;
import org.eclipse.dawnsci.analysis.dataset.roi.LinearROI;
import org.eclipse.dawnsci.analysis.dataset.roi.ParabolicROI;
import org.eclipse.dawnsci.analysis.dataset.roi.PerimeterBoxROI;
import org.eclipse.dawnsci.analysis.dataset.roi.PointROI;
import org.eclipse.dawnsci.analysis.dataset.roi.PolygonalROI;
import org.eclipse.dawnsci.analysis.dataset.roi.PolylineROI;
import org.eclipse.dawnsci.analysis.dataset.roi.RectangularROI;
import org.eclipse.dawnsci.analysis.dataset.roi.RingROI;
import org.eclipse.dawnsci.analysis.dataset.roi.SectorROI;
import org.eclipse.dawnsci.analysis.dataset.roi.XAxisBoxROI;
import org.eclipse.dawnsci.analysis.dataset.roi.YAxisBoxROI;
import org.eclipse.dawnsci.json.internal.MarshallerServiceClassRegistry;
import org.eclipse.dawnsci.json.internal.MarshallerServiceClassRegistry.ClassRegistryDuplicateIdException;
import org.eclipse.dawnsci.json.internal.ROIClassRegistry;
import org.eclipse.dawnsci.json.internal.RegisteredClassIdResolver;
import org.eclipse.dawnsci.json.mixin.roi.CircularFitROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.CircularROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.EllipticalFitMixIn;
import org.eclipse.dawnsci.json.mixin.roi.EllipticalROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.FreeDrawROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.GridROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.HyperbolicROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.IOrientableROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.IROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.IRectangularROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.LinearROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.ParabolicROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.PerimeterROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.PolygonalROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.PolylineROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.RectangularROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.RingROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.SectorROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.XAxisBoxROIMixIn;
import org.eclipse.dawnsci.json.mixin.roi.YAxisBoxROIMixIn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTypeResolverBuilder;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.module.SimpleModule;
/**
* JSON marshaller implementation which allows objects to be converted to and from JSON strings
* <p>
* This implementation adds type information to encoded JSON strings and will use it when deserializing.
* <p>
* Type information is typically obtained from the attribute definition of the class in which it is defined,
* or the class passed to the {@link #marshal(Object anyObject, boolean useRegisteredClassTyping)}.
* <p>
* This is not useful when working with interface, abstract base class or Object definitions, as it could be a number of
* different classes. In these situations, the key "@type" is used to indicate a string for class identification.
*
* @author Colin Palmer
* @author Martin Gaughran
*/
public class MarshallerService implements IMarshallerService {
private static final String TYPE_INFO_FIELD_NAME = "@type";
private static final Logger logger = LoggerFactory.getLogger(MarshallerService.class);
private List<IClassRegistry> extra_registries;
private ObjectMapper registeredClassMapper;
private ObjectMapper standardMapper;
private ObjectMapper oldMapper;
private List<IMarshaller> marshallers;
private boolean platformIsRunning;
static {
System.out.println("Started " + IMarshallerService.class.getSimpleName());
}
/**
* Default public constructor - for testing purposes only! Otherwise use OSGi to get the service.
*/
public MarshallerService() {
this(null, null);
}
/**
* Constructor for testing to allow IClassRegistry('s) to be injected.
*
* @param extra_registries
*/
public MarshallerService(IClassRegistry... extra_registries) {
this(Arrays.asList(extra_registries), null);
}
/**
* Constructor for testing to allow IMarshaller(s) to be injected.
*
* @param marshallers
*/
public MarshallerService(IMarshaller... marshallers) {
this(null, Arrays.asList(marshallers));
}
/**
* Constructor for testing to allow IMarshaller(s) or IClassRegistry('s) to be injected.
*
* @param extra_registries
* @param marshallers
*/
public MarshallerService(List<IClassRegistry> extra_registries, List<IMarshaller> marshallers) {
platformIsRunning = Platform.isRunning();
if (marshallers!=null) this.marshallers = Collections.unmodifiableList(marshallers);
this.extra_registries = new ArrayList<IClassRegistry>();
this.extra_registries.add(new ROIClassRegistry());
this.extra_registries.add(new ArrayRegistry());
if (extra_registries!=null) this.extra_registries.addAll(extra_registries);
this.extra_registries = Collections.unmodifiableList(this.extra_registries);
}
/**
* Serialize the given object to a JSON string
* <p>
* Objects to be marshalled with this implementation should have no-arg constructors, and getters and setters for
* their fields. Primitive and collection types (arrays, Collections and Maps) should work correctly. More
* complicated types (generics other than collections, inner classes etc) might or might not work properly.
*/
@Override
public String marshal(Object anyObject) throws Exception {
// Use registered class typing by default, as it is only an issue with out-of-date software.
return marshal(anyObject, true);
}
@Override
public String marshal(Object anyObject, boolean useRegisteredClassTyping) throws Exception {
String json;
if (useRegisteredClassTyping) {
if (registeredClassMapper==null) registeredClassMapper = createRegisteredClassMapper();
json = registeredClassMapper.writeValueAsString(anyObject);
} else {
if (standardMapper==null) standardMapper = createJacksonMapper();
json = standardMapper.writeValueAsString(anyObject);
}
return json;
}
private ObjectMapper createRegisteredClassMapper() throws InstantiationException, IllegalAccessException, ClassRegistryDuplicateIdException, CoreException {
ObjectMapper mapper = createJacksonMapper();
mapper.setDefaultTyping(createRegisteredTypeIdResolver());
return mapper;
}
/**
* Deserialize the given JSON string as an instance of the given class
* <p>
* This method will try to find the correct classes for deserialization from the class id, if type not
* given.
*/
@Override
public <U> U unmarshal(String string, Class<U> beanClass) throws Exception {
try {
if (registeredClassMapper == null) registeredClassMapper = createRegisteredClassMapper();
if (beanClass != null) {
return registeredClassMapper.readValue(string, beanClass);
}
// If bean class is not supplied, try using Object
@SuppressWarnings("unchecked")
U result = (U) registeredClassMapper.readValue(string, Object.class);
return result;
} catch (JsonMappingException | IllegalArgumentException ex) {
// Check if this is due to missing type info. This can appear in two ways: the type info field can be
// missing from an object, in which case JsonMappingException is thrown; or the first element of an array
// might be wrongly interpreted as a class name, in which case we get a ClassNotFoundException
if ((ex instanceof JsonMappingException && ex.getMessage().contains(TYPE_INFO_FIELD_NAME))
|| ex instanceof IllegalArgumentException && ex.getCause() instanceof ClassNotFoundException) {
// This code is used to decode, for instance, trees consisting of Map<String, Object>'s nested
// inside each other, such as in TreeServlet. This behaviour is necessary to avoid inclusion of type
// id's for every map, or extensive modifications to the ObjectMapper.
try {
if (oldMapper == null) oldMapper = createOldMapper();
return oldMapper.readValue(string, beanClass);
} catch (Exception withoutTypeException) {
throw ex;
}
}
throw ex;
}
}
private final ObjectMapper createJacksonMapper() throws InstantiationException, IllegalAccessException, CoreException {
ObjectMapper mapper = new ObjectMapper();
// Use custom serialization for IPosition objects
// (Otherwise all IPosition subclasses will need to become simple beans, i.e. no-arg constructors with getters
// and setters for all fields. MapPosition.getNames() caused problems because it just returns keys from the map
// and has no corresponding setter.)
SimpleModule module = new SimpleModule();
// Add mix-in annotations for ROIs
module.setMixInAnnotation(IROI.class, IROIMixIn.class);
module.setMixInAnnotation(IOrientableROI.class, IOrientableROIMixIn.class);
module.setMixInAnnotation(LinearROI.class, LinearROIMixIn.class);
module.setMixInAnnotation(CircularROI.class, CircularROIMixIn.class);
module.setMixInAnnotation(GridROI.class, GridROIMixIn.class);
module.setMixInAnnotation(PerimeterBoxROI.class, PerimeterROIMixIn.class);
module.setMixInAnnotation(IRectangularROI.class, IRectangularROIMixIn.class);
module.setMixInAnnotation(RectangularROI.class, RectangularROIMixIn.class);
module.setMixInAnnotation(RingROI.class, RingROIMixIn.class);
module.setMixInAnnotation(SectorROI.class, SectorROIMixIn.class);
module.setMixInAnnotation(FreeDrawROI.class, FreeDrawROIMixIn.class);
module.setMixInAnnotation(PolylineROI.class, PolylineROIMixIn.class);
module.setMixInAnnotation(PolygonalROI.class, PolygonalROIMixIn.class);
module.setMixInAnnotation(XAxisBoxROI.class, XAxisBoxROIMixIn.class);
module.setMixInAnnotation(YAxisBoxROI.class, YAxisBoxROIMixIn.class);
module.setMixInAnnotation(EllipticalROI.class, EllipticalROIMixIn.class);
module.setMixInAnnotation(CircularFitROI.class, CircularFitROIMixIn.class);
module.setMixInAnnotation(EllipticalFitROI.class, EllipticalFitMixIn.class);
module.setMixInAnnotation(ParabolicROI.class, ParabolicROIMixIn.class);
module.setMixInAnnotation(HyperbolicROI.class, HyperbolicROIMixIn.class);
// Add handlers
createModuleExtensions(module);
mapper.registerModule(module);
// Be careful adjusting these settings - changing them will probably cause various unit tests to fail which
// check the exact contents of the serialized JSON string
mapper.setSerializationInclusion(Include.NON_NULL);
mapper.enable(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
//mapper.enable(SerializationFeature.INDENT_OUTPUT);
return mapper;
}
@Override
public boolean isObjMixInSupported(Object roi) {
if(roi instanceof PointROI
|| roi instanceof LinearROI
|| roi instanceof CircularROI
|| roi instanceof GridROI
|| roi instanceof PerimeterBoxROI
|| roi instanceof RectangularROI
|| roi instanceof RingROI
|| roi instanceof SectorROI
|| roi instanceof FreeDrawROI
|| roi instanceof PolylineROI
|| roi instanceof PolygonalROI
|| roi instanceof XAxisBoxROI
|| roi instanceof YAxisBoxROI
|| roi instanceof EllipticalROI
|| roi instanceof CircularFitROI
|| roi instanceof EllipticalFitROI
|| roi instanceof ParabolicROI
|| roi instanceof HyperbolicROI)
return true;
return false;
}
private void createModuleExtensions(SimpleModule module) throws InstantiationException, IllegalAccessException, CoreException {
List<IMarshaller> ms = new ArrayList<>(7);
ms.addAll(getAvailableMarshallerExtensions());
if (marshallers!=null && !marshallers.isEmpty()) ms.addAll(marshallers);
applyMarshallersToModule(module, ms);
}
private List<IMarshaller> getAvailableMarshallerExtensions() throws CoreException {
List<IMarshaller> marshallers = new ArrayList<>(7);
if (!platformIsRunning) return marshallers;
IConfigurationElement[] elements = Platform.getExtensionRegistry().getConfigurationElementsFor("org.eclipse.dawnsci.analysis.api.marshaller");
for (IConfigurationElement e : elements) {
final IMarshaller marshaller = (IMarshaller) e.createExecutableExtension("class");
marshallers.add(marshaller);
}
return marshallers;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void applyMarshallersToModule(SimpleModule module, List<IMarshaller> marshallers) throws InstantiationException, IllegalAccessException {
for (IMarshaller marshaller : marshallers) {
Class<?> objectClass = marshaller.getObjectClass();
if (objectClass!=null) {
module.addSerializer(objectClass, (JsonSerializer)marshaller.getSerializerClass().newInstance());
module.addDeserializer(objectClass,(JsonDeserializer)marshaller.getDeserializerClass().newInstance());
}
Class<?> mixInType = marshaller.getMixinAnnotationType();
Class<?> mixInClass = marshaller.getMixinAnnotationClass();
if (mixInClass!=null && mixInType!=null) {
module.setMixInAnnotation(mixInType, mixInClass);
}
}
}
/**
* Create a TypeResolverBuilder which will add class id information to JSON-serialized objects to
* allow the correct classes to be loaded during deserialization.
* <p>
* Any IMarshaller-provided serializers / deserializers take precedence over this class identification info.
* <p>
* NOTE: this strongly relies on the exact implementation of the Jackson library - it was written to work with
* version 2.2.0 and has not been tested with any other version.
*
* @return the customised TypeResolverBuilder
* @throws ClassRegistryDuplicateIdException
* @throws CoreException
*/
private TypeResolverBuilder<?> createRegisteredTypeIdResolver() throws ClassRegistryDuplicateIdException, CoreException {
MarshallerServiceClassRegistry registry = new MarshallerServiceClassRegistry(extra_registries);
TypeResolverBuilder<?> typer = new RegisteredTypeResolverBuilder(registry);
typer = typer.init(JsonTypeInfo.Id.CUSTOM, null);
typer = typer.inclusion(JsonTypeInfo.As.PROPERTY);
typer = typer.typeProperty(TYPE_INFO_FIELD_NAME);
return typer;
}
/**
* A TypeResolverBuilder for use with registered classes.
*/
private class RegisteredTypeResolverBuilder extends DefaultTypeResolverBuilder {
private static final long serialVersionUID = 1L;
private MarshallerServiceClassRegistry registry;
public RegisteredTypeResolverBuilder(MarshallerServiceClassRegistry registry) {
this(null, registry);
}
public RegisteredTypeResolverBuilder(DefaultTyping typing, MarshallerServiceClassRegistry registry) {
super(typing);
this.registry = registry;
}
// Override StdTypeResolverBuilder#idResolver() to return our custom RegisteredClassIdResolver
// (We need this override, rather than just providing a custom resolver in StdTypeResolverBuilder#init(), because
// the default implementation does not normally pass the base type to the custom resolver but we need it.)
@Override
protected TypeIdResolver idResolver(MapperConfig<?> config,
JavaType baseType, Collection<NamedType> subtypes,
boolean forSer, boolean forDeser) {
return new RegisteredClassIdResolver(baseType, config.getTypeFactory(), registry);
}
// Override DefaultTypeResolverBuilder#useForType() to add type information only to those required.
@Override
public boolean useForType(JavaType type) {
Class<?> clazz = type.getRawClass();
// Note: This does not work well with generics defined at or above the same scope as the call
// to marshal or unmarshal. As a result, only use such generics there when dealing with a primitive
// type or registered class, as these will cope with the idResolver.
// We can lookup the class in the registry, for marshalling and unmarshalling.
Boolean registryHasClass = registry.isClass(clazz);
// We only ever declare as object if we intend to use one of our own classes (or a primitive).
Boolean isObject = (Object.class.equals(clazz));
// Also include abstract classes and interfaces as these are always defined with a type id. This
// is not the case for container types, however, so these are excluded.
Boolean isAbstract = type.isAbstract();
Boolean isInterface = type.isInterface();
Boolean isNotContainer = !type.isContainerType();
// Primitive types are considered abstract, so exclude these as well.
Boolean isNotPrimitive = !type.isPrimitive();
return registryHasClass || ((isObject || isAbstract || isInterface) && isNotContainer && isNotPrimitive);
}
}
private final ObjectMapper createOldMapper() {
ObjectMapper mapper = new ObjectMapper();
// Use custom serialization for IPosition objects
// (Otherwise all IPosition subclasses will need to become simple beans, i.e. no-arg constructors with getters
// and setters for all fields. MapPosition.getNames() caused problems because it just returns keys from the map
// and has no corresponding setter.)
SimpleModule module = new SimpleModule();
try { // Extension points might still work
createModuleExtensions(module);
} catch (Exception ne) {
// Ignored, we allow the non-osgi mapper to continue without extension points.
}
mapper.registerModule(module);
// Be careful adjusting these settings - changing them will probably cause various unit tests to fail which
// check the exact contents of the serialized JSON string
mapper.setSerializationInclusion(Include.NON_NULL);
//mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
return mapper;
}
}