/* * 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.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import net.formio.data.RequestContext; import net.formio.format.Location; import net.formio.internal.FormUtils; import net.formio.upload.MaxSizeExceededError; import net.formio.upload.RequestProcessingError; import net.formio.validation.ConstraintViolationMessage; import net.formio.validation.ValidationResult; /** * Implementation of {@link FormMapping} that is expanded to list of indexed mappings * when filled with data (list of objects). 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 BasicListFormMapping<T> extends BasicFormMapping<T> { // public because of introspection required by some template frameworks, constructors are not public // make another type parameter for element of list (this is new parameter) and for the list itself = T? /** * Mappings for individual elements in list of edited objects. */ private final List<FormMapping<T>> listOfMappings; /** * Construct the mapping from 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 */ BasicListFormMapping(BasicFormMappingBuilder<T> builder, boolean simpleCopy) { super(builder, simpleCopy); this.listOfMappings = newListOfMappings(builder.listOfMappings); } /** * Returns copy with given order (called when appending this nested mapping to outer builder). * @param src * @param order */ BasicListFormMapping(BasicListFormMapping<T> src, int order) { super(src, order); this.listOfMappings = newListOfMappings(src.listOfMappings); } /** * Returns copy with given parent and config. * @param src * @param parent * @param required */ BasicListFormMapping(BasicListFormMapping<T> src, FormMapping<?> parent) { super(src, parent); this.listOfMappings = newListOfMappings(src.listOfMappings); } @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, 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) { final Location givenOrCfgLoc = getLocation(loc); final RequestProcessingError error = paramsProvider.getRequestError(); // Finding how many parameters are in the request - check for max. index available in request params name, // according to this mapping path int maxIndex = FormUtils.findMaxIndex(paramsProvider.getParamNames(), getName()); // Constructing mappings for each index up to max. index. // Nested mapping of this list mapping will become nested mappings of each // index-related mapping. List<FormMapping<T>> listMappings = new ArrayList<FormMapping<T>>(); for (int index = 0; index <= maxIndex; index++) { ValidationResult res = null; if (this.getValidationResult() != null) { res = new ValidationResult( new LinkedHashMap<String, List<ConstraintViolationMessage>>(this.getValidationResult().getFieldMessages()), new ArrayList<ConstraintViolationMessage>(this.getValidationResult().getGlobalMessages())); } // constructing single mapping for index: BasicFormMappingBuilder<T> builder = new BasicFormMappingBuilder<T>(this, this.fields, this.nested) .index(Integer.valueOf(index)) .order(index) .validationResult(res); builder.mappingType = MappingType.SINGLE; listMappings.add(builder.build(getConfig())); } // Loading data for constructed mappings for individual indexes // Tie these nested objects together to a list, this will be later converted to configured type of a collection for list mappings List<T> data = new ArrayList<T>(); Map<String, List<ConstraintViolationMessage>> fieldMsgs = new LinkedHashMap<String, List<ConstraintViolationMessage>>(); List<ConstraintViolationMessage> globalMsgs = new ArrayList<ConstraintViolationMessage>(); for (int index = 0; index < listMappings.size(); index++) { FormMapping<T> m = listMappings.get(index); T instanceForIndex = null; if (instance != null) { Iterable<T> itColl = checkIterable(instance); int j = 0; for (Iterator<T> it = itColl.iterator(); it.hasNext(); ) { T itValue = it.next(); if (j == index) { instanceForIndex = itValue; break; } j++; } } FormData<T> formData = m.bind(paramsProvider, givenOrCfgLoc, instanceForIndex, context, validationGroups); data.add(formData.getData()); fieldMsgs.putAll(formData.getValidationResult().getFieldMessages()); globalMsgs.addAll(formData.getValidationResult().getGlobalMessages()); } if (!(error instanceof MaxSizeExceededError)) { // Must be executed after processing of nested mappings if (this.secured && isRootMapping()) { throw new UnsupportedOperationException("Verification of authorization token is not supported " + "in root list mapping. Please create SINGLE root mapping with nested list mapping."); } if (this.secured) { AuthTokens.verifyAuthToken(context, getConfig().getTokenAuthorizer(), getRootMappingPath(), paramsProvider, isRootMapping(), getPathSeparator()); } } ValidationResult validationRes = new ValidationResult(fieldMsgs, globalMsgs); Object boundObjects = getConfig().getCollectionBuilders().buildCollection(getConfig().getListMappingCollection(), getDataClass(), data); FormData<Object> formData = new FormData<Object>(boundObjects, validationRes); return (FormData<T>)formData; } private <U> Iterable<U> checkIterable(Object instance) { if (!(instance instanceof Iterable)) { throw new IllegalStateException("Collection for property " + propertyName + " is not iterable."); } return (Iterable<U>)instance; } @Override BasicFormMappingBuilder<T> fillInternal(FormData<T> editedObj, Location loc, RequestContext ctx) { final Location givenOrCfgLoc = getLocation(loc); List<FormMapping<T>> newMappings = new ArrayList<FormMapping<T>>(); Set<String> propNames = FormUtils.getPropertiesFromFields(this.fields); if (editedObj != null && editedObj.getData() != null) { Iterable<T> itColl = checkIterable(editedObj.getData()); int index = 0; for (Iterator<T> it = itColl.iterator(); it.hasNext(); ) { T dataAtIndex = it.next(); FormData<T> formDataAtIndex = new FormData<T>(dataAtIndex, editedObj.getValidationResult()); // Create filled nested mappings for current list index (data at current index) Map<String, FormMapping<?>> filledIndexedNestedMappings = indexAndFillNestedMappings(formDataAtIndex, givenOrCfgLoc, ctx); // Prepare values for mapping that is constructed for current list index. // Previously created filled nested mappings will be assigned to mapping for current list index. Map<String, Object> propValues = gatherPropertyValues(dataAtIndex, propNames, ctx); // Fill the fields of this mapping with prepared values for current list index Map<String, FormField<?>> filledFields = fillFields( propValues, editedObj.getValidationResult() != null ? editedObj.getValidationResult().getFieldMessages() : new LinkedHashMap<String, List<ConstraintViolationMessage>>(), index, givenOrCfgLoc); // Returning copy of this mapping (for current index) that is filled with form data, // but with single mapping type (for an index) and now without list mappings BasicFormMappingBuilder<T> builder = new BasicFormMappingBuilder<T>(this, filledFields, filledIndexedNestedMappings) .index(Integer.valueOf(index)) .order(index) .validationResult(formDataAtIndex.getValidationResult()) .filledObject(formDataAtIndex.getData()); builder.mappingType = MappingType.SINGLE; newMappings.add(builder.build(getConfig())); index++; } } // unindexed fields (that are only recipes for indexed fields) will not be part of filled form // as well as unindexed nested mappings -> empty maps are used: BasicFormMappingBuilder<T> builder = new BasicFormMappingBuilder<T>(this, Collections.unmodifiableMap(Collections.<String, FormField<?>>emptyMap()), Collections.unmodifiableMap(Collections.<String, FormMapping<?>>emptyMap())) .validationResult(editedObj != null ? editedObj.getValidationResult() : ValidationResult.empty) .filledObject(editedObj != null ? editedObj.getData() : null); builder.listOfMappings = Collections.unmodifiableList(newMappings); return builder; } @Override public List<FormMapping<T>> getList() { return this.listOfMappings; } @Override public BasicListFormMapping<T> withOrder(int order) { return new BasicListFormMapping<T>(this, order); } @Override public BasicListFormMapping<T> withParent(FormMapping<?> parent) { return new BasicListFormMapping<T>(this, parent); } @Override ValidationResult validate(Locale locale, Class<?> ... validationGroups) { Collection<ValidationResult> validationResults = new ArrayList<ValidationResult>(); List<FormMapping<T>> listMappings = getList(); for (int index = 0; index < listMappings.size(); index++) { validationResults.add(((BasicFormMapping<?>)listMappings.get(index)).validate(locale, validationGroups)); } return Clones.mergedValidationResults(validationResults); } Map<String, FormMapping<?>> indexAndFillNestedMappings(FormData<T> editedObj, Location loc, RequestContext ctx) { Map<String, FormMapping<?>> newNestedMappings = new LinkedHashMap<String, FormMapping<?>>(); for (Map.Entry<String, FormMapping<?>> e : this.nested.entrySet()) { // 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 formData = new FormData<Object>(data, editedObj.getValidationResult()); newNestedMappings.put(e.getKey(), e.getValue().fill(formData, loc, ctx)); } return newNestedMappings; } private List<FormMapping<T>> newListOfMappings(List<FormMapping<T>> listOfMappings) { return Collections.unmodifiableList(new ArrayList<FormMapping<T>>(listOfMappings)); } }