package alien4cloud.ui.form; import static alien4cloud.ui.form.GenericFormConstants.ABSTRACT_TYPE; import static alien4cloud.ui.form.GenericFormConstants.ARRAY_TYPE; import static alien4cloud.ui.form.GenericFormConstants.BOOLEAN_TYPE; import static alien4cloud.ui.form.GenericFormConstants.COMPLEX_TYPE; import static alien4cloud.ui.form.GenericFormConstants.CONTENT_TYPE_KEY; import static alien4cloud.ui.form.GenericFormConstants.DATE_TYPE; import static alien4cloud.ui.form.GenericFormConstants.IMPLEMENTATIONS_KEY; import static alien4cloud.ui.form.GenericFormConstants.IS_PASSWORD_KEY; import static alien4cloud.ui.form.GenericFormConstants.LABEL_KEY; import static alien4cloud.ui.form.GenericFormConstants.MAP_TYPE; import static alien4cloud.ui.form.GenericFormConstants.NOT_NULL_KEY; import static alien4cloud.ui.form.GenericFormConstants.NUMBER_TYPE; import static alien4cloud.ui.form.GenericFormConstants.ORDER_KEY; import static alien4cloud.ui.form.GenericFormConstants.PROPERTY_TYPE_KEY; import static alien4cloud.ui.form.GenericFormConstants.STRING_TYPE; import static alien4cloud.ui.form.GenericFormConstants.SUGGESTION_KEY; import static alien4cloud.ui.form.GenericFormConstants.TOSCA_DEFINITION_KEY; import static alien4cloud.ui.form.GenericFormConstants.TOSCA_TYPE; import static alien4cloud.ui.form.GenericFormConstants.TYPE_KEY; import static alien4cloud.ui.form.GenericFormConstants.VALID_VALUES_KEY; import java.beans.PropertyDescriptor; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Resource; import javax.validation.constraints.NotNull; import org.springframework.stereotype.Component; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.alien4cloud.tosca.model.definitions.PropertyDefinition; import alien4cloud.ui.form.annotation.FormContentTypes; import alien4cloud.ui.form.annotation.FormCustomType; import alien4cloud.ui.form.annotation.FormLabel; import alien4cloud.ui.form.annotation.FormPassword; import alien4cloud.ui.form.annotation.FormProperties; import alien4cloud.ui.form.annotation.FormPropertyDefinition; import alien4cloud.ui.form.annotation.FormSuggestion; import alien4cloud.ui.form.annotation.FormType; import alien4cloud.ui.form.annotation.FormTypes; import alien4cloud.ui.form.annotation.FormValidValues; import alien4cloud.ui.form.exception.FormDescriptorGenerationException; import alien4cloud.utils.ReflectionUtil; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Slf4j @Component public class PojoFormDescriptorGenerator { @Resource @Setter private FormSuggestionDescriptorGenerator suggestionDescriptorGenerator; @Resource @Setter private PropertyDefinitionConverter propertyDefinitionConverter; public Map<String, Object> generateDescriptor(Class<?> clazz) { return buildComplexTypeDescriptor(clazz); } private Map<String, Object> buildComplexTypeDescriptor(Class<?> clazz) { Map<String, Object> type = Maps.newHashMap(); type.put(TYPE_KEY, COMPLEX_TYPE); doParseClass(clazz, type); return type; } private boolean isPrimitive(Class<?> clazz) { return (clazz.isPrimitive() || Number.class.isAssignableFrom(clazz) || Boolean.class == clazz || String.class == clazz || Date.class.isAssignableFrom(clazz)); } private Map<String, Object> buildSimpleTypeDescriptor(Class<?> clazz) { Map<String, Object> type = Maps.newHashMap(); String typeName; if (clazz.isPrimitive()) { if (char.class == clazz) { throw new FormDescriptorGenerationException("Cannot handle 'char' type"); } else if (boolean.class == clazz) { typeName = BOOLEAN_TYPE; } else { typeName = NUMBER_TYPE; } } else if (Number.class.isAssignableFrom(clazz)) { typeName = NUMBER_TYPE; } else if (Boolean.class == clazz) { typeName = BOOLEAN_TYPE; } else if (Character.class == clazz) { throw new FormDescriptorGenerationException("Cannot handle 'Character' type"); } else if (String.class == clazz) { typeName = STRING_TYPE; } else if (Date.class.isAssignableFrom(clazz)) { typeName = DATE_TYPE; } else { throw new FormDescriptorGenerationException("Unmanaged primitive type [" + clazz.getName() + "]"); } type.put(TYPE_KEY, typeName); return type; } private Map<String, Object> buildSequenceTypeDescriptor(String sequenceTypeName, Class<?> contentType) { Map<String, Object> type = Maps.newHashMap(); type.put(TYPE_KEY, sequenceTypeName); if (isPrimitive(contentType)) { type.put(CONTENT_TYPE_KEY, buildSimpleTypeDescriptor(contentType)); } else if (Map.class.isAssignableFrom(contentType)) { throw new FormDescriptorGenerationException( "Cannot generate meta data for field of type set of set, set of array, array of array, map of set ... "); } else if (List.class.isAssignableFrom(contentType) || Set.class.isAssignableFrom(contentType)) { throw new FormDescriptorGenerationException( "Cannot generate meta data for field of type set of set, set of array, array of array, map of set ... "); } else if (contentType.isArray()) { throw new FormDescriptorGenerationException( "Cannot generate meta data for field of type set of set, set of array, array of array, map of set ... "); } else { type.put(CONTENT_TYPE_KEY, buildComplexTypeDescriptor(contentType)); } return type; } private Map<String, Object> buildSequenceAbstractTypeDescriptor(String sequenceTypeName, Map<String, FormType> implementations) { Map<String, Object> type = Maps.newHashMap(); type.put(TYPE_KEY, sequenceTypeName); type.put(CONTENT_TYPE_KEY, buildAbstractTypeDescriptor(implementations)); return type; } private Map<String, Object> buildAbstractTypeDescriptor(Map<String, FormType> implementations) { // Implementations are given Map<String, Object> type = Maps.newHashMap(); type.put(TYPE_KEY, ABSTRACT_TYPE); Map<String, Object> implementationDescriptors = Maps.newHashMap(); type.put(IMPLEMENTATIONS_KEY, implementationDescriptors); for (Map.Entry<String, FormType> implEntry : implementations.entrySet()) { Map<String, Object> implementationDescriptor = Maps.newHashMap(); implementationDescriptors.put(implEntry.getKey(), implementationDescriptor); implementationDescriptor.put(LABEL_KEY, implEntry.getValue().label()); doParseClass(implEntry.getValue().implementation(), implementationDescriptor); } return type; } private void doParseClass(Class<?> clazz, Map<String, Object> descriptors) { Map<String, Object> propertyTypes = Maps.newHashMap(); descriptors.put(PROPERTY_TYPE_KEY, propertyTypes); PropertyDescriptor[] properties = ReflectionUtil.getPropertyDescriptors(clazz); String[] orderedPropertiesNames; Set<String> propertiesNames; FormProperties formPropAnnotation = clazz.getAnnotation(FormProperties.class); if (formPropAnnotation != null && formPropAnnotation.value() != null) { orderedPropertiesNames = formPropAnnotation.value(); propertiesNames = Sets.newHashSet(orderedPropertiesNames); } else { propertiesNames = Sets.newLinkedHashSet(); for (PropertyDescriptor property : properties) { if (property.getReadMethod() != null && property.getWriteMethod() != null) { propertiesNames.add(property.getName()); } } orderedPropertiesNames = propertiesNames.toArray(new String[propertiesNames.size()]); if (log.isDebugEnabled()) { log.debug("Class " + clazz.getName() + " do not have FormProperties annotation, all properties will be read and order will not be assured"); } } for (PropertyDescriptor property : properties) { if ((!propertiesNames.contains(property.getName())) || property.getReadMethod() == null || property.getWriteMethod() == null) { continue; } Class<?> propClazz = property.getPropertyType(); Map<String, FormType> implementations = getImplementations(clazz, property); Map<String, FormType> contentImplementations = getContentImplementations(clazz, property); PropertyDefinition propertyDefinition = getPropertyDefinition(clazz, property); String customFormType = getCustomFormType(clazz, property); String[] validValues = getValidValues(clazz, property); String label = getLabel(clazz, property); Map<String, Object> type = Maps.newHashMap(); if (customFormType != null) { // Custom type that cannot be processed in a generic way on ui side type.put(TYPE_KEY, customFormType); } else if (propertyDefinition != null) { type.put(TYPE_KEY, TOSCA_TYPE); type.put(TOSCA_DEFINITION_KEY, propertyDefinition); } else if (isPrimitive(propClazz)) { // Primitive type type = buildSimpleTypeDescriptor(propClazz); if (validValues != null && validValues.length > 0) { type.put(VALID_VALUES_KEY, validValues); } } else if (Map.class.isAssignableFrom(propClazz)) { // Map type if (contentImplementations != null && !contentImplementations.isEmpty()) { type = buildSequenceAbstractTypeDescriptor(MAP_TYPE, contentImplementations); } else { ParameterizedType parameterizedType = (ParameterizedType) property.getReadMethod().getGenericReturnType(); Type[] types = parameterizedType.getActualTypeArguments(); Class<?> keyClass = (Class<?>) types[0]; Class<?> valueClass = (Class<?>) types[1]; if (keyClass != String.class) { throw new FormDescriptorGenerationException("Cannot generate meta data for map with key not of type String"); } type = buildSequenceTypeDescriptor(MAP_TYPE, valueClass); } } else if (List.class.isAssignableFrom(propClazz) || Set.class.isAssignableFrom(propClazz)) { // List or set types if (contentImplementations != null && !contentImplementations.isEmpty()) { type = buildSequenceAbstractTypeDescriptor(ARRAY_TYPE, contentImplementations); } else { ParameterizedType pt = (ParameterizedType) property.getReadMethod().getGenericReturnType(); Type[] types = pt.getActualTypeArguments(); Class<?> valueClass = (Class<?>) types[0]; type = buildSequenceTypeDescriptor(ARRAY_TYPE, valueClass); } } else if (propClazz.isArray()) { // Array type if (contentImplementations != null && !contentImplementations.isEmpty()) { type = buildSequenceAbstractTypeDescriptor(ARRAY_TYPE, contentImplementations); } else { Class<?> valueClass = property.getReadMethod().getReturnType().getComponentType(); type = buildSequenceTypeDescriptor(ARRAY_TYPE, valueClass); } } else if (implementations != null && !implementations.isEmpty()) { type = buildAbstractTypeDescriptor(implementations); } else { // Complex type Class<?> valueClass = property.getReadMethod().getReturnType(); type = buildComplexTypeDescriptor(valueClass); } Map<String, Object> suggestion = getSuggestion(clazz, property); if (suggestion != null) { type.put(SUGGESTION_KEY, suggestion); } if (isNotNull(clazz, property)) { type.put(NOT_NULL_KEY, true); } if (isPassword(clazz, property)) { type.put(IS_PASSWORD_KEY, true); } if (label != null) { type.put(LABEL_KEY, label); } propertyTypes.put(property.getName(), type); } descriptors.put(TYPE_KEY, COMPLEX_TYPE); descriptors.put(ORDER_KEY, orderedPropertiesNames); } private PropertyDefinition getPropertyDefinition(Class<?> clazz, PropertyDescriptor property) { FormPropertyDefinition propertyDefinition = ReflectionUtil.getAnnotation(clazz, FormPropertyDefinition.class, property); if (propertyDefinition != null) { return propertyDefinitionConverter.convert(propertyDefinition); } else { return null; } } private String[] getValidValues(Class<?> clazz, PropertyDescriptor property) { FormValidValues validValues = ReflectionUtil.getAnnotation(clazz, FormValidValues.class, property); if (validValues == null) { return null; } else { return validValues.value(); } } private Map<String, FormType> getImplementations(Class<?> clazz, PropertyDescriptor property) { FormTypes formTypes = ReflectionUtil.getAnnotation(clazz, FormTypes.class, property); if (formTypes == null) { return null; } return getImplementations(formTypes.value()); } private Map<String, FormType> getContentImplementations(Class<?> clazz, PropertyDescriptor property) { FormContentTypes formTypes = ReflectionUtil.getAnnotation(clazz, FormContentTypes.class, property); if (formTypes == null) { return null; } return getImplementations(formTypes.value()); } private Map<String, FormType> getImplementations(FormType[] formTypeValues) { if (formTypeValues == null || formTypeValues.length == 0) { return null; } Map<String, FormType> implementations = Maps.newHashMap(); for (FormType formType : formTypeValues) { implementations.put(formType.discriminantProperty(), formType); } return implementations; } private String getCustomFormType(Class<?> clazz, PropertyDescriptor property) { FormCustomType formCustomType = ReflectionUtil.getAnnotation(clazz, FormCustomType.class, property); return formCustomType != null ? formCustomType.value() : null; } private Map<String, Object> getSuggestion(Class<?> clazz, PropertyDescriptor property) { FormSuggestion formSuggestion = ReflectionUtil.getAnnotation(clazz, FormSuggestion.class, property); if (formSuggestion != null) { return suggestionDescriptorGenerator.generateSuggestionDescriptor(formSuggestion.fromClass(), formSuggestion.path()); } else { return null; } } private String getLabel(Class<?> clazz, PropertyDescriptor property) { FormLabel formLabel = ReflectionUtil.getAnnotation(clazz, FormLabel.class, property); if (formLabel != null) { return formLabel.value(); } else { return null; } } private boolean isNotNull(Class<?> clazz, PropertyDescriptor property) { return ReflectionUtil.getAnnotation(clazz, NotNull.class, property) != null; } private boolean isPassword(Class<?> clazz, PropertyDescriptor property) { return ReflectionUtil.getAnnotation(clazz, FormPassword.class, property) != null; } }