/*
* 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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import net.formio.binding.BoundData;
import net.formio.binding.BoundValuesInfo;
import net.formio.binding.InstanceHoldingInstantiator;
import net.formio.binding.Instantiator;
import net.formio.binding.ParseError;
import net.formio.choice.ChoiceProvider;
import net.formio.data.RequestContext;
import net.formio.format.Formatter;
import net.formio.format.Location;
import net.formio.internal.FormUtils;
import net.formio.props.FormMappingProperties;
import net.formio.props.FormMappingPropertiesImpl;
import net.formio.upload.MaxSizeExceededError;
import net.formio.upload.RequestProcessingError;
import net.formio.upload.UploadedFile;
import net.formio.validation.ConstraintViolationMessage;
import net.formio.validation.InterpolatedMessage;
import net.formio.validation.ValidationResult;
/**
* Default implementation of {@link FormMapping}. Immutable when not filled.
* After the filling, new instance of mapping is created and its immutability
* depends on the character of filled data.
*
* @author Radek Beran
*/
public class BasicFormMapping<T> extends AbstractFormElement<T> implements FormMapping<T> {
// public because of introspection required by some template frameworks, constructors are not public
final Class<T> dataClass;
final Instantiator instantiator;
final Config config;
final T filledObject;
/** Mapping simple property names to fields. */
final Map<String, FormField<?>> fields;
/** Mapping simple property names to nested mappings. Property name is a part of full path of nested mapping. */
final Map<String, FormMapping<?>> nested;
final ValidationResult validationResult;
final FormMappingProperties formProperties;
final boolean secured;
final String labelKey;
final int order;
final Integer index;
/**
* Constructs a mapping from the given builder.
* @param builder
* @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
*/
BasicFormMapping(BasicFormMappingBuilder<T> builder, boolean simpleCopy) {
super(builder.parent, builder.propertyName, builder.validators);
this.config = builder.config;
this.dataClass = assertNotNullArg(builder.dataClass, "data class must be filled before configuring fields");
if (builder.instantiator != null) {
this.instantiator = builder.instantiator;
} else {
this.instantiator = getConfig().getDefaultInstantiator();
}
this.filledObject = builder.filledObject;
this.secured = builder.secured;
this.labelKey = builder.labelKey;
this.validationResult = builder.validationResult;
this.formProperties = new FormMappingPropertiesImpl(builder.properties);
this.order = builder.order;
this.index = builder.index;
this.fields = simpleCopy ? Collections.unmodifiableMap(builder.fields) :
Clones.fieldsWithParent(this, builder.fields);
this.nested = simpleCopy ? Collections.unmodifiableMap(builder.nested) :
Clones.mappingsWithParent(this, builder.nested, builder.dataClass, getConfig());
}
/**
* Returns copy with given order (called when appending this nested mapping to outer builder).
* @param src
* @param order
*/
BasicFormMapping(BasicFormMapping<T> src, int order) {
this(new BasicFormMappingBuilder<T>(src,
src.fields,
src.nested)
.order(order),
true); // true = simple copy of builder's data
}
/**
* Returns copy with given config.
* @param src
* @param config
* @param required
*/
BasicFormMapping(BasicFormMapping<T> src, FormMapping<?> parent) {
this(new BasicFormMappingBuilder<T>(src,
src.fields,
src.nested)
.parent(parent),
false); // false = parent will be propagated to nested elements
}
@Override
public FormMapping<?> getParent() {
return this.parent;
}
@Override
public String getName() {
String name = null;
if (getParent() != null) {
String pathSep = getPathSeparator();
if (index != null) {
name = getParent().getName() + pathSep + propertyName + "[" + index + "]";
} else {
name = getParent().getName() + pathSep + propertyName;
}
} else {
if (index != null) {
name = propertyName + "[" + index + "]";
} else {
name = propertyName;
}
}
if (name == null || name.isEmpty()) {
throw new IllegalStateException("Name must be filled");
}
return name;
}
@Override
public String getPropertyName() {
return propertyName;
}
@Override
public Class<T> getDataClass() {
return dataClass;
}
@Override
public Instantiator getInstantiator() {
return instantiator;
}
@Override
public ValidationResult getValidationResult() {
return validationResult;
}
@Override
public List<FormElement<?>> getElements() {
List<FormElement<?>> elems = new ArrayList<FormElement<?>>();
elems.addAll(this.nested.values());
elems.addAll(this.fields.values());
Collections.sort(elems, new FormElementOrderAscComparator());
return Collections.unmodifiableList(elems);
}
@Override
public Map<String, FormField<?>> getFields() {
return fields;
}
@Override
public <U> FormField<U> getField(Class<U> dataClass, String propertyName) {
FormField<?> field = getFields().get(propertyName);
if (field != null) {
Object filledObject = field.getFilledObject();
if (filledObject != null && !dataClass.isAssignableFrom(filledObject.getClass())) {
throw new IllegalStateException("Type of value in field '" + propertyName +
"' is not compatible with requested type " + dataClass.getName());
}
}
return (FormField<U>)field;
}
@Override
public Map<String, FormMapping<?>> getNested() {
return Collections.unmodifiableMap(nested);
}
@Override
public <U> FormMapping<U> getMapping(Class<U> dataClass, String propertyName) {
Map<String, FormMapping<?>> nestedMappings = getNested();
FormMapping<?> mapping = nestedMappings.get(propertyName);
if (mapping != null) {
if (!dataClass.isAssignableFrom(mapping.getDataClass())) {
throw new IllegalStateException("Type of object in nested mapping '" + propertyName +
"' is not compatible with requested type " + dataClass.getName());
}
}
return (FormMapping<U>)mapping;
}
@Override
public List<FormMapping<T>> getList() {
return Collections.<FormMapping<T>>emptyList();
}
@Override
public BasicFormMapping<T> fill(FormData<T> editedObj, Location loc, RequestContext ctx) {
return fillInternal(editedObj, loc, ctx).build(getConfig());
}
@Override
public BasicFormMapping<T> fill(FormData<T> editedObj, Location loc) {
return fill(editedObj, loc, (RequestContext)null);
}
@Override
public BasicFormMapping<T> fill(FormData<T> editedObj, RequestContext ctx) {
return fill(editedObj, getLocation(null), ctx);
}
@Override
public BasicFormMapping<T> fill(FormData<T> editedObj) {
return fill(editedObj, getLocation(null));
}
@Override
public BasicFormMapping<T> fillAndValidate(FormData<T> formData, Location loc, RequestContext ctx, Class<?> ... validationGroups) {
final Location givenOrCfgLocation = getLocation(loc);
BasicFormMapping<T> mapping = fill(formData, givenOrCfgLocation, ctx);
FormData<T> validatedFormData = new FormData<T>(formData.getData(), mapping.validate(givenOrCfgLocation.getLocale(), validationGroups));
return fill(validatedFormData, givenOrCfgLocation, ctx);
}
@Override
public FormMapping<T> fillAndValidate(FormData<T> formData, Location loc, Class<?>... validationGroups) {
return fillAndValidate(formData, loc, (RequestContext)null, validationGroups);
}
@Override
public FormMapping<T> fillAndValidate(FormData<T> formData, Class<?>... validationGroups) {
return fillAndValidate(formData, (Location)null, validationGroups);
}
@Override
public FormElement<?> fillTdiAjaxSrcElement(RequestParams requestParams, Location loc, Class<?>... validationGroups) {
FormElement<?> el = null;
if (requestParams.isTdiAjaxRequest()) {
FormMapping<?> filledMapping = fill(bind(requestParams, loc, validationGroups), loc);
el = filledMapping.findElement(requestParams.getTdiAjaxSrcElementName());
}
return el;
}
@Override
public FormElement<?> fillTdiAjaxSrcElement(RequestParams requestParams, Class<?>... validationGroups) {
return fillTdiAjaxSrcElement(requestParams, (Location)null, validationGroups);
}
@Override
public FormData<T> bind(RequestParams paramsProvider, Location loc, Class<?>... validationGroups) {
return bind(paramsProvider, loc, (RequestContext)null, validationGroups);
}
@Override
public FormData<T> bind(RequestParams paramsProvider, Location loc, RequestContext ctx, Class<?>... validationGroups) {
return bind(paramsProvider, loc, (T)null, ctx, validationGroups);
}
@Override
public FormData<T> bind(RequestParams paramsProvider, Class<?>... validationGroups) {
return bind(paramsProvider, (RequestContext)null, validationGroups);
}
@Override
public FormData<T> bind(RequestParams paramsProvider, RequestContext ctx, Class<?>... validationGroups) {
return bind(paramsProvider, getLocation(null), ctx, validationGroups);
}
@Override
public FormData<T> bind(RequestParams paramsProvider, T instance, Class<?>... validationGroups) {
return bind(paramsProvider, instance, (RequestContext)null, validationGroups);
}
@Override
public FormData<T> bind(RequestParams paramsProvider, T instance, RequestContext ctx, Class<?>... validationGroups) {
return bind(paramsProvider, getLocation(null), instance, ctx, validationGroups);
}
@Override
public FormData<T> bind(RequestParams paramsProvider, Location loc, T instance, Class<?>... validationGroups) {
return bind(paramsProvider, loc, instance, (RequestContext)null, validationGroups);
}
@Override
public FormData<T> bind(final RequestParams paramsProvider, final Location loc, final T instance, final RequestContext context, final Class<?>... validationGroups) {
if (paramsProvider == null) throw new IllegalArgumentException("paramsProvider cannot be null");
final Location givenOrCfgLoc = getLocation(loc);
final RequestProcessingError error = paramsProvider.getRequestError();
Map<String, BoundValuesInfo> valuesToBind = prepareValuesToBindForFields(paramsProvider, givenOrCfgLoc);
// binding (and validating) data from paramsProvider to objects for nested mappings
// and adding it to available values to bind
Map<String, FormData<?>> nestedFormData = loadDataForMappings(nested, paramsProvider, givenOrCfgLoc, instance, context, validationGroups);
for (Map.Entry<String, FormData<?>> e : nestedFormData.entrySet()) {
valuesToBind.put(e.getKey(), BoundValuesInfo.getInstance(
new Object[] { e.getValue().getData() },
(String)null,
(Formatter<Object>)null,
givenOrCfgLoc));
}
if (!(error instanceof MaxSizeExceededError) && this.secured) {
// Must be executed after processing of nested mappings
AuthTokens.verifyAuthToken(context, getConfig().getTokenAuthorizer(), getRootMappingPath(), paramsProvider, isRootMapping(), getPathSeparator());
}
// binding data from "values" to resulting object for this mapping
Instantiator instantiator = this.instantiator;
if (instance != null) {
// use instance already prepared by client which the client wish to fill
instantiator = new InstanceHoldingInstantiator<T>(instance);
}
final BoundData<T> boundData = getConfig().getBinder().bindToNewInstance(this.dataClass, instantiator, valuesToBind);
// validation of resulting object for this mapping
ValidationResult validationRes = validateInternal(
boundData.getData(),
error,
FormUtils.flatten(boundData.getPropertyBindErrors().values()),
givenOrCfgLoc.getLocale(),
validationGroups);
Collection<ValidationResult> validationResults = new ArrayList<ValidationResult>();
validationResults.add(validationRes);
for (FormData<?> fd : nestedFormData.values()) {
validationResults.add(fd.getValidationResult());
}
return new FormData<T>(boundData.getData(), Clones.mergedValidationResults(validationResults));
}
@Override
public String getLabelKey() {
String key = labelKey;
if (key == null) {
key = FormUtils.labelKeyForName(getName());
}
return key;
}
/**
* Object filled in this mapping.
* @return
*/
@Override
public T getFilledObject() {
return this.filledObject;
}
@Override
public Config getConfig() {
Config cfg = this.config;
if (cfg == null && this.parent != null) {
cfg = this.parent.getConfig();
}
if (cfg == null) {
// fallback to default config
cfg = Forms.defaultConfig(this.dataClass);
}
return cfg;
}
@Override
public String toString() {
return toString("");
}
@Override
public BasicFormMapping<T> withOrder(int order) {
return new BasicFormMapping<T>(this, order);
}
@Override
public BasicFormMapping<T> withParent(FormMapping<?> parent) {
return new BasicFormMapping<T>(this, parent);
}
@Override
public String toString(String indent) {
return new MappingStringBuilder<T>(
getDataClass(),
getName(),
order,
fields,
nested,
getList()).build(indent);
}
@Override
public FormMappingProperties getProperties() {
return this.formProperties;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public Integer getIndex() {
return this.index;
}
@Override
public boolean isRootMapping() {
return this.parent == null;
}
/**
* Returns separator of names in full path to form fields.
* @return
*/
protected String getPathSeparator() {
return getConfig().getPathSeparator();
}
ValidationResult validate(Locale locale, Class<?> ... validationGroups) {
Collection<ValidationResult> validationResults = new ArrayList<ValidationResult>();
if (getFilledObject() != null) {
validationResults.add(validateInternal(getFilledObject(), (RequestProcessingError)null, new ArrayList<ParseError>(), locale, validationGroups));
}
for (FormMapping<?> mapping : nested.values()) {
validationResults.add(((BasicFormMapping<?>)mapping).validate(locale, validationGroups));
}
return Clones.mergedValidationResults(validationResults);
}
ValidationResult validateInternal(T object, RequestProcessingError error, List<ParseError> parseErrors, Locale locale, Class<?> ... validationGroups) {
List<InterpolatedMessage> customMessages = new ArrayList<InterpolatedMessage>();
if (error != null) {
customMessages.add(error);
}
customMessages.addAll(parseErrors);
return getConfig().getBeanValidator().validate(
object,
getName(),
this,
customMessages,
locale,
validationGroups);
}
Map<String, FormData<?>> loadDataForMappings(
Map<String, FormMapping<?>> mappings,
RequestParams paramsProvider,
Location loc,
T instance,
RequestContext ctx,
Class<?> ... validationGroups) {
final Map<String, FormData<?>> dataMap = new LinkedHashMap<String, FormData<?>>();
// Transformation from ? to Object (to satisfy generics)
final Map<String, FormMapping<?>> inputMappings = new LinkedHashMap<String, FormMapping<?>>();
for (Map.Entry<String, FormMapping<?>> e : mappings.entrySet()) {
inputMappings.put(e.getKey(), e.getValue());
}
for (Map.Entry<String, FormMapping<?>> e : inputMappings.entrySet()) {
FormMapping<Object> mapping = (FormMapping<Object>)e.getValue();
if (!mapping.getProperties().isDetached()) {
Object nestedInstance = null;
if (instance != null) {
nestedInstance = nestedData(e.getKey(), instance);
}
FormData<Object> formData = mapping.bind(paramsProvider, loc, nestedInstance, ctx, validationGroups);
dataMap.put(e.getKey(), formData);
}
}
// Transformation from Object to ? (to satisfy generics)
final Map<String, FormData<?>> outputData = new LinkedHashMap<String, FormData<?>>();
for (Map.Entry<String, FormData<?>> e : dataMap.entrySet()) {
outputData.put(e.getKey(), e.getValue());
}
return outputData;
}
BasicFormMappingBuilder<T> fillInternal(FormData<T> editedObj, Location loc, RequestContext ctx) {
final Location givenOrCfgLoc = getLocation(loc);
Map<String, FormMapping<?>> filledNestedMappings = fillNestedMappings(editedObj, givenOrCfgLoc, ctx);
// Preparing values for this mapping
Map<String, Object> propValues = gatherPropertyValues(editedObj.getData(), FormUtils.getPropertiesFromFields(fields), ctx);
// Fill the definitions of fields of this mapping with prepared values
Map<String, FormField<?>> filledFields = fillFields(
propValues,
editedObj.getValidationResult().getFieldMessages(),
-1,
givenOrCfgLoc);
// Returning copy of this form that is filled with form data
BasicFormMappingBuilder<T> builder = new BasicFormMappingBuilder<T>(this,
filledFields,
Collections.unmodifiableMap(filledNestedMappings))
.filledObject(editedObj.getData())
.validationResult(editedObj.getValidationResult());
return builder;
}
/**
* Gather values of object's formProperties.
* @param object
* @param allowedProperties set of allowed formProperties, does not influence order of returned entries
* @param ctx request context
* @return
*/
Map<String, Object> gatherPropertyValues(T object, Set<String> allowedProperties, RequestContext ctx) {
Map<String, Object> beanValues = getConfig().getBeanExtractor().extractBean(object, allowedProperties);
Map<String, Object> propValues = new LinkedHashMap<String, Object>(beanValues);
if (isRootMapping() && secured) {
propValues.put(Forms.AUTH_TOKEN_FIELD_NAME,
AuthTokens.generateAuthToken(ctx, getConfig().getTokenAuthorizer(), getRootMappingPath()));
}
return Collections.unmodifiableMap(propValues);
}
String getRootMappingPath() {
FormMapping<?> rootMapping = this;
while (rootMapping.getParent() != null) {
rootMapping = rootMapping.getParent();
}
return rootMapping.getName();
}
Map<String, FormField<?>> fillFields(
Map<String, Object> propValues,
Map<String, List<ConstraintViolationMessage>> fieldMsgs,
int indexInList,
Location loc) {
Map<String, FormField<?>> filledFields = new LinkedHashMap<String, FormField<?>>();
// For each field from form definition, let's fill this field with value -> filled form field
for (Map.Entry<String, FormField<?>> fieldDefEntry : this.fields.entrySet()) {
final String propertyName = fieldDefEntry.getKey();
if (indexInList >= 0 && Forms.AUTH_TOKEN_FIELD_NAME.equals(propertyName)) {
if (isRootMapping() && this.secured) {
throw new UnsupportedOperationException("Verification of authorization token is not supported "
+ "in root list mapping. Please create SINGLE root mapping with nested list mapping.");
}
}
final FormField<?> field = fieldDefEntry.getValue();
FormField<?> filledField = null;
if (field.getProperties().isDetached()) {
filledField = field; // field is not filled
} else {
Object value = propValues.get(propertyName);
List<ConstraintViolationMessage> fieldMessages = fieldMsgs.get(field.getName());
String preferedStringValue = null;
if (fieldMessages != null && !fieldMessages.isEmpty()) {
preferedStringValue = getOriginalStringValueFromParseError(fieldMessages);
}
filledField = createFilledFormField((FormField<Object>)field, value, loc, preferedStringValue);
}
filledFields.put(propertyName, filledField);
}
filledFields = Collections.unmodifiableMap(filledFields);
return filledFields;
}
Map<String, FormMapping<?>> fillNestedMappings(FormData<T> editedObj, Location loc, RequestContext ctx) {
Map<String, FormMapping<?>> newNestedMappings = new LinkedHashMap<String, FormMapping<?>>();
// For each definition of nested mapping, fill this mapping with edited data -> filled mapping
for (Map.Entry<String, FormMapping<?>> e : this.nested.entrySet()) {
FormMapping<?> filledMapping = null;
if (e.getValue().getProperties().isDetached()) {
// mapping will not be filled
filledMapping = e.getValue();
} else {
// nested data - nested object or list of nested objects in case of mapping to list
Object data = nestedData(e.getKey(), editedObj.getData());
// the outer report is propagated to nested
FormData<Object> formData = new FormData<Object>(data, editedObj.getValidationResult());
FormMapping<Object> mapping = (FormMapping<Object>)e.getValue();
filledMapping = mapping.fill(formData, loc, ctx);
}
newNestedMappings.put(e.getKey(), filledMapping);
}
return newNestedMappings;
}
/**
* Returns nested object extracted as value of given property of given data.
* @param propName
* @param data
* @return
*/
<U> U nestedData(String propName, T data) {
Map<String, Object> props = getConfig().getBeanExtractor().extractBean(data, Collections.singleton(propName));
return (U)props.get(propName); // can be null if nested object is not required
}
/**
* Converts parameters from request (RequestParams) using field definitions and given locale
* to descriptions of values for individual properties, ready to bind to form data object
* via binder.
* @param paramsProvider
* @param loc
* @return
*/
private Map<String, BoundValuesInfo> prepareValuesToBindForFields(RequestParams paramsProvider, Location loc) {
Map<String, BoundValuesInfo> values = new LinkedHashMap<String, BoundValuesInfo>();
// Get values for each defined field
for (Map.Entry<String, FormField<?>> e : fields.entrySet()) {
FormField<?> field = e.getValue();
if (!field.getProperties().isDetached()) {
String formPrefixedName = field.getName(); // already prefixed with form name
Object[] paramValues = null;
UploadedFile[] files = paramsProvider.getUploadedFiles(formPrefixedName);
if (files == null || files.length == 0) {
files = paramsProvider.getUploadedFiles(formPrefixedName + "[]");
}
if (files != null && files.length > 0) {
// non-empty files array returned
paramValues = files;
} else {
String[] strValues = paramsProvider.getParamValues(formPrefixedName);
if (strValues == null) strValues = paramsProvider.getParamValues(formPrefixedName + "[]");
if (getConfig().isInputTrimmed()) {
strValues = FormUtils.trimValues(strValues);
}
paramValues = strValues;
if (strValues != null && field.getChoices() != null && field.getChoiceRenderer() != null) {
// There is a codebook with choices to select from
paramValues = ChoiceItems.convertParamsToChoiceItems(field, strValues);
}
}
String propertyName = e.getKey();
values.put(propertyName, BoundValuesInfo.getInstance(
paramValues, field.getPattern(), field.getFormatter(), loc));
}
}
return values;
}
private <U> FormField<U> createFilledFormField(final FormField<U> field, U value, Location loc, String preferedStringValue) {
ChoiceProvider<U> choiceProvider = field.getChoices();
if (choiceProvider == null && field.getType() != null && !field.getType().isEmpty()) {
Field formComponent = Field.findByType(field.getType());
if (formComponent != null && formComponent.isChoice()) {
// TODO: choice provider can be initialized here to some default but class of value must be
// propagated here
}
}
return new FieldProps<U>(field,
FormUtils.<U>convertObjectToList(value),
loc,
getConfig().getFormatters(),
preferedStringValue).choices(choiceProvider).build();
}
private String getOriginalStringValueFromParseError(List<ConstraintViolationMessage> fieldMessages) {
String value = null;
if (fieldMessages != null) {
for (ConstraintViolationMessage msg : fieldMessages) {
if (value == null && msg.getMsgArgs() != null) {
value = (String)msg.getMsgArgs().get(ParseError.MSG_ARG_VALUE_AS_STRING);
}
}
}
return value;
}
protected Location getLocation(Location loc) {
Location a = null;
if (loc != null) {
a = loc;
} else {
a = getConfig().getLocation();
}
return a;
}
private static <U> U assertNotNullArg(U arg, String message) {
if (arg == null) throw new IllegalArgumentException(message);
return arg;
}
}