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