/* * 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; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import net.formio.ajax.JsEvent; import net.formio.ajax.action.AjaxAction; import net.formio.ajax.action.AjaxHandler; import net.formio.binding.BeanExtractor; import net.formio.binding.BindingReflectionUtils; import net.formio.binding.ConstructionDescription; import net.formio.binding.Instantiator; import net.formio.binding.PrimitiveType; import net.formio.binding.PropertyMethodRegex; import net.formio.binding.collection.CollectionSpec; import net.formio.binding.collection.ItemsOrder; import net.formio.common.heterog.HeterogCollections; import net.formio.common.heterog.HeterogMap; import net.formio.format.Location; import net.formio.props.FormElementProperty; import net.formio.upload.UploadedFile; import net.formio.validation.ValidationResult; import net.formio.validation.Validator; import net.formio.validation.validators.RequiredValidator; /** * Basic builder of {@link FormMapping}. * @author Radek Beran */ public class BasicFormMappingBuilder<T> { FormMapping<?> parent; String propertyName; Class<T> dataClass; Instantiator instantiator; /** Mapping simple property names to fields. */ Map<String, FormField<?>> fields = new LinkedHashMap<String, FormField<?>>(); /** Mapping simple property names to nested mappings. Property name is a part of full path of nested mapping. */ Map<String, FormMapping<?>> nested = new LinkedHashMap<String, FormMapping<?>>(); List<FormMapping<T>> listOfMappings = new ArrayList<FormMapping<T>>(); Config config; List<Validator<T>> validators; ValidationResult validationResult; MappingType mappingType; T filledObject; boolean automatic; boolean secured; String labelKey; HeterogMap<String> properties; int order; private int nextNestedElementOrder; Integer index; /** * Should be constructed only via {@link Forms} entry point of API. */ BasicFormMappingBuilder(Class<T> dataClass, String propertyName, Instantiator instantiator, boolean automatic, MappingType mappingType) { if (dataClass == null) throw new IllegalArgumentException("dataClass must be filled"); if (propertyName == null || propertyName.isEmpty()) throw new IllegalArgumentException("propertyName must be filled"); if (mappingType == null) throw new IllegalArgumentException("mappingType must be filled"); this.dataClass = dataClass; this.propertyName = propertyName; this.instantiator = instantiator; this.mappingType = mappingType; this.automatic = automatic; this.properties = FormElementProperty.createDefaultProperties(); this.validators = new ArrayList<Validator<T>>(); } BasicFormMappingBuilder(Class<T> objectClass, String propertyName, Instantiator inst, boolean automatic) { this(objectClass, propertyName, inst, automatic, MappingType.SINGLE); } BasicFormMappingBuilder(BasicFormMapping<T> src, Map<String, FormField<?>> fields, Map<String, FormMapping<?>> nested) { // src already contains composed/created fields -> automatic = false for this case this(src.dataClass, src.propertyName, src.instantiator, false, (src instanceof BasicListFormMapping) ? MappingType.LIST : MappingType.SINGLE); this.parent = src.parent; this.config = src.config; this.filledObject = src.filledObject; this.fields = fields; this.nested = Collections.unmodifiableMap(nested); this.secured = src.secured; this.validationResult = src.validationResult; final HeterogMap<String> properties = HeterogCollections.<String>newLinkedMap(); properties.putAllFromSource(src.formProperties.getHeterogMap()); this.properties = properties; this.order = src.order; this.index = src.index; this.validators = new ArrayList<Validator<T>>(src.validators); } /** * True if this form should be secured with authorization token. * @param secured * @return */ public BasicFormMappingBuilder<T> secured(boolean secured) { this.secured = secured; if (secured) { fieldForAuthToken(); } return this; } /** * Key for translation of the label. * @param labelKey * @return */ public BasicFormMappingBuilder<T> labelKey(String labelKey) { this.labelKey = labelKey; return this; } /** Only for internal usage. */ BasicFormMappingBuilder<T> parent(FormMapping<?> parent) { this.parent = parent; return this; } /** Only for internal usage. */ BasicFormMappingBuilder<T> order(int order) { this.order = order; return this; } /** Only for internal usage. */ BasicFormMappingBuilder<T> index(Integer index) { this.index = index; return this; } /** Only for internal usage. */ BasicFormMappingBuilder<T> dataClass(Class<T> dataClass) { this.dataClass = dataClass; return this; } /** Only for internal usage. */ BasicFormMappingBuilder<T> instantiator(Instantiator instantiator) { this.instantiator = instantiator; return this; } /** Only for internal usage. */ BasicFormMappingBuilder<T> config(Config config) { this.config = config; return this; } /** Only for internal usage. */ BasicFormMappingBuilder<T> filledObject(T filledObject) { this.filledObject = filledObject; return this; } /** Only for internal usage. */ BasicFormMappingBuilder<T> validationResult(ValidationResult validationResult) { this.validationResult = validationResult; return this; } /** * Adds validator. * @param validator * @return */ public BasicFormMappingBuilder<T> validator(Validator<T> validator) { this.validators.add(validator); return this; } /** * Adds form field specification. * @param fieldProps * @return */ public <U> BasicFormMappingBuilder<T> field(FieldProps<U> fieldProps) { fields.put(fieldProps.getPropertyName(), fieldProps.build(nextNestedElementOrder++)); return this; } /** * Adds form field specification. * @param form field with specified property name * @return */ public <U> BasicFormMappingBuilder<T> field(FormField<U> formField) { fields.put(formField.getName(), new FormFieldImpl<U>(formField, (FormMapping<?>)null, this.nextNestedElementOrder++)); return this; } /** * Adds form field specification. * @param propertyName name of mapped property * @param type type of form field, for e.g.: text, checkbox, textarea, date-picker, ... * @param inputType type of HTML input(s) that is used to render the form field * @return */ public <U> BasicFormMappingBuilder<T> field(String propertyName, String type, String inputType) { return field(Forms.field(propertyName, type, inputType)); } /** * Adds form field specification. * @param propertyName name of mapped property * @param type type of form field, for e.g.: text, checkbox, textarea, date-picker, ... * @return */ public <U> BasicFormMappingBuilder<T> field(String propertyName, String type) { return field(propertyName, type, null); } /** * Adds form field specification. * @param propertyName name of mapped property * @param type type of form field * @return */ public <U> BasicFormMappingBuilder<T> field(String propertyName, Field type) { return field(propertyName, type.getType(), type.getInputType()); } /** * Adds form field specification. * @param propertyName name of mapped property * @return */ public <U> BasicFormMappingBuilder<T> field(String propertyName) { return field(propertyName, (String)null); } /** * Adds specifications of form fields. * @param fields * @return */ public BasicFormMappingBuilder<T> fields(FieldProps<?> ... fields) { for (int i = 0; i < fields.length; i++) { field(fields[i]); } return this; } /** * Adds specifications of form fields. * @param fieldNames * @return */ public BasicFormMappingBuilder<T> fields(String ... fieldNames) { for (int i = 0; i < fieldNames.length; i++) { field(fieldNames[i]); } return this; } /** * Registers form mapping for nested object in form data. * Path of this mapping is added as a prefix to given nested mapping. * @param mapping nested mapping * @return */ public <U> BasicFormMappingBuilder<T> nested(FormMapping<U> mapping) { return nestedInternal(mapping); } /** * Registers form mappings for nested objects in form data. * @param mappings nested mappings * @return */ public BasicFormMappingBuilder<T> nested(FormMapping<?> ... mappings) { if (mappings != null) { for (FormMapping<?> b : mappings) { nested(b); } } return this; } public <U> BasicFormMappingBuilder<T> property(FormElementProperty<U> fieldProperty, U value) { this.properties.putTyped(fieldProperty, value); return this; } public BasicFormMappingBuilder<T> visible(boolean visible) { return property(FormElementProperty.VISIBLE, Boolean.valueOf(visible)); } public BasicFormMappingBuilder<T> enabled(boolean enabled) { return property(FormElementProperty.ENABLED, Boolean.valueOf(enabled)); } public BasicFormMappingBuilder<T> readonly(boolean readonly) { return property(FormElementProperty.READ_ONLY, Boolean.valueOf(readonly)); } public BasicFormMappingBuilder<T> required(boolean required) { if (required) { Validator<T> validator = RequiredValidator.getInstance(); if (!validators.contains(validator)) { validators.add(validator); } } return this; } public BasicFormMappingBuilder<T> help(String help) { return property(FormElementProperty.HELP, help); } public BasicFormMappingBuilder<T> labelVisible(boolean visible) { return property(FormElementProperty.LABEL_VISIBLE, Boolean.valueOf(visible)); } public <U> BasicFormMappingBuilder<T> ajaxHandler(AjaxAction<U> action) { return ajaxHandler(action, (JsEvent)null); } public <U> BasicFormMappingBuilder<T> ajaxHandler(AjaxAction<U> action, JsEvent event) { return ajaxHandlers(Arrays.asList(new AjaxHandler<U>(action, event))); } public <U> BasicFormMappingBuilder<T> ajaxHandler(AjaxAction<U> action, String requestParam) { return ajaxHandlers(Arrays.asList(new AjaxHandler<U>(action, requestParam))); } public BasicFormMappingBuilder<T> ajaxHandlers(List<? extends AjaxHandler<?>> handlers) { return property(FormElementProperty.AJAX_HANDLERS, handlers.toArray(new AjaxHandler[0])); } public BasicFormMappingBuilder<T> detached(boolean detached) { return property(FormElementProperty.DETACHED, Boolean.valueOf(detached)); } public BasicFormMappingBuilder<T> fieldsetDisplayed(boolean displayed) { return property(FormElementProperty.FIELDSET_DISPLAYED, Boolean.valueOf(displayed)); } public FormMapping<T> build() { return build(this.config); } /** * Builds form mapping with given user-defined configuration. * @param config * @return */ public BasicFormMapping<T> build(Config config) { boolean plainCopy = false; return buildInternal(config, plainCopy); } /** * Builds form mapping with given localization settings. * @param loc * @return */ public BasicFormMapping<T> build(Location loc) { return build(Forms.config().location(loc).build()); } <U> BasicFormMappingBuilder<T> nestedInternal(FormMapping<U> nestedMapping) { if (nestedMapping.getName().contains(nestedMapping.getConfig().getPathSeparator())) { throw new IllegalStateException("Nested mapping should be defined with path that is one simple name " + "that corresponds to the name of the property"); } this.nested.put(nestedMapping.getPropertyName(), nestedMapping.withOrder(nextNestedElementOrder++)); // should return BasicFormMappingBuilder, not ConfigurableBasicFormMappingBuilder // all configurable dependencies are taken from outer mapping return this; } /** Adding multiple form fields. Operation for internal use only. */ BasicFormMappingBuilder<T> fields(Map<String, FormField<?>> fields) { if (fields == null) throw new IllegalArgumentException("fields cannot be null"); Map<String, FormField<?>> flds = new LinkedHashMap<String, FormField<?>>(); for (Map.Entry<String, FormField<?>> e : fields.entrySet()) { flds.put(e.getKey(), e.getValue()); } this.fields.putAll(flds); return this; } /** * Only for internal usage. * @param fields * @return */ BasicFormMappingBuilder<T> fieldsReplaceAll(Map<String, FormField<?>> fields) { this.fields = Collections.unmodifiableMap(fields); return this; } /** * Builds form mapping. * @param config * @param simpleCopy true if simple copy of builder's data should be constructed, otherwise propagation * of parent mapping into fields and nested mappings is processed * @return */ BasicFormMapping<T> buildInternal(Config config, boolean simpleCopy) { this.config = config; if (this.automatic) { Config c = config; if (c == null) { c = Forms.defaultConfig(this.dataClass); } buildFieldsAndNestedMappingsAutomatically(c); } BasicFormMapping<T> mapping = null; if (this.mappingType == MappingType.LIST) { mapping = new BasicListFormMapping<T>(this, simpleCopy); } else { mapping = new BasicFormMapping<T>(this, simpleCopy); } checkValidFormMapping(mapping); return mapping; } Map<String, Method> getClassProperties(Class<?> beanClass, BeanExtractor extractor, PropertyMethodRegex accessorRegex) { final Map<String, Method> properties = new LinkedHashMap<String, Method>(); final Method[] objMethods = beanClass.getMethods(); for (Method objMethod : objMethods) { if (extractor.isIgnored(objMethod)) continue; if (objMethod.getName().equals("getClass")) continue; if (accessorRegex.isAccessor(objMethod)) { String propName = accessorRegex.getPropertyName(objMethod.getName()); if (propName != null) { properties.put(propName, objMethod); } } } return Collections.unmodifiableMap(properties); } void buildFieldsAndNestedMappingsAutomatically(Config config) { if (config == null) throw new IllegalArgumentException("config cannot be null"); Map<String, Method> propertiesByNames = getClassProperties(this.dataClass, config.getBeanExtractor(), config.getAccessorRegex()); Instantiator inst = this.instantiator; if (inst == null) { inst = config.getDefaultInstantiator(); } ConstructionDescription constrDesc = inst.getDescription(this.dataClass, config.getArgumentNameResolver()); Method[] methods = this.dataClass.getMethods(); for (Map.Entry<String, Method> e : propertiesByNames.entrySet()) { String propertyName = e.getKey(); if (!this.fields.containsKey(propertyName) && !this.nested.containsKey(propertyName)) { // Not in fields or nested mappings which are defined explicitly. // Check if also setter (or construction method argument) for this property exists, otherwise getter // can serve as an auxiliary method only. if (isSettable(constrDesc, methods, config.getSetterRegex(), propertyName)) { Class<?> propertyType = e.getValue().getReturnType(); if (propertyType.getName().equals(Class.class.getName())) throw new IllegalStateException("Cannot map property " + propertyName + " of type " + propertyType.getName() + " in class " + this.dataClass.getName()); if (isDataClassForField(propertyType, config)) { this.field(propertyName); // single value field } else { if (isCollection(propertyType, config)) { Class<?> itemClass = BindingReflectionUtils.itemTypeFromGenericCollType(e.getValue().getGenericReturnType()); if (itemClass != null && isDataClassForField(itemClass, config)) { this.field(propertyName); // multiple value field } else { // nested collection of complex types or unknown types if (itemClass == null) throw new IllegalStateException("Cannot resolve item type of collection type of property " + propertyName + " in class " + this.dataClass.getName()); BasicFormMapping<?> mapping = null; if (this.secured) { mapping = Forms.automaticSecured(itemClass, propertyName, null, MappingType.LIST).build(config); } else { mapping = Forms.automatic(itemClass, propertyName, null, MappingType.LIST).build(config); } this.nested(mapping); } } else { // some complex or unknown type assertValidComplexTypeProperty(propertyType, propertyName); BasicFormMapping<?> mapping = null; if (this.secured) { mapping = Forms.automaticSecured(propertyType, propertyName).build(config); } else { mapping = Forms.automatic(propertyType, propertyName).build(config); } this.nested(mapping); } } } } } } void fieldForAuthToken() { if (!fields.containsKey(Forms.AUTH_TOKEN_FIELD_NAME)) { field(Forms.AUTH_TOKEN_FIELD_NAME, "hidden"); } } private void checkValidFormMapping(BasicFormMapping<T> mapping) { if (mapping.propertyName == null || mapping.propertyName.isEmpty()) { throw new IllegalStateException("propertyName must not be empty"); } // All fields must have names prefixed with mapping path for (FormField<?> field : mapping.fields.values()) { if (field.getName() == null || field.getName().isEmpty()) { throw new IllegalStateException("Field name must not be empty"); } if (!field.getName().contains(mapping.getPathSeparator())) { throw new IllegalStateException("Full path (name) of field '" + field.getName() + "' must contain at least one path separator that separates mapping path '" + mapping.getName() + "' from property name (or more complex path) mapped to field"); } if (!field.getName().startsWith(mapping.getName())) { throw new IllegalStateException("Field name '" + field.getName() + "' does not start with mapping name '" + mapping.getName() + "'"); } if (!field.getProperties().isDetached()) { checkRequired(field); } } if (!mapping.getProperties().isDetached()) { checkRequired(mapping); } } private <U> void checkRequired(FormElement<U> element) { // This is just to check if the property represented by the element exists (by introspection): boolean required = element.isRequired(); if (required) { // nothing } } private void assertValidComplexTypeProperty(Class<?> propertyType, String propertyName) { if (String.class.isAssignableFrom(propertyType)) throw new IllegalStateException("Cannot map property " + propertyName + " of type " + propertyType.getName() + " in class " + this.dataClass.getName()); if (propertyType.isEnum()) throw new IllegalStateException("Cannot map property " + propertyName + " of type " + propertyType.getName() + " in class " + this.dataClass.getName()); } private boolean isSettable(ConstructionDescription constrDesc, Method[] methods, PropertyMethodRegex setterRegex, String propertyName) { if (setterRegex == null) throw new IllegalArgumentException("setterRegex cannot be null"); boolean settable = false; for (Method method : methods) { if (isPropertySetter(setterRegex, method, propertyName)) { settable = true; break; } } if (!settable) { List<String> argNames = constrDesc.getArgNames(); if (argNames != null) { for (String argName : argNames) { if (argName != null && argName.equals(propertyName)) { settable = true; break; } } } } return settable; } private boolean isPropertySetter(PropertyMethodRegex setterRegex, Method method, String propertyName) { return setterRegex.matchesPropertyMethod(method.getName(), propertyName) && method.getParameterTypes().length == 1; } private boolean isCollection(Class<?> type, Config config) { return config.getCollectionBuilders().canHandle(CollectionSpec.getInstance(type, ItemsOrder.LINEAR)) || config.getCollectionBuilders().canHandle(CollectionSpec.getInstance(type, ItemsOrder.HASH)) || config.getCollectionBuilders().canHandle(CollectionSpec.getInstance(type, ItemsOrder.SORTED)); } private boolean isDataClassForField(Class<?> retType, Config config) { return PrimitiveType.byPrimitiveClass(retType) != null || PrimitiveType.byWrapperClass(retType) != null || config.getFormatters().canHandle(retType) || UploadedFile.class.isAssignableFrom(retType); } }