View Javadoc

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