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