001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.widgets;
029
030import org.opencms.main.CmsLog;
031import org.opencms.util.CmsStringUtil;
032
033import java.util.ArrayList;
034import java.util.Collections;
035import java.util.List;
036
037import org.apache.commons.logging.Log;
038
039/**
040 * An option of a select type widget.<p>
041 *
042 * If options are passed from XML content schema definitions as widget configuration options,
043 * the following syntax is used for defining the option values:<p>
044 *
045 * <code>value='{text}' default='{true|false}' option='{text}' help='{text}|{more option definitions}</code><p>
046 *
047 * For example:<p>
048 *
049 * <code>value='value1' default='true' option='option1' help='help1'|value='value2' option='option2' help='help2'</code><p>
050 *
051 * The elements <code>default</code>, <code>option</code> and <code>help</code> are all optional, only a
052 * <code>value</code> must be present in the input.
053 * There should be only one <code>default</code> set to <code>true</code>
054 * in the input, if more than one is detected, only the first <code>default</code> found is actually used.
055 * If no <code>option</code> is given, the value of <code>option</code> defaults to the value of the given <code>value</code>.
056 * If no <code>help</code> is given, the default is <code>null</code>.<p>
057 *
058 * Shortcut syntax options:<p>
059 *
060 * If you don't specify the <code>value</code> key, the value is assumed to start at the first position of an
061 * option definition. In this case the value must not be surrounded by the <code>'</code> chars.
062 * Example: <code>value='some value' default='true'</code> can also be written as <code>some value default='true'</code>.<p>
063 *
064 * Only if you use the short value definition as described above, a default value can be marked with a <code>*</code>
065 * at the end of the value definition.
066 * Example: <code>value='some value' default='true'</code> can also be written as <code>some value*</code>.<p>
067 *
068 * Only if you use the short value definition as described above, you can also append the <code>option</code>
069 * to the <code>value</code> using a <code>:</code>. In this case no <code>'</code> must surround the <code>option</code>.
070 * Please keep in mind that in this case the value
071 * itself can not longer contain a <code>:</code> char, since it would then be interpreted as a delimiter.
072 * Example: <code>value='some value' option='some option'</code> can also be written as <code>some value:some option</code>.<p>
073 *
074 * Any combinations of the above described shortcuts are allowed in the configuration option String.
075 * Here are some more examples of valid configuration option Strings:<p>
076 *
077 * <code>1*|2|3|4|5|6|7</code><br>
078 * <code>1 default='true'|2|3|4|5|6|7</code><br>
079 * <code>value='1' default='true'|value='2'|value='3'</code><br>
080 * <code>value='1'|2*|value='3'</code><br>
081 * <code>1*:option text|2|3|4</code><br>
082 * <code>1* option='option text' help='some'|2|3|4</code><p>
083 *
084 * Please note: If an entry in the configuration String is malformed, this error is silently ignored (but written
085 * to the log channel of this class at <code>INFO</code>level.<p>
086 *
087 * @since 6.0.0
088 */
089public class CmsSelectWidgetOption {
090
091    /** Optional shortcut default marker. */
092    private static final char DEFAULT_MARKER = '*';
093
094    /** Delimiter between option sets. */
095    private static final char INPUT_DELIMITER = '|';
096
097    /** Key prefix for the 'default'. */
098    private static final String KEY_DEFAULT = "default='";
099
100    /** Key prefix for the 'help' text. */
101    private static final String KEY_HELP = "help='";
102
103    /** Key prefix for the 'option' text. */
104    private static final String KEY_OPTION = "option='";
105
106    /** Key prefix for the 'value'. */
107    private static final String KEY_VALUE = "value='";
108
109    /** The log object for this class. */
110    private static final Log LOG = CmsLog.getLog(CmsSelectWidgetOption.class);
111
112    /** Optional shortcut option delimiter. */
113    private static final char OPTION_DELIMITER = ':';
114
115    /** Indicates if this is the default value of the selection. */
116    private boolean m_default;
117
118    /** The hashcode of this object. */
119    private int m_hashcode;
120
121    /** The (optional) help text of this select option. */
122    private String m_help;
123
124    /** The (optional) display text of this select option. */
125    private String m_option;
126
127    /** The value of this select option. */
128    private String m_value;
129
130    /**
131     * Creates a new select option for the given value.<p>
132     *
133     * @param value the value of this select option
134     */
135    public CmsSelectWidgetOption(String value) {
136
137        this(value, false, null, null);
138    }
139
140    /**
141     * Creates a new select option form the given values.<p>
142     *
143     * @param value the value of this select option
144     * @param isDefault indicates if this is the default value of the selection (default is <code>false</code>)
145     */
146    public CmsSelectWidgetOption(String value, boolean isDefault) {
147
148        this(value, isDefault, null, null);
149    }
150
151    /**
152     * Creates a new select option form the given values.<p>
153     *
154     * @param value the value of this select option
155     * @param isDefault indicates if this is the default value of the selection (default is <code>false</code>)
156     * @param optionText the (optional) display text of this select option
157     */
158    public CmsSelectWidgetOption(String value, boolean isDefault, String optionText) {
159
160        this(value, isDefault, optionText, null);
161    }
162
163    /**
164     * Creates a new select option form the given values.<p>
165     *
166     * @param value the value of this select option
167     * @param isDefault indicates if this is the default value of the selection (default is <code>false</code>)
168     * @param optionText the (optional) display text of this select option
169     * @param helpText the (optional) help text of this select option
170     */
171    public CmsSelectWidgetOption(String value, boolean isDefault, String optionText, String helpText) {
172
173        m_default = isDefault;
174        m_value = value;
175        m_option = optionText;
176        m_help = helpText;
177    }
178
179    /**
180     * Returns a select widget configuration String created from the given list of select options.<p>
181     *
182     * If an element found in the given list is not of type
183     * <code>{@link CmsSelectWidgetOption}</code>, it is ignored.<p>
184     *
185     * @param options the list of select options to create the configuration String for
186     *
187     * @return a select widget configuration String created from the given list of select options
188     */
189    public static String createConfigurationString(List<CmsSelectWidgetOption> options) {
190
191        if ((options == null) || (options.size() == 0)) {
192            return "";
193        }
194        StringBuffer result = new StringBuffer(256);
195        boolean first = true;
196        for (int i = 0; i < options.size(); i++) {
197            CmsSelectWidgetOption o = options.get(i);
198            if (!first) {
199                result.append(CmsSelectWidgetOption.INPUT_DELIMITER);
200            } else {
201                first = false;
202            }
203            result.append(o.toString().replace("|", "\\|"));
204        }
205        return result.toString();
206    }
207
208    /**
209     * Returns the default option from the given list of select options,
210     * or <code>null</code> in case there is no default option in the given list.<p>
211     *
212     * If an element found in the given list is not of type
213     * <code>{@link CmsSelectWidgetOption}</code>, this is ignored.<p>
214     *
215     * @param options the list of select options to get the default from
216     *
217     * @return the default option from the given list of select options, or <code>null</code> in case there is no default option
218     */
219    public static CmsSelectWidgetOption getDefaultOption(List<CmsSelectWidgetOption> options) {
220
221        if ((options == null) || (options.size() == 0)) {
222            return null;
223        }
224        for (int i = 0; i < options.size(); i++) {
225            Object o = options.get(i);
226            if (o instanceof CmsSelectWidgetOption) {
227                CmsSelectWidgetOption option = (CmsSelectWidgetOption)o;
228                if (option.isDefault()) {
229                    return option;
230                }
231            }
232        }
233        return null;
234    }
235
236    /**
237     * Returns a list of default options from the given list of select options.<p>
238     *
239     * If an element found in the given list is not of type
240     * <code>{@link CmsSelectWidgetOption}</code>, this is ignored.<p>
241     *
242     * @param options the list of select options to get the default from
243     *
244     * @return a list of <code>{@link CmsSelectWidgetOption}</code> objects
245     */
246    public static List<CmsSelectWidgetOption> getDefaultOptions(List<CmsSelectWidgetOption> options) {
247
248        List<CmsSelectWidgetOption> defaults = new ArrayList<CmsSelectWidgetOption>();
249        if ((options == null) || (options.size() == 0)) {
250            return defaults;
251        }
252        for (int i = 0; i < options.size(); i++) {
253            Object o = options.get(i);
254            if (o instanceof CmsSelectWidgetOption) {
255                CmsSelectWidgetOption option = (CmsSelectWidgetOption)o;
256                if (option.isDefault()) {
257                    defaults.add(option);
258                }
259            }
260        }
261        return defaults;
262    }
263
264    /**
265     * Parses a widget configuration String for select option values.<p>
266     *
267     * If the input is <code>null</code> or empty, a <code>{@link Collections#EMPTY_LIST}</code>
268     * is returned.<p>
269     *
270     * Please note: No exception is thrown in case the input is malformed, all malformed entries are silently ignored.<p>
271     *
272     * @param input the widget input string to parse
273     *
274     * @return a List of <code>{@link CmsSelectWidgetOption}</code> elements
275     */
276    public static List<CmsSelectWidgetOption> parseOptions(String input) {
277
278        if (CmsStringUtil.isEmptyOrWhitespaceOnly(input)) {
279            // default result for empty input
280            return Collections.emptyList();
281        }
282
283        // cut along the delimiter
284        String[] parts = splitOptions(input);
285        List<CmsSelectWidgetOption> result = new ArrayList<CmsSelectWidgetOption>(parts.length);
286
287        // indicates if a default of 'true' was already set in this result list
288        boolean foundDefault = false;
289
290        for (int i = 0; i < parts.length; i++) {
291
292            String part = parts[i].trim();
293            if (part.length() == 0) {
294                // skip empty parts
295                continue;
296            }
297
298            try {
299                part = part.replace("\\|", "|");
300                String value = null;
301                String option = null;
302                String help = null;
303                boolean isDefault = false;
304
305                int posValue = part.indexOf(KEY_VALUE);
306                int posDefault = part.indexOf(KEY_DEFAULT);
307                int posOption = part.indexOf(KEY_OPTION);
308                int posHelp = part.indexOf(KEY_HELP);
309
310                boolean shortValue = false;
311                if (posValue < 0) {
312                    // shortcut syntax, value key must be at first position
313                    if ((posDefault == 0) || (posOption == 0) || (posHelp == 0)) {
314                        // malformed part - no value given
315                        throw new CmsWidgetException(
316                            Messages.get().container(Messages.ERR_MALFORMED_SELECT_OPTIONS_1, input));
317                    }
318                    posValue = 0;
319                    shortValue = true;
320                }
321
322                // a 'value' must be always present
323                int end = part.length();
324                // check where the 'value' ends
325                if (posHelp > posValue) {
326                    end = posHelp;
327                }
328                if ((posDefault > posValue) && (posDefault < end)) {
329                    end = posDefault;
330                }
331                if ((posOption > posValue) && (posOption < end)) {
332                    end = posOption;
333                }
334                if (shortValue) {
335                    // no explicit setting using the key, value must be at the first position
336                    value = part.substring(0, end).trim();
337                } else {
338                    value = part.substring((posValue + KEY_VALUE.length()) - 1, end).trim();
339                    // cut of trailing '
340                    value = value.substring(1, value.length() - 1);
341                }
342
343                boolean shortOption = false;
344                // check if the option is appended using the ':' shortcut
345                if ((shortValue) && (posOption < 0)) {
346                    int pos = value.indexOf(OPTION_DELIMITER);
347                    if (pos >= 0) {
348                        // shortcut syntax is used
349                        posOption = pos;
350                        shortOption = true;
351                        value = value.substring(0, pos);
352                    }
353                }
354
355                if (posDefault >= 0) {
356                    // there was an explicit 'default' setting using the key, check where it ends
357                    end = part.length();
358                    if (posHelp > posDefault) {
359                        end = posHelp;
360                    }
361                    if ((posOption > posDefault) && (posOption < end)) {
362                        end = posOption;
363                    }
364                    if ((posValue > posDefault) && (posValue < end)) {
365                        end = posValue;
366                    }
367                    String sub = part.substring(posDefault + KEY_DEFAULT.length(), end).trim();
368                    // cut of trailing '
369                    sub = sub.substring(0, sub.length() - 1);
370                    isDefault = Boolean.valueOf(sub).booleanValue();
371                } else {
372                    // check for shortcut syntax, value must end with a '*'
373                    if ((value != null) && (value.charAt(value.length() - 1) == DEFAULT_MARKER)) {
374                        isDefault = true;
375                        value = value.substring(0, value.length() - 1);
376                    }
377                }
378
379                if (posOption >= 0) {
380                    // an 'option' setting is available, check where it ends
381                    end = part.length();
382                    if (posHelp > posOption) {
383                        end = posHelp;
384                    }
385                    if ((posDefault > posOption) && (posDefault < end)) {
386                        end = posDefault;
387                    }
388                    if ((posValue > posOption) && (posValue < end)) {
389                        end = posValue;
390                    }
391                    if (shortOption) {
392                        // shortcut syntax used for option with ':' appended to value
393                        option = part.substring(posOption + 1, end).trim();
394                    } else {
395                        option = part.substring((posOption + KEY_OPTION.length()) - 1, end).trim();
396                        // cut of trailing '
397                        option = option.substring(1, option.length() - 1);
398                    }
399                }
400
401                if (posHelp >= 0) {
402                    // a 'help' setting is available, check where it ends
403                    end = part.length();
404                    if (posOption > posHelp) {
405                        end = posOption;
406                    }
407                    if ((posDefault > posHelp) && (posDefault < end)) {
408                        end = posDefault;
409                    }
410                    if ((posValue > posHelp) && (posValue < end)) {
411                        end = posValue;
412                    }
413                    help = part.substring((posHelp + KEY_HELP.length()) - 1, end).trim();
414                    // cut of trailing '
415                    help = help.substring(1, help.length() - 1);
416                }
417
418                // check if there was already a 'true' default, if so all other entries are 'false'
419                if (foundDefault) {
420                    isDefault = false;
421                } else if (isDefault) {
422                    foundDefault = true;
423                }
424
425                result.add(new CmsSelectWidgetOption(value, isDefault, option, help));
426
427            } catch (Exception e) {
428                if (LOG.isInfoEnabled()) {
429                    LOG.info(Messages.get().getBundle().key(Messages.ERR_MALFORMED_SELECT_OPTIONS_1, input));
430                }
431            }
432        }
433
434        return result;
435    }
436
437    /**
438     * Splits the options string at every unescaped input delimiter, i.e., every unescaped "|".
439     * @param input the options string
440     * @return the array with the various options
441     */
442    public static String[] splitOptions(String input) {
443
444        //Note that we use a regex matching all "|" characters not prefixed by "\"
445        //Since we define a regex for matching, the input delimiter "|" needs to be escaped, as well as "\",
446        //which is even double-escaped - one escaping is due to the String, one due to the regex.
447        return input.split("(?<!\\\\)\\" + INPUT_DELIMITER);
448    }
449
450    /**
451     * @see java.lang.Object#equals(java.lang.Object)
452     */
453    @Override
454    public boolean equals(Object obj) {
455
456        if (!(obj instanceof CmsSelectWidgetOption)) {
457            return false;
458        }
459        CmsSelectWidgetOption other = (CmsSelectWidgetOption)obj;
460        if (m_default != other.m_default) {
461            return false;
462        }
463        if (m_value == null) {
464            if (other.m_value != null) {
465                return false;
466            }
467        } else if (!m_value.equals(other.m_value)) {
468            return false;
469        }
470        if (m_option == null) {
471            if (other.m_option != null) {
472                return false;
473            }
474        } else if (!m_option.equals(other.m_option)) {
475            return false;
476        }
477        if (m_help == null) {
478            if (other.m_help != null) {
479                return false;
480            }
481        } else if (!m_help.equals(other.m_help)) {
482            return false;
483        }
484        return true;
485    }
486
487    /**
488     * Returns the (optional) help text of this select option.<p>
489     *
490     * @return the (optional) help text of this select option
491     */
492    public String getHelp() {
493
494        return m_help;
495    }
496
497    /**
498     * Returns the option text of this select option.<p>
499     *
500     * If this has not been set, the result of <code>{@link #getValue()}</code> is returned,
501     * there will always be a result other than <code>null</code> returned.<p>
502     *
503     * @return the option text of this select option
504     */
505    public String getOption() {
506
507        if (m_option == null) {
508            return getValue();
509        }
510        return m_option;
511    }
512
513    /**
514     * Returns the value of this select option.<p>
515     *
516     * @return the value of this select option
517     */
518    public String getValue() {
519
520        return m_value;
521    }
522
523    /**
524     * @see java.lang.Object#hashCode()
525     */
526    @Override
527    public int hashCode() {
528
529        if (m_hashcode == 0) {
530            StringBuffer hash = new StringBuffer(128);
531            hash.append(m_value);
532            hash.append('|');
533            hash.append(m_default);
534            hash.append('|');
535            hash.append(m_option);
536            hash.append('|');
537            hash.append(m_help);
538            m_hashcode = hash.toString().hashCode();
539        }
540        return m_hashcode;
541    }
542
543    /**
544     * Returns <code>true</code> if this is the default value of the selection.<p>
545     *
546     * @return <code>true</code> if this is the default value of the selection
547     */
548    public boolean isDefault() {
549
550        return m_default;
551    }
552
553    /**
554     * @see java.lang.Object#toString()
555     */
556    @Override
557    public String toString() {
558
559        StringBuffer result = new StringBuffer(128);
560
561        result.append(KEY_VALUE);
562        result.append(m_value);
563        result.append('\'');
564        if (m_default) {
565            result.append(' ');
566            result.append(KEY_DEFAULT);
567            result.append(m_default);
568            result.append('\'');
569        }
570        if (m_option != null) {
571            result.append(' ');
572            result.append(KEY_OPTION);
573            result.append(m_option);
574            result.append('\'');
575        }
576        if (m_help != null) {
577            result.append(' ');
578            result.append(KEY_HELP);
579            result.append(m_help);
580            result.append('\'');
581        }
582        return result.toString();
583    }
584}