/* Copyright 2005-2006 Tim Fennell * * 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 net.sourceforge.stripes.controller; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.FileBean; import net.sourceforge.stripes.action.Wizard; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.exception.BindingDeniedException; import net.sourceforge.stripes.exception.StripesRuntimeException; import net.sourceforge.stripes.util.CollectionUtil; import net.sourceforge.stripes.util.CryptoUtil; import net.sourceforge.stripes.util.HtmlUtil; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.bean.BeanUtil; import net.sourceforge.stripes.util.bean.ExpressionException; import net.sourceforge.stripes.util.bean.NoSuchPropertyException; import net.sourceforge.stripes.util.bean.PropertyExpression; import net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation; import net.sourceforge.stripes.validation.ScopedLocalizableError; import net.sourceforge.stripes.validation.TypeConverter; import net.sourceforge.stripes.validation.TypeConverterFactory; import net.sourceforge.stripes.validation.ValidationError; import net.sourceforge.stripes.validation.ValidationErrors; import net.sourceforge.stripes.validation.ValidationMetadata; import net.sourceforge.stripes.validation.expression.ExpressionValidator; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; /** * <p> * Implementation of the ActionBeanPropertyBinder interface that uses Stripes' built in property * expression support to perform JavaBean property binding. Several additions/enhancements are * available above and beyond the standard JavaBean syntax. These include: * </p> * * <ul> * <li>The ability to instantiate and set null intermediate properties in a property chain</li> * <li>The ability to use Lists and Maps directly for indexed properties</li> * <li>The ability to infer type information from the generics information in classes</li> * </ul> * * @author Tim Fennell * @since Stripes 1.4 */ public class DefaultActionBeanPropertyBinder implements ActionBeanPropertyBinder { private static final Log log = Log.getInstance(DefaultActionBeanPropertyBinder.class); /** Configuration instance passed in at initialization time. */ private Configuration configuration; /** * Looks up and caches in a useful form the metadata necessary to perform validations as * properties are bound to the bean. */ public void init(Configuration configuration) throws Exception { this.configuration = configuration; } /** Returns the Configuration object that was passed to the init() method. */ protected Configuration getConfiguration() { return configuration; } /** * <p> * Loops through the parameters contained in the request and attempts to bind each one to the * supplied ActionBean. Invokes validation for each of the properties on the bean before binding * is attempted. Only fields which do not produce validation errors will be bound to the * ActionBean. * </p> * * <p> * Individual property binding is delegated to the other interface method, bind(ActionBean, * String, Object), in order to allow for easy extension of this class. * </p> * * @param bean the ActionBean whose properties are to be validated and bound * @param context the ActionBeanContext of the current request * @param validate true indicates that validation should be run, false indicates that only type * conversion should occur */ public ValidationErrors bind(ActionBean bean, ActionBeanContext context, boolean validate) { ValidationErrors fieldErrors = context.getValidationErrors(); Map<String, ValidationMetadata> validationInfos = this.configuration .getValidationMetadataProvider().getValidationMetadata(bean.getClass()); // Take the ParameterMap and turn the keys into ParameterNames Map<ParameterName, String[]> parameters = getParameters(bean); // Run the required validation first to catch fields that weren't even submitted if (validate) { validateRequiredFields(parameters, bean, fieldErrors); } // Converted values for all fields are accumulated in this map to make post-conversion // validation go a little easier Map<ParameterName, List<Object>> allConvertedFields = new TreeMap<ParameterName, List<Object>>(); // First we bind all the regular parameters for (Map.Entry<ParameterName, String[]> entry : parameters.entrySet()) { List<Object> convertedValues = null; ParameterName name = entry.getKey(); try { String pname = name.getName(); // exact name of the param in the request if (!StripesConstants.SPECIAL_URL_KEYS.contains(pname) && !fieldErrors.containsKey(pname)) { log.trace("Running binding for property with name: ", name); // Determine the target type ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName()); PropertyExpressionEvaluation eval; try { eval = new PropertyExpressionEvaluation(PropertyExpression .getExpression(pname), bean); } catch (Exception e) { if (pname.equals(context.getEventName())) continue; else throw e; } Class<?> type = eval.getType(); Class<?> scalarType = eval.getScalarType(); // Check to see if binding into this expression is permitted if (!isBindingAllowed(eval)) continue; if (type == null && (validationInfo == null || validationInfo.converter() == null)) { if (!pname.equals(context.getEventName())) { log.trace("Could not find type for property '", name.getName(), "' of '", bean.getClass().getSimpleName(), "' probably because it's not ", "a property of the bean. Skipping binding."); } continue; } String[] values = entry.getValue(); // Do Validation and type conversion List<ValidationError> errors = new ArrayList<ValidationError>(); // If the property should be ignored, skip to the next property if (validationInfo != null && validationInfo.ignore()) { continue; } if (validate && validationInfo != null) { doPreConversionValidations(name, values, validationInfo, errors); } // Only do type conversion if there aren't errors already if (errors.isEmpty()) { convertedValues = convert(bean, name, values, type, scalarType, validationInfo, errors); allConvertedFields.put(name, convertedValues); } // If we have errors, save them, otherwise bind the parameter to the form if (errors.size() > 0) { fieldErrors.addAll(name.getName(), errors); } else if (convertedValues.size() > 0) { bindNonNullValue(bean, eval, convertedValues, type, scalarType); } else { bindNullValue(bean, name.getName(), type); } } } catch (Exception e) { handlePropertyBindingError(bean, name, convertedValues, e, fieldErrors); } } // Null out any values that were in the form, but values were not supplied bindMissingValuesAsNull(bean, context); // Then we figure out if any files were uploaded and bind those too StripesRequestWrapper request = StripesRequestWrapper.findStripesWrapper(context .getRequest()); if (request.isMultipart()) { Enumeration<String> fileParameterNames = request.getFileParameterNames(); while (fileParameterNames.hasMoreElements()) { String fileParameterName = fileParameterNames.nextElement(); FileBean fileBean = request.getFileParameterValue(fileParameterName); log.trace("Attempting to bind file parameter with name [", fileParameterName, "] and value: ", fileBean); if (fileBean != null) { try { bind(bean, fileParameterName, fileBean); } catch (Exception e) { log.debug(e, "Could not bind file property with name [", fileParameterName, "] and value: ", fileBean); } } } } // Run post-conversion validation after absolutely everything has been bound // and validated so that the expression validation can have access to the full // state of the bean if (validate) { doPostConversionValidations(bean, allConvertedFields, fieldErrors); } return fieldErrors; } /** * <p> * Checks to see if binding is permitted for the provided expression evaluation. Note that the * expression is available through the {@code getExpression()} and the ActionBean is available * through the {@code getBean()} method on the evaluation. * </p> * * <p> * By default checks to ensure that the expression is not attempting to bind into the * ActionBeanContext for security reasons. * </p> * * @param eval the expression evaluation to check for binding permission * @return true if binding can/should proceed, false to veto binding */ protected boolean isBindingAllowed(PropertyExpressionEvaluation eval) { boolean allowed = BindingPolicyManager.getInstance(eval.getBean().getClass()) .isBindingAllowed(eval); if (!allowed) { String param = eval.getExpression().getSource(); if (configuration.isDebugMode()) { throw new BindingDeniedException(param); } log.warn("Binding denied for parameter [", param, "]. Use @Validate to allow binding in conjunction with @StrictBinding."); } return allowed; } /** * Invoked whenever an exception is thrown when attempting to bind a property to an ActionBean. * By default logs some information about the occurrence, but could be overridden to do more * intelligent things based on the application. * * @param bean the ActionBean that was the subject of binding * @param name the ParameterName object for the parameter being bound * @param values the list of values being bound, potentially null if the error occurred when * binding a null value * @param e the exception raised during binding * @param errors the validation errors object associated to the ActionBean */ protected void handlePropertyBindingError(ActionBean bean, ParameterName name, List<Object> values, Exception e, ValidationErrors errors) { if (e instanceof NoSuchPropertyException) { NoSuchPropertyException nspe = (NoSuchPropertyException) e; // No stack trace if it's a no such property exception log.debug("Could not bind property with name [", name, "] to bean of type: ", bean .getClass().getSimpleName(), " : ", nspe.getMessage()); } else { log.debug(e, "Could not bind property with name [", name, "] to bean of type: ", bean .getClass().getSimpleName()); } } /** * Uses a hidden field to determine what (if any) fields were present in the form but did not get * submitted to the server. For each such field the value is "softly" set to null on the * ActionBean. This is not uncommon for checkboxes, and also for multi-selects. * * @param bean the ActionBean being bound to * @param context the current ActionBeanContext */ @SuppressWarnings("unchecked") protected void bindMissingValuesAsNull(ActionBean bean, ActionBeanContext context) { Set<String> parametersSubmitted = context.getRequest().getParameterMap().keySet(); for (String name : getFieldsPresentInfo(bean)) { if (!parametersSubmitted.contains(name)) { try { BeanUtil.setPropertyToNull(name, bean); } catch (Exception e) { handlePropertyBindingError(bean, new ParameterName(name), null, e, context .getValidationErrors()); } } } } /** * In a lot of cases (and specifically during wizards) the Stripes form field writes out a * hidden field containing a set of field names. This is encrypted to stop the user from * monkeying with it. This method retrieves the list of field names, decrypts it and splits it * out into a Collection of field names. * * @param bean the current ActionBean * @return a non-null (though possibly empty) list of field names */ protected Collection<String> getFieldsPresentInfo(ActionBean bean) { ActionBeanContext ctx = bean.getContext(); String fieldsPresent = ctx.getRequest().getParameter(StripesConstants.URL_KEY_FIELDS_PRESENT); Wizard wizard = bean.getClass().getAnnotation(Wizard.class); boolean isWizard = wizard != null; if (fieldsPresent == null || "".equals(fieldsPresent)) { if (isWizard && !CollectionUtil.contains(wizard.startEvents(), ctx.getEventName())) { throw new StripesRuntimeException( "Submission of a wizard form in Stripes absolutely requires that " + "the hidden field Stripes writes containing the names of the fields " + "present on the form is present and encrypted (as Stripes write it). " + "This is necessary to prevent a user from spoofing the system and " + "getting around any security/data checks."); } else { return Collections.emptySet(); } } else { fieldsPresent = CryptoUtil.decrypt(fieldsPresent); return HtmlUtil.splitValues(fieldsPresent); } } /** * Internal helper method to bind one or more values to a single property on an ActionBean. If * the target type is an array of Collection, then all values are bound. If the target type is a * scalar type then the first value in the List of values is bound. * * @param bean the ActionBean instance to which the property is being bound * @param propertyEvaluation the property evaluation to be used to set the property * @param valueOrValues a List containing one or more values * @param targetType the declared type of the property on the ActionBean * @throws Exception if the property cannot be bound for any reason */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected void bindNonNullValue(ActionBean bean, PropertyExpressionEvaluation propertyEvaluation, List<Object> valueOrValues, Class targetType, Class scalarType) throws Exception { Class valueType = valueOrValues.iterator().next().getClass(); // If the target type is an array, set it as one, otherwise set as scalar if (targetType.isArray() && !valueType.isArray()) { Object typedArray = Array.newInstance(scalarType, valueOrValues.size()); for (int i = 0; i < valueOrValues.size(); ++i) { Array.set(typedArray, i, valueOrValues.get(i)); } propertyEvaluation.setValue(typedArray); } else if (Collection.class.isAssignableFrom(targetType) && !Collection.class.isAssignableFrom(valueType)) { Collection collection; if (EnumSet.class.isAssignableFrom(targetType) && Enum.class.isAssignableFrom(scalarType)) { collection = EnumSet.noneOf(scalarType.asSubclass(Enum.class)); } else { collection = getConfiguration().getObjectFactory().newInstance( (Class<? extends Collection>) targetType); } collection.addAll(valueOrValues); propertyEvaluation.setValue(collection); } else { propertyEvaluation.setValue(valueOrValues.get(0)); } } /** * Internal helper method that determines what to do when no value was supplied for a given form * field (but the field was present on the page). In all cases if the property is already null, * or intervening objects in a nested property are null, nothing is done. If the property is * non-null, it will be set to null. Unless the property is a collection, in which case it will * be clear()'d. * * @param bean the ActionBean to which properties are being bound * @param property the name of the property being bound * @param type the declared type of the property on the ActionBean * @throws ExpressionException if the value cannot be manipulated for any reason */ protected void bindNullValue(ActionBean bean, String property, Class<?> type) throws ExpressionException { BeanUtil.setPropertyToNull(property, bean); } /** * Converts the map of parameters in the request into a Map of ParameterName to String[]. * Returns a SortedMap so that when iterated over parameter names are accessed in order of * length of parameter name. */ @SuppressWarnings("unchecked") protected SortedMap<ParameterName, String[]> getParameters(ActionBean bean) { Map<String, String[]> requestParameters = bean.getContext().getRequest().getParameterMap(); Map<String, ValidationMetadata> validations = StripesFilter.getConfiguration() .getValidationMetadataProvider().getValidationMetadata(bean.getClass()); SortedMap<ParameterName, String[]> parameters = new TreeMap<ParameterName, String[]>(); for (Map.Entry<String, String[]> entry : requestParameters.entrySet()) { ParameterName paramName = new ParameterName(entry.getKey().trim()); ValidationMetadata validation = validations.get(paramName.getStrippedName()); parameters.put(paramName, trim(entry.getValue(), validation)); } return parameters; } /** * Attempt to set the named property on the target bean. If the binding fails for any reason * (property does not exist, type conversion not possible etc.) an exception will be thrown. * * @param bean the ActionBean on to which the property is to be bound * @param propertyName the name of the property to be bound (simple or complex) * @param propertyValue the value of the target property * @throws Exception thrown if the property cannot be bound for any reason */ public void bind(ActionBean bean, String propertyName, Object propertyValue) throws Exception { BeanUtil.setPropertyValue(propertyName, bean, propertyValue); } /** * Validates that all required fields have been submitted. This is done by looping through the * set of validation annotations and checking that each field marked as required was submitted * in the request and submitted with a non-empty value. */ protected void validateRequiredFields(Map<ParameterName, String[]> parameters, ActionBean bean, ValidationErrors errors) { log.debug("Running required field validation on bean class ", bean.getClass().getName()); // Assemble a set of names that we know have indexed parameters, so we won't check // for required-ness the regular way Set<String> indexedParams = new HashSet<String>(); for (ParameterName name : parameters.keySet()) { if (name.isIndexed()) { indexedParams.add(name.getStrippedName()); } } Map<String, ValidationMetadata> validationInfos = this.configuration .getValidationMetadataProvider().getValidationMetadata(bean.getClass()); ActionBeanContext context = bean.getContext(); HttpServletRequest request = context.getRequest(); StripesRequestWrapper stripesReq = StripesRequestWrapper.findStripesWrapper(request); if (validationInfos != null) { boolean wizard = bean.getClass().getAnnotation(Wizard.class) != null; Collection<String> fieldsOnPage = getFieldsPresentInfo(bean); for (Map.Entry<String, ValidationMetadata> entry : validationInfos.entrySet()) { String propertyName = entry.getKey(); ValidationMetadata validationInfo = entry.getValue(); // If the field is required, and we don't have index params that collapse // to that property name, check that it was supplied if (validationInfo.requiredOn(context.getEventName()) && !indexedParams.contains(propertyName)) { // Make the added check that if the form is a wizard, the required field is // in the set of fields that were on the page if (!wizard || fieldsOnPage.contains(propertyName)) { String[] values = trim(request.getParameterValues(propertyName), validationInfo); // Decrypt encrypted fields before checking for null if (validationInfo.encrypted()) { for (int i = 0, n = values.length; i < n; i++) { if (values[i] != null) values[i] = CryptoUtil.decrypt(values[i]); } } log.debug("Checking required field: ", propertyName, ", with values: ", values); checkSingleRequiredField(propertyName, propertyName, values, stripesReq, errors); } } } } // Now the easy work is done, figure out which rows of indexed props had values submitted // and what to flag up as failing required field validation if (indexedParams.size() > 0) { Map<String, Row> rows = new HashMap<String, Row>(); for (Map.Entry<ParameterName, String[]> entry : parameters.entrySet()) { ParameterName name = entry.getKey(); String[] values = entry.getValue(); if (name.isIndexed()) { String rowKey = name.getName().substring(0, name.getName().indexOf(']') + 1); if (!rows.containsKey(rowKey)) { rows.put(rowKey, new Row()); } rows.get(rowKey).put(name, values); } } for (Row row : rows.values()) { if (row.hasNonEmptyValues()) { for (Map.Entry<ParameterName, String[]> entry : row.entrySet()) { ParameterName name = entry.getKey(); String[] values = entry.getValue(); ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName()); if (validationInfo != null && validationInfo.requiredOn(context.getEventName())) { checkSingleRequiredField(name.getName(), name.getStrippedName(), values, stripesReq, errors); } } } else { // If the row is full of empty data, get rid of it all to // prevent problems in downstream validation for (ParameterName name : row.keySet()) { parameters.remove(name); } } } } } /** * <p> * Checks to see if a single field's set of values are 'present', where that is defined as * having one or more values, and where each value is a non-empty String after it has had white * space trimmed from each end. * <p> * * <p> * For any fields that fail validation, creates a ScopedLocaliableError that uses the stripped * name of the field to find localized info (e.g. foo.bar instead of foo[1].bar). The error is * bound to the actual field on the form though, e.g. foo[1].bar. * </p> * * @param name the name of the parameter verbatim from the request * @param strippedName the name of the parameter with any indexing removed from it * @param values the String[] of values that was submitted in the request * @param errors a ValidationErrors object into which errors can be placed */ protected void checkSingleRequiredField(String name, String strippedName, String[] values, StripesRequestWrapper req, ValidationErrors errors) { // Firstly if the post is a multipart request, check to see if a file was // sent under that parameter name FileBean file; if (req.isMultipart() && (file = req.getFileParameterValue(name)) != null) { if (file.getSize() <= 0) { errors.add(name, new ScopedLocalizableError("validation.required", "valueNotPresent")); } } // And if not, see if any regular parameters were sent else if (values == null || values.length == 0) { ValidationError error = new ScopedLocalizableError("validation.required", "valueNotPresent"); error.setFieldValue(null); errors.add(name, error); } else { for (String value : values) { if (value == null || value.length() == 0) { ValidationError error = new ScopedLocalizableError("validation.required", "valueNotPresent"); error.setFieldValue(value); errors.add(name, error); } } } } /** * Performs several basic validations on the String value supplied in the HttpServletRequest, * based on information provided in annotations on the ActionBean. * * @param propertyName the name of the property being validated (used for constructing errors) * @param values the String[] of values from the request being validated * @param validationInfo the ValidationMetadata for the property being validated * @param errors a collection of errors to be populated with any validation errors discovered */ protected void doPreConversionValidations(ParameterName propertyName, String[] values, ValidationMetadata validationInfo, List<ValidationError> errors) { for (String value : values) { // Only run validations when there are non-empty values if (value != null && value.length() > 0) { if (validationInfo.minlength() != null && value.length() < validationInfo.minlength()) { ValidationError error = new ScopedLocalizableError("validation.minlength", "valueTooShort", validationInfo.minlength()); error.setFieldValue(value); errors.add(error); } if (validationInfo.maxlength() != null && value.length() > validationInfo.maxlength()) { ValidationError error = new ScopedLocalizableError("validation.maxlength", "valueTooLong", validationInfo.maxlength()); error.setFieldValue(value); errors.add(error); } if (validationInfo.mask() != null && !validationInfo.mask().matcher(value).matches()) { ValidationError error = new ScopedLocalizableError("validation.mask", "valueDoesNotMatch"); error.setFieldValue(value); errors.add(error); } } } } /** * Performs basic post-conversion validations on the properties of the ActionBean after they * have been converted to their rich type by the type conversion system. Validates single * properties in isolation from other properties. * * @param bean the ActionBean that is undergoing validation and binding * @param convertedValues a map of ParameterName to all converted values for each field * @param errors the validation errors object to put errors in to */ protected void doPostConversionValidations(ActionBean bean, Map<ParameterName, List<Object>> convertedValues, ValidationErrors errors) { Map<String, ValidationMetadata> validationInfos = this.configuration .getValidationMetadataProvider().getValidationMetadata(bean.getClass()); for (Map.Entry<ParameterName, List<Object>> entry : convertedValues.entrySet()) { // Sort out what we need to validate this field ParameterName name = entry.getKey(); List<Object> values = entry.getValue(); ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName()); if (values.size() == 0 || validationInfo == null) { continue; } for (Object value : values) { // If the value is a number then we should check to see if there are range // boundaries // established, and check them. if (value instanceof Number) { Number number = (Number) value; if (validationInfo.minvalue() != null && number.doubleValue() < validationInfo.minvalue()) { ValidationError error = new ScopedLocalizableError("validation.minvalue", "valueBelowMinimum", validationInfo.minvalue()); error.setFieldValue(String.valueOf(value)); errors.add(name.getName(), error); } if (validationInfo.maxvalue() != null && number.doubleValue() > validationInfo.maxvalue()) { ValidationError error = new ScopedLocalizableError("validation.maxvalue", "valueAboveMaximum", validationInfo.maxvalue()); error.setFieldValue(String.valueOf(value)); errors.add(name.getName(), error); } } } // And then do any expression validation doExpressionValidation(bean, name, values, validationInfo, errors); } } /** * Performs validation of attribute values using a JSP EL expression if one is defined in the * {@literal @}Validate annotation. The expression is evaluated once for each value converted. * See {@link net.sourceforge.stripes.validation.expression.ExpressionValidator} for details * on how this is implemented. * * @param bean the ActionBean who's property is being validated * @param name the name of the property being validated * @param values the non-null post-conversion values for the property * @param validationInfo the validation metadata for the property * @param errors the validation errors object to add errors to */ protected void doExpressionValidation(ActionBean bean, ParameterName name, List<Object> values, ValidationMetadata validationInfo, ValidationErrors errors) { if (validationInfo.expression() != null) ExpressionValidator.evaluate(bean, name, values, validationInfo, errors); } /** * <p> * Converts the String[] of values for a given parameter in the HttpServletRequest into the * desired type of Object. If a converter is declared using an annotation for the property (or * getter/setter) then that converter will be used - if it does not convert to the right type an * exception will be logged and values will not be converted. If no Converter was specified then * a default converter will be looked up based on the target type of the property. If there is * no default converter, then a Constructor will be looked for on the target type which takes a * single String parameter. If such a Constructor exists it will be invoked. * </p> * * <p> * Only parameter values that are non-null and do not equal the empty String will be converted * and returned. So an input array with one entry equaling the empty string, [""], will result * in an <b>empty</b> List being returned. Similarly, if a length three array is passed in with * one item equaling the empty String, a List of length two will be returned. * </p> * * @param bean the ActionBean on which the property to convert exists * @param propertyName the name of the property being converted * @param values a String array of values to attempt conversion of * @param declaredType the declared type of the ActionBean property * @param scalarType if the declaredType is a collection, map or array then this will * be the type contained within the collection/map value/array, otherwise * the same as declaredType * @param validationInfo the validation metadata for the property if defined * @param errors a List into which ValidationError objects will be populated for any errors * discovered during conversion. * @return List<Object> a List of objects containing only objects of the desired type. It is * not guaranteed to be the same length as the values array passed in. */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected List<Object> convert(ActionBean bean, ParameterName propertyName, String[] values, Class<?> declaredType, Class<?> scalarType, ValidationMetadata validationInfo, List<ValidationError> errors) throws Exception { List<Object> returns = new ArrayList<Object>(); Class returnType; // Dig up the type converter. This gets a bit tricky because we need to handle // the following cases: // 1. We need to simply find a converter for the declared type of a simple property // 2. We need to find a converter for the element type in a list/array/map // 3. We have a domain model object that implements List/Map and has a converter itself! TypeConverterFactory factory = this.configuration.getTypeConverterFactory(); TypeConverter<?> converter; Locale locale = bean.getContext().getRequest().getLocale(); converter = factory.getTypeConverter(declaredType, locale); if (validationInfo != null && validationInfo.converter() != null) { // If a specific converter was requested and it's the same type as one we'd use // for the declared type, set the return type appropriately if (converter != null && validationInfo.converter().isAssignableFrom(converter.getClass())) { returnType = declaredType; } // Otherwise assume that it's a converter for the scalar type inside a collection else { returnType = scalarType; } converter = factory.getInstance(validationInfo.converter(), locale); } // Else, if we got a converter for the declared type (e.g. Foo implements List<Bar>) // then convert for the declared type else if (converter != null) { returnType = declaredType; } // Else look for a converter for the scalar type (Bar in List<Bar>) else { converter = factory.getTypeConverter(scalarType, locale); returnType = scalarType; } log.debug("Converting ", values.length, " value(s) using ", (converter != null ? "converter " + converter.getClass().getName() : "Constructor(String) if available")); for (String value : values) { if (validationInfo != null && validationInfo.encrypted()) { value = CryptoUtil.decrypt(value); } if (value != null && value.length() > 0) { try { Object retval = null; if (converter != null) { retval = converter.convert(value, returnType, errors); } else { try { retval = getConfiguration().getObjectFactory() .constructor(returnType, String.class).newInstance(value); } catch (StripesRuntimeException e) { log.debug("Could not find a way to convert the parameter ", propertyName.getName(), " to a ", returnType.getSimpleName(), ". No TypeConverter could be ", "found and the class does not ", "have a constructor that takes a ", "single String parameter."); } } // If we managed to get a non-null converted value, add it to the return set if (retval != null) { returns.add(retval); } // Set the field name and value on the error for (ValidationError error : errors) { error.setFieldName(propertyName.getStrippedName()); error.setFieldValue(value); } } catch (Exception e) { log.warn(e, "Looks like type converter ", converter, " threw an exception."); } } } return returns; } /** * Inspects the given {@link ValidationMetadata} object to determine if the given {@code values} * should be trimmed. If so, then the trimmed values are returned. Otherwise, the values are * returned unchanged. If {@code meta} is null, then the default action is taken, and the values * are trimmed. Either {@code values} or {@code meta} (or both) may be null. */ protected String[] trim(String[] values, ValidationMetadata meta) { if (values != null && values.length > 0 && (meta == null || meta.trim())) { String[] copy = new String[values.length]; for (int i = 0; i < values.length; i++) { if (values[i] != null) copy[i] = values[i].trim(); } return copy; } else { return values; } } /** * An inner class that represents a "row" of form properties that all have the same index * so that we can validate all those properties together. */ protected static class Row extends HashMap<ParameterName, String[]> { private static final long serialVersionUID = 1L; private boolean hasNonEmptyValues = false; /** * Adds the value to the map, along the way checking to see if there are any non-null values for * the row so far. */ @Override public String[] put(ParameterName key, String[] values) { if (!hasNonEmptyValues) { hasNonEmptyValues = (values != null) && (values.length > 0) && (values[0] != null) && (values[0].trim().length() > 0); } return super.put(key, values); } /** Returns true if the row had any non-empty values in it, otherwise false. */ public boolean hasNonEmptyValues() { return this.hasNonEmptyValues; } } }