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