/* Copyright (c) 2014, Effektif GmbH. * * 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 com.effektif.workflow.impl.json; import com.effektif.workflow.api.json.*; import com.effektif.workflow.api.types.DataType; import com.effektif.workflow.api.types.JavaBeanType; import com.effektif.workflow.api.types.ListType; import com.effektif.workflow.impl.data.types.MapType; import com.effektif.workflow.impl.json.types.BeanMapper; import com.effektif.workflow.impl.json.types.PolymorphicBeanMapper; import com.effektif.workflow.impl.util.Reflection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.*; import java.util.*; /** * Registry for static information used to map API model classes to and from JSON. The purpose of this class is to * provide a static cache of class information that is programmatically registered or discovered by reflection. * * @author Tom Baeyens */ public class Mappings { private static final Logger log = LoggerFactory.getLogger(Mappings.class); /** Initialized from the mapping builder information */ protected Map<Field,String> fieldNames = new HashMap<>(); /** Initialized from the mapping builder information */ protected Map<Field,FieldMapping> fieldsMappings; /** Initialized from the mapping builder information */ protected Set<Field> inlineFields = new HashSet<>(); /** Initialized from the mapping builder information */ protected Set<Field> ignoredFields = new HashSet<>(); /** Initialized from the mapping builder information */ protected List<JsonTypeMapperFactory> jsonTypeMapperFactories = new ArrayList<>(); /** Initialized from the mapping builder information */ protected Map<Type,DataType> dataTypesByValueClass = new HashMap<>(); /** Maps registered base classes (like e.g. <code>Activity</code>) to *unparameterized* polymorphic mappings. * Polymorphic parameterized types are not yet supported. * Initialized from the mapping builder information */ protected Map<Class<?>, PolymorphicMapping> polymorphicMappings = new HashMap<>(); /** Initialized from the mapping builder information in registerSubClass */ protected Map<Class<?>, TypeField> typeFields = new HashMap<>(); /** Type mappings contain the field mappings for each type. * Types can be parameterized. * Dynamically initialized */ protected Map<Type, TypeMapping> typeMappings = new HashMap<>(); /** * JSON type mappers are the SPI to plug in support for particular types. * Dynamically initialized. */ protected Map<Type, JsonTypeMapper> typeMappers = new HashMap<>(); /** dynamically initialized */ protected Map<Class<?>, Map<String,Type>> fieldTypes = new HashMap<>(); public Mappings(MappingsBuilder mappingsBuilder) { this.inlineFields = mappingsBuilder.inlineFields; this.ignoredFields = mappingsBuilder.ignoredFields; this.fieldNames = mappingsBuilder.fieldNames; this.fieldsMappings = mappingsBuilder.fieldsMappings; this.jsonTypeMapperFactories = mappingsBuilder.typeMapperFactories; this.dataTypesByValueClass = mappingsBuilder.dataTypesByValueClass; for (Class baseClass: mappingsBuilder.baseClasses.keySet()) { String typeField = mappingsBuilder.baseClasses.get(baseClass); PolymorphicMapping subclassMapping = new PolymorphicMapping(baseClass, typeField); polymorphicMappings.put(baseClass, subclassMapping); } for (Class<?> subClass: mappingsBuilder.subClasses) { registerSubClass(subClass); } } public Mappings(Mappings other) { this.fieldNames = other.fieldNames; this.inlineFields = other.inlineFields; this.jsonTypeMapperFactories = other.jsonTypeMapperFactories; this.dataTypesByValueClass = other.dataTypesByValueClass; this.polymorphicMappings = other.polymorphicMappings; this.typeFields = other.typeFields; this.typeMappings = other.typeMappings; this.typeMappers = other.typeMappers; this.fieldTypes = other.fieldTypes; } public void registerSubClass(Class< ? > subClass) { TypeName typeName = subClass.getAnnotation(TypeName.class); if (typeName!=null) { registerSubClass(subClass, typeName.value(), subClass); } else { for (Class<?> baseClass: polymorphicMappings.keySet()) { if (baseClass.isAssignableFrom(subClass)) { throw new RuntimeException(subClass.getName()+" does not declare "+TypeName.class.toString()); } } } } protected void registerSubClass(Class<?> baseClass, String typeName, Class<?> subClass) { PolymorphicMapping polymorphicMapping = polymorphicMappings.get(baseClass); if (polymorphicMapping!=null) { TypeMapping typeMapping = getTypeMapping(subClass); polymorphicMapping.registerSubtypeMapping(typeName, subClass, typeMapping); typeFields.put(subClass, new TypeField(polymorphicMapping.getTypeField(), typeName)); } Class< ? > superClass = baseClass.getSuperclass(); if (superClass!=null) { registerSubClass(superClass, typeName, subClass); } for (Class<?> i: baseClass.getInterfaces()) { registerSubClass(i, typeName, subClass); } } public void writeTypeField(JsonWriter jsonWriter, Object o) { TypeField typeField = typeFields.get(o.getClass()); if (typeField!=null) { jsonWriter.writeFieldName(typeField.getTypeField()); jsonWriter.writeString(typeField.getTypeName()); } } public synchronized Type getFieldType(Class< ? > clazz, String fieldName) { // could be cached in this mappings object Type fieldType = getFieldTypeFromCache(clazz, fieldName); if (fieldType!=null) { return fieldType; } Map<String,Type> fieldTypesForClass = fieldTypes.get(clazz); if (fieldTypesForClass==null) { fieldTypesForClass = new HashMap<>(); fieldTypes.put(clazz, fieldTypesForClass); } fieldType = findFieldType(clazz, fieldName); if (fieldType==null) { throw new RuntimeException("Field "+clazz.getName()+"."+fieldName+" not found"); } fieldTypesForClass.put(fieldName, fieldType); return fieldType; } private Type findFieldType(Class< ? > clazz, String fieldName) { try { for (Field field: clazz.getDeclaredFields()) { if (field.getName().equals(fieldName)) { return field.getGenericType(); } } if (clazz.getSuperclass()!=Object.class) { return findFieldType(clazz.getSuperclass(), fieldName); } } catch (Exception e) { throw new RuntimeException(e); } return null; } private Type getFieldTypeFromCache(Class< ? > type, String fieldName) { Map<String,Type> types = fieldTypes.get(type); if (types==null) { return null; } return types.get(fieldName); } public DataType getTypeByValue(Object value) { if (value==null) { return null; } if (value instanceof Collection) { return getTypeByCollection((Collection) value); } if (value instanceof Map) { return getTypeByMap((Map) value); } Class<?> clazz = value.getClass(); DataType dataType = dataTypesByValueClass.get(clazz); if (dataType!=null) { return dataType; } return new JavaBeanType(clazz); } private DataType getTypeByMap(Map map) { if (map==null || map.isEmpty()) { return null; } DataType valueType = getTypeByCollection(map.values()); return new MapType(valueType); } private DataType getTypeByCollection(Collection collection) { if (collection==null || collection.isEmpty()) { return null; } Iterator iterator = collection.iterator(); DataType commonDataType = getTypeByValue(iterator.next()); if (commonDataType instanceof JavaBeanType) { JavaBeanType javaBeanType = (JavaBeanType) commonDataType; while (iterator.hasNext()) { Object elementValue = iterator.next(); Class elementValueClass = elementValue.getClass(); Class javaBeanClass = javaBeanType.getJavaClass(); while (!javaBeanClass.isAssignableFrom(elementValueClass) && javaBeanClass!=Object.class) { javaBeanType.setJavaClass(javaBeanClass.getSuperclass()); } } } return new ListType(commonDataType); } public JsonTypeMapper getTypeMapper(Type type) { JsonTypeMapper jsonTypeMapper = typeMappers.get(type); if (jsonTypeMapper!=null) { return jsonTypeMapper; } Class clazz = Reflection.getRawClass(type); for (JsonTypeMapperFactory factory: jsonTypeMapperFactories) { jsonTypeMapper = factory.createTypeMapper(type, clazz, this); if (jsonTypeMapper!=null) { break; } } if (jsonTypeMapper==null) { PolymorphicMapping polymorphicMapping = getPolymorphicMapping(type); if (polymorphicMapping!=null) { polymorphicMapping = getParameterizedPolymorphicMapping(type, polymorphicMapping); jsonTypeMapper = new PolymorphicBeanMapper(polymorphicMapping); } else { TypeMapping typeMapping = getTypeMapping(type); jsonTypeMapper = new BeanMapper(typeMapping); } } jsonTypeMapper.setMappings(this); typeMappers.put(type, jsonTypeMapper); return jsonTypeMapper; } /** finds the most concrete polymorphic mapping that matches the given type. */ public PolymorphicMapping getPolymorphicMapping(Type type) { Class<?> clazz = Reflection.getRawClass(type); PolymorphicMapping polymorphicMapping = polymorphicMappings.get(clazz); while (polymorphicMapping==null && clazz!=null && clazz!=Object.class) { clazz = clazz.getSuperclass(); polymorphicMapping = polymorphicMappings.get(clazz); } return polymorphicMapping; } private PolymorphicMapping getParameterizedPolymorphicMapping(Type type, PolymorphicMapping untypedPolymorphicMapping) { if (!Reflection.isParameterized(type)) { return untypedPolymorphicMapping; } throw new RuntimeException("TODO polymorphic, parameterized types are not yet supported"); } public TypeMapping getTypeMapping(Type type) { TypeMapping typeMapping = typeMappings.get(type); if (typeMapping!=null) { // log.debug("Found type mapping "+typeMapping+" in cache for type "+Reflection.getSimpleName(type)); return typeMapping; } // log.debug("Creating type mapping for "+Reflection.getSimpleName(type)); typeMapping = new TypeMapping(type); typeMappings.put(type, typeMapping); scanFieldMappings(type, typeMapping); // log.debug("Creating type mapping "+typeMapping); return typeMapping; } public void scanFieldMappings(Type type, TypeMapping typeMapping) { List<FieldMapping> fieldMappings = new ArrayList<>(); scanFields(fieldMappings, type); Class<?> clazz = Reflection.getRawClass(type); Set<FieldMapping> inlineFieldMappings = new HashSet<>(); for (FieldMapping fieldMapping: fieldMappings) { // apply the json field name overwriting String jsonFieldName = fieldNames.get(fieldMapping.field); if (jsonFieldName!=null) { fieldMapping.jsonFieldName = jsonFieldName; } // capture the inline field mappings in a collection if (inlineFields.contains(fieldMapping.field)) { inlineFieldMappings.add(fieldMapping); } } if (!inlineFieldMappings.isEmpty()) { List<String> fieldNames = new ArrayList<>(); for (FieldMapping fieldMapping: fieldMappings) { fieldNames.add(fieldMapping.jsonFieldName); } for (FieldMapping inlineFieldMapping: inlineFieldMappings) { inlineFieldMapping.inline = fieldNames; } } JsonPropertyOrder jsonPropertyOrder = clazz.getAnnotation(JsonPropertyOrder.class); if (jsonPropertyOrder!=null) { String[] fieldNamesOrder = jsonPropertyOrder.value(); for (int i=fieldNamesOrder.length-1; i>=0; i--) { String fieldName = fieldNamesOrder[i]; FieldMapping fieldMapping = removeField(fieldMappings, fieldName); if (fieldMapping!=null) { fieldMappings.add(0, fieldMapping); } } } typeMapping.setFieldMappings(fieldMappings); } private FieldMapping removeField(List<FieldMapping> fieldMappings, String fieldName) { Iterator<FieldMapping> iterator = fieldMappings.iterator(); while (iterator.hasNext()) { FieldMapping fieldMapping = iterator.next(); if (fieldMapping.getFieldName().equals(fieldName)) { iterator.remove(); return fieldMapping; } } return null; } public static Type resolveFieldType(TypeVariable fieldType, Class<?> clazz, Type type) { Map<String,Type> typeArgs = new HashMap<>(); TypeVariable< ? >[] typeParameters = clazz.getTypeParameters(); Type[] actualTypeArguments = null; if (type instanceof ParameterizedType) { actualTypeArguments = ((ParameterizedType)type).getActualTypeArguments(); } else if (type instanceof GenericType) { actualTypeArguments = ((GenericType)type).getTypeArgs(); } else { return null; } for (int i=0; i<typeParameters.length; i++) { String name = typeParameters[i].getName(); Type typeArg = actualTypeArguments[i]; typeArgs.put(name, typeArg); } String typeArgName = fieldType.toString(); return typeArgs.get(typeArgName); } /** * Updates the given field mappings with mappings for the given type, by recursively scanning its fields. */ public void scanFields(List<FieldMapping> fieldMappings, Type type) { if (type == null) { throw new IllegalArgumentException("type may not be null"); } Class<?> clazz = Reflection.getRawClass(type); Map<TypeVariable,Type> typeArgs = Reflection.getTypeArgsMap(type); Field[] declaredFields = clazz.getDeclaredFields(); if (declaredFields!=null) { int index = 0; for (Field field: declaredFields) { if (!Modifier.isStatic(field.getModifiers()) && field.getAnnotation(JsonIgnore.class)==null && !ignoredFields.contains(field)) { field.setAccessible(true); FieldMapping fieldMapping = fieldsMappings.get(field); if (fieldMapping==null) { // log.debug(" Scanning "+Reflection.getSimpleName(field)); Type fieldType = field.getGenericType(); if (fieldType instanceof TypeVariable) { fieldType = typeArgs!=null ? typeArgs.get((TypeVariable)fieldType) : Object.class; } JsonTypeMapper fieldTypeMapper = getTypeMapper(fieldType); fieldMapping = new FieldMapping(field, fieldTypeMapper); } // Annotation-based field name override. JsonFieldName jsonFieldNameAnnotation = field.getAnnotation(JsonFieldName.class); if (jsonFieldNameAnnotation != null) { fieldMapping.setJsonFieldName(jsonFieldNameAnnotation.value()); } fieldMappings.add(index, fieldMapping); index++; } } } if (clazz.isEnum()) { return; } Class<? > superclass = clazz.getSuperclass(); if (superclass!=null && superclass!=Object.class) { Type supertype = Reflection.getSuperclass(type); if (supertype!=null) { scanFields(fieldMappings, supertype); } else { // TODO find out which field is not handled properly throw new RuntimeException("null supertype for " + type ); } } } public Map<Type, DataType> getDataTypesByValueClass() { return dataTypesByValueClass; } public void setDataTypesByValueClass(Map<Type, DataType> dataTypesByValueClass) { this.dataTypesByValueClass = dataTypesByValueClass; } public Map<Field, String> getFieldNames() { return fieldNames; } public Map<Field, FieldMapping> getFieldsMappings() { return fieldsMappings; } public Set<Field> getInlineFields() { return inlineFields; } public Set<Field> getIgnoredFields() { return ignoredFields; } public List<JsonTypeMapperFactory> getJsonTypeMapperFactories() { return jsonTypeMapperFactories; } public Map<Class< ? >, PolymorphicMapping> getPolymorphicMappings() { return polymorphicMappings; } public Map<Class< ? >, TypeField> getTypeFields() { return typeFields; } public Map<Type, TypeMapping> getTypeMappings() { return typeMappings; } public Map<Type, JsonTypeMapper> getTypeMappers() { return typeMappers; } public Map<Class< ? >, Map<String, Type>> getFieldTypes() { return fieldTypes; } public boolean isIgnored(Field field) { return ignoredFields.contains(field); } }