Java tutorial
/* * Copyright 2014 Nicolas Morel * * 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.github.nmorel.gwtjackson.rebind.bean; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.UUID; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIdentityReference; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.ObjectIdGenerator; import com.fasterxml.jackson.annotation.ObjectIdGenerators; import com.fasterxml.jackson.annotation.ObjectIdGenerators.IntSequenceGenerator; import com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator; import com.fasterxml.jackson.annotation.ObjectIdGenerators.UUIDGenerator; import com.github.nmorel.gwtjackson.rebind.BeanJsonDeserializerCreator; import com.github.nmorel.gwtjackson.rebind.CreatorUtils; import com.github.nmorel.gwtjackson.rebind.JacksonTypeOracle; import com.github.nmorel.gwtjackson.rebind.RebindConfiguration; import com.github.nmorel.gwtjackson.rebind.property.PropertiesContainer; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.core.ext.typeinfo.JAbstractMethod; import com.google.gwt.core.ext.typeinfo.JClassType; import com.google.gwt.core.ext.typeinfo.JConstructor; import com.google.gwt.core.ext.typeinfo.JMethod; import com.google.gwt.core.ext.typeinfo.JParameter; import com.google.gwt.core.ext.typeinfo.JType; import com.google.gwt.thirdparty.guava.common.base.Optional; import com.google.gwt.thirdparty.guava.common.base.Strings; import com.google.gwt.thirdparty.guava.common.collect.ImmutableList; import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap; import static com.github.nmorel.gwtjackson.rebind.CreatorUtils.findFirstEncounteredAnnotationsOnAllHierarchy; import static com.github.nmorel.gwtjackson.rebind.CreatorUtils.getAnnotation; import static com.github.nmorel.gwtjackson.rebind.CreatorUtils.isAnnotationPresent; import static com.github.nmorel.gwtjackson.rebind.CreatorUtils.isObjectOrSerializable; /** * @author Nicolas Morel. */ public final class BeanProcessor { public static BeanInfo processBean(TreeLogger logger, JacksonTypeOracle typeOracle, RebindConfiguration configuration, JClassType beanType) throws UnableToCompleteException { BeanInfoBuilder builder = new BeanInfoBuilder(); builder.setType(beanType); if (null != beanType.isGenericType()) { builder.setParameterizedTypes(Arrays.<JClassType>asList(beanType.isGenericType().getTypeParameters())); } determineInstanceCreator(configuration, typeOracle, logger, beanType, builder); Optional<JsonAutoDetect> jsonAutoDetect = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, beanType, JsonAutoDetect.class); if (jsonAutoDetect.isPresent()) { builder.setCreatorVisibility(jsonAutoDetect.get().creatorVisibility()); builder.setFieldVisibility(jsonAutoDetect.get().fieldVisibility()); builder.setGetterVisibility(jsonAutoDetect.get().getterVisibility()); builder.setIsGetterVisibility(jsonAutoDetect.get().isGetterVisibility()); builder.setSetterVisibility(jsonAutoDetect.get().setterVisibility()); } Optional<JsonIgnoreProperties> jsonIgnoreProperties = findFirstEncounteredAnnotationsOnAllHierarchy( configuration, beanType, JsonIgnoreProperties.class); if (jsonIgnoreProperties.isPresent()) { builder.setIgnoredFields(new LinkedHashSet<String>(Arrays.asList(jsonIgnoreProperties.get().value()))); builder.setIgnoreUnknown(jsonIgnoreProperties.get().ignoreUnknown()); } Optional<JsonPropertyOrder> jsonPropertyOrder = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, beanType, JsonPropertyOrder.class); builder.setPropertyOrderAlphabetic(jsonPropertyOrder.isPresent() && jsonPropertyOrder.get().alphabetic()); if (jsonPropertyOrder.isPresent() && jsonPropertyOrder.get().value().length > 0) { builder.setPropertyOrderList(Arrays.asList(jsonPropertyOrder.get().value())); } else if (!builder.getCreatorParameters().isEmpty()) { List<String> propertyOrderList = new ArrayList<String>(builder.getCreatorParameters().keySet()); builder.setPropertyOrderList(propertyOrderList); if (builder.isPropertyOrderAlphabetic()) { Collections.sort(propertyOrderList); } } Optional<JsonInclude> jsonInclude = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, beanType, JsonInclude.class); if (jsonInclude.isPresent()) { builder.setInclude(Optional.of(jsonInclude.get().value())); } builder.setIdentityInfo(processIdentity(logger, typeOracle, configuration, beanType)); builder.setTypeInfo(processType(logger, typeOracle, configuration, beanType)); return builder.build(); } /** * Look for the method to create a new instance of the bean. If none are found or the bean is abstract or an interface, we considered it * as non instantiable. * * @param typeOracle the oracle * @param logger logger * @param beanType type to look for constructor * @param builder current bean builder */ private static void determineInstanceCreator(RebindConfiguration configuration, JacksonTypeOracle typeOracle, TreeLogger logger, JClassType beanType, BeanInfoBuilder builder) { if (isObjectOrSerializable(beanType)) { return; } Optional<JClassType> mixinClass = configuration.getMixInAnnotations(beanType); List<JClassType> accessors = new ArrayList<JClassType>(); if (mixinClass.isPresent()) { accessors.add(mixinClass.get()); } accessors.add(beanType); // Look for a builder class Optional<Annotation> jsonDeserialize = CreatorUtils .getAnnotation("com.fasterxml.jackson.databind.annotation.JsonDeserialize", accessors); if (jsonDeserialize.isPresent()) { Optional<JClassType> builderClass = typeOracle.getClassFromJsonDeserializeAnnotation(logger, jsonDeserialize.get(), "builder"); if (builderClass.isPresent()) { builder.setBuilder(builderClass.get()); return; } } // we search for @JsonCreator annotation JConstructor creatorDefaultConstructor = null; JConstructor creatorConstructor = null; // we keep the list containing the mixin creator and the real creator List<? extends JAbstractMethod> creators = Collections.emptyList(); if (null == beanType.isInterface() && !beanType.isAbstract()) { for (JConstructor constructor : beanType.getConstructors()) { if (constructor.getParameters().length == 0) { creatorDefaultConstructor = constructor; continue; } // A constructor is considered as a creator if // - he is annotated with JsonCreator and // * all its parameters are annotated with JsonProperty // * or it has only one parameter // - or all its parameters are annotated with JsonProperty List<JConstructor> constructors = new ArrayList<JConstructor>(); if (mixinClass.isPresent() && null == mixinClass.get().isInterface()) { JConstructor mixinConstructor = mixinClass.get() .findConstructor(constructor.getParameterTypes()); if (null != mixinConstructor) { constructors.add(mixinConstructor); } } constructors.add(constructor); Optional<JsonIgnore> jsonIgnore = getAnnotation(JsonIgnore.class, constructors); if (jsonIgnore.isPresent() && jsonIgnore.get().value()) { continue; } boolean isAllParametersAnnotatedWithJsonProperty = isAllParametersAnnotatedWith(constructors.get(0), JsonProperty.class); if ((isAnnotationPresent(JsonCreator.class, constructors) && ((isAllParametersAnnotatedWithJsonProperty) || (constructor.getParameters().length == 1))) || isAllParametersAnnotatedWithJsonProperty) { creatorConstructor = constructor; creators = constructors; break; } } } JMethod creatorFactory = null; if (null == creatorConstructor) { // searching for factory method for (JMethod method : beanType.getMethods()) { if (method.isStatic()) { List<JMethod> methods = new ArrayList<JMethod>(); if (mixinClass.isPresent() && null == mixinClass.get().isInterface()) { JMethod mixinMethod = mixinClass.get().findMethod(method.getName(), method.getParameterTypes()); if (null != mixinMethod && mixinMethod.isStatic()) { methods.add(mixinMethod); } } methods.add(method); Optional<JsonIgnore> jsonIgnore = getAnnotation(JsonIgnore.class, methods); if (jsonIgnore.isPresent() && jsonIgnore.get().value()) { continue; } if (isAnnotationPresent(JsonCreator.class, methods) && (method.getParameters().length == 1 || isAllParametersAnnotatedWith(methods.get(0), JsonProperty.class))) { creatorFactory = method; creators = methods; break; } } } } final Optional<JAbstractMethod> creatorMethod; boolean defaultConstructor = false; if (null != creatorConstructor) { creatorMethod = Optional.<JAbstractMethod>of(creatorConstructor); } else if (null != creatorFactory) { creatorMethod = Optional.<JAbstractMethod>of(creatorFactory); } else if (null != creatorDefaultConstructor) { defaultConstructor = true; creatorMethod = Optional.<JAbstractMethod>of(creatorDefaultConstructor); } else { creatorMethod = Optional.absent(); } builder.setCreatorMethod(creatorMethod); builder.setCreatorDefaultConstructor(defaultConstructor); if (creatorMethod.isPresent() && !defaultConstructor) { if (creatorMethod.get().getParameters().length == 1 && !isAllParametersAnnotatedWith(creators.get(0), JsonProperty.class)) { // delegation constructor builder.setCreatorDelegation(true); builder.setCreatorParameters(ImmutableMap.of(BeanJsonDeserializerCreator.DELEGATION_PARAM_NAME, creatorMethod.get().getParameters()[0])); } else { // we want the property name define in the mixin and the parameter defined in the real creator method ImmutableMap.Builder<String, JParameter> creatorParameters = ImmutableMap.builder(); for (int i = 0; i < creatorMethod.get().getParameters().length; i++) { creatorParameters.put( creators.get(0).getParameters()[i].getAnnotation(JsonProperty.class).value(), creators.get(creators.size() - 1).getParameters()[i]); } builder.setCreatorParameters(creatorParameters.build()); } } } private static <T extends Annotation> boolean isAllParametersAnnotatedWith(JAbstractMethod method, Class<T> annotation) { for (JParameter parameter : method.getParameters()) { if (!parameter.isAnnotationPresent(annotation)) { return false; } } return true; } private static Optional<BeanIdentityInfo> processIdentity(TreeLogger logger, JacksonTypeOracle typeOracle, RebindConfiguration configuration, JClassType type) throws UnableToCompleteException { return processIdentity(logger, typeOracle, configuration, type, Optional.<JsonIdentityInfo>absent(), Optional.<JsonIdentityReference>absent()); } public static Optional<BeanIdentityInfo> processIdentity(TreeLogger logger, JacksonTypeOracle typeOracle, RebindConfiguration configuration, JClassType type, Optional<JsonIdentityInfo> jsonIdentityInfo, Optional<JsonIdentityReference> jsonIdentityReference) throws UnableToCompleteException { if (!jsonIdentityInfo.isPresent()) { jsonIdentityInfo = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, type, JsonIdentityInfo.class); } if (jsonIdentityInfo.isPresent() && ObjectIdGenerators.None.class != jsonIdentityInfo.get().generator()) { if (!jsonIdentityReference.isPresent()) { jsonIdentityReference = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, type, JsonIdentityReference.class); } String propertyName = jsonIdentityInfo.get().property(); boolean alwaysAsId = jsonIdentityReference.isPresent() && jsonIdentityReference.get().alwaysAsId(); Class<? extends ObjectIdGenerator<?>> generator = jsonIdentityInfo.get().generator(); Class<?> scope = jsonIdentityInfo.get().scope(); BeanIdentityInfo beanIdentityInfo; if (generator.isAssignableFrom(PropertyGenerator.class)) { beanIdentityInfo = new BeanIdentityInfo(propertyName, alwaysAsId, generator, scope); } else { JType identityType; if (IntSequenceGenerator.class == generator) { identityType = typeOracle.getType(Integer.class.getName()); } else if (UUIDGenerator.class == generator) { identityType = typeOracle.getType(UUID.class.getName()); } else { JClassType generatorType = typeOracle.getType(generator.getCanonicalName()); JClassType objectIdGeneratorType = generatorType.getSuperclass(); while (!objectIdGeneratorType.getQualifiedSourceName() .equals(ObjectIdGenerator.class.getName())) { objectIdGeneratorType = objectIdGeneratorType.getSuperclass(); } identityType = objectIdGeneratorType.isParameterized().getTypeArgs()[0]; } beanIdentityInfo = new BeanIdentityInfo(propertyName, alwaysAsId, generator, scope, identityType); } return Optional.of(beanIdentityInfo); } return Optional.absent(); } private static Optional<BeanTypeInfo> processType(TreeLogger logger, JacksonTypeOracle typeOracle, RebindConfiguration configuration, JClassType type) throws UnableToCompleteException { return processType(logger, typeOracle, configuration, type, Optional.<JsonTypeInfo>absent(), Optional.<JsonSubTypes>absent()); } public static Optional<BeanTypeInfo> processType(TreeLogger logger, JacksonTypeOracle typeOracle, RebindConfiguration configuration, JClassType type, Optional<JsonTypeInfo> jsonTypeInfo, Optional<JsonSubTypes> propertySubTypes) throws UnableToCompleteException { if (!jsonTypeInfo.isPresent()) { jsonTypeInfo = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, type, JsonTypeInfo.class); if (!jsonTypeInfo.isPresent()) { return Optional.absent(); } } Id use = jsonTypeInfo.get().use(); As include = jsonTypeInfo.get().include(); String propertyName = jsonTypeInfo.get().property().isEmpty() ? jsonTypeInfo.get().use().getDefaultPropertyName() : jsonTypeInfo.get().property(); Optional<JsonSubTypes> typeSubTypes = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, type, JsonSubTypes.class); // TODO we could do better, we actually extract metadata twice for a lot of classes ImmutableMap<JClassType, String> classToSerializationMetadata = extractMetadata(logger, configuration, type, jsonTypeInfo, propertySubTypes, typeSubTypes, CreatorUtils.filterSubtypesForSerialization(logger, configuration, type)); ImmutableMap<JClassType, String> classToDeserializationMetadata = extractMetadata(logger, configuration, type, jsonTypeInfo, propertySubTypes, typeSubTypes, CreatorUtils.filterSubtypesForDeserialization(logger, configuration, type)); return Optional.of(new BeanTypeInfo(use, include, propertyName, classToSerializationMetadata, classToDeserializationMetadata)); } private static ImmutableMap<JClassType, String> extractMetadata(TreeLogger logger, RebindConfiguration configuration, JClassType type, Optional<JsonTypeInfo> jsonTypeInfo, Optional<JsonSubTypes> propertySubTypes, Optional<JsonSubTypes> typeSubTypes, ImmutableList<JClassType> allSubtypes) throws UnableToCompleteException { ImmutableMap.Builder<JClassType, String> classToMetadata = ImmutableMap.builder(); classToMetadata.put(type, extractTypeMetadata(logger, configuration, type, type, jsonTypeInfo.get(), propertySubTypes, typeSubTypes, allSubtypes)); for (JClassType subtype : allSubtypes) { classToMetadata.put(subtype, extractTypeMetadata(logger, configuration, type, subtype, jsonTypeInfo.get(), propertySubTypes, typeSubTypes, allSubtypes)); } return classToMetadata.build(); } private static String extractTypeMetadata(TreeLogger logger, RebindConfiguration configuration, JClassType baseType, JClassType subtype, JsonTypeInfo typeInfo, Optional<JsonSubTypes> propertySubTypes, Optional<JsonSubTypes> baseSubTypes, ImmutableList<JClassType> allSubtypes) throws UnableToCompleteException { switch (typeInfo.use()) { case NAME: // we first look the name on JsonSubTypes annotations. Top ones override the bottom ones. String name = findNameOnJsonSubTypes(baseType, subtype, allSubtypes, propertySubTypes, baseSubTypes); if (null != name && !"".equals(name)) { return name; } // we look if the name is defined on the type with JsonTypeName Optional<JsonTypeName> typeName = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, subtype, JsonTypeName.class); if (typeName.isPresent() && !Strings.isNullOrEmpty(typeName.get().value())) { return typeName.get().value(); } // we use the default name (ie simple name of the class) String simpleBinaryName = subtype.getQualifiedBinaryName(); int indexLastDot = simpleBinaryName.lastIndexOf('.'); if (indexLastDot != -1) { simpleBinaryName = simpleBinaryName.substring(indexLastDot + 1); } return simpleBinaryName; case MINIMAL_CLASS: if (!baseType.getPackage().isDefault()) { String basePackage = baseType.getPackage().getName(); if (subtype.getQualifiedBinaryName().startsWith(basePackage + ".")) { return subtype.getQualifiedBinaryName().substring(basePackage.length()); } } case CLASS: return subtype.getQualifiedBinaryName(); default: logger.log(TreeLogger.Type.ERROR, "JsonTypeInfo.Id." + typeInfo.use() + " is not supported"); throw new UnableToCompleteException(); } } private static String findNameOnJsonSubTypes(JClassType baseType, JClassType subtype, ImmutableList<JClassType> allSubtypes, Optional<JsonSubTypes> propertySubTypes, Optional<JsonSubTypes> baseSubTypes) { JsonSubTypes.Type typeFound = findTypeOnSubTypes(subtype, propertySubTypes); if (null != typeFound) { return typeFound.name(); } typeFound = findTypeOnSubTypes(subtype, baseSubTypes); if (null != typeFound) { return typeFound.name(); } if (baseType != subtype) { // we look in all the hierarchy JClassType type = subtype; while (null != type) { if (allSubtypes.contains(type)) { JsonSubTypes.Type found = findTypeOnSubTypes(subtype, Optional.fromNullable(type.getAnnotation(JsonSubTypes.class))); if (null != found) { typeFound = found; } } type = type.getSuperclass(); } if (null != typeFound) { return typeFound.name(); } } return null; } private static JsonSubTypes.Type findTypeOnSubTypes(JClassType subtype, Optional<JsonSubTypes> jsonSubTypes) { if (jsonSubTypes.isPresent()) { for (JsonSubTypes.Type type : jsonSubTypes.get().value()) { if (type.value().getName().equals(subtype.getQualifiedBinaryName())) { return type; } } } return null; } /** * Process the properties of the bean to find additionnal informations like @JsonValue. * * @param configuration the configuration * @param logger the logger * @param typeOracle the oracle * @param beanInfo the previous bean information * @param properties the properties of the bean * * @return the new informations about the bean and its properties */ public static BeanInfo processProperties(RebindConfiguration configuration, TreeLogger logger, JacksonTypeOracle typeOracle, BeanInfo beanInfo, PropertiesContainer properties) { if (!properties.getValuePropertyInfo().isPresent() && !properties.getAnyGetterPropertyInfo().isPresent() && !properties.getAnySetterPropertyInfo().isPresent()) { return beanInfo; } BeanInfoBuilder builder = new BeanInfoBuilder(beanInfo); builder.setValuePropertyInfo(properties.getValuePropertyInfo()); if (properties.getValuePropertyInfo().isPresent() && beanInfo.getTypeInfo().isPresent() && As.PROPERTY.equals(beanInfo.getTypeInfo().get().getInclude())) { // if the bean has type info on property with @JsonValue, we change it to WRAPPER_ARRAY because the value may not be an object BeanTypeInfo typeInfo = beanInfo.getTypeInfo().get(); builder.setTypeInfo(Optional.of(new BeanTypeInfo(typeInfo.getUse(), As.WRAPPER_ARRAY, typeInfo.getPropertyName(), typeInfo.getMapTypeToSerializationMetadata(), typeInfo.getMapTypeToDeserializationMetadata()))); } builder.setAnyGetterPropertyInfo(properties.getAnyGetterPropertyInfo()); builder.setAnySetterPropertyInfo(properties.getAnySetterPropertyInfo()); return builder.build(); } }