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