/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.formio.binding; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import net.formio.Forms; import net.formio.binding.collection.BasicCollectionBuilders; import net.formio.binding.collection.CollectionBuilders; import net.formio.binding.collection.CollectionSpec; import net.formio.binding.collection.ItemsOrder; import net.formio.format.Location; import net.formio.format.BasicFormatters; import net.formio.format.Formatter; import net.formio.format.Formatters; import net.formio.format.StringParseException; import net.formio.upload.UploadedFile; /** * Binds given values to new/existing instance of class. * * @author Radek Beran */ public class DefaultBinder implements Binder { private final Formatters formatters; private final ArgumentNameResolver argNameResolver; private final CollectionBuilders collectionBuilders; private final PropertyMethodRegex setterRegex; /** * Default regular expression for matching name of setter of a property and property name within it. */ public static final PropertyMethodRegex DEFAULT_SETTER_REGEX = new PropertyMethodRegex("set([_a-zA-Z][_a-zA-Z0-9]*)", 1); public DefaultBinder( Formatters formatters, CollectionBuilders collectionBuilders, ArgumentNameResolver argNameResolver, PropertyMethodRegex setterRegex) { if (formatters == null) throw new IllegalArgumentException("formatters cannot be null"); if (argNameResolver == null) throw new IllegalArgumentException("argNameResolver cannot be null"); if (collectionBuilders == null) throw new IllegalArgumentException("collectionBuilders cannot be null"); if (setterRegex == null) throw new IllegalArgumentException("setterRegex cannot be null"); this.formatters = formatters; this.argNameResolver = argNameResolver; this.collectionBuilders = collectionBuilders; this.setterRegex = setterRegex; } public DefaultBinder(Formatters formatters, CollectionBuilders collBuilders, ArgumentNameResolver argNameResolver) { this(formatters, collBuilders, argNameResolver, DEFAULT_SETTER_REGEX); } public DefaultBinder(Formatters formatters, CollectionBuilders collBuilders) { this(formatters, collBuilders, new AnnotationArgumentNameResolver()); } public DefaultBinder(Formatters formatters) { this(formatters, new BasicCollectionBuilders()); } public DefaultBinder() { this(new BasicFormatters()); } /** * Returns new instance of given class created via binding values to * constructor arguments annotated by {@link ArgumentName} and binding rest * of values to setters. * * @param objClass class of new instance * @param instantiator instantiator of class T, {@code null} for default instantiator * @param values values to bind; specify only values that must be bound * @return new instance of given class filled with bound values * @throws BindingException if construction of new instance or binding failed or some * value cannot be bound to created instance (parameter name with the value is present but * there is no way to bind it to the instance) - better to have reliable strict binding! */ @Override public <T> BoundData<T> bindToNewInstance(Class<T> objClass, Instantiator instantiator, Map<String, BoundValuesInfo> values) { Map<String, List<ParseError>> propertyBindErrors = new LinkedHashMap<String, List<ParseError>>(); if (instantiator == null) throw new IllegalArgumentException("instantiator cannot be null"); if (values == null) throw new IllegalArgumentException("values cannot be null"); Set<String> notBoundYetParamNames = values.keySet(); ConstructionDescription cd = instantiator.getDescription(objClass, this.argNameResolver); if (cd == null) throw new IllegalStateException("No usable construction method of " + objClass.getName() + " was found."); // Preparing arguments of construction method Object[] args = buildConstructionArguments(cd, values, propertyBindErrors); notBoundYetParamNames.removeAll(cd.getArgNames()); T obj = instantiator.instantiate(objClass, cd, args); // Using setters for the rest of values for (String paramName : notBoundYetParamNames) { BoundValuesInfo valueInfo = values.get(paramName); if (valueInfo == null) throw new BindingException("Property '" + paramName + " could not be bound. Value to bind was not found. " + "The appropriate field was probably not declared."); boolean clientProvidedInstance = instantiator instanceof InstanceHoldingInstantiator; propertyBindErrors.putAll(updatePropertyValue(objClass, obj, paramName, valueInfo, clientProvidedInstance)); // notBoundYetParamNames cannot be reduced here in cycle (ConcurrentModificationException) } return new BoundData<T>(obj, propertyBindErrors); } protected CollectionBuilders getCollectionBuilders() { return collectionBuilders; } protected Formatters getFormatters() { return formatters; } protected PropertyMethodRegex getSetterRegex() { return setterRegex; } protected ArgumentNameResolver getArgNameResolver() { return argNameResolver; } protected boolean isPropertySetter(Method method, String propertyName) { return setterRegex.matchesPropertyMethod(method.getName(), propertyName) && method.getParameterTypes().length == 1; } protected Object[] buildConstructionArguments(ConstructionDescription cd, Map<String, BoundValuesInfo> values, Map<String, List<ParseError>> propertyBindErrors) { List<String> argNames = cd.getArgNames(); Object[] args = new Object[argNames.size()]; for (int i = 0; i < argNames.size(); i++) { String argName = argNames.get(i); BoundValuesInfo valueInfo = values.get(argName); if (valueInfo == null) throw new BindingException("Property '" + argName + "' required by the constructor of form data object could not be bound. Value to bind was not found. " + "The appropriate field was probably not declared."); ParsedValue parsedValue = convertToValue(cd.getConstructedClass(), argName, valueInfo, cd.getArgTypes()[i], cd.getGenericParamTypes()[i]); args[i] = parsedValue.getValue(); if (!parsedValue.isSuccessfullyParsed()) { addParseError(propertyBindErrors, argName, parsedValue.getParseErrors()); } } return args; } /** * Updates given property of given object to given value. * @param parentClass name of class for which the value of its property is converted * @param obj object with property * @param propertyName name of property (without set, get or is - according to JavaBeans convention) * @param propertyValueInfo value to set for the property * @param clientProvidedInstance flag that client provided own instance that should be filled * @return binding errors * @throws BindingException if setter was not found or some other error occurred */ protected Map<String, List<ParseError>> updatePropertyValue( Class<?> parentClass, Object obj, String propertyName, BoundValuesInfo propertyValueInfo, boolean clientProvidedInstance) { if (propertyName == null || propertyName.isEmpty()) { throw new IllegalArgumentException("Name of property is missing."); } final Map<String, List<ParseError>> propertyBindErrors = new LinkedHashMap<String, List<ParseError>>(); boolean propertySet = false; String setterName = null; try { Method[] objMethods = obj.getClass().getMethods(); for (Method objMethod : objMethods) { if (!isPropertySetter(objMethod, propertyName)) { continue; } setterName = objMethod.getName(); Class<?> methodParamClass = objMethod.getParameterTypes()[0]; Type genericParamType = objMethod.getGenericParameterTypes()[0]; ParsedValue parsedValue = convertToValue(parentClass, propertyName, propertyValueInfo, methodParamClass, genericParamType); Object propertyValue = parsedValue.getValue(); if (!parsedValue.isSuccessfullyParsed()) { addParseError(propertyBindErrors, propertyName, parsedValue.getParseErrors()); } if (propertyValue == null || canBeImplicitlyConverted(propertyValue, methodParamClass)) { if (PrimitiveType.isPrimitiveType(methodParamClass) && propertyValue == null) { // Using initial value for primitive type propertyValue = PrimitiveType.byPrimitiveClass(methodParamClass).getInitialValue(); } objMethod.invoke(obj, propertyValue); propertySet = true; break; } } } catch (Exception ex) { throw new BindingException("Invoking setter " + setterName + " of class " + obj.getClass().getSimpleName() + " failed: " + ex.getMessage(), ex); } // client-provided instance need not to use all the values from the form, // because it can have constructor arguments already set to some different values if (!clientProvidedInstance && !Forms.AUTH_TOKEN_FIELD_NAME.equals(propertyName) && !propertySet) { throw new BindingException("Setter for property " + propertyName + " was not found in " + obj.getClass().getSimpleName()); } return propertyBindErrors; } /** * Converts form field string value(s) to one typed value (single value or collection/array of values) * with possible parse errors (when a string value cannot be converted properly). * @param parentClass name of class for which the value of its property is converted * @param propertyName * @param valueInfo * @param targetClass * @param genericParamType * @return */ protected ParsedValue convertToValue( Class<?> parentClass, String propertyName, BoundValuesInfo valueInfo, Class<?> targetClass, Type genericParamType) { List<ParseError> parseErrors = new ArrayList<ParseError>(); ParsedValue parsedValue = null; // TODO: Configurable prefered items order (collection type), linear as default CollectionSpec<?> collSpec = CollectionSpec.getInstance(targetClass, ItemsOrder.LINEAR); if (getCollectionBuilders().canHandle(collSpec)) { // binding to collection Object resultValue = null; if (valueInfo.getValues() != null && valueInfo.getValues().length == 1 && valueInfo.getValues()[0] instanceof List) { // already one list value (from list mapping) resultValue = valueInfo.getValues()[0]; } else { resultValue = convertFormValueToCollection( propertyName, valueInfo, collSpec, getCollectionBuilders().getItemClass(parentClass, propertyName, genericParamType), parseErrors); } parsedValue = new ParsedValue(resultValue, parseErrors); } else { if (valueInfo == null || valueInfo.getValues() == null || valueInfo.getValues().length == 0) { parsedValue = new ParsedValue(null, new ArrayList<ParseError>()); } else { Object formValue = valueInfo.getValues()[0]; Object resultValue = convertOneFormValue(propertyName, formValue, targetClass, valueInfo.getFormatter(), valueInfo.getPattern(), valueInfo.getLocation(), parseErrors); parsedValue = new ParsedValue(resultValue, parseErrors); } } return parsedValue; } /** * Parses the value from string with given user-defined formatter of with other suitable formatter * found in inner formatters. * @param strValue * @param targetClass * @param formatter * @param pattern * @param loc * @return */ protected Object parseFromString(String strValue, Class<?> targetClass, Formatter<Object> formatter, String pattern, Location loc) { Object resultValue; if (formatter != null) { // user defined formatter resultValue = formatter.parseFromString(strValue, (Class<Object>)targetClass, pattern, loc); } else { resultValue = getFormatters().parseFromString(strValue, targetClass, pattern, loc); } return resultValue; } protected boolean canBeImplicitlyConverted(Object fromValue, Class<?> toClass) { // Convertible from wrapper class (fromClass) to primitive class (toClass) return PrimitiveType.byClasses(toClass, fromValue.getClass()) != null || toClass.isAssignableFrom(fromValue.getClass()) || toClass.isInstance(fromValue); } protected void checkInvalidStringValueForUploadedFile(String propertyName, Class<?> targetClass) { if (UploadedFile.class.isAssignableFrom(targetClass)) { throw new IllegalStateException("Invalid String value for property '" + propertyName + "' of type " + UploadedFile.class.getSimpleName() + ". Did you forget to use POST method for the form with an uploaded file?"); } } /** * Converts the value from request parameters to target collection type. * @param propertyName * @param valueInfo * @param collSpec * @param itemClass * @param parseErrors * @return */ protected <C, I> C convertFormValueToCollection( String propertyName, BoundValuesInfo valueInfo, CollectionSpec<C> collSpec, Class<I> itemClass, List<ParseError> parseErrors) { // we will return empty collection if values are empty List<I> resultItems = new ArrayList<I>(); if (valueInfo != null && valueInfo.getValues() != null) { for (Object formValue : valueInfo.getValues()) { Object value = convertOneFormValue(propertyName, formValue, itemClass, valueInfo.getFormatter(), valueInfo.getPattern(), valueInfo.getLocation(), parseErrors); resultItems.add((I)value); } } return getCollectionBuilders().buildCollection(collSpec, itemClass, resultItems); } /** * Converts the value from request parameters (form value) to value of given target type. * @param propertyName * @param formValue * @param parseErrors * @param targetClass * @param formatter * @param pattern * @param loc * @return */ protected Object convertOneFormValue( String propertyName, Object formValue, Class<?> targetClass, Formatter<Object> formatter, String pattern, Location loc, List<ParseError> parseErrors) { Object resultValue = null; if (formValue instanceof String && !canBeImplicitlyConverted(formValue, targetClass)) { // We must parse the value from string (using formatters) checkInvalidStringValueForUploadedFile(propertyName, targetClass); // Convert from the String to targetClass String strValue = (String)formValue; try { // Throws StringParseException also when parsing empty String to some primitive type if (strValue.isEmpty() && !String.class.equals(targetClass)) { // resultValue remains null // for e.g. transformation of "" to Date will return null } else { resultValue = parseFromString(strValue, targetClass, formatter, pattern, loc); } } catch (StringParseException ex) { resultValue = null; parseErrors.add(new ParseError(propertyName, targetClass, strValue)); } } else { // if form value is instanceof UploadedFile, it is automatically // set to property of compatible type // also list of values from list mapping is directly returned resultValue = formValue; } return resultValue; } private void addParseError(Map<String, List<ParseError>> parseErrors, String propName, List<ParseError> errsToAdd) { List<ParseError> errors = parseErrors.get(propName); if (errors == null) { errors = new ArrayList<ParseError>(); } errors.addAll(errsToAdd); parseErrors.put(propName, errors); } }