content: /*
   * This library is part of OpenCms -
   * the Open Source Content Management System
   *
   * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
   *
   * This library is free software; you can redistribute it and/or
   * modify it under the terms of the GNU Lesser General Public
   * License as published by the Free Software Foundation; either
   * version 2.1 of the License, or (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
   * Lesser General Public License for more details.
   *
   * For further information about Alkacon Software GmbH & Co. KG, please see the
   * company website: https://www.alkacon.com
   *
   * For further information about OpenCms, please see the
   * project website: https://www.opencms.org
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   */
  
  package org.opencms.widgets;
  
  import org.opencms.main.CmsLog;
  import org.opencms.util.CmsStringUtil;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  
  import org.apache.commons.logging.Log;
  
  /**
   * An option of a select type widget.<p>
   *
   * If options are passed from XML content schema definitions as widget configuration options,
   * the following syntax is used for defining the option values:<p>
   *
   * <code>value='{text}' default='{true|false}' option='{text}' help='{text}|{more option definitions}</code><p>
   *
   * For example:<p>
   *
   * <code>value='value1' default='true' option='option1' help='help1'|value='value2' option='option2' help='help2'</code><p>
   *
   * The elements <code>default</code>, <code>option</code> and <code>help</code> are all optional, only a
   * <code>value</code> must be present in the input.
   * There should be only one <code>default</code> set to <code>true</code>
   * in the input, if more than one is detected, only the first <code>default</code> found is actually used.
   * If no <code>option</code> is given, the value of <code>option</code> defaults to the value of the given <code>value</code>.
   * If no <code>help</code> is given, the default is <code>null</code>.<p>
   *
   * Shortcut syntax options:<p>
   *
   * If you don't specify the <code>value</code> key, the value is assumed to start at the first position of an
   * option definition. In this case the value must not be surrounded by the <code>'</code> chars.
   * Example: <code>value='some value' default='true'</code> can also be written as <code>some value default='true'</code>.<p>
   *
   * Only if you use the short value definition as described above, a default value can be marked with a <code>*</code>
   * at the end of the value definition.
   * Example: <code>value='some value' default='true'</code> can also be written as <code>some value*</code>.<p>
   *
   * Only if you use the short value definition as described above, you can also append the <code>option</code>
   * to the <code>value</code> using a <code>:</code>. In this case no <code>'</code> must surround the <code>option</code>.
   * Please keep in mind that in this case the value
   * itself can not longer contain a <code>:</code> char, since it would then be interpreted as a delimiter.
   * Example: <code>value='some value' option='some option'</code> can also be written as <code>some value:some option</code>.<p>
   *
   * Any combinations of the above described shortcuts are allowed in the configuration option String.
   * Here are some more examples of valid configuration option Strings:<p>
   *
   * <code>1*|2|3|4|5|6|7</code><br>
   * <code>1 default='true'|2|3|4|5|6|7</code><br>
   * <code>value='1' default='true'|value='2'|value='3'</code><br>
   * <code>value='1'|2*|value='3'</code><br>
   * <code>1*:option text|2|3|4</code><br>
   * <code>1* option='option text' help='some'|2|3|4</code><p>
   *
   * Please note: If an entry in the configuration String is malformed, this error is silently ignored (but written
   * to the log channel of this class at <code>INFO</code>level.<p>
   *
   * @since 6.0.0
   */
  public class CmsSelectWidgetOption {
  
      /** Optional shortcut default marker. */
      private static final char DEFAULT_MARKER = '*';
  
      /** Delimiter between option sets. */
      private static final char INPUT_DELIMITER = '|';
  
      /** Key prefix for the 'default'. */
      private static final String KEY_DEFAULT = "default='";
  
      /** Key prefix for the 'help' text. */
      private static final String KEY_HELP = "help='";
  
      /** Key prefix for the 'option' text. */
      private static final String KEY_OPTION = "option='";
  
      /** Key prefix for the 'value'. */
      private static final String KEY_VALUE = "value='";
  
      /** The log object for this class. */
      private static final Log LOG = CmsLog.getLog(CmsSelectWidgetOption.class);
  
      /** Optional shortcut option delimiter. */
      private static final char OPTION_DELIMITER = ':';
  
      /** Indicates if this is the default value of the selection. */
      private boolean m_default;
  
      /** The hashcode of this object. */
      private int m_hashcode;
  
      /** The (optional) help text of this select option. */
      private String m_help;
  
      /** The (optional) display text of this select option. */
      private String m_option;
  
      /** The value of this select option. */
      private String m_value;
  
      /**
       * Creates a new select option for the given value.<p>
       *
       * @param value the value of this select option
       */
      public CmsSelectWidgetOption(String value) {
  
          this(value, false, null, null);
      }
  
      /**
       * Creates a new select option form the given values.<p>
       *
       * @param value the value of this select option
       * @param isDefault indicates if this is the default value of the selection (default is <code>false</code>)
       */
      public CmsSelectWidgetOption(String value, boolean isDefault) {
  
          this(value, isDefault, null, null);
      }
  
      /**
       * Creates a new select option form the given values.<p>
       *
       * @param value the value of this select option
       * @param isDefault indicates if this is the default value of the selection (default is <code>false</code>)
       * @param optionText the (optional) display text of this select option
       */
      public CmsSelectWidgetOption(String value, boolean isDefault, String optionText) {
  
          this(value, isDefault, optionText, null);
      }
  
      /**
       * Creates a new select option form the given values.<p>
       *
       * @param value the value of this select option
       * @param isDefault indicates if this is the default value of the selection (default is <code>false</code>)
       * @param optionText the (optional) display text of this select option
       * @param helpText the (optional) help text of this select option
       */
      public CmsSelectWidgetOption(String value, boolean isDefault, String optionText, String helpText) {
  
          m_default = isDefault;
          m_value = value;
          m_option = optionText;
          m_help = helpText;
      }
  
      /**
       * Returns a select widget configuration String created from the given list of select options.<p>
       *
       * If an element found in the given list is not of type
       * <code>{@link CmsSelectWidgetOption}</code>, it is ignored.<p>
       *
       * @param options the list of select options to create the configuration String for
       *
       * @return a select widget configuration String created from the given list of select options
       */
      public static String createConfigurationString(List<CmsSelectWidgetOption> options) {
  
          if ((options == null) || (options.size() == 0)) {
              return "";
          }
          StringBuffer result = new StringBuffer(256);
          boolean first = true;
          for (int i = 0; i < options.size(); i++) {
              CmsSelectWidgetOption o = options.get(i);
              if (!first) {
                  result.append(CmsSelectWidgetOption.INPUT_DELIMITER);
              } else {
                  first = false;
              }
              result.append(o.toString().replace("|", "\\|"));
          }
          return result.toString();
      }
  
      /**
       * Returns the default option from the given list of select options,
       * or <code>null</code> in case there is no default option in the given list.<p>
       *
       * If an element found in the given list is not of type
       * <code>{@link CmsSelectWidgetOption}</code>, this is ignored.<p>
       *
       * @param options the list of select options to get the default from
       *
       * @return the default option from the given list of select options, or <code>null</code> in case there is no default option
       */
      public static CmsSelectWidgetOption getDefaultOption(List<CmsSelectWidgetOption> options) {
  
          if ((options == null) || (options.size() == 0)) {
              return null;
          }
          for (int i = 0; i < options.size(); i++) {
              Object o = options.get(i);
              if (o instanceof CmsSelectWidgetOption) {
                  CmsSelectWidgetOption option = (CmsSelectWidgetOption)o;
                  if (option.isDefault()) {
                      return option;
                  }
              }
          }
          return null;
      }
  
      /**
       * Returns a list of default options from the given list of select options.<p>
       *
       * If an element found in the given list is not of type
       * <code>{@link CmsSelectWidgetOption}</code>, this is ignored.<p>
       *
       * @param options the list of select options to get the default from
       *
       * @return a list of <code>{@link CmsSelectWidgetOption}</code> objects
       */
      public static List<CmsSelectWidgetOption> getDefaultOptions(List<CmsSelectWidgetOption> options) {
  
          List<CmsSelectWidgetOption> defaults = new ArrayList<CmsSelectWidgetOption>();
          if ((options == null) || (options.size() == 0)) {
              return defaults;
          }
          for (int i = 0; i < options.size(); i++) {
              Object o = options.get(i);
              if (o instanceof CmsSelectWidgetOption) {
                  CmsSelectWidgetOption option = (CmsSelectWidgetOption)o;
                  if (option.isDefault()) {
                      defaults.add(option);
                  }
              }
          }
          return defaults;
      }
  
      /**
       * Parses a widget configuration String for select option values.<p>
       *
       * If the input is <code>null</code> or empty, a <code>{@link Collections#EMPTY_LIST}</code>
       * is returned.<p>
       *
       * Please note: No exception is thrown in case the input is malformed, all malformed entries are silently ignored.<p>
       *
       * @param input the widget input string to parse
       *
       * @return a List of <code>{@link CmsSelectWidgetOption}</code> elements
       */
      public static List<CmsSelectWidgetOption> parseOptions(String input) {
  
          if (CmsStringUtil.isEmptyOrWhitespaceOnly(input)) {
              // default result for empty input
              return Collections.emptyList();
          }
  
          // cut along the delimiter
          String[] parts = splitOptions(input);
          List<CmsSelectWidgetOption> result = new ArrayList<CmsSelectWidgetOption>(parts.length);
  
          // indicates if a default of 'true' was already set in this result list
          boolean foundDefault = false;
  
          for (int i = 0; i < parts.length; i++) {
  
              String part = parts[i].trim();
              if (part.length() == 0) {
                  // skip empty parts
                  continue;
              }
  
              try {
                  part = part.replace("\\|", "|");
                  String value = null;
                  String option = null;
                  String help = null;
                  boolean isDefault = false;
  
                  int posValue = part.indexOf(KEY_VALUE);
                  int posDefault = part.indexOf(KEY_DEFAULT);
                  int posOption = part.indexOf(KEY_OPTION);
                  int posHelp = part.indexOf(KEY_HELP);
  
                  boolean shortValue = false;
                  if (posValue < 0) {
                      // shortcut syntax, value key must be at first position
                      if ((posDefault == 0) || (posOption == 0) || (posHelp == 0)) {
                          // malformed part - no value given
                          throw new CmsWidgetException(
                              Messages.get().container(Messages.ERR_MALFORMED_SELECT_OPTIONS_1, input));
                      }
                      posValue = 0;
                      shortValue = true;
                  }
  
                  // a 'value' must be always present
                  int end = part.length();
                  // check where the 'value' ends
                  if (posHelp > posValue) {
                      end = posHelp;
                  }
                  if ((posDefault > posValue) && (posDefault < end)) {
                      end = posDefault;
                  }
                  if ((posOption > posValue) && (posOption < end)) {
                      end = posOption;
                  }
                  if (shortValue) {
                      // no explicit setting using the key, value must be at the first position
                      value = part.substring(0, end).trim();
                  } else {
                      value = part.substring((posValue + KEY_VALUE.length()) - 1, end).trim();
                      // cut of trailing '
                      value = value.substring(1, value.length() - 1);
                  }
  
                  boolean shortOption = false;
                  // check if the option is appended using the ':' shortcut
                  if ((shortValue) && (posOption < 0)) {
                      int pos = value.indexOf(OPTION_DELIMITER);
                      if (pos >= 0) {
                          // shortcut syntax is used
                          posOption = pos;
                          shortOption = true;
                          value = value.substring(0, pos);
                      }
                  }
  
                  if (posDefault >= 0) {
                      // there was an explicit 'default' setting using the key, check where it ends
                      end = part.length();
                      if (posHelp > posDefault) {
                          end = posHelp;
                      }
                      if ((posOption > posDefault) && (posOption < end)) {
                          end = posOption;
                      }
                      if ((posValue > posDefault) && (posValue < end)) {
                          end = posValue;
                      }
                      String sub = part.substring(posDefault + KEY_DEFAULT.length(), end).trim();
                      // cut of trailing '
                      sub = sub.substring(0, sub.length() - 1);
                      isDefault = Boolean.valueOf(sub).booleanValue();
                  } else {
                      // check for shortcut syntax, value must end with a '*'
                      if ((value != null) && (value.charAt(value.length() - 1) == DEFAULT_MARKER)) {
                          isDefault = true;
                          value = value.substring(0, value.length() - 1);
                      }
                  }
  
                  if (posOption >= 0) {
                      // an 'option' setting is available, check where it ends
                      end = part.length();
                      if (posHelp > posOption) {
                          end = posHelp;
                      }
                      if ((posDefault > posOption) && (posDefault < end)) {
                          end = posDefault;
                      }
                      if ((posValue > posOption) && (posValue < end)) {
                          end = posValue;
                      }
                      if (shortOption) {
                          // shortcut syntax used for option with ':' appended to value
                          option = part.substring(posOption + 1, end).trim();
                      } else {
                          option = part.substring((posOption + KEY_OPTION.length()) - 1, end).trim();
                          // cut of trailing '
                          option = option.substring(1, option.length() - 1);
                      }
                  }
  
                  if (posHelp >= 0) {
                      // a 'help' setting is available, check where it ends
                      end = part.length();
                      if (posOption > posHelp) {
                          end = posOption;
                      }
                      if ((posDefault > posHelp) && (posDefault < end)) {
                          end = posDefault;
                      }
                      if ((posValue > posHelp) && (posValue < end)) {
                          end = posValue;
                      }
                      help = part.substring((posHelp + KEY_HELP.length()) - 1, end).trim();
                      // cut of trailing '
                      help = help.substring(1, help.length() - 1);
                  }
  
                  // check if there was already a 'true' default, if so all other entries are 'false'
                  if (foundDefault) {
                      isDefault = false;
                  } else if (isDefault) {
                      foundDefault = true;
                  }
  
                  result.add(new CmsSelectWidgetOption(value, isDefault, option, help));
  
              } catch (Exception e) {
                  if (LOG.isInfoEnabled()) {
                      LOG.info(Messages.get().getBundle().key(Messages.ERR_MALFORMED_SELECT_OPTIONS_1, input));
                  }
              }
          }
  
          return result;
      }
  
      /**
       * Splits the options string at every unescaped input delimiter, i.e., every unescaped "|".
       * @param input the options string
       * @return the array with the various options
       */
      public static String[] splitOptions(String input) {
  
          //Note that we use a regex matching all "|" characters not prefixed by "\"
          //Since we define a regex for matching, the input delimiter "|" needs to be escaped, as well as "\",
          //which is even double-escaped - one escaping is due to the String, one due to the regex.
          return input.split("(?<!\\\\)\\" + INPUT_DELIMITER);
      }
  
      /**
       * @see java.lang.Object#equals(java.lang.Object)
       */
      @Override
      public boolean equals(Object obj) {
  
          if (!(obj instanceof CmsSelectWidgetOption)) {
              return false;
          }
          CmsSelectWidgetOption other = (CmsSelectWidgetOption)obj;
          if (m_default != other.m_default) {
              return false;
          }
          if (m_value == null) {
              if (other.m_value != null) {
                  return false;
              }
          } else if (!m_value.equals(other.m_value)) {
              return false;
          }
          if (m_option == null) {
              if (other.m_option != null) {
                  return false;
              }
          } else if (!m_option.equals(other.m_option)) {
              return false;
          }
          if (m_help == null) {
              if (other.m_help != null) {
                  return false;
              }
          } else if (!m_help.equals(other.m_help)) {
              return false;
          }
          return true;
      }
  
      /**
       * Returns the (optional) help text of this select option.<p>
       *
       * @return the (optional) help text of this select option
       */
      public String getHelp() {
  
          return m_help;
      }
  
      /**
       * Returns the option text of this select option.<p>
       *
       * If this has not been set, the result of <code>{@link #getValue()}</code> is returned,
       * there will always be a result other than <code>null</code> returned.<p>
       *
       * @return the option text of this select option
       */
      public String getOption() {
  
          if (m_option == null) {
              return getValue();
          }
          return m_option;
      }
  
      /**
       * Returns the value of this select option.<p>
       *
       * @return the value of this select option
       */
      public String getValue() {
  
          return m_value;
      }
  
      /**
       * @see java.lang.Object#hashCode()
       */
      @Override
      public int hashCode() {
  
          if (m_hashcode == 0) {
              StringBuffer hash = new StringBuffer(128);
              hash.append(m_value);
              hash.append('|');
              hash.append(m_default);
              hash.append('|');
              hash.append(m_option);
              hash.append('|');
              hash.append(m_help);
              m_hashcode = hash.toString().hashCode();
          }
          return m_hashcode;
      }
  
      /**
       * Returns <code>true</code> if this is the default value of the selection.<p>
       *
       * @return <code>true</code> if this is the default value of the selection
       */
      public boolean isDefault() {
  
          return m_default;
      }
  
      /**
       * @see java.lang.Object#toString()
       */
      @Override
      public String toString() {
  
          StringBuffer result = new StringBuffer(128);
  
          result.append(KEY_VALUE);
          result.append(m_value);
          result.append('\'');
          if (m_default) {
              result.append(' ');
              result.append(KEY_DEFAULT);
              result.append(m_default);
              result.append('\'');
          }
          if (m_option != null) {
              result.append(' ');
              result.append(KEY_OPTION);
              result.append(m_option);
              result.append('\'');
          }
          if (m_help != null) {
              result.append(' ');
              result.append(KEY_HELP);
              result.append(m_help);
              result.append('\'');
          }
          return result.toString();
      }
  }