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.jcr.util.NodeUtil;
37  import info.magnolia.jcr.util.SessionUtil;
38  import info.magnolia.objectfactory.Components;
39  import info.magnolia.ui.api.context.UiContext;
40  import info.magnolia.ui.api.i18n.I18NAuthoringSupport;
41  import info.magnolia.ui.form.field.definition.SelectFieldDefinition;
42  import info.magnolia.ui.form.field.definition.SelectFieldOptionDefinition;
43  import info.magnolia.ui.form.field.transformer.UndefinedPropertyType;
44  
45  import java.util.ArrayList;
46  import java.util.Collections;
47  import java.util.Comparator;
48  import java.util.HashSet;
49  import java.util.Iterator;
50  import java.util.List;
51  
52  import javax.inject.Inject;
53  import javax.jcr.Node;
54  import javax.jcr.RepositoryException;
55  
56  import org.apache.commons.collections4.ComparatorUtils;
57  import org.apache.commons.lang3.StringUtils;
58  import org.apache.jackrabbit.commons.predicate.Predicate;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  import com.vaadin.server.Resource;
63  import com.vaadin.server.ThemeResource;
64  import com.vaadin.v7.data.Item;
65  import com.vaadin.v7.data.Property;
66  import com.vaadin.v7.data.util.IndexedContainer;
67  import com.vaadin.v7.ui.AbstractSelect;
68  import com.vaadin.v7.ui.AbstractSelect.ItemCaptionMode;
69  import com.vaadin.v7.ui.ComboBox;
70  
71  /**
72   * Creates and initializes a selection field based on a field definition.
73   *
74   * @param <D> type of definition
75   */
76  public class SelectFieldFactory<D extends SelectFieldDefinition> extends AbstractFieldFactory<D, Object> {
77  
78      private static final Logger log = LoggerFactory.getLogger(SelectFieldFactory.class);
79  
80      private List<String> initialSelectedKeys = new ArrayList<>();
81      private String optionValueName;
82      private String optionLabelName;
83      private final String optionIconName = SelectFieldDefinition.OPTION_ICONSRC_PROPERTY_NAME;
84      private boolean hasOptionIcon = false;
85      private boolean sortOptions = true;
86  
87      protected AbstractSelect select;
88  
89      @Inject
90      public SelectFieldFactory(D definition, Item relatedFieldItem, UiContext uiContext, I18NAuthoringSupport i18nAuthoringSupport) {
91          super(definition, relatedFieldItem, uiContext, i18nAuthoringSupport);
92      }
93  
94      /**
95       * @deprecated since 5.4.7 - use {@link #SelectFieldFactory(SelectFieldDefinition, Item, UiContext, I18NAuthoringSupport)} instead.
96       */
97      @Deprecated
98      public SelectFieldFactory(D definition, Item relatedFieldItem) {
99          this(definition, relatedFieldItem, null, Components.getComponent(I18NAuthoringSupport.class));
100     }
101 
102     @Override
103     protected AbstractSelect createFieldComponent() {
104         // Get name of the Label and Value property.
105         optionValueName = definition.getValueProperty();
106         optionLabelName = definition.getLabelProperty();
107         sortOptions = definition.isSortOptions();
108 
109         select = createSelectionField();
110         select.setContainerDataSource(buildOptions());
111         select.setNullSelectionAllowed(false);
112         select.setMultiSelect(false);
113         select.setNewItemsAllowed(false);
114         if (select instanceof ComboBox) {
115             ((ComboBox) select).setFilteringMode(definition.getFilteringMode());
116             ((ComboBox) select).setTextInputAllowed(definition.isTextInputAllowed());
117             ((ComboBox) select).setPageLength(definition.getPageLength());
118         }
119         select.setItemCaptionMode(ItemCaptionMode.PROPERTY);
120         select.setItemCaptionPropertyId(optionLabelName);
121 
122         return select;
123     }
124 
125     /**
126      * Used to initialize the desired subclass of AbstractSelect field component. Subclasses can override this method.
127      */
128     protected AbstractSelect createSelectionField() {
129         return new ComboBox();
130     }
131 
132     /**
133      * Create a IndexContainer containing the options.
134      * First element of the container is the Value.
135      * Second element is the Label.
136      * Third element is the Icon is defined.
137      * By default, options labels are sorted alphabetically (in ascending order) unless diversely specified by {@link SelectFieldDefinition#setSortOptions(boolean)}.
138      */
139     @SuppressWarnings("unchecked")
140     private IndexedContainer buildOptions() {
141         IndexedContainer optionContainer = new IndexedContainer();
142 
143         List<SelectFieldOptionDefinition> options = getOptions();
144         sortOptions(options);
145 
146         if (!options.isEmpty()) {
147             Class<?> resolvedFieldType = getFieldType();
148             // same fallback as in DefaultPropertyUtil#getFieldTypeClass in case of UndefinedPropertyType—keep this compatibility for now
149             Class<?> fieldType = resolvedFieldType != UndefinedPropertyType.class ? resolvedFieldType : String.class;
150             optionContainer.addContainerProperty(optionValueName, fieldType, null);
151             optionContainer.addContainerProperty(optionLabelName, String.class, null);
152             if (hasOptionIcon) {
153                 optionContainer.addContainerProperty(optionIconName, Resource.class, null);
154             }
155             for (SelectFieldOptionDefinition option : options) {
156                 Object value = createTypedValue(option.getValue(), fieldType);
157                 Item item = optionContainer.addItem(value);
158                 item.getItemProperty(optionValueName).setValue(value);
159                 item.getItemProperty(optionLabelName).setValue(option.getLabel());
160                 if (StringUtils.isNotBlank(option.getIconSrc())) {
161                     item.getItemProperty(optionIconName).setValue(getIconResource(option));
162                 }
163             }
164         }
165         return optionContainer;
166     }
167 
168     private void sortOptions(List<SelectFieldOptionDefinition> options) {
169         if (sortOptions) {
170             if (definition.getComparatorClass() != null) {
171                 Comparator<SelectFieldOptionDefinition> comparator = initializeComparator(definition.getComparatorClass());
172                 Collections.sort(options, comparator);
173             } else {
174                 Collections.sort(options, new DefaultOptionComparator());
175             }
176         }
177     }
178 
179     protected Comparator<SelectFieldOptionDefinition> initializeComparator(Class<? extends Comparator<SelectFieldOptionDefinition>> comparatorClass) {
180         return getComponentProvider().newInstance(comparatorClass, item, definition, getFieldType());
181     }
182 
183     /**
184      * Get the list of SelectFieldOptionDefinition.
185      *
186      * If there is an explicitly configured option list in the definition - use it.
187      * Else, if path is not empty, build an option list based on the node referred to
188      * the path and property value.
189      * Else, if nothing is defined, return an empty list.
190      * <b>Default value and i18n of the Label is also part of the responsibility of this method.</b>
191      */
192     public List<SelectFieldOptionDefinition> getOptions() {
193         // Method body is kept inside #getSelectFieldOptionDefinition for compatibility
194         // TODO when deprecated method is removed, inline #getSelectFieldOptionDefinition here
195         return getSelectFieldOptionDefinition();
196     }
197 
198     /**
199      * @since 5.4.9 renamed to {@link #getOptions}
200      */
201     @Deprecated
202     public List<SelectFieldOptionDefinition> getSelectFieldOptionDefinition() {
203         List<SelectFieldOptionDefinition> res = new ArrayList<>();
204 
205         if (definition.getOptions() != null && !definition.getOptions().isEmpty()) {
206             for (SelectFieldOptionDefinition option : definition.getOptions()) {
207                 option.setValue(getValue(option));
208                 if (option.isSelected()) {
209                     initialSelectedKeys.add(getValue(option));
210                 }
211                 if (!hasOptionIcon && StringUtils.isNotBlank(option.getIconSrc())) {
212                     hasOptionIcon = true;
213                 }
214                 res.add(option);
215             }
216         } else if (StringUtils.isNotBlank(definition.getPath())) {
217             // Build an option based on the referred node.
218             buildRemoteOptions(res);
219         }
220 
221         return res;
222     }
223 
224     /**
225      * Default Implementation to retrieve an Icon.
226      * Sub class should override this method in order to retrieve
227      * others Resource.
228      */
229     public Resource getIconResource(SelectFieldOptionDefinition option) {
230         return new ThemeResource(option.getIconSrc());
231     }
232 
233     /**
234      * Backward compatibility.
235      * If value is null, <br>
236      * - get the Name as value.<br>
237      * - If Name is empty, set Label as value.
238      */
239     private String getValue(SelectFieldOptionDefinition option) {
240         if (StringUtils.isBlank(option.getValue())) {
241             if (StringUtils.isNotBlank(option.getName())) {
242                 return option.getName();
243             } else {
244                 return option.getLabel();
245             }
246         } else {
247             return option.getValue();
248         }
249     }
250 
251     /**
252      * If value is null but single-select field mandates a non-null value, then pick the first value as default.
253      */
254     @Override
255     protected Object createDefaultValue(Property property) {
256         Object defaultValue = super.createDefaultValue(property);
257         if (defaultValue == null) {
258             boolean shouldPreselectFirstValue = !select.isNullSelectionAllowed() && !select.isMultiSelect() && property.getValue() == null;
259             if (shouldPreselectFirstValue) {
260                 // sanity check — make sure there's a first value up for grabs
261                 if (select.getItemIds() != null && !select.getItemIds().isEmpty()) {
262                     defaultValue = select.getItemIds().iterator().next();
263                 }
264             }
265         }
266         return defaultValue;
267     }
268 
269     /**
270      * @return the set of configured {@linkplain SelectFieldOptionDefinition#isSelected() selected} options if the field supports multi-selection,
271      * otherwise just the first occurrence of such configured {@linkplain SelectFieldOptionDefinition#isSelected() selected} option.
272      */
273     protected Object getConfiguredDefaultValue() {
274         if (initialSelectedKeys.isEmpty()) {
275             return null;
276         }
277         if (select.isMultiSelect()) {
278             return new HashSet<>(initialSelectedKeys);
279         } else {
280             return initialSelectedKeys.get(0);
281         }
282     }
283 
284     /**
285      * Build options based on a remote Node.
286      * Simply get the remote Node, Iterate his child nodes and for every child
287      * try to get the Value and Label property.
288      * In addition create an ArrayList<SelectFieldOptionDefinition> representing this options.
289      */
290     private void buildRemoteOptions(List<SelectFieldOptionDefinition> res) {
291         Node parent = SessionUtil.getNode(definition.getRepository(), definition.getPath());
292         if (parent != null) {
293             try {
294                 // Get only relevant child nodes
295                 Iterable<Node> iterable = NodeUtil.getNodes(parent, createRemoteOptionFilterPredicate());
296                 Iterator<Node> iterator = iterable.iterator();
297                 // Iterate parent children
298                 while (iterator.hasNext()) {
299                     SelectFieldOptionDefinitionctFieldOptionDefinition.html#SelectFieldOptionDefinition">SelectFieldOptionDefinition option = new SelectFieldOptionDefinition();
300                     Node child = iterator.next();
301                     // Get Label and Value
302                     String label = getRemoteOptionsName(child, optionLabelName);
303                     String value = getRemoteOptionsValue(child, optionValueName);
304                     option.setLabel(label);
305                     option.setValue(value);
306 
307                     if (child.hasProperty(SelectFieldDefinition.OPTION_SELECTED_PROPERTY_NAME) && Boolean.parseBoolean(child.getProperty(SelectFieldDefinition.OPTION_SELECTED_PROPERTY_NAME).getString())) {
308                         option.setSelected(true);
309                         initialSelectedKeys.add(option.getValue());
310                     }
311                     if (child.hasProperty(SelectFieldDefinition.OPTION_NAME_PROPERTY_NAME)) {
312                         option.setName(child.getProperty(SelectFieldDefinition.OPTION_NAME_PROPERTY_NAME).getString());
313                     }
314                     if (child.hasProperty(SelectFieldDefinition.OPTION_ICONSRC_PROPERTY_NAME)) {
315                         option.setIconSrc(child.getProperty(SelectFieldDefinition.OPTION_ICONSRC_PROPERTY_NAME).getString());
316                         hasOptionIcon = true;
317                     }
318                     res.add(option);
319                 }
320             } catch (Exception e) {
321                 log.warn("Not able to build options based on option node " + parent.toString(), e);
322             }
323         }
324     }
325 
326     /**
327      * @return {@link Predicate} used to filter the remote children option nodes.
328      */
329     protected Predicate createRemoteOptionFilterPredicate() {
330         return NodeUtil.MAGNOLIA_FILTER;
331     }
332 
333     /**
334      * Get the specific node property. <br>
335      * If this property is not defined, return the node name.
336      * Expose this method in order to let subclass define their own implementation.
337      */
338     protected String getRemoteOptionsName(Node option, String propertyName) throws RepositoryException {
339         if (option.hasProperty(propertyName)) {
340             return option.getProperty(propertyName).getString();
341         } else {
342             return option.getName();
343         }
344     }
345 
346     /**
347      * Get the specific node property. <br>
348      * If this property is not defined, return the node name.
349      * Expose this method in order to let subclass define their own implementation.
350      */
351     protected String getRemoteOptionsValue(Node option, String propertyName) throws RepositoryException {
352         return getRemoteOptionsName(option, propertyName);
353     }
354 
355     /**
356      * A null safe comparator based on the label.
357      */
358     public static class DefaultOptionComparator implements Comparator<SelectFieldOptionDefinition> {
359 
360         @Override
361         public int compare(SelectFieldOptionDefinition./info/magnolia/ui/form/field/definition/SelectFieldOptionDefinition.html#SelectFieldOptionDefinition">SelectFieldOptionDefinition def, SelectFieldOptionDefinition otherDef) {
362             // Null safe comparison of Comparables. null is assumed to be less than a non-null value.
363             return ComparatorUtils.nullLowComparator(String.CASE_INSENSITIVE_ORDER).compare(def.getLabel(), otherDef.getLabel());
364         }
365     }
366 }