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