/*************************************************************************
* Copyright 2013-2014 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.cloudformation.resources;
import com.eucalyptus.cloudformation.CloudFormationException;
import com.eucalyptus.cloudformation.InternalFailureException;
import com.eucalyptus.cloudformation.ValidationErrorException;
import com.eucalyptus.cloudformation.resources.annotations.Property;
import com.eucalyptus.cloudformation.resources.annotations.Required;
import com.eucalyptus.cloudformation.template.JsonHelper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.log4j.Logger;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public class ResourcePropertyResolver {
private static final Logger LOG = Logger.getLogger(ResourcePropertyResolver.class);
public static JsonNode getJsonNodeFromResourceProperties(ResourceProperties resourceProperties) throws CloudFormationException {
return getJsonNodeFromObject(resourceProperties);
}
private static JsonNode getJsonNodeFromObject(Object object) throws CloudFormationException {
if (object == null) return null;
ObjectNode jsonNode = JsonHelper.createObjectNode();
BeanInfo beanInfo = null;
try {
beanInfo = Introspector.getBeanInfo(object.getClass());
} catch (IntrospectionException ex) {
LOG.error("Unable to create bean info for class " + object.getClass().getCanonicalName() + ". Check signatures for getters and setters");
throw new InternalFailureException(ex.getMessage());
}
Map<String, PropertyDescriptor> propertyDescriptorMap = Maps.newHashMap();
for (PropertyDescriptor propertyDescriptor:beanInfo.getPropertyDescriptors()) {
propertyDescriptorMap.put(propertyDescriptor.getName(), propertyDescriptor);
}
for (Field field: object.getClass().getDeclaredFields()) {
Property property = field.getAnnotation(Property.class);
if (property == null) continue;
String defaultName = field.getName().substring(0,1).toUpperCase() + field.getName().substring(1);
String name = (property.name() == null || property.name().isEmpty() ? defaultName: property.name());
Object objectValue = getField(propertyDescriptorMap, field, object);
if (objectValue == null) {
continue;
} else if (objectValue instanceof String) {
jsonNode.put(name, (String) objectValue);
} else if (objectValue instanceof Integer) {
jsonNode.put(name, String.valueOf((Integer) objectValue));
} else if (objectValue instanceof Long) {
jsonNode.put(name, String.valueOf((Long) objectValue));
} else if (objectValue instanceof Float) {
jsonNode.put(name, String.valueOf((Float) objectValue));
} else if (objectValue instanceof Double) {
jsonNode.put(name, String.valueOf((Double) objectValue));
} else if (objectValue instanceof Boolean) {
jsonNode.put(name, String.valueOf((Boolean) objectValue));
} else if (objectValue instanceof JsonNode) {
jsonNode.put(name, (JsonNode) objectValue);
} else if (objectValue instanceof Collection) {
jsonNode.put(name, getJsonNodeFromCollection((Collection<?>) objectValue));
} else {
jsonNode.put(name, getJsonNodeFromObject(objectValue));
}
}
return jsonNode;
}
private static JsonNode getJsonNodeFromCollection(Collection<?> collection) throws CloudFormationException {
if (collection == null) return null;
ArrayNode jsonNode = JsonHelper.createArrayNode();
for (Object object: collection) {
if (object == null) {
jsonNode.add((JsonNode) null); // TODO: really?
} else if (object instanceof String) {
jsonNode.add((String) object);
} else if (object instanceof Integer) {
jsonNode.add(String.valueOf((Integer) object));
} else if (object instanceof Long) {
jsonNode.add(String.valueOf((Long) object));
} else if (object instanceof Float) {
jsonNode.add(String.valueOf((Float) object));
} else if (object instanceof Double) {
jsonNode.add(String.valueOf((Double) object));
} else if (object instanceof Boolean) {
jsonNode.add(String.valueOf((Boolean) object));
} else if (object instanceof JsonNode) {
jsonNode.add((JsonNode) object);
} else if (object instanceof Collection) {
jsonNode.add(getJsonNodeFromCollection((Collection) object));
} else {
jsonNode.add(getJsonNodeFromObject(object));
}
}
return jsonNode;
}
public static void populateResourceProperties(Object object, JsonNode jsonNode, boolean enforceStrictProperties) throws CloudFormationException {
if (jsonNode == null) return; // TODO: consider this case
BeanInfo beanInfo = null;
try {
beanInfo = Introspector.getBeanInfo(object.getClass());
} catch (IntrospectionException ex) {
LOG.error("Unable to create bean info for class " + object.getClass().getCanonicalName() + ". Check signatures for getters and setters");
throw new InternalFailureException(ex.getMessage());
}
Map<String, PropertyDescriptor> propertyDescriptorMap = Maps.newHashMap();
for (PropertyDescriptor propertyDescriptor:beanInfo.getPropertyDescriptors()) {
propertyDescriptorMap.put(propertyDescriptor.getName(), propertyDescriptor);
}
Set<String> unprocessedFieldNames = Sets.newHashSet(jsonNode.fieldNames());
for (Field field: object.getClass().getDeclaredFields()) {
Property property = field.getAnnotation(Property.class);
if (property == null) continue;
String defaultName = field.getName().substring(0,1).toUpperCase() + field.getName().substring(1);
String name = (property.name() == null || property.name().isEmpty() ? defaultName: property.name());
Required required = field.getAnnotation(Required.class);
if (required != null && !jsonNode.has(name)) {
throw new ValidationErrorException("Template error: " + name + " is a required field");
}
if (!jsonNode.has(name)) continue; // no value to populate...
JsonNode valueNode = jsonNode.get(name);
// once here, trying to set a field, remove from propertyFields
unprocessedFieldNames.remove(name);
LOG.debug("Populating property with: " + name + "=" + valueNode + " " + valueNode.getClass());
if (field.getType().equals(String.class)) {
if (!valueNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + name + " must be of type String");
} else {
setField(propertyDescriptorMap, field, object, valueNode.asText());
}
} else if (field.getType().equals(Integer.class)) {
if (!valueNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + name + " must be of type Number");
} else {
try {
if (valueNode.asText().isEmpty()) {
if (required != null) {
throw new ValidationErrorException("Template error: " + name + " can not be blank (" + valueNode.asText() + ")");
} else {
setField(propertyDescriptorMap, field, object, null);
}
} else {
setField(propertyDescriptorMap, field, object, Integer.valueOf(valueNode.asText()));
}
} catch (NumberFormatException ex) {
throw new ValidationErrorException("Template error: " + name + " must be of type Integer (" + valueNode.asText() + ")");
}
}
} else if (field.getType().equals(Long.class)) {
if (!valueNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + name + " must be of type Number");
} else {
try {
if (valueNode.asText().isEmpty()) {
if (required != null) {
throw new ValidationErrorException("Template error: " + name + " can not be blank (" + valueNode.asText() + ")");
} else {
setField(propertyDescriptorMap, field, object, null);
}
} else {
setField(propertyDescriptorMap, field, object, Long.valueOf(valueNode.asText()));
}
} catch (NumberFormatException ex) {
throw new ValidationErrorException("Template error: " + name + " must be of type Long (" + valueNode.asText() + ")");
}
}
} else if (field.getType().equals(Float.class)) {
if (!valueNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + name + " must be of type Number");
} else {
try {
if (valueNode.asText().isEmpty()) {
if (required != null) {
throw new ValidationErrorException("Template error: " + name + " can not be blank (" + valueNode.asText() + ")");
} else {
setField(propertyDescriptorMap, field, object, null);
}
} else {
setField(propertyDescriptorMap, field, object, Float.valueOf(valueNode.asText()));
}
} catch (NumberFormatException ex) {
throw new ValidationErrorException("Template error: " + name + " must be of type Number (" + valueNode.asText() + ")");
}
}
} else if (field.getType().equals(Double.class)) {
if (!valueNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + name + " must be of type Number");
} else {
try {
if (valueNode.asText().isEmpty()) {
if (required != null) {
throw new ValidationErrorException("Template error: " + name + " can not be blank (" + valueNode.asText() + ")");
} else {
setField(propertyDescriptorMap, field, object, null);
}
} else {
setField(propertyDescriptorMap, field, object, Double.valueOf(valueNode.asText()));
}
} catch (NumberFormatException ex) {
throw new ValidationErrorException("Template error: " + name + " must be of type Number (" + valueNode.asText() + ")");
}
}
} else if (field.getType().equals(Boolean.class)) {
if (!valueNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + name + " must be of type Boolean");
} else {
setField(propertyDescriptorMap, field, object, Boolean.valueOf(valueNode.asText()));
}
} else if (field.getType().equals(Object.class)) {
setField(propertyDescriptorMap, field, object, new Object());
} else if (JsonNode.class.isAssignableFrom(field.getType())) {
setField(propertyDescriptorMap, field, object, valueNode);
} else if (Collection.class.isAssignableFrom(field.getType())) {
Type genericFieldType = field.getGenericType();
if(genericFieldType instanceof ParameterizedType){
if (!valueNode.isArray()) {
throw new ValidationErrorException("Template error: " + name + " must be of type List");
}
Type collectionType = ((ParameterizedType) genericFieldType).getActualTypeArguments()[0];
if (getField(propertyDescriptorMap, field, object) == null) {
LOG.error("Class " + object.getClass() + " has a Collection type " + field.getName() + " that must be " +
"non-null ResourcePropertyResolver.populateResourceProperties can be called");
throw new InternalFailureException("Class " + object.getClass() + " has a Collection type " + field.getName() + " that must be " +
"non-null ResourcePropertyResolver.populateResourceProperties can be called");
}
populateList((Collection<?>) getField(propertyDescriptorMap, field, object), valueNode, collectionType, field.getName(), enforceStrictProperties);
} else {
LOG.error("Class " + object.getClass() + " has a Collection type " + field.getName() + " which is a non-parameterized type. This " +
"is not supported for ResourcePropertyResolver.populateResourceProperties");
throw new InternalFailureException("Class " + object.getClass() + " has a Collection type " + field.getName() + " which is a non-parameterized type. This " +
"is not supported for ResourcePropertyResolver.populateResourceProperties");
}
} else {
if (getField(propertyDescriptorMap, field, object) == null) {
try {
setField(propertyDescriptorMap, field, object, field.getType().newInstance());
} catch (IllegalAccessException | InstantiationException ex) {
LOG.error("Class " + object.getClass() + " may not have a public no-arg constructor. This is needed for ResourcePropertyResolver.populateResourceProperties()");
throw new InternalFailureException(ex.getMessage());
}
}
populateResourceProperties(getField(propertyDescriptorMap, field, object), valueNode, enforceStrictProperties);
}
}
if (!unprocessedFieldNames.isEmpty() && enforceStrictProperties) {
throw new ValidationErrorException("Encountered unsupported property or properties " + unprocessedFieldNames);
}
}
private static void setField(Map<String, PropertyDescriptor> propertyDescriptorMap, Field field, Object object, Object value)
throws CloudFormationException {
if (!propertyDescriptorMap.containsKey(field.getName()) ||
propertyDescriptorMap.get(field.getName()).getWriteMethod() == null) {
LOG.error("No public setter for " + field.getName() + " in class " + object.getClass().getName());
throw new InternalFailureException("No public setter for " + field.getName() + " in class " + object.getClass().getName());
}
try {
propertyDescriptorMap.get(field.getName()).getWriteMethod().invoke(object, value);
} catch (IllegalAccessException | InvocationTargetException ex) {
LOG.error(ex, ex);
throw new InternalFailureException(ex.getMessage());
}
}
private static Object getField(Map<String, PropertyDescriptor> propertyDescriptorMap, Field field, Object object)
throws CloudFormationException{
if (!propertyDescriptorMap.containsKey(field.getName()) ||
propertyDescriptorMap.get(field.getName()).getReadMethod() == null) {
LOG.error("No public getter for " + field.getName() + " in class " + object.getClass().getName());
throw new InternalFailureException("No public getter for " + field.getName() + " in class " + object.getClass().getName());
}
try {
return propertyDescriptorMap.get(field.getName()).getReadMethod().invoke(object);
} catch (IllegalAccessException | InvocationTargetException ex) {
LOG.error(ex, ex);
throw new InternalFailureException(ex.getMessage());
}
}
private static void populateList(Collection<?> collection, JsonNode valueNode,
Type collectionType, String fieldName, boolean enforceStrictProperties) throws CloudFormationException {
for (int i=0;i<valueNode.size();i++) {
Class<?> collectionTypeClass = (Class) collectionType;
JsonNode itemNode = valueNode.get(i);
if (collectionTypeClass.equals(String.class)) {
if (!itemNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type String");
} else {
addToCollection(collection, collectionTypeClass, itemNode.asText());
}
} else if (collectionTypeClass.equals(Integer.class)) {
if (!itemNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type Integer");
} else {
try {
addToCollection(collection, collectionTypeClass, Integer.valueOf(itemNode.asText()));
} catch (NumberFormatException ex) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type Integer (" + itemNode.asText() + ")");
}
}
} else if (collectionTypeClass.equals(Long.class)) {
if (!itemNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type Long");
} else {
try {
addToCollection(collection, collectionTypeClass, Long.valueOf(itemNode.asText()));
} catch (NumberFormatException ex) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type Long (" + itemNode.asText() + ")");
}
}
} else if (collectionTypeClass.equals(Float.class)) {
if (!itemNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type Number");
} else {
try {
addToCollection(collection, collectionTypeClass, Float.valueOf(itemNode.asText()));
} catch (NumberFormatException ex) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type Number (" + itemNode.asText() + ")");
}
}
} else if (collectionTypeClass.equals(Double.class)) {
if (!itemNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type Number");
} else {
try {
addToCollection(collection, collectionTypeClass, Double.valueOf(itemNode.asText()));
} catch (NumberFormatException ex) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type Number (" + itemNode.asText() + ")");
}
}
} else if (collectionTypeClass.equals(Boolean.class)) {
if (!itemNode.isValueNode()) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type Boolean");
} else {
addToCollection(collection, collectionTypeClass, Boolean.valueOf(itemNode.asText()));
}
} else if (Collection.class.isAssignableFrom(collectionTypeClass)) {
if(collectionType instanceof ParameterizedType){
if (!itemNode.isArray()) {
throw new ValidationErrorException("Template error: " + fieldName + " must have members of type List");
}
Type innerCollectionType = ((ParameterizedType) collection).getActualTypeArguments()[0];
Object newObject = null;
try {
newObject = collectionTypeClass.newInstance();
} catch (IllegalAccessException | InstantiationException ex) {
LOG.error("Class " + collectionTypeClass.getCanonicalName() + " may not have a public no-arg constructor. This is needed for ResourcePropertyResolver.populateResourceProperties()");
throw new InternalFailureException(ex.getMessage());
}
populateList((Collection<?>) newObject, itemNode, innerCollectionType, fieldName, enforceStrictProperties);
addToCollection(collection, collectionTypeClass, newObject);
} else {
LOG.error("Class " + collectionTypeClass.getCanonicalName() + " has a Collection type which is a non-parameterized type. This " +
"is not supported for ResourcePropertyResolver.populateResourceProperties");
throw new InternalFailureException("Class " + collectionTypeClass.getCanonicalName() + " has a Collection type which is a non-parameterized type. This " +
"is not supported for ResourcePropertyResolver.populateResourceProperties");
}
} else {
Object newObject = null;
try {
newObject = collectionTypeClass.newInstance();
} catch (IllegalAccessException | InstantiationException ex) {
LOG.error("Class " + collectionTypeClass + " may not have a public no-arg constructor. This is needed for ResourceData.populateFields()");
throw new InternalFailureException(ex.getMessage());
}
populateResourceProperties(newObject, itemNode, enforceStrictProperties);
addToCollection(collection, collectionTypeClass, newObject);
}
}
}
private static void addToCollection(Collection<?> collection, Class<?> collectionTypeClass, Object newObject)
throws CloudFormationException {
try {
// TODO: object check
Method method = collection.getClass().getMethod("add", Object.class);
method.invoke(collection, newObject);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
LOG.error("It appears class " + collection.getClass().getCanonicalName() + " which implements collection does not have a public 'add' method.");
LOG.error(ex, ex);
throw new InternalFailureException(ex.getMessage());
}
}
}