View Javadoc
1   /**
2    * This file Copyright (c) 2013-2015 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  
46  import java.util.ArrayList;
47  import java.util.Iterator;
48  import java.util.List;
49  import java.util.Locale;
50  
51  import org.apache.commons.lang3.StringUtils;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  import com.vaadin.data.Item;
56  import com.vaadin.data.Property;
57  import com.vaadin.data.util.PropertysetItem;
58  import com.vaadin.server.ErrorMessage;
59  import com.vaadin.ui.AbstractComponent;
60  import com.vaadin.ui.AbstractField;
61  import com.vaadin.ui.AbstractOrderedLayout;
62  import com.vaadin.ui.Component;
63  import com.vaadin.ui.CustomField;
64  import com.vaadin.ui.Field;
65  import com.vaadin.ui.HasComponents;
66  
67  /**
68   * Abstract implementation of {@link CustomField} used for multi fields components.<br>
69   * It expose generic methods allowing to: <br>
70   * - Build a {@link Field} based on a {@link ConfiguredFieldDefinition}. <br>
71   * - Retrieve the list of Fields contained into the main component <br>
72   * - Override Validate and get Error Message in order to include these call to the embedded Fields.<br>
73   *
74   * @param <T> Property Type linked to this Field.
75   * @param <D> FieldDefinition Implementation used by the implemented Field.
76   */
77  public abstract class AbstractCustomMultiField<D extends FieldDefinition, T> extends CustomField<T> {
78  
79      private static final Logger log = LoggerFactory.getLogger(AbstractCustomMultiField.class);
80  
81      protected final FieldFactoryFactory fieldFactoryFactory;
82  
83      /** @deprecated since 5.3.5 (actually unused way before that). Besides, fields should use i18nAuthoringSupport for internationalization. */
84      @Deprecated
85      protected final I18nContentSupport i18nContentSupport = null;
86      private final I18NAuthoringSupport i18nAuthoringSupport;
87  
88      protected final ComponentProvider componentProvider;
89      protected final D definition;
90      protected final Item relatedFieldItem;
91      protected AbstractOrderedLayout root;
92  
93      protected AbstractCustomMultiField(D definition, FieldFactoryFactory fieldFactoryFactory, ComponentProvider componentProvider, Item relatedFieldItem, I18NAuthoringSupport i18nAuthoringSupport) {
94          this.definition = definition;
95          this.fieldFactoryFactory = fieldFactoryFactory;
96          this.componentProvider = componentProvider;
97          this.relatedFieldItem = relatedFieldItem;
98          this.i18nAuthoringSupport = i18nAuthoringSupport;
99      }
100 
101     /**
102      * @deprecated since 5.3.5 removing i18nContentSupport dependency (actually unused way before that). Besides, fields should use i18nAuthoringSupport for internationalization.
103      */
104     @Deprecated
105     protected AbstractCustomMultiField(D definition, FieldFactoryFactory fieldFactoryFactory, I18nContentSupport i18nContentSupport, ComponentProvider componentProvider, Item relatedFieldItem) {
106         this(definition, fieldFactoryFactory, componentProvider, relatedFieldItem, componentProvider.getComponent(I18NAuthoringSupport.class));
107     }
108 
109     /**
110      * Initialize the fields based on the newValues.<br>
111      * Implemented logic should: <br>
112      * - remove all component from the root component. <br>
113      * - for every fieldValues value, add the appropriate field.<br>
114      * - add all others needed component (like add button...)
115      */
116     protected abstract void initFields(T fieldValues);
117 
118     /**
119      * Handle {@link info.magnolia.ui.api.i18n.I18NAuthoringSupport#i18nize(HasComponents, Locale)} events in order to refresh the field <br>
120      * and display the new property.
121      */
122     @Override
123     public void setLocale(Locale locale) {
124         if (root != null) {
125             initFields();
126         }
127         super.setLocale(locale);
128     }
129 
130     @SuppressWarnings("unchecked")
131     protected void initFields() {
132         T fieldValues = (T) getPropertyDataSource().getValue();
133         initFields(fieldValues);
134         // Update DataSource in order to handle the fields default values
135         if (relatedFieldItem instanceof ItemAdapter && ((ItemAdapter) relatedFieldItem).isNew()) {
136             getPropertyDataSource().setValue(getValue());
137         }
138     }
139 
140     /**
141      * Helper method to find propertyId for a given property within item datasource.
142      */
143     protected int findPropertyId(Item item, Property<?> property) {
144         Iterator<?> it = item.getItemPropertyIds().iterator();
145         while (it.hasNext()) {
146             Object pos = it.next();
147             if (pos.getClass().isAssignableFrom(Integer.class) && property == item.getItemProperty(pos)) {
148                 return (Integer) pos;
149             } else {
150                 log.debug("Property id {} is not an integer and as such property can't be located", pos);
151             }
152         }
153         return -1;
154     }
155 
156     /**
157      * Create a new {@link Field} based on a {@link FieldDefinition}.
158      */
159     protected Field<?> createLocalField(FieldDefinition fieldDefinition, Property<?> property, boolean setCaptionToNull) {
160 
161         // If the property holds an item, use this item directly for the field creation (doesn't apply to ProperysetItems)
162         FieldFactory fieldfactory = fieldFactoryFactory.createFieldFactory(fieldDefinition, holdsItem(property) ? property.getValue() : new NullItem());
163         fieldfactory.setComponentProvider(componentProvider);
164         // FIXME change i18n setting : MGNLUI-1548
165         if (fieldDefinition instanceof ConfiguredFieldDefinition) {
166             ((ConfiguredFieldDefinition) fieldDefinition).setI18nBasename(definition.getI18nBasename());
167         }
168         Field<?> field = fieldfactory.createField();
169 
170         // If the value property is not an Item but a property, set this property as datasource to the field
171         // and add a value change listener in order to propagate changes
172         if (!holdsItem(property)) {
173             if (property != null && property.getValue() != null) {
174                 field.setPropertyDataSource(property);
175             }
176             field.addValueChangeListener(selectionListener);
177         }
178 
179         if (field instanceof AbstractComponent) {
180             ((AbstractComponent) field).setImmediate(true);
181         }
182         // Set Caption if desired
183         if (setCaptionToNull) {
184             field.setCaption(null);
185         } else if (StringUtils.isNotBlank(fieldDefinition.getLabel())) {
186             field.setCaption(fieldDefinition.getLabel());
187         }
188 
189         field.setWidth(100, Unit.PERCENTAGE);
190 
191         // propagate locale to complex fields further down, in case they have i18n-aware fields
192         if (field instanceof AbstractCustomMultiField) {
193             ((AbstractCustomMultiField) field).setLocale(getLocale());
194         }
195         // i18nize field entry — crazily depends upon component hierarchy so we must do this after field is attached
196         if (fieldDefinition.isI18n()) {
197             field.addAttachListener(new AttachListener() {
198                 @Override
199                 public void attach(AttachEvent event) {
200                     i18nAuthoringSupport.i18nize(((Component) event.getSource()).getParent(), getLocale());
201                 }
202             });
203         }
204 
205         return field;
206     }
207 
208     boolean holdsItem(Property<?> property) {
209         return property != null && property.getValue() instanceof Item && !(property.getValue() instanceof PropertysetItem);
210     }
211 
212     /**
213      * Listener used to update the Data source property.
214      */
215     protected Property.ValueChangeListener selectionListener = new ValueChangeListener() {
216         @SuppressWarnings("unchecked")
217         @Override
218         public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
219             fireValueChange(false);
220             // 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)
221             // but the setValue on the property is never called.
222             getPropertyDataSource().setValue(getValue());
223         }
224     };
225 
226     /**
227      * Utility method that return a list of Fields embedded into a root custom field.
228      *
229      * @param root
230      * @param onlyValid if set to true, return only the isValid() fields.
231      */
232     @SuppressWarnings("unchecked")
233     protected List<AbstractField<T>> getFields(HasComponents root, boolean onlyValid) {
234         Iterator<Component> it = root.iterator();
235         List<AbstractField<T>> fields = new ArrayList<AbstractField<T>>();
236         while (it.hasNext()) {
237             Component c = it.next();
238             if (c instanceof AbstractField) {
239                 if (!onlyValid || (onlyValid && ((AbstractField<T>) c).isValid())) {
240                     fields.add((AbstractField<T>) c);
241                 }
242             } else if (c instanceof HasComponents) {
243                 fields.addAll(getFields((HasComponents) c, onlyValid));
244             }
245         }
246         return fields;
247     }
248 
249     /**
250      * Validate all fields from the root container.
251      */
252     @Override
253     public boolean isValid() {
254         boolean isValid = true;
255         List<AbstractField<T>> fields = getFields(this, false);
256         for (AbstractField<T> field : fields) {
257             isValid = field.isValid();
258             if (!isValid) {
259                 return isValid;
260             }
261         }
262         return isValid;
263     }
264 
265     /**
266      * Get the error message.
267      */
268     @Override
269     public ErrorMessage getErrorMessage() {
270         ErrorMessage errorMessage = null;
271         List<AbstractField<T>> fields = getFields(this, false);
272         for (AbstractField<T> field : fields) {
273             errorMessage = field.getErrorMessage();
274             if (errorMessage != null) {
275                 return errorMessage;
276             }
277         }
278         return errorMessage;
279     }
280 
281     @Override
282     public boolean isEmpty() {
283         boolean isEmpty = false;
284         List<AbstractField<T>> fields = getFields(this, false);
285         for (AbstractField<T> field : fields) {
286             isEmpty = field.getValue() == null;
287             if (isEmpty) {
288                 return isEmpty;
289             }
290         }
291         return isEmpty;
292     }
293 
294 }