package alien4cloud.json.deserializer; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.io.IOException; import java.lang.reflect.Modifier; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.alien4cloud.tosca.model.definitions.PropertyConstraint; import org.alien4cloud.tosca.model.definitions.constraints.InRangeConstraint; import alien4cloud.tosca.properties.constraints.exception.InvalidPropertyConstraintImplementationException; import alien4cloud.utils.TypeScanner; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.PropertyNamingStrategy.PropertyNamingStrategyBase; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Maps; public class PropertyConstraintDeserializer extends StdDeserializer<PropertyConstraint> { private static final String CONSTRAINT_PACKAGE = "org.alien4cloud.tosca.model.definitions.constraints"; private static final long serialVersionUID = 1L; private Map<String, Class<? extends PropertyConstraint>> constraints; // It's a little bit scary to see something like that ? /** * A cache which map the naming strategy to the mapping of constraint key to constraint class. It's when a specific naming strategy is defined on our * desserialization config. */ private Map<PropertyNamingStrategy, Map<String, Class<? extends PropertyConstraint>>> constraintsCache; @SuppressWarnings("unchecked") public PropertyConstraintDeserializer() throws IOException, ClassNotFoundException, IntrospectionException { super(PropertyConstraint.class); this.constraints = Maps.newHashMap(); this.constraintsCache = Maps.newConcurrentMap(); // Retrieve all classes which belong to this package Set<Class<?>> classes = TypeScanner.scanTypes(CONSTRAINT_PACKAGE, PropertyConstraint.class); for (Class<?> propClass : classes) { if (!Modifier.isAbstract(propClass.getModifiers())) { // Use property as jackson use property PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(propClass).getPropertyDescriptors(); String constraintName = null; for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { if (propertyDescriptor.getReadMethod() != null && propertyDescriptor.getWriteMethod() != null) { constraintName = propertyDescriptor.getName(); break; } } if (constraintName != null && !constraintName.isEmpty()) { this.constraints.put(constraintName, (Class<? extends PropertyConstraint>) propClass); } else { // First field name is also constraint name throw new InvalidPropertyConstraintImplementationException( "A constraint validator must have a readable/writable property to inject configuration."); } } } } @Override public PropertyConstraint deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { ObjectMapper mapper = (ObjectMapper) jp.getCodec(); ObjectNode node = mapper.readTree(jp); String constraintName = null; Iterator<String> fieldIterator = node.fieldNames(); // First field is also the constraint name ? if (fieldIterator.hasNext()) { constraintName = fieldIterator.next(); } else { throw JsonMappingException.from(jp, "Constraint definition must contain one field"); } PropertyNamingStrategy namingStrategy = mapper.getDeserializationConfig().getPropertyNamingStrategy(); Map<String, Class<? extends PropertyConstraint>> constraintsMapping = getTranslatedConstraintsMap(namingStrategy); if (!constraintsMapping.containsKey(constraintName)) { if ("rangeMinValue".equals(constraintName) || "rangeMaxValue".equals(constraintName)) { return mapper.treeToValue(node, InRangeConstraint.class); } else { throw JsonMappingException.from(jp, "Constraint not found [" + constraintName + "], expect one of [" + this.constraints.keySet() + "]"); } } Class<? extends PropertyConstraint> constraintClass = constraintsMapping.get(constraintName); return mapper.treeToValue(node, constraintClass); } private Map<String, Class<? extends PropertyConstraint>> getTranslatedConstraintsMap(PropertyNamingStrategy namingStrategy) { if (namingStrategy instanceof PropertyNamingStrategyBase) { Map<String, Class<? extends PropertyConstraint>> cachedConstraints = constraintsCache.get(namingStrategy); if (cachedConstraints != null) { return cachedConstraints; } else { Map<String, Class<? extends PropertyConstraint>> translatedConstraints = Maps.newHashMap(); for (Map.Entry<String, Class<? extends PropertyConstraint>> entry : this.constraints.entrySet()) { translatedConstraints.put(((PropertyNamingStrategyBase) namingStrategy).translate(entry.getKey()), entry.getValue()); } constraintsCache.put(namingStrategy, translatedConstraints); return translatedConstraints; } } else { return this.constraints; } } }