View Javadoc
1   /**
2    * This file Copyright (c) 2013-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.form.field;
35  
36  import info.magnolia.cms.i18n.I18nContentSupport;
37  import info.magnolia.objectfactory.ComponentProvider;
38  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
39  import info.magnolia.ui.form.field.definition.ConfiguredFieldDefinition;
40  import info.magnolia.ui.form.field.definition.FieldDefinition;
41  import info.magnolia.ui.form.field.factory.FieldFactory;
42  import info.magnolia.ui.form.field.factory.FieldFactoryFactory;
43  import info.magnolia.ui.vaadin.integration.ItemAdapter;
44  import info.magnolia.ui.vaadin.integration.NullItem;
45  import info.magnolia.ui.vaadin.server.ErrorMessageUtil;
46  
47  import java.util.ArrayList;
48  import java.util.Iterator;
49  import java.util.List;
50  import java.util.Locale;
51  
52  import org.apache.commons.lang3.StringUtils;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  import com.vaadin.server.AbstractErrorMessage.ContentMode;
57  import com.vaadin.server.CompositeErrorMessage;
58  import com.vaadin.server.ErrorMessage;
59  import com.vaadin.server.UserError;
60  import com.vaadin.ui.AbstractOrderedLayout;
61  import com.vaadin.ui.Component;
62  import com.vaadin.ui.HasComponents;
63  import com.vaadin.v7.data.Item;
64  import com.vaadin.v7.data.Property;
65  import com.vaadin.v7.data.util.PropertysetItem;
66  import com.vaadin.v7.ui.AbstractField;
67  import com.vaadin.v7.ui.CustomField;
68  import com.vaadin.v7.ui.Field;
69  
70  /**
71   * Abstract implementation of {@link CustomField} used for multi fields components.<br>
72   * It expose generic methods allowing to: <br>
73   * - Build a {@link Field} based on a {@link ConfiguredFieldDefinition}. <br>
74   * - Retrieve the list of Fields contained into the main component <br>
75   * - Override Validate and get Error Message in order to include these call to the embedded Fields.<br>
76   *
77   * @param <T> Property Type linked to this Field.
78   * @param <D> FieldDefinition Implementation used by the implemented Field.
79   */
80  public abstract class AbstractCustomMultiField<D extends FieldDefinition, T> extends CustomField<T> {
81  
82      private static final Logger log = LoggerFactory.getLogger(AbstractCustomMultiField.class);
83  
84      protected final FieldFactoryFactory fieldFactoryFactory;
85  
86      /** @deprecated since 5.3.5 (actually unused way before that). Besides, fields should use i18nAuthoringSupport for internationalization. */
87      @Deprecated
88      protected final I18nContentSupport i18nContentSupport = null;
89  
90      protected final ComponentProvider componentProvider;
91      protected final D definition;
92      protected final Item relatedFieldItem;
93      protected AbstractOrderedLayout root;
94  
95      protected AbstractCustomMultiField(D definition, FieldFactoryFactory fieldFactoryFactory, ComponentProvider componentProvider, Item relatedFieldItem, I18NAuthoringSupport i18nAuthoringSupport) {
96          this.definition = definition;
97          this.fieldFactoryFactory = fieldFactoryFactory;
98          this.componentProvider = componentProvider;
99          this.relatedFieldItem = relatedFieldItem;
100     }
101 
102     /**
103      * @deprecated since 5.3.5 removing i18nContentSupport dependency (actually unused way before that). Besides, fields should use i18nAuthoringSupport for internationalization.
104      */
105     @Deprecated
106     protected AbstractCustomMultiField(D definition, FieldFactoryFactory fieldFactoryFactory, I18nContentSupport i18nContentSupport, ComponentProvider componentProvider, Item relatedFieldItem) {
107         this(definition, fieldFactoryFactory, componentProvider, relatedFieldItem, componentProvider.getComponent(I18NAuthoringSupport.class));
108     }
109 
110     /**
111      * Initialize the fields based on the newValues.<br>
112      * Implemented logic should: <br>
113      * - remove all component from the root component. <br>
114      * - for every fieldValues value, add the appropriate field.<br>
115      * - add all others needed component (like add button...)
116      */
117     protected abstract void initFields(T fieldValues);
118 
119     /**
120      * Handle {@link info.magnolia.ui.api.i18n.I18NAuthoringSupport#i18nize(HasComponents, Locale)} events in order to refresh the field <br>
121      * and display the new property.
122      */
123     @Override
124     public void setLocale(Locale locale) {
125         if (root != null) {
126             initFields();
127         }
128         super.setLocale(locale);
129     }
130 
131     @SuppressWarnings("unchecked")
132     protected void initFields() {
133         T fieldValues = (T) getPropertyDataSource().getValue();
134         initFields(fieldValues);
135         // Update DataSource in order to handle the fields default values
136         if (relatedFieldItem instanceof ItemAdapter/../../info/magnolia/ui/vaadin/integration/ItemAdapter.html#ItemAdapter">ItemAdapter && ((ItemAdapter) relatedFieldItem).isNew() && !definition.isReadOnly()) {
137             getPropertyDataSource().setValue(getValue());
138         }
139     }
140 
141     /**
142      * Helper method to find propertyId for a given property within item datasource.
143      */
144     protected int findPropertyId(Item item, Property<?> property) {
145         Iterator<?> it = item.getItemPropertyIds().iterator();
146         while (it.hasNext()) {
147             Object pos = it.next();
148             if (pos.getClass().isAssignableFrom(Integer.class) && property == item.getItemProperty(pos)) {
149                 return (Integer) pos;
150             } else {
151                 log.debug("Property id {} is not an integer and as such property can't be located", pos);
152             }
153         }
154         return -1;
155     }
156 
157     /**
158      * Create a new {@link Field} based on a {@link FieldDefinition}.
159      */
160     protected Field<?> createLocalField(FieldDefinition fieldDefinition, Property<?> property, boolean setCaptionToNull) {
161 
162         // If the property holds an item, use this item directly for the field creation (doesn't apply to ProperysetItems)
163         FieldFactory fieldfactory = fieldFactoryFactory.createFieldFactory(fieldDefinition, holdsItem(property) ? property.getValue() : new NullItem());
164         fieldfactory.setComponentProvider(componentProvider);
165         // FIXME change i18n setting : MGNLUI-1548
166         if (fieldDefinition instanceof ConfiguredFieldDefinition) {
167             ((ConfiguredFieldDefinition) fieldDefinition).setI18nBasename(definition.getI18nBasename());
168         }
169         Field<?> field = fieldfactory.createField();
170 
171         // If the value property is not an Item but a property, set this property as datasource to the field
172         // and add a value change listener in order to propagate changes
173         if (!holdsItem(property)) {
174             if (property != null && property.getValue() != null) {
175                 field.setPropertyDataSource(property);
176             }
177             field.addValueChangeListener(selectionListener);
178         }
179 
180         // Set Caption if desired
181         if (setCaptionToNull) {
182             field.setCaption(null);
183         } else if (StringUtils.isBlank(field.getCaption()) && StringUtils.isNotBlank(fieldDefinition.getLabel())) {
184             field.setCaption(fieldDefinition.getLabel());
185         }
186 
187         field.setWidth(100, Unit.PERCENTAGE);
188 
189         // propagate locale to complex fields further down, in case they have i18n-aware fields
190         if (field instanceof AbstractCustomMultiField) {
191             ((AbstractCustomMultiField) field).setLocale(getLocale());
192         }
193 
194         // Set read only based on the single field definition
195         field.setReadOnly(fieldDefinition.isReadOnly());
196 
197         return field;
198     }
199 
200     boolean holdsItem(Property<?> property) {
201         return property != null && property.getValue() instanceof Item && !(property.getValue() instanceof PropertysetItem);
202     }
203 
204     /**
205      * Listener used to update the Data source property.
206      */
207     protected Property.ValueChangeListener selectionListener = new ValueChangeListener() {
208         @SuppressWarnings("unchecked")
209         @Override
210         public void valueChange(com.vaadin.v7.data.Property.ValueChangeEvent event) {
211             fireValueChange(false);
212             // In case PropertysetItem is used as property set of field's property, in case an individual field is updated, the PropertysetItem is coherent (has also the changes)
213             // but the setValue on the property is never called.
214             getPropertyDataSource().setValue(getValue());
215         }
216     };
217 
218     /**
219      * Utility method that return a list of Fields embedded into a root custom field.
220      *
221      * @param root
222      * @param onlyValid if set to true, return only the isValid() fields.
223      */
224     @SuppressWarnings("unchecked")
225     protected List<AbstractField<T>> getFields(HasComponents root, boolean onlyValid) {
226         Iterator<Component> it = root.iterator();
227         List<AbstractField<T>> fields = new ArrayList<>();
228         while (it.hasNext()) {
229             Component c = it.next();
230             if (c instanceof AbstractField) {
231                 if (!onlyValid || (((AbstractField<T>) c).isValid())) {
232                     fields.add((AbstractField<T>) c);
233                 }
234             } else if (c instanceof HasComponents) {
235                 fields.addAll(getFields((HasComponents) c, onlyValid));
236             }
237         }
238         return fields;
239     }
240 
241     /**
242      * Validate all fields from the root container. Fields which are not visible are skipped. Stops at the first error encountered.
243      */
244     @Override
245     public boolean isValid() {
246         // first validate self
247         if (!super.isValid()) {
248             // TODO: This is temporary and when MGNLUI-3668 is fixed, should be deleted.
249             markAsDirty();
250             return false;
251         }
252 
253         List<AbstractField<T>> fields = getFields(this, false);
254         for (AbstractField<T> field : fields) {
255             if (!field.isVisible()) {
256                 continue;
257             }
258             if (!field.isValid()) {
259                 // TODO: This is temporary and when MGNLUI-3668 is fixed, should be deleted.
260                 markAsDirty();
261                 return false;
262             }
263         }
264         return true;
265     }
266 
267     /**
268      * Get the error message, if any. Aggregates all errors for all fields in this custom multi field.
269      */
270     @Override
271     public ErrorMessage getErrorMessage() {
272         List<ErrorMessage> errors = new ArrayList<>();
273         ErrorMessage errorMessage = super.getErrorMessage();
274         if (errorMessage != null) {
275             errors.add(errorMessage);
276         }
277         List<AbstractField<T>> fields = getFields(this, false);
278 
279         for (AbstractField<T> field : fields) {
280             // skip non visible fields or validation will pass (see this class isValid() method) but an annoying error message will display just for a moment (e.g. before a form is closed).
281             if (!field.isVisible()) {
282                 continue;
283             }
284             errorMessage = field.getErrorMessage();
285             if (errorMessage != null) {
286                 String errorAsString = getFormattedHtmlAggregatedErrorMessage(errorMessage, field.getCaption());
287                 errors.add(new UserError(errorAsString, ContentMode.HTML, errorMessage.getErrorLevel()));
288             }
289         }
290         return errors.isEmpty() ? null : new CompositeErrorMessage(errors);
291     }
292 
293     /**
294      * For a custom multi field <em>empty</em> means it contains no sub-fields <em>OR</em> at least one of its sub-fields <code>isEmpty()</code> method returns <code>true</code>.
295      */
296     @Override
297     public boolean isEmpty() {
298         List<AbstractField<T>> fields = getFields(this, false);
299         if (fields.isEmpty()) {
300             return true;
301         }
302         for (AbstractField<T> field : fields) {
303             if (!field.isVisible()) {
304                 continue;
305             }
306             if (field.isEmpty()) {
307                 return true;
308             }
309         }
310         return false;
311     }
312 
313     private String getFormattedHtmlAggregatedErrorMessage(final ErrorMessage message, final String caption) {
314         return (StringUtils.isNotBlank(caption) ? caption + ": " : "") + StringUtils.join(ErrorMessageUtil.getCausesMessages(message), ", ");
315     }
316 }