View Javadoc
1   /**
2    * This file Copyright (c) 2012-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.factory;
35  
36  import info.magnolia.cms.i18n.I18nContentSupport;
37  import info.magnolia.objectfactory.ComponentProvider;
38  import info.magnolia.ui.api.view.View;
39  import info.magnolia.ui.form.AbstractFormItem;
40  import info.magnolia.ui.form.field.definition.FieldDefinition;
41  import info.magnolia.ui.form.field.definition.TextFieldDefinition;
42  import info.magnolia.ui.form.field.transformer.TransformedProperty;
43  import info.magnolia.ui.form.field.transformer.Transformer;
44  import info.magnolia.ui.form.field.transformer.UndefinedPropertyType;
45  import info.magnolia.ui.form.field.transformer.basic.BasicTransformer;
46  import info.magnolia.ui.form.validator.definition.FieldValidatorDefinition;
47  import info.magnolia.ui.form.validator.factory.FieldValidatorFactory;
48  import info.magnolia.ui.form.validator.registry.FieldValidatorFactoryFactory;
49  import info.magnolia.ui.vaadin.integration.ItemAdapter;
50  import info.magnolia.ui.vaadin.integration.jcr.DefaultPropertyUtil;
51  
52  import java.util.Locale;
53  
54  import org.apache.commons.lang3.StringUtils;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  import com.vaadin.data.Item;
59  import com.vaadin.data.Property;
60  import com.vaadin.data.util.converter.AbstractStringToNumberConverter;
61  import com.vaadin.data.util.converter.Converter;
62  import com.vaadin.server.Sizeable.Unit;
63  import com.vaadin.ui.AbstractField;
64  import com.vaadin.ui.AbstractTextField;
65  import com.vaadin.ui.Component;
66  import com.vaadin.ui.CssLayout;
67  import com.vaadin.ui.Field;
68  import com.vaadin.ui.Label;
69  
70  /**
71   * Abstract FieldFactory implementations. This class handle all common attributes defined in {@link FieldDefinition} and binds Vaadin {@link Field} instances created
72   * by subclasses to the {@link Property} they will be reading and writing to.
73   *
74   * @param <D> definition type
75   * @param <T> field value type
76   */
77  public abstract class AbstractFieldFactory<D extends FieldDefinition, T> extends AbstractFormItem implements FieldFactory {
78      private static final Logger log = LoggerFactory.getLogger(AbstractFieldFactory.class);
79      protected Item item;
80      protected Field<T> field;
81      protected D definition;
82      private FieldValidatorFactoryFactory fieldValidatorFactoryFactory;
83      private ComponentProvider componentProvider;
84  
85      public AbstractFieldFactory(D definition, Item relatedFieldItem) {
86          this.definition = definition;
87          this.item = relatedFieldItem;
88      }
89  
90      @Override
91      public void setFieldValidatorFactoryFactory(FieldValidatorFactoryFactory fieldValidatorFactoryFactory) {
92          this.fieldValidatorFactoryFactory = fieldValidatorFactoryFactory;
93      }
94  
95      /**
96       * @deprecated This is deprecated since 5.3.4; {@link i18nContentSupport} was never used within any {@link FieldFactory}, rightfully so.
97       * If any, {@link info.magnolia.ui.api.i18n.I18NAuthoringSupport I18NAuthoringSupport} is the one that should be used.
98       */
99      @Override
100     @Deprecated
101     public void setI18nContentSupport(I18nContentSupport i18nContentSupport) {
102     }
103 
104     @Override
105     public Field<T> createField() {
106         if (field == null) {
107             // Create the Vaadin field
108             this.field = createFieldComponent();
109             if (field instanceof AbstractField && definition.getConverterClass() != null) {
110                 Converter<?, ?> converter = initializeConverter(definition.getConverterClass());
111                 ((AbstractField) field).setConverter(converter);
112             }
113 
114             Property<?> property = initializeProperty();
115 
116             // MGNLUI-1855 we need to assign converter for properties with type Long because otherwise Vaadin assigns incompatible StringToNumberConverter.
117             if (Long.class.equals(property.getType()) && field instanceof AbstractTextField) {
118                 ((AbstractTextField) field).setConverter(new StringToLongConverter());
119             }
120             // Set the created property with the default value as field Property datasource.
121             setPropertyDataSourceAndDefaultValue(property);
122 
123             if (StringUtils.isNotBlank(definition.getStyleName())) {
124                 this.field.addStyleName(definition.getStyleName());
125             }
126 
127             field.setWidth(100, Unit.PERCENTAGE);
128 
129             // Set label and required marker
130             if (StringUtils.isNotBlank(getFieldDefinition().getLabel())) {
131                 this.field.setCaption(getFieldDefinition().getLabel() + (getFieldDefinition().isRequired() ? "<span class=\"requiredfield\">*</span>" : ""));
132             }
133 
134             setConstraints();
135 
136         }
137         return this.field;
138     }
139 
140     /**
141      * Set the DataSource of the current field.<br>
142      * Set the default value if : <br>
143      * - the item is an instance of {@link ItemAdapter} and this is a new Item (Not yet stored in the related datasource).<br>
144      * - the item is not an instance of {@link ItemAdapter}.<br>
145      * 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>
146      */
147     public void setPropertyDataSourceAndDefaultValue(Property<?> property) {
148         this.field.setPropertyDataSource(property);
149 
150         if ((item instanceof ItemAdapter && ((ItemAdapter) item).isNew() && property.getValue() == null) || (!(item instanceof ItemAdapter) && property.getValue() == null)) {
151             setPropertyDataSourceDefaultValue(property);
152         }
153     }
154 
155     /**
156      * Set the Field default value is required.
157      */
158     protected void setPropertyDataSourceDefaultValue(Property property) {
159         Object defaultValue = createDefaultValue(property);
160         if (defaultValue != null && !definition.isReadOnly()) {
161             if (defaultValue.getClass().isAssignableFrom(property.getType())) {
162                 property.setValue(defaultValue);
163             } else {
164                 log.warn("Default value {} is not assignable to the field of type {}.", defaultValue, field.getPropertyDataSource().getType().getName());
165             }
166         }
167     }
168 
169     /**
170      * Create a typed default value.
171      */
172     protected Object createDefaultValue(Property<?> property) {
173         String defaultValue = definition.getDefaultValue();
174         if (StringUtils.isNotBlank(defaultValue)) {
175             return DefaultPropertyUtil.createTypedValue(property.getType(), defaultValue);
176         }
177         return null;
178     }
179 
180     @Override
181     public D getFieldDefinition() {
182         return this.definition;
183     }
184 
185     /**
186      * Implemented by subclasses to create and initialize the Vaadin Field instance to use.
187      */
188     protected abstract Field<T> createFieldComponent();
189 
190     @Override
191     public View getView() {
192         final CssLayout fieldView = new CssLayout();
193         fieldView.setStyleName("field-view");
194 
195         Label label = new Label();
196         label.setSizeUndefined();
197         label.setCaption(getFieldDefinition().getLabel());
198 
199         if (getFieldDefinition().getClass().isAssignableFrom(TextFieldDefinition.class)) {
200             final TextFieldDefinition textFieldDefinition = (TextFieldDefinition) getFieldDefinition();
201             if (textFieldDefinition.getRows() > 0) {
202                 label.addStyleName("textarea");
203             }
204         }
205         if (definition.getConverterClass() != null) {
206             Converter converter = initializeConverter(definition.getConverterClass());
207             label.setConverter(converter);
208         }
209 
210         Property<?> property = initializeProperty();
211 
212         label.setPropertyDataSource(property);
213 
214         fieldView.addComponent(label);
215 
216         return new View() {
217             @Override
218             public Component asVaadinComponent() {
219                 return fieldView;
220             }
221         };
222     }
223 
224     /**
225      * Initialize the property used as field's Datasource.<br>
226      * If no {@link Transformer} is configure to the field definition, use the default {@link BasicTransformer} <br>
227      */
228     @SuppressWarnings("unchecked")
229     private Property<?> initializeProperty() {
230         Class<? extends Transformer<?>> transformerClass = definition.getTransformerClass();
231 
232         if (transformerClass == null) {
233             // TODO explain why down cast
234             transformerClass = (Class<? extends Transformer<?>>) (Object) BasicTransformer.class;
235         }
236         Transformer<?> transformer = initializeTransformer(transformerClass);
237 
238         return new TransformedProperty(transformer);
239     }
240 
241     /**
242      * Exposed method used by field's factory to initialize the property {@link Transformer}.<br>
243      * This allows to add additional constructor parameter if needed.<br>
244      */
245     protected Transformer<?> initializeTransformer(Class<? extends Transformer<?>> transformerClass) {
246         return this.componentProvider.newInstance(transformerClass, item, definition, getFieldType());
247     }
248 
249     /**
250      * Exposed method used by field's factory to initialize the property {@link Converter}.<br>
251      * This allows to add additional constructor parameter if needed.<br>
252      */
253     protected Converter<?, ?> initializeConverter(Class<? extends Converter<?, ?>> converterClass) {
254         return this.componentProvider.newInstance(converterClass, item, definition, getFieldType());
255     }
256 
257 
258     /**
259      * Define the field property value type Class.<br>
260      * Return the value defined by the configuration ('type' property).<br>
261      * If this value is not defined, return the value of the overriding method {@link AbstractFieldFactory#getDefaultFieldType()}.<br>
262      * If this method is not override, return {@link UndefinedPropertyType}.<br>
263      * In this case, the {@link Transformer} will have the responsibility to define the property type.
264      */
265     protected Class<?> getFieldType() {
266         Class<?> type = getDefinitionType();
267         if (type == null) {
268             type = getDefaultFieldType();
269         }
270         return type;
271     }
272 
273     /**
274      * @return Class Type defined into the field definition or null if not defined.
275      */
276     protected Class<?> getDefinitionType() {
277         if (StringUtils.isNotBlank(definition.getType())) {
278             return DefaultPropertyUtil.getFieldTypeClass(definition.getType());
279         }
280         return null;
281     }
282 
283     /**
284      * Exposed method used by field's factory in order to define a default Field Type (decoupled from the definition).
285      */
286     protected Class<?> getDefaultFieldType() {
287         return UndefinedPropertyType.class;
288     }
289 
290     @Override
291     protected String getI18nBasename() {
292         return definition.getI18nBasename();
293     }
294 
295     /**
296      * Set all constraints linked to the field:
297      * Build Validation rules.
298      * Set Required field.
299      * Set Read Only.
300      */
301     private void setConstraints() {
302         // Set Validation
303         for (FieldValidatorDefinition validatorDefinition : definition.getValidators()) {
304             FieldValidatorFactory validatorFactory = this.fieldValidatorFactoryFactory.createFieldValidatorFactory(validatorDefinition, item);
305             if (validatorFactory != null) {
306                 field.addValidator(validatorFactory.createValidator());
307             } else {
308                 log.warn("Not able to create Validation for the following definition {}", definition.toString());
309             }
310         }
311         // Set Required
312         if (definition.isRequired()) {
313             field.setInvalidCommitted(true);
314             field.setRequired(true);
315             field.setRequiredError(definition.getRequiredErrorMessage());
316         }
317 
318         // Set ReadOnly (field property has to be updated)
319         if (field.getPropertyDataSource() != null) {
320             field.getPropertyDataSource().setReadOnly(definition.isReadOnly());
321         }
322     }
323 
324     @Override
325     public void setComponentProvider(ComponentProvider componentProvider) {
326         this.componentProvider = componentProvider;
327     }
328 
329     protected ComponentProvider getComponentProvider() {
330         return componentProvider;
331     }
332 
333     /**
334      * The StringToLongConverter.<br>
335      * MGNLUI-1855 This should be handled by vaadin, but StringToNumberConverter throws conversion exception when used
336      * with a Long property in Vaadin 7.1. This should be fixed, unfortunately not before 7.2, so we need that converter
337      * for the time being.<br>
338      * As a result, this class will have a short life span, this is why it is kept private and deprecated.
339      */
340     @Deprecated
341     private static class StringToLongConverter extends AbstractStringToNumberConverter<Long> {
342 
343         @Override
344         public Long convertToModel(String value, Class<? extends Long> targetType, Locale locale) throws ConversionException {
345             Number n = convertToNumber(value, targetType, locale);
346             return n == null ? null : n.longValue();
347         }
348 
349         @Override
350         public Class<Long> getModelType() {
351             return Long.class;
352         }
353     }
354 
355 }