/* * Copyright (c) 2012 EMC Corporation * All Rights Reserved */ package com.emc.storageos.api.service.impl.resource; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.xml.bind.annotation.XmlElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.emc.storageos.db.client.util.EndpointUtility; import com.emc.storageos.model.valid.Endpoint; import com.emc.storageos.model.valid.EnumType; import com.emc.storageos.model.valid.Length; import com.emc.storageos.model.valid.Range; import com.emc.storageos.svcs.errorhandling.resources.APIException; /** * Validates input against JAXB annotations. JAXB itself doesn't * provide a OOB way of validating against annotated classes. Since * we don't want to rug around a schema for validation, I am writing * this code... */ public class InputValidator { private static final Logger _log = LoggerFactory.getLogger(InputValidator.class); private static final String JAXB_DEFAULT = "##default"; // what to validate static class Validator { static class FieldInfo { public String name; public boolean required; public boolean nillable; public Class<? extends Enum> enumType; public Long min; public Long max; public Enum type; // Field is set only if this field is a public field public Field field; // Alternatively this field could be a property with read/write methods public PropertyDescriptor descriptor; public Object getValue(Object bean) throws IllegalAccessException, InvocationTargetException { if (field != null) { return field.get(bean); } else if (descriptor != null) { return descriptor.getReadMethod().invoke(bean); } return null; } public void setValue(Object bean, Object value) throws IllegalAccessException, InvocationTargetException { if (field != null) { field.set(bean, value); } if (descriptor != null) { descriptor.getWriteMethod().invoke(bean, value); } } public Type getGenericType() { if (field != null) { return field.getGenericType(); } else if (descriptor != null) { return descriptor.getReadMethod().getGenericReturnType(); } return null; } } private Class clazz; private Map<String, FieldInfo> fieldInfo = new HashMap<String, FieldInfo>(); /** * Constructor * * @param clazz JAXB annotated input class */ public Validator(Class clazz) { this.clazz = clazz; processAnnotations(); } /** * Process JAXB annotations and stash them for validation use */ private void processAnnotations() { Field[] fields = clazz.getFields(); for (int i = 0; i < fields.length; i++) { Field field = fields[i]; Annotation[] annotations = field.getAnnotations(); for (int j = 0; j < annotations.length; j++) { Annotation a = annotations[j]; if (a instanceof XmlElement) { XmlElement e = (XmlElement) a; FieldInfo fi = getField(field); fi.required = e.required(); fi.nillable = e.nillable(); String name = e.name(); if (!JAXB_DEFAULT.equals(name)) { fi.name = name; } } else if (a instanceof EnumType) { EnumType e = (EnumType) a; FieldInfo fi = getField(field); fi.enumType = e.value(); } else if (a instanceof Length) { Length l = (Length) a; FieldInfo fi = getField(field); fi.min = (long) l.min(); fi.max = (long) l.max(); } else if (a instanceof Range) { Range l = (Range) a; FieldInfo fi = getField(field); fi.min = l.min(); fi.max = l.max(); } else if (a instanceof Endpoint) { Endpoint l = (Endpoint) a; FieldInfo fi = getField(field); fi.type = l.type(); } } } try { PropertyDescriptor[] descriptors = Introspector.getBeanInfo(clazz).getPropertyDescriptors(); for (PropertyDescriptor descriptor : descriptors) { Annotation[] annotations = descriptor.getReadMethod().getAnnotations(); for (Annotation a : annotations) { if (a instanceof XmlElement) { XmlElement e = (XmlElement) a; FieldInfo fi = getField(descriptor); fi.required = e.required(); fi.nillable = e.nillable(); String name = e.name(); if (!JAXB_DEFAULT.equals(name)) { fi.name = name; } } else if (a instanceof EnumType) { EnumType e = (EnumType) a; FieldInfo fi = getField(descriptor); fi.enumType = e.value(); } else if (a instanceof Length) { Length l = (Length) a; FieldInfo fi = getField(descriptor); fi.min = (long) l.min(); fi.max = (long) l.max(); } else if (a instanceof Range) { Range l = (Range) a; FieldInfo fi = getField(descriptor); fi.min = l.min(); fi.max = l.max(); } else if (a instanceof Endpoint) { Endpoint l = (Endpoint) a; FieldInfo fi = getField(descriptor); fi.type = l.type(); } } } } catch (IntrospectionException e) { _log.error("Unexpected exception:", e); throw APIException.internalServerErrors.genericApisvcError(e.getMessage(), e); } } /** * Retrieve validation info for given field. * * @param field * @return */ private FieldInfo getField(Field field) { FieldInfo info = fieldInfo.get(field.getName()); if (info == null) { info = new FieldInfo(); info.name = field.getName(); fieldInfo.put(info.name, info); } info.field = field; return info; } /** * Retrieve validation info for given field. * * @param descriptor * @return */ private FieldInfo getField(PropertyDescriptor descriptor) { FieldInfo info = fieldInfo.get(descriptor.getName()); if (info == null) { info = new FieldInfo(); info.name = descriptor.getName(); fieldInfo.put(info.name, info); } info.descriptor = descriptor; return info; } /** * Validates input object and throws BadRequestException if * any error is encountered * * @param val */ @SuppressWarnings("unchecked") public void validate(Object val) { if (!clazz.isInstance(val)) { throw APIException.badRequests.notAnInstanceOf(clazz.getSimpleName()); } try { Iterator<Map.Entry<String, FieldInfo>> it = fieldInfo.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, FieldInfo> field = it.next(); FieldInfo fi = field.getValue(); Object fieldVal = fi.getValue(val); // required check if (fi.required) { if (fieldVal == null) { throw APIException.badRequests.requiredParameterMissingOrEmpty(fi.name); } } // nillable check if (!fi.nillable) { if (fieldVal instanceof String && ((String) fieldVal).trim().isEmpty()) { throw APIException.badRequests.requiredParameterMissingOrEmpty(fi.name); } } // enum type check if (fi.enumType != null && fieldVal instanceof String) { try { Enum.valueOf(fi.enumType, (String) fieldVal); } catch (IllegalArgumentException e) { throw APIException.badRequests.invalidField(fi.name, (String) fieldVal, e); } } // endpoint type check if (fi.type != null && fi.type instanceof Endpoint.EndpointType && fieldVal instanceof String) { if (!EndpointUtility.isValidEndpoint((String) fieldVal, (Endpoint.EndpointType) fi.type)) { throw APIException.badRequests.invalidField(fi.name, (String) fieldVal); } } // trim strings if (fieldVal instanceof String) { fi.setValue(val, ((String) fieldVal).trim()); } // length check if (fieldVal instanceof String && (fi.min != null || fi.max != null)) { int length = ((String) fieldVal).length(); ArgValidator.checkFieldRange(length, fi.min, fi.max, "length"); } // range check if (fieldVal instanceof Number && (fi.min != null || fi.max != null)) { Number value = (Number) fieldVal; ArgValidator.checkFieldRange(value.longValue(), fi.min, fi.max, fi.name); } // collection check if (fieldVal instanceof Collection) { ParameterizedType paramType = (ParameterizedType) fi.getGenericType(); Type elementType = paramType.getActualTypeArguments()[0]; if (elementType.equals(String.class)) { boolean needNillCheck = false; boolean needLengthCheck = false; if (!fi.nillable) { needNillCheck = true; } if (fi.min != null || fi.max != null) { needLengthCheck = true; } // trim all the elements Iterator<String> iterator = ((Collection<String>) fieldVal).iterator(); Collection<String> trimmedList = new ArrayList<String>(); while (iterator.hasNext()) { String element = iterator.next().trim(); // nillable check if (needNillCheck && element.isEmpty()) { throw APIException.badRequests.requiredParameterMissingOrEmpty(fi.name); } // length check if (needLengthCheck) { lengthCheck(element, fi); } iterator.remove(); trimmedList.add(element); } for (String element : trimmedList) { // have to iterator over trimmedList since some Collection // implementations have overridden addAll(), like StringSet ((Collection<String>) fieldVal).add(element); } } else if (elementType.equals(Number.class)) { // perform range check on the elements if (fi.min != null || fi.max != null) { for (Number element : (Collection<Number>) fieldVal) { rangeCheck(element, fi); } } } } } } catch (InvocationTargetException e) { _log.error("Unexpected exception:", e); throw APIException.internalServerErrors.genericApisvcError(e.getMessage(), e); } catch (IllegalAccessException e) { _log.error("Unexpected exception:", e); throw APIException.internalServerErrors.genericApisvcError(e.getMessage(), e); } } private void lengthCheck(String fieldVal, FieldInfo fi) { int length = fieldVal.length(); ArgValidator.checkFieldRange(length, fi.min, fi.max, "length"); } private void rangeCheck(Number value, FieldInfo fi) { if (value.longValue() < fi.min || value.longValue() > fi.max) { throw APIException.badRequests.parameterNotWithinRange(fi.name, value, fi.min, fi.max, ""); } } } private static final InputValidator _validator = new InputValidator(); private ConcurrentMap<Class, Validator> _inputTypeMap = new ConcurrentHashMap<Class, Validator>(); /** * Validates a given input object * * @param obj */ public void validate(Object obj) { Class clazz = obj.getClass(); Validator validator = _inputTypeMap.get(clazz); if (validator == null) { validator = new Validator(clazz); _inputTypeMap.putIfAbsent(clazz, validator); } validator.validate(obj); } /** * Retrieve singleton input validator * * @return */ public static InputValidator getInstance() { return _validator; } }