1. Project Clover database Sat Feb 2 2019 06:45:20 CET
  2. Package com.xpn.xwiki.objects.classes

File ListClass.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart8.png
56% of files have more coverage

Code metrics

134
296
51
2
966
609
136
0.46
5.8
25.5
2.67

Classes

Class Line # Actions
ListClass 53 286 0% 130 133
0.71274371.3%
ListClass.MapComparator 770 10 0% 6 6
0.666666766.7%
 

Contributing tests

This file is covered by 128 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package com.xpn.xwiki.objects.classes;
21   
22    import java.util.ArrayList;
23    import java.util.Arrays;
24    import java.util.Collections;
25    import java.util.Comparator;
26    import java.util.LinkedHashMap;
27    import java.util.LinkedList;
28    import java.util.List;
29    import java.util.Map;
30   
31    import org.apache.commons.lang3.StringUtils;
32    import org.apache.ecs.xhtml.input;
33    import org.apache.ecs.xhtml.option;
34    import org.apache.ecs.xhtml.select;
35    import org.dom4j.Element;
36    import org.hibernate.collection.PersistentCollection;
37    import org.xwiki.model.reference.EntityReference;
38    import org.xwiki.xar.internal.property.ListXarObjectPropertySerializer;
39    import org.xwiki.xml.XMLUtils;
40   
41    import com.xpn.xwiki.XWikiContext;
42    import com.xpn.xwiki.doc.merge.MergeConfiguration;
43    import com.xpn.xwiki.doc.merge.MergeResult;
44    import com.xpn.xwiki.internal.xml.XMLAttributeValueFilter;
45    import com.xpn.xwiki.objects.BaseCollection;
46    import com.xpn.xwiki.objects.BaseProperty;
47    import com.xpn.xwiki.objects.DBStringListProperty;
48    import com.xpn.xwiki.objects.ListProperty;
49    import com.xpn.xwiki.objects.StringListProperty;
50    import com.xpn.xwiki.objects.StringProperty;
51    import com.xpn.xwiki.objects.meta.PropertyMetaClass;
52   
 
53    public abstract class ListClass extends PropertyClass
54    {
55    /**
56    * @since 6.2RC1
57    */
58    public static final String DISPLAYTYPE_INPUT = "input";
59   
60    /**
61    * @since 6.2RC1
62    */
63    public static final String DISPLAYTYPE_RADIO = "radio";
64   
65    /**
66    * @since 6.2RC1
67    */
68    public static final String DISPLAYTYPE_CHECKBOX = "checkbox";
69   
70    /**
71    * @since 6.2RC1
72    */
73    public static final String DISPLAYTYPE_SELECT = "select";
74   
75    /**
76    * Default separator/delimiter to use to split or join a list stored as a string. Not to be confused with
77    * {@link #getSeparator()} and {@link #getSeparator()} which are used only for UI view and edit operations.
78    *
79    * @since 7.0M2
80    */
81    public static final String DEFAULT_SEPARATOR = "|";
82   
83    /**
84    * Used to escape a separator character inside a string serialized list item.
85    *
86    * @since 7.0M2
87    */
88    public static final char SEPARATOR_ESCAPE = '\\';
89   
90    /**
91    * @since 10.11RC1
92    */
93    public static final String FREE_TEXT_DISCOURAGED = "discouraged";
94   
95    /**
96    * @since 10.11RC1
97    */
98    public static final String FREE_TEXT_FORBIDDEN = "forbidden";
99   
100    /**
101    * @since 10.11RC1
102    */
103    public static final String FREE_TEXT_ALLOWED = "allowed";
104   
105    private static final String XCLASSNAME = "list";
106   
107    /**
108    * Regex used to split lists stored in a string. Supports escaped separators inside values. The individually
109    * regex-escaped separators string needs to be passed as parameter.
110    */
111    private static final String LIST_ITEM_SEPARATOR_REGEX_FORMAT = "(?<!\\\\)[%s]";
112   
113    /**
114    * Regex used to unescape separators inside the actual values of the list. The individually regex-escaped separators
115    * string needs to be passed as parameter.
116    */
117    private static final String ESCAPED_SEPARATORS_REGEX_FORMAT = "\\%s([%s])";
118   
119    /**
120    * Regex used to find unescaped separators in a list item's value. Regex-escaped separators string needs to be
121    * passed as parameter.
122    */
123    private static final String UNESCAPED_SEPARATORS_REGEX_FORMAT = "([%s])";
124   
125    /**
126    * Replacement string used to escaped a separator found by the String.replace regex.
127    */
128    private static final String UNESCAPED_SEPARATOR_REPLACEMENT = String.format("\\%s$1", SEPARATOR_ESCAPE);
129   
 
130  28991 toggle public ListClass(String name, String prettyname, PropertyMetaClass wclass)
131    {
132  28991 super(name, prettyname, wclass);
133  28991 setRelationalStorage(false);
134  28991 setDisplayType(DISPLAYTYPE_SELECT);
135  28991 setMultiSelect(false);
136  28991 setSize(1);
137  28991 setSeparator(" ");
138  28991 setCache(false);
139    }
140   
 
141  0 toggle public ListClass(PropertyMetaClass wclass)
142    {
143  0 this(XCLASSNAME, "List", wclass);
144    }
145   
 
146  0 toggle public ListClass()
147    {
148  0 this(null);
149    }
150   
151    /**
152    * @return a string of separator characters used to split/deserialize an input string coming from the UI (filled by
153    * the user) that represents a serialized list
154    * @see #displayEdit(StringBuffer, String, String, BaseCollection, XWikiContext)
155    * @see #fromString(String)
156    */
 
157  767 toggle public String getSeparators()
158    {
159  767 String separators = getStringValue("separators");
160  767 if (separators == null || separators.equals("")) {
161  37 separators = "|,";
162    }
163  767 return separators;
164    }
165   
166    /**
167    * @param separators a string of characters used to split/deserialize an input string coming from the UI (filled by
168    * the user) that represents a serialized list
169    */
 
170  22426 toggle public void setSeparators(String separators)
171    {
172  22426 setStringValue("separators", separators);
173    }
174   
 
175  3415 toggle public String getDisplayType()
176    {
177  3415 return getStringValue("displayType");
178    }
179   
 
180  34742 toggle public void setDisplayType(String type)
181    {
182  34742 setStringValue("displayType", type);
183    }
184   
 
185  2901 toggle public String getSort()
186    {
187  2901 return getStringValue("sort");
188    }
189   
 
190  9 toggle public void setSort(String sort)
191    {
192  9 setStringValue("sort", sort);
193    }
194   
 
195  1160 toggle public int getSize()
196    {
197  1160 return getIntValue("size");
198    }
199   
 
200  41334 toggle public void setSize(int size)
201    {
202  41334 setIntValue("size", size);
203    }
204   
 
205  386 toggle public boolean isCache()
206    {
207  386 return (getIntValue("cache") == 1);
208    }
209   
 
210  28993 toggle public void setCache(boolean cache)
211    {
212  28993 setIntValue("cache", cache ? 1 : 0);
213    }
214   
 
215  143124 toggle public boolean isMultiSelect()
216    {
217  143124 return (getIntValue("multiSelect") == 1);
218    }
219   
 
220  37327 toggle public void setMultiSelect(boolean multiSelect)
221    {
222  37327 setIntValue("multiSelect", multiSelect ? 1 : 0);
223    }
224   
 
225  75288 toggle public boolean isRelationalStorage()
226    {
227  75288 return (getIntValue("relationalStorage") == 1);
228    }
229   
230    /**
231    * @param relationalStorage if false, the list items will be concatenated into a VARCHAR column on a single row.
232    * Otherwise, items are stored in their own entries in the database. In most cases, this property should
233    * have the same value as the {@code multiSelect} property
234    */
 
235  36060 toggle public void setRelationalStorage(boolean relationalStorage)
236    {
237  36060 setIntValue("relationalStorage", relationalStorage ? 1 : 0);
238    }
239   
 
240  110 toggle public boolean isPicker()
241    {
242  110 return (getIntValue("picker") == 1);
243    }
244   
 
245  3520 toggle public void setPicker(boolean picker)
246    {
247  3520 setIntValue("picker", picker ? 1 : 0);
248    }
249   
250    /**
251    * @return a string (usually just 1 character long) used to join this list's items when displaying it in the UI in
252    * view mode.
253    * @see #displayView(StringBuffer, String, String, BaseCollection, XWikiContext)
254    */
 
255  5400 toggle public String getSeparator()
256    {
257  5400 return getStringValue("separator");
258    }
259   
260    /**
261    * @param separator a string (usually just 1 character long) used to join this list's items when displaying it in
262    * the UI in view mode.
263    */
 
264  30424 toggle public void setSeparator(String separator)
265    {
266  30424 setStringValue("separator", separator);
267    }
268   
269    /**
270    * @return the default value used in the select editor
271    * @since 10.9
272    * @since 10.8.1
273    */
 
274  1007 toggle public String getDefaultValue()
275    {
276  1007 return getStringValue("defaultValue");
277    }
278   
279    /**
280    * @param separator the default value used in the select editor
281    * @since 10.9
282    * @since 10.8.1
283    */
 
284  437 toggle public void setDefaultValue(String separator)
285    {
286  437 setStringValue("defaultValue", separator);
287    }
288   
289   
290    /**
291    * @return the value of freeText (forbidden, discouraged or allowed)
292    * @since 10.11RC1
293    */
 
294  0 toggle public String getFreeText()
295    {
296  0 return getStringValue("freeText");
297    }
298   
299    /**
300    * @param type the value of freeText (forbidden, discouraged or allowed)
301    * @since 10.11RC1
302    */
 
303  571 toggle public void setFreeText(String type)
304    {
305  571 setStringValue("freeText", type);
306    }
307   
308    /**
309    * Convenience method, using {@value #DEFAULT_SEPARATOR} as separator and parsing key=value items.
310    *
311    * @param value the string holding a serialized list
312    * @return the list that was stored in the input string
313    * @see #getListFromString(String, String, boolean)
314    */
 
315  5800 toggle public static List<String> getListFromString(String value)
316    {
317  5800 return getListFromString(value, null, true);
318    }
319   
320    /**
321    * @param value the string holding a serialized list
322    * @param separators the separator characters (given as a string) used to delimit the list's items inside the input
323    * string. These separators can also be present, in escaped ({@value #SEPARATOR_ESCAPE}) form, inside
324    * list items
325    * @param withMap set to true if the list's values contain map entries (key=value pairs) that should also be parsed.
326    * Only the keys are extracted from such list items
327    * @return the list that was stored in the input string
328    */
 
329  12463 toggle public static List<String> getListFromString(String value, String separators, boolean withMap)
330    {
331  12463 List<String> list = new ArrayList<>();
332  12463 if (value == null) {
333  0 return list;
334    }
335  12463 if (separators == null) {
336  5800 separators = DEFAULT_SEPARATOR;
337    }
338   
339    // Escape the list of separators individually to be safely used in regexes.
340  12462 String regexEscapedSeparatorsRegexPart =
341    SEPARATOR_ESCAPE + StringUtils.join(separators.toCharArray(), SEPARATOR_ESCAPE);
342   
343  12463 String escapedSeparatorsRegex =
344    String.format(ESCAPED_SEPARATORS_REGEX_FORMAT, SEPARATOR_ESCAPE, regexEscapedSeparatorsRegexPart);
345   
346    // Split the values and process each list item.
347  12463 String listItemSeparatorRegex =
348    String.format(LIST_ITEM_SEPARATOR_REGEX_FORMAT, regexEscapedSeparatorsRegexPart);
349  12463 String[] elements = value.split(listItemSeparatorRegex);
350  12462 for (String element : elements) {
351    // Adjacent separators are treated as one separator.
352  17358 if (StringUtils.isBlank(element)) {
353  6334 continue;
354    }
355   
356    // Unescape any escaped separator in the individual list item.
357  11025 String unescapedElement = element.replaceAll(escapedSeparatorsRegex, "$1");
358  11024 String item = unescapedElement;
359   
360    // Check if it is a map entry, i.e. "key=value"
361  11025 if (withMap && (unescapedElement.indexOf('=') != -1)) {
362    // Get just the key, ignore the value/label.
363  411 item = StringUtils.split(unescapedElement, '=')[0];
364    }
365   
366    // Ignore empty items.
367  11025 if (StringUtils.isNotBlank(item.trim())) {
368  11025 list.add(item);
369    }
370    }
371   
372  12463 return list;
373    }
374   
375    /**
376    * Convenience method, using {@value #DEFAULT_SEPARATOR} as separator.
377    *
378    * @param list the list to serialize
379    * @return a string representing a serialized list, delimited by the first separator character (from the ones inside
380    * the separators string). Separators inside list items are safely escaped ({@value #SEPARATOR_ESCAPE}).
381    * @see #getStringFromList(List, String)
382    */
 
383  2 toggle public static String getStringFromList(List<String> list)
384    {
385  2 return getStringFromList(list, null);
386    }
387   
388    /**
389    * @param list the list to serialize
390    * @param separators the separator characters (given as a string) used when the list was populated with values. The
391    * list's items can contain these separators in plain/unescaped form. The first separator character will
392    * be used to join the list in the output.
393    * @return a string representing a serialized list, delimited by the first separator character (from the ones inside
394    * the separators string). Separators inside list items are safely escaped ({@value #SEPARATOR_ESCAPE}).
395    */
 
396  4097 toggle public static String getStringFromList(List<String> list, String separators)
397    {
398  4097 if ((list instanceof PersistentCollection) && (!((PersistentCollection) list).wasInitialized())) {
399  0 return "";
400    }
401   
402  4097 if (separators == null) {
403  2 separators = DEFAULT_SEPARATOR;
404    }
405   
406    // Escape the list of separators individually to be safely used in regexes.
407  4097 String regexEscapedSeparatorsRegexPart =
408    SEPARATOR_ESCAPE + StringUtils.join(separators.toCharArray(), SEPARATOR_ESCAPE);
409   
410  4097 String unescapedSeparatorsRegex =
411    String.format(UNESCAPED_SEPARATORS_REGEX_FORMAT, regexEscapedSeparatorsRegexPart);
412   
413  4097 List<String> escapedValues = new ArrayList<>();
414  4097 for (String value : list) {
415  1232 String escapedValue = value.replaceAll(unescapedSeparatorsRegex, UNESCAPED_SEPARATOR_REPLACEMENT);
416  1232 escapedValues.add(escapedValue);
417    }
418   
419    // Use the first separator to join the list.
420  4097 return StringUtils.join(escapedValues, separators.charAt(0));
421    }
422   
 
423  33291 toggle public static Map<String, ListItem> getMapFromString(String value)
424    {
425  33291 Map<String, ListItem> map = new LinkedHashMap<>();
426  33291 if (value == null) {
427  0 return map;
428    }
429   
430  33291 String val = StringUtils.replace(value, SEPARATOR_ESCAPE + DEFAULT_SEPARATOR, "%PIPE%");
431  33291 String[] result = StringUtils.split(val, "|");
432  33291 for (String element2 : result) {
433  118737 String element = StringUtils.replace(element2, "%PIPE%", DEFAULT_SEPARATOR);
434  118737 if (element.indexOf('=') != -1) {
435  30339 String[] data = StringUtils.split(element, "=", 2);
436  30339 map.put(data[0], new ListItem(data[0], data[1]));
437    } else {
438  88398 map.put(element, new ListItem(element, element));
439    }
440    }
441  33291 return map;
442    }
443   
444    /**
445    * Used in {@link #displayEdit(StringBuffer, String, String, BaseCollection, XWikiContext)}.
446    *
447    * @param property a property to be used in an form input
448    * @return the text value to be used in an form input. If a {@link ListProperty} is passed, the list's separators
449    * defined by {@link #getSeparators()} are escaped for each list item and the items are joined by the first
450    * separator
451    * @see #getStringFromList(List, String)
452    */
 
453  110 toggle public String toFormString(BaseProperty property)
454    {
455  110 String result;
456  110 if (property instanceof ListProperty) {
457  110 ListProperty listProperty = (ListProperty) property;
458  110 result = ListClass.getStringFromList(listProperty.getList(), getSeparators());
459    } else {
460  0 result = property.toText();
461    }
462  110 return result;
463    }
464   
 
465  75268 toggle @Override
466    public BaseProperty newProperty()
467    {
468  75267 BaseProperty lprop;
469   
470  75268 if (isRelationalStorage() && isMultiSelect()) {
471  1000 lprop = new DBStringListProperty();
472  74268 } else if (isMultiSelect()) {
473  1626 lprop = new StringListProperty();
474    } else {
475  72642 lprop = new StringProperty();
476    }
477   
478  75268 lprop.setName(getName());
479  75268 return lprop;
480    }
481   
 
482  65143 toggle @Override
483    public BaseProperty fromString(String value)
484    {
485  65143 BaseProperty prop = newProperty();
486  65142 if (isMultiSelect()) {
487  555 ((ListProperty) prop).setList(getListFromString(value, getSeparators(), false));
488    } else {
489  64587 prop.setValue(value);
490    }
491  65142 return prop;
492    }
493   
 
494  520 toggle @Override
495    public BaseProperty fromStringArray(String[] strings)
496    {
497  520 if (!isMultiSelect()) {
498  424 return fromString(strings[0]);
499    }
500  96 BaseProperty prop = newProperty();
501  96 if (prop instanceof StringProperty) {
502  0 return fromString(strings[0]);
503    }
504   
505  96 List<String> list = new ArrayList<>();
506   
507  96 if (strings.length == 0) {
508  0 return prop;
509    }
510   
511  96 if ((strings.length == 1) && (getDisplayType().equals(DISPLAYTYPE_INPUT) || isMultiSelect())) {
512  78 ((ListProperty) prop).setList(getListFromString(strings[0], getSeparators(), false));
513  78 return prop;
514    }
515   
516    // If Multiselect and multiple results
517  18 for (String item : strings) {
518  43 if (!item.trim().equals("")) {
519  26 list.add(item);
520    }
521    }
522   
523    // setList will copy the list, so this call must be made last.
524  18 ((ListProperty) prop).setList(list);
525   
526  18 return prop;
527    }
528   
 
529  0 toggle @Override
530    public BaseProperty newPropertyfromXML(Element ppcel)
531    {
532  0 if (!isMultiSelect()) {
533  0 return super.newPropertyfromXML(ppcel);
534    }
535   
536  0 @SuppressWarnings("unchecked")
537    List<Element> elist = ppcel.elements(ListXarObjectPropertySerializer.ELEMENT_VALUE);
538  0 BaseProperty lprop = newProperty();
539   
540  0 if (lprop instanceof ListProperty) {
541  0 List<String> llist = ((ListProperty) lprop).getList();
542  0 for (int i = 0; i < elist.size(); i++) {
543  0 Element el = elist.get(i);
544  0 llist.add(el.getText());
545    }
546    } else {
547  0 for (int i = 0; i < elist.size(); i++) {
548  0 Element el = elist.get(i);
549  0 ((StringProperty) lprop).setValue(el.getText());
550    }
551    }
552  0 return lprop;
553    }
554   
555    /**
556    * Search for an internationalizable display text for the current value. The search process is:
557    * <ol>
558    * <li>let V = the internal value of the option, used as the "value" attribute of the {@code <option>} element, and
559    * D = the displayed value</li>
560    * <li>if a message with the key {@code <fieldFullName>_<V>} exists, return it as D</li>
561    * <li>else, if a message with the key {@code option_<fieldName>_<V>} exists, return it as D</li>
562    * <li>else, if a message with the key {@code option_<V>} exists, return it as D</li>
563    * <li>else, D can be specified in the values parameter of the property by using V=D</li>
564    * <li>else return V</li>
565    * </ol>
566    *
567    * @param value The internal value.
568    * @param name The name of the ListProperty.
569    * @param map The value=name mapping specified in the "values" parameter of the property.
570    * @param context The request context.
571    * @return The text that should be displayed, representing a human-understandable name for the internal value.
572    */
 
573  8543 toggle protected String getDisplayValue(String value, String name, Map<String, ListItem> map, XWikiContext context)
574    {
575  8543 return getDisplayValue(value, name, map, value, context);
576    }
577   
 
578  9519 toggle private String getDisplayValue(String value, String name, Map<String, ListItem> map, String def,
579    XWikiContext context)
580    {
581  9519 ListItem item = map.get(value);
582  9519 String displayValue;
583  9519 if (item == null) {
584  1323 displayValue = def;
585    } else {
586  8196 displayValue = item.getValue();
587    }
588  9519 if ((context == null) || (context.getWiki() == null)) {
589  13 return displayValue;
590    }
591  9506 String msgname = getFieldFullName() + "_" + value;
592  9506 String newresult = localizePlain(msgname);
593  9506 if (newresult == null) {
594  8904 msgname = "option_" + name + "_" + value;
595  8904 newresult = localizePlain(msgname);
596  8904 if (newresult == null) {
597  8904 msgname = "option_" + value;
598  8904 newresult = localizePlain(msgname);
599  8904 if (newresult == null) {
600  8904 newresult = displayValue;
601    }
602    }
603    }
604  9506 return newresult;
605    }
606   
607    /**
608    * Search for an internationalizable display text for the current value. The value can be either a simple string, or
609    * a value=name pair selected from the database.
610    *
611    * @see #getDisplayValue(String, String, Map, XWikiContext)
612    * @param rawvalue The internal value, or a value=name pair.
613    * @param name The name of the ListProperty.
614    * @param map The value=name mapping specified in the "values" parameter of the property.
615    * @param context The request context.
616    * @return The text that should be displayed, representing a human-understandable name for the internal value.
617    */
 
618  4781 toggle protected String getDisplayValue(Object rawvalue, String name, Map<String, ListItem> map, XWikiContext context)
619    {
620  4781 if (rawvalue == null) {
621  0 return "";
622    }
623  4781 if (rawvalue instanceof Object[]) {
624  0 return ((Object[]) rawvalue)[1].toString();
625    }
626  4781 return getDisplayValue(rawvalue.toString(), name, map, context);
627    }
628   
629    /**
630    * If the list is populated with value=name pairs selected from the database, then return only the value. Otherwise,
631    * it is a simple value.
632    *
633    * @param rawvalue
634    * @return The list value
635    */
 
636  3366 toggle protected String getElementValue(Object rawvalue)
637    {
638  3366 if (rawvalue == null) {
639  0 return "";
640    }
641  3366 if (rawvalue instanceof Object[]) {
642  0 return ((Object[]) rawvalue)[0].toString();
643    }
644  3366 return rawvalue.toString();
645    }
646   
 
647  21 toggle @Override
648    public void displayHidden(StringBuffer buffer, String name, String prefix, BaseCollection object,
649    XWikiContext context)
650    {
651  21 input input = new input();
652  21 input.setAttributeFilter(new XMLAttributeValueFilter());
653  21 BaseProperty prop = (BaseProperty) object.safeget(name);
654  21 if (prop != null) {
655  21 input.setValue(prop.toText());
656    }
657   
658  21 input.setType("hidden");
659  21 input.setName(prefix + name);
660  21 input.setID(prefix + name);
661  21 buffer.append(input.toString());
662    }
663   
 
664  5306 toggle @Override
665    public void displayView(StringBuffer buffer, String name, String prefix, BaseCollection object,
666    XWikiContext context)
667    {
668  5306 List<String> selectlist;
669  5306 String separator = getSeparator();
670  5306 BaseProperty prop = (BaseProperty) object.safeget(name);
671  5306 Map<String, ListItem> map = getMap(context);
672   
673    // Skip unset values.
674  5306 if (prop == null) {
675  0 return;
676    }
677   
678  5306 if (prop instanceof ListProperty) {
679  571 selectlist = ((ListProperty) prop).getList();
680  571 List<String> newlist = new ArrayList<>();
681  571 for (String value : selectlist) {
682  359 newlist.add(getDisplayValue(value, name, map, context));
683    }
684  571 buffer.append(StringUtils.join(newlist, separator));
685    } else {
686  4735 buffer.append(getDisplayValue(prop.getValue(), name, map, context));
687    }
688    }
689   
 
690  0 toggle @Override
691    public void displayEdit(StringBuffer buffer, String name, String prefix, BaseCollection object,
692    XWikiContext context)
693    {
694  0 if (getDisplayType().equals(DISPLAYTYPE_INPUT)) {
695  0 input input = new input();
696  0 input.setAttributeFilter(new XMLAttributeValueFilter());
697  0 BaseProperty prop = (BaseProperty) object.safeget(name);
698  0 if (prop != null) {
699  0 input.setValue(this.toFormString(prop));
700    }
701  0 input.setType("text");
702  0 input.setSize(getSize());
703  0 input.setName(prefix + name);
704  0 input.setID(prefix + name);
705  0 input.setDisabled(isDisabled());
706  0 buffer.append(input.toString());
707  0 } else if (getDisplayType().equals(DISPLAYTYPE_RADIO) || getDisplayType().equals(DISPLAYTYPE_CHECKBOX)) {
708  0 displayRadioEdit(buffer, name, prefix, object, context);
709    } else {
710  0 displaySelectEdit(buffer, name, prefix, object, context);
711    }
712   
713  0 if (!getDisplayType().equals(DISPLAYTYPE_INPUT)) {
714  0 org.apache.ecs.xhtml.input hidden = new input(input.hidden, prefix + name, "");
715  0 hidden.setAttributeFilter(new XMLAttributeValueFilter());
716  0 buffer.append(hidden);
717    }
718    }
719   
 
720  10 toggle protected void displayRadioEdit(StringBuffer buffer, String name, String prefix, BaseCollection object,
721    XWikiContext context)
722    {
723  10 List<String> list = getList(context);
724  10 Map<String, ListItem> map = getMap(context);
725   
726  10 BaseProperty prop = (BaseProperty) object.safeget(name);
727  10 List<String> selectlist = toList(prop);
728   
729    // Add the selected values that are not in the predefined list.
730  10 for (String item : selectlist) {
731    // The empty value means no selection when it's not in the predefined list. Both the radio and the checkbox
732    // input support empty selection (unlike the select input which automatically selects the first value when
733    // single selection is on) so we don't have to generate a radio/checkbox for the empty value.
734  5 if (!StringUtils.isEmpty(item) && !list.contains(item)) {
735  0 list.add(item);
736    }
737    }
738   
739    // Add options from Set
740  10 int count = 0;
741  10 for (Object rawvalue : list) {
742  30 String value = getElementValue(rawvalue);
743  30 String display = XMLUtils.escape(getDisplayValue(rawvalue, name, map, context));
744  30 input radio = new input(
745  30 (getDisplayType().equals(DISPLAYTYPE_RADIO) && !isMultiSelect()) ? input.radio : input.checkbox,
746    prefix + name, value);
747  30 radio.setAttributeFilter(new XMLAttributeValueFilter());
748  30 radio.setID("xwiki-form-" + name + "-" + object.getNumber() + "-" + count);
749  30 radio.setDisabled(isDisabled());
750   
751  30 if (selectlist.contains(value)) {
752  3 radio.setChecked(true);
753    }
754  30 radio.addElement(display);
755   
756  30 buffer.append("<label class=\"xwiki-form-listclass\" for=\"xwiki-form-" + XMLUtils.escape(name) + "-"
757    + object.getNumber() + "-" + count++ + "\">");
758  30 buffer.append(radio.toString());
759  30 buffer.append("</label>");
760    }
761   
762    // We need a hidden input with an empty value to be able to clear the selected values when no value is selected
763    // from the above radio/checkbox buttons.
764  10 org.apache.ecs.xhtml.input hidden = new input(input.hidden, prefix + name, "");
765  10 hidden.setAttributeFilter(new XMLAttributeValueFilter());
766  10 hidden.setDisabled(isDisabled());
767  10 buffer.append(hidden);
768    }
769   
 
770    protected class MapComparator implements Comparator<String>
771    {
772    protected Map<String, ListItem> map;
773   
 
774  22 toggle public MapComparator(Map<String, ListItem> map)
775    {
776  22 this.map = map;
777    }
778   
 
779  99 toggle @Override
780    public int compare(String o1, String o2)
781    {
782  99 ListItem s1 = this.map.get(o1);
783  99 ListItem s2 = this.map.get(o2);
784   
785  99 if ((s1 == null) && (s2 == null)) {
786  0 return 0;
787    }
788   
789  99 if (s1 == null) {
790  0 return -1;
791    }
792   
793  99 if (s2 == null) {
794  0 return 1;
795    }
796   
797  99 return s1.getValue().compareTo(s2.getValue());
798    }
799    }
800   
 
801  1007 toggle protected void displaySelectEdit(StringBuffer buffer, String name, String prefix, BaseCollection object,
802    XWikiContext context)
803    {
804  1007 select select = new select(prefix + name, 1);
805  1007 select.setAttributeFilter(new XMLAttributeValueFilter());
806  1007 select.setMultiple(isMultiSelect());
807  1007 select.setSize(getSize());
808  1007 select.setName(prefix + name);
809  1007 select.setID(prefix + name);
810  1007 select.setDisabled(isDisabled());
811   
812  1007 List<String> list = getList(context);
813  1007 Map<String, ListItem> map = getMap(context);
814   
815  1007 String sort = getSort();
816  1007 if (!"none".equals(sort)) {
817  844 if ("id".equals(sort)) {
818  0 Collections.sort(list);
819    }
820  844 if ("value".equals(sort)) {
821  22 Collections.sort(list, new MapComparator(map));
822    }
823    }
824   
825  1007 List<String> selectlist = toList((BaseProperty) object.safeget(name));
826   
827  1007 String defaultValue = getDefaultValue();
828   
829    // Add the selected values that are not in the predefined list.
830  1007 for (String item : selectlist) {
831  826 if (!StringUtils.isEmpty(item) && !defaultValue.equals(item) && !list.contains(item)) {
832  88 list.add(item);
833    }
834    }
835   
836    // Add default if not already part of the list
837  1007 if (!isMultiSelect() && !list.contains(defaultValue)) {
838  976 String display =
839  976 getDisplayValue(defaultValue, name, map, defaultValue.isEmpty() ? "---" : defaultValue, context);
840   
841  976 select.addElement(createOption("", display, selectlist));
842    }
843   
844    // Add options from Set
845  1007 for (String rawvalue : list) {
846  3336 String value = getElementValue(rawvalue);
847  3336 String display = getDisplayValue(rawvalue, name, map, context);
848   
849  3336 select.addElement(createOption(value, display, selectlist));
850    }
851   
852  1007 buffer.append(select.toString());
853    }
854   
 
855  4312 toggle private option createOption(String value, String display, List<String> selectlist)
856    {
857  4312 option option = new option(display, value);
858  4312 option.setAttributeFilter(new XMLAttributeValueFilter());
859  4312 option.addElement(XMLUtils.escape(display));
860  4312 if (selectlist.contains(value)) {
861  820 option.setSelected(true);
862    }
863   
864  4312 return option;
865    }
866   
867    public abstract List<String> getList(XWikiContext context);
868   
869    public abstract Map<String, ListItem> getMap(XWikiContext context);
870   
871    /**
872    * {@link ListClass} does not produce only {@link ListProperty}s and this method allows to access the value as
873    * {@link List} whatever property is actually storing it.
874    * <p>
875    * There is no guarantees the returned {@link List} will be modifiable.
876    *
877    * @param property the property created by this class
878    * @return the {@link List} representation of this property
879    * @since 6.2M1
880    */
 
881  1017 toggle public List<String> toList(BaseProperty<?> property)
882    {
883  1017 List<String> list;
884   
885  1017 if (property == null) {
886  173 list = Collections.emptyList();
887  844 } else if (property instanceof ListProperty) {
888  24 list = ((ListProperty) property).getList();
889    } else {
890  820 list = Arrays.asList(String.valueOf(property.getValue()));
891    }
892   
893  1017 return list;
894    }
895   
896    /**
897    * Set the passed {@link List} into the passed property.
898    *
899    * @param property the property to modify
900    * @param list the list to set
901    * @since 6.2M1
902    */
 
903  0 toggle public void fromList(BaseProperty<?> property, List<String> list)
904    {
905  0 if (property instanceof ListProperty) {
906  0 ((ListProperty) property).setList(list);
907    } else {
908  0 property.setValue(list == null || list.isEmpty() ? null : list.get(0));
909    }
910    }
911   
 
912  0 toggle @Override
913    public <T extends EntityReference> void mergeProperty(BaseProperty<T> currentProperty,
914    BaseProperty<T> previousProperty, BaseProperty<T> newProperty, MergeConfiguration configuration,
915    XWikiContext context, MergeResult mergeResult)
916    {
917    // If it's not a multiselect then we don't have any special merge to do. We keep default StringProperty behavior
918  0 if (isMultiSelect()) {
919    // If not a free input assume it's not an ordered list
920  0 if (!DISPLAYTYPE_INPUT.equals(getDisplayType()) && currentProperty instanceof ListProperty) {
921  0 mergeNotOrderedListProperty(currentProperty, previousProperty, newProperty, configuration, context,
922    mergeResult);
923   
924  0 return;
925    }
926    }
927   
928    // Fallback on default ListProperty merging
929  0 super.mergeProperty(currentProperty, previousProperty, newProperty, configuration, context, mergeResult);
930    }
931   
 
932  0 toggle protected <T extends EntityReference> void mergeNotOrderedListProperty(BaseProperty<T> currentProperty,
933    BaseProperty<T> previousProperty, BaseProperty<T> newProperty, MergeConfiguration configuration,
934    XWikiContext context, MergeResult mergeResult)
935    {
936  0 List<String> currentList = new LinkedList<>(toList(currentProperty));
937  0 List<String> previousList = toList(previousProperty);
938  0 List<String> newList = toList(newProperty);
939   
940    // Remove elements to remove
941  0 if (previousList != null) {
942  0 for (String element : previousList) {
943  0 if (newList == null || !newList.contains(element)) {
944  0 currentList.remove(element);
945  0 mergeResult.setModified(true);
946    }
947    }
948    }
949   
950    // Add missing elements
951  0 if (newList != null) {
952  0 for (String element : newList) {
953  0 if ((previousList == null || !previousList.contains(element))) {
954  0 if (!currentList.contains(element)) {
955  0 currentList.add(element);
956  0 mergeResult.setModified(true);
957    }
958    }
959    }
960    }
961   
962  0 fromList(currentProperty, currentList);
963   
964  0 return;
965    }
966    }