View Javadoc
1   /**
2    * This file Copyright (c) 2012-2016 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.factory;
35  
36  import info.magnolia.cms.i18n.I18nContentSupport;
37  import info.magnolia.objectfactory.Classes;
38  import info.magnolia.objectfactory.ComponentProvider;
39  import info.magnolia.objectfactory.Components;
40  import info.magnolia.ui.api.app.SubAppContext;
41  import info.magnolia.ui.api.context.UiContext;
42  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
43  import info.magnolia.ui.api.view.View;
44  import info.magnolia.ui.form.AbstractFormItem;
45  import info.magnolia.ui.form.field.definition.FieldDefinition;
46  import info.magnolia.ui.form.field.definition.TextFieldDefinition;
47  import info.magnolia.ui.form.field.transformer.TransformedProperty;
48  import info.magnolia.ui.form.field.transformer.Transformer;
49  import info.magnolia.ui.form.field.transformer.UndefinedPropertyType;
50  import info.magnolia.ui.form.field.transformer.basic.BasicTransformer;
51  import info.magnolia.ui.form.validator.definition.FieldValidatorDefinition;
52  import info.magnolia.ui.form.validator.factory.FieldValidatorFactory;
53  import info.magnolia.ui.form.validator.registry.FieldValidatorFactoryFactory;
54  import info.magnolia.ui.vaadin.integration.ItemAdapter;
55  import info.magnolia.ui.vaadin.integration.jcr.DefaultProperty;
56  import info.magnolia.ui.vaadin.integration.jcr.DefaultPropertyUtil;
57  import info.magnolia.util.EnumCaseInsensitive;
58  
59  import java.util.Locale;
60  
61  import javax.inject.Inject;
62  
63  import org.apache.commons.lang3.StringUtils;
64  import org.slf4j.Logger;
65  import org.slf4j.LoggerFactory;
66  
67  import com.vaadin.data.Item;
68  import com.vaadin.data.Property;
69  import com.vaadin.data.util.BeanItem;
70  import com.vaadin.data.util.converter.Converter;
71  import com.vaadin.data.util.converter.ConverterUtil;
72  import com.vaadin.server.Sizeable.Unit;
73  import com.vaadin.ui.AbstractField;
74  import com.vaadin.ui.Component;
75  import com.vaadin.ui.CssLayout;
76  import com.vaadin.ui.Field;
77  import com.vaadin.ui.Label;
78  
79  /**
80   * Abstract FieldFactory implementations. This class handle all common attributes defined in {@link FieldDefinition} and binds Vaadin {@link Field} instances created
81   * by subclasses to the {@link Property} they will be reading and writing to.
82   *
83   * @param <D> definition type
84   * @param <T> field value type
85   */
86  public abstract class AbstractFieldFactory<D extends FieldDefinition, T> extends AbstractFormItem implements FieldFactory {
87  
88      private static final Logger log = LoggerFactory.getLogger(AbstractFieldFactory.class);
89  
90      protected Field<T> field;
91      protected D definition;
92      protected Item item;
93  
94      private FieldValidatorFactoryFactory fieldValidatorFactoryFactory;
95      private I18NAuthoringSupport i18NAuthoringSupport;
96      private ComponentProvider componentProvider;
97      private UiContext uiContext;
98      private Locale locale;
99      private Converter<?, ?> converter;
100 
101     @Inject
102     public AbstractFieldFactory(D definition, Item relatedFieldItem, UiContext uiContext, I18NAuthoringSupport i18NAuthoringSupport) {
103         this.definition = definition;
104         this.item = relatedFieldItem;
105         this.uiContext = uiContext;
106         this.i18NAuthoringSupport = i18NAuthoringSupport;
107     }
108 
109     /**
110      * @deprecated since 5.4.2 - use {@link #AbstractFieldFactory(FieldDefinition, Item, UiContext, I18NAuthoringSupport)} instead.
111      */
112     @Deprecated
113     public AbstractFieldFactory(D definition, Item relatedFieldItem) {
114         // Since we can't use Components utility for retreiving UiContext - leave it as null for the time being
115         // and get it via component provider when it is set (#setComponentProvider(..) method)
116         this(definition, relatedFieldItem, null, Components.getComponent(I18NAuthoringSupport.class));
117     }
118 
119     @Override
120     public void setFieldValidatorFactoryFactory(FieldValidatorFactoryFactory fieldValidatorFactoryFactory) {
121         this.fieldValidatorFactoryFactory = fieldValidatorFactoryFactory;
122     }
123 
124     /**
125      * @deprecated This is deprecated since 5.3.4; {@link I18nContentSupport} was never used within any {@link FieldFactory}, rightfully so.
126      * If any, {@link info.magnolia.ui.api.i18n.I18NAuthoringSupport I18NAuthoringSupport} is the one that should be used.
127      */
128     @Override
129     @Deprecated
130     public void setI18nContentSupport(I18nContentSupport i18nContentSupport) {
131     }
132 
133     @Override
134     public Field<T> createField() {
135         if (locale == null) {
136             if (uiContext instanceof SubAppContext) {
137                 final Locale authoringLocale = ((SubAppContext) uiContext).getAuthoringLocale();
138                 setLocale(authoringLocale == null ? i18NAuthoringSupport.getDefaultLocale(item) : authoringLocale);
139             }
140         }
141 
142         if (field == null) {
143             // Create the Vaadin field
144             this.field = createFieldComponent();
145 
146             if (field instanceof AbstractField) {
147                 final AbstractField field = (AbstractField) this.field;
148                 if (definition.getConverterClass() != null) {
149                     Converter<?, ?> converter = initializeConverter(definition.getConverterClass());
150                     if (!field.getType().isAssignableFrom(converter.getModelType())) {
151                         // only set converter if field doesn't support the model type
152                         // for example text-fields don't support numbers, while selects support anything
153                         field.setConverter(converter);
154                     }
155                 }
156                 field.setLocale(locale);
157             }
158 
159             Property<?> property = initializeProperty();
160             // Set the created property with the default value as field Property datasource.
161             setPropertyDataSourceAndDefaultValue(property);
162 
163             if (StringUtils.isNotBlank(definition.getStyleName())) {
164                 this.field.addStyleName(definition.getStyleName());
165             }
166 
167             field.setWidth(100, Unit.PERCENTAGE);
168 
169             setFieldCaption();
170             setConstraints();
171 
172         }
173         return this.field;
174     }
175 
176 
177     private void setFieldCaption() {
178         // Set field caption and append locale indicator if needed
179         if (StringUtils.isNotBlank(getFieldDefinition().getLabel())) {
180             String caption = getFieldDefinition().getLabel();
181 
182             if (locale != null && definition.isI18n()) {
183                 caption = String.format("%s (%s)", caption, locale.toString());
184             }
185 
186             this.field.setCaption(caption);
187         }
188     }
189 
190     /**
191      * Set the DataSource of the current field.<br>
192      * Set the default value if : <br>
193      * - the item is an instance of {@link ItemAdapter} and this is a new Item (Not yet stored in the related datasource).<br>
194      * - the item is not an instance of {@link ItemAdapter}.<br>
195      * In this case, the Item is a custom implementation of {@link Item} and we have no possibility to define if it is or not a new Item.<br>
196      */
197     public void setPropertyDataSourceAndDefaultValue(Property property) {
198         this.field.setPropertyDataSource(property);
199 
200         if ((item instanceof ItemAdapter && ((ItemAdapter) item).isNew() && property.getValue() == null) || (!(item instanceof ItemAdapter) && property.getValue() == null)) {
201             setPropertyDataSourceDefaultValue(property);
202         }
203     }
204 
205     /**
206      * Set the Field default value is required.
207      */
208     protected void setPropertyDataSourceDefaultValue(Property property) {
209         Object defaultValue = createDefaultValue(property);
210         if (defaultValue != null && !definition.isReadOnly()) {
211             if (property.getType().isAssignableFrom(defaultValue.getClass())) {
212                 property.setValue(defaultValue);
213             } else {
214                 log.warn("Default value {} cannot be assigned to property of type {}.", defaultValue, property.getType());
215             }
216         }
217     }
218 
219     protected Object createDefaultValue(Property property) {
220         Object defaultValue = getConfiguredDefaultValue();
221         Class<?> propertyType = property != null ? property.getType() : getFieldType();
222         return createTypedValue(defaultValue, propertyType);
223     }
224 
225     /**
226      * Create a typed value from an arbitrary value object to the given property type.
227      * This primarily favors JCR types conversion, but also supports broader conversions via configured converterClass.
228      */
229     protected Object createTypedValue(Object defaultValue, Class<?> propertyType) {
230         // favor JCR conversions first via DefaultPropertyUtil
231         if (defaultValue instanceof String && DefaultPropertyUtil.canConvertStringValue(propertyType)) {
232             return DefaultPropertyUtil.createTypedValue(propertyType, (String) defaultValue);
233 
234         } else if (defaultValue != null && definition.getConverterClass() != null) {
235             // Mirror AbstractField#convertToModel
236             // - expect configured value in english locale (resp. number format), no i18n in config
237             Converter converter = initializeConverter(definition.getConverterClass());
238             Class<?> modelType = propertyType != null ? propertyType : converter.getModelType();
239             Locale locale = Locale.ENGLISH;
240             try {
241                 defaultValue = ConverterUtil.convertToModel(defaultValue, modelType, converter, locale);
242             } catch (Converter.ConversionException e) {
243                 log.error("Default value {} could not be converted to property type {}.", defaultValue, propertyType, e);
244             }
245 
246         } else if (propertyType != null && propertyType.isEnum() && defaultValue instanceof String) {
247             Class<? extends Enum> enumType = (Class<? extends Enum>) propertyType;
248             EnumCaseInsensitive enumFinder = new EnumCaseInsensitive();
249             defaultValue = enumFinder.valueOf(enumType, (String) defaultValue);
250         }
251 
252         return defaultValue;
253     }
254 
255     protected Object getConfiguredDefaultValue() {
256         return definition.getDefaultValue();
257     }
258 
259     @Override
260     public D getFieldDefinition() {
261         return this.definition;
262     }
263 
264     /**
265      * Implemented by subclasses to create and initialize the Vaadin Field instance to use.
266      */
267     protected abstract Field<T> createFieldComponent();
268 
269     @Override
270     public View getView() {
271         final CssLayout fieldView = new CssLayout();
272         fieldView.setStyleName("field-view");
273 
274         Label label = new Label();
275         label.setSizeUndefined();
276         label.setCaption(getFieldDefinition().getLabel());
277 
278         if (getFieldDefinition().getClass().isAssignableFrom(TextFieldDefinition.class)) {
279             final TextFieldDefinition textFieldDefinition = (TextFieldDefinition) getFieldDefinition();
280             if (textFieldDefinition.getRows() > 0) {
281                 label.addStyleName("textarea");
282             }
283         }
284         if (definition.getConverterClass() != null) {
285             Converter converter = initializeConverter(definition.getConverterClass());
286             label.setConverter(converter);
287         }
288 
289         Property<?> property = initializeProperty();
290 
291         label.setPropertyDataSource(property);
292 
293         fieldView.addComponent(label);
294 
295         return new View() {
296             @Override
297             public Component asVaadinComponent() {
298                 return fieldView;
299             }
300         };
301     }
302 
303     /**
304      * Initialize the property used as field's Datasource.<br>
305      * If no {@link Transformer} is configure to the field definition, use the default {@link BasicTransformer} <br>
306      */
307     @SuppressWarnings("unchecked")
308     protected Property<T> initializeProperty() {
309         // exclude selectively for now; ultimately we might reduce that to JCR adapters only.
310         boolean useTransformers = !(item instanceof BeanItem);
311 
312         if (useTransformers) {
313             Class<? extends Transformer<?>> transformerClass = definition.getTransformerClass();
314             if (transformerClass == null) {
315                 // Down casting is needed due to API of the #initializeTransformer(Class<? extends Transformer<?>>)
316                 // the second wildcard in '? extends Transformer< --> ?>' is unnecessary and only forces compiler
317                 // to claim that BasicTransformer.class is not convertible into Class<? extends Transformer<?>>.
318                 // At runtime it all works due to type erasure.
319                 transformerClass = (Class<? extends Transformer<?>>) (Object) BasicTransformer.class;
320             }
321             Transformer<?> transformer = initializeTransformer(transformerClass);
322             transformer.setLocale(locale);
323             return new TransformedProperty(transformer);
324 
325         } else {
326             // return property straight from the Item for the field binding, no assumption on conversion/saving strategy here.
327             Property property = item.getItemProperty(definition.getName());
328             if (property == null) {
329                 log.warn(String.format("BeanItem doesn't have any property for id %s, returning default property", definition.getName()));
330                 Class<?> propertyType = DefaultPropertyUtil.getFieldTypeClass(definition.getType());
331                 property = new DefaultProperty(propertyType, null);
332                 item.addItemProperty(definition.getName(), property);
333             }
334             return property;
335         }
336     }
337 
338     /**
339      * Exposed method used by field's factory to initialize the property {@link Transformer}.<br>
340      * This allows to add additional constructor parameter if needed.<br>
341      */
342     protected Transformer<?> initializeTransformer(Class<? extends Transformer<?>> transformerClass) {
343         return this.componentProvider.newInstance(transformerClass, item, definition, getFieldType(), i18NAuthoringSupport);
344     }
345 
346     /**
347      * Exposed method used by field's factory to initialize the property {@link Converter}.<br>
348      * This allows to add additional constructor parameter if needed.<br>
349      */
350     protected Converter<?, ?> initializeConverter(Class<? extends Converter<?, ?>> converterClass) {
351         if (converter == null) {
352             converter = componentProvider.newInstance(converterClass);
353         }
354         return converter;
355     }
356 
357     /**
358      * Define the field property value type Class.<br>
359      * Return the value defined by the configuration ('type' property).<br>
360      * If this value is not defined, return the value of the overriding method {@link AbstractFieldFactory#getDefaultFieldType()}.<br>
361      * If this method is not override, return {@link UndefinedPropertyType}.<br>
362      * In this case, the {@link Transformer} will have the responsibility to define the property type.
363      */
364     protected Class<?> getFieldType() {
365         Class<?> type = getDefinitionType();
366         if (type == null && definition.getConverterClass() != null) {
367             Converter converter = initializeConverter(definition.getConverterClass());
368             type = converter.getModelType();
369         }
370         if (type == null) {
371             type = getDefaultFieldType();
372         }
373         return type;
374     }
375 
376     /**
377      * @return Class Type defined into the field definition or null if not defined.
378      */
379     protected Class<?> getDefinitionType() {
380         if (StringUtils.isNotBlank(definition.getType())) {
381             if (DefaultPropertyUtil.isKnownJcrTypeName(definition.getType())) {
382                 return DefaultPropertyUtil.getFieldTypeClass(definition.getType());
383             } else try {
384                 return Classes.getClassFactory().forName(definition.getType());
385             } catch (ClassNotFoundException e) {
386                 log.error("Unknown configured type {}", definition.getType());
387             }
388         }
389         return null;
390     }
391 
392     /**
393      * Exposed method used by field's factory in order to define a default Field Type (decoupled from the definition).
394      */
395     protected Class<?> getDefaultFieldType() {
396         return UndefinedPropertyType.class;
397     }
398 
399     @Override
400     protected String getI18nBasename() {
401         return definition.getI18nBasename();
402     }
403 
404     /**
405      * Set all constraints linked to the field:
406      * Build Validation rules.
407      * Set Required field.
408      * Set Read Only.
409      */
410     private void setConstraints() {
411         // Set Validation
412         for (FieldValidatorDefinition validatorDefinition : definition.getValidators()) {
413             FieldValidatorFactory validatorFactory = this.fieldValidatorFactoryFactory.createFieldValidatorFactory(validatorDefinition, item);
414             if (validatorFactory != null) {
415                 field.addValidator(validatorFactory.createValidator());
416             } else {
417                 log.warn("Not able to create Validation for the following definition {}", definition.toString());
418             }
419         }
420         // Set Required
421         if (definition.isRequired()) {
422             field.setInvalidCommitted(true);
423             field.setRequired(true);
424             field.setRequiredError(definition.getRequiredErrorMessage());
425         }
426 
427         // Set ReadOnly (field property has to be updated)
428         if (field.getPropertyDataSource() != null) {
429             field.getPropertyDataSource().setReadOnly(definition.isReadOnly());
430         }
431     }
432 
433     @Override
434     public void setComponentProvider(ComponentProvider componentProvider) {
435         this.componentProvider = componentProvider;
436         if (uiContext == null) {
437             uiContext = componentProvider.getComponent(UiContext.class);
438         }
439     }
440 
441     protected ComponentProvider getComponentProvider() {
442         return componentProvider;
443     }
444 
445     public void setLocale(Locale locale) {
446         this.locale = locale;
447     }
448 
449     public Locale getLocale() {
450         return locale;
451     }
452 }