CustomField.java

  1. /*
  2.  * This software was designed and created by Jason Carroll.
  3.  * Copyright (c) 2002, 2003, 2004 Jason Carroll.
  4.  * The author can be reached at jcarroll@cowsultants.com
  5.  * ITracker website: http://www.cowsultants.com
  6.  * ITracker forums: http://www.cowsultants.com/phpBB/index.php
  7.  *
  8.  * This program is free software; you can redistribute it and/or modify
  9.  * it only under the terms of the GNU General Public License as published by
  10.  * the Free Software Foundation; either version 2 of the License, or
  11.  * (at your option) any later version.
  12.  *
  13.  * This program is distributed in the hope that it will be useful,
  14.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  16.  * GNU General Public License for more details.
  17.  */

  18. package org.itracker.model;

  19. import org.apache.commons.lang.builder.ToStringBuilder;
  20. import org.apache.log4j.Logger;
  21. import org.itracker.IssueException;
  22. import org.itracker.model.util.CustomFieldUtilities;

  23. import java.text.ParseException;
  24. import java.text.SimpleDateFormat;
  25. import java.util.ArrayList;
  26. import java.util.List;
  27. import java.util.Locale;
  28. import java.util.ResourceBundle;

  29. /**
  30.  * A custom field that can be added to an Issue.
  31.  * <p/>
  32.  * <p>
  33.  * Allows the user to dynamically extend the set of attributes/properties of the
  34.  * Issue class.
  35.  * </p>
  36.  * <p/>
  37.  * <p>
  38.  * A CustomField must be configured to be used in a Project in order to extend
  39.  * the attributes/properties of all Issues created for that project. A
  40.  * CustomField may be used in more than 1 project. (Project - CustomField is a
  41.  * M-N relathionship).
  42.  * </p>
  43.  * <p/>
  44.  * <p>
  45.  * A CustomField has a type, which indicates the data type of its value. <br>
  46.  * The special type <code>LIST</code>, allows to associate a list of string
  47.  * options to a CustomField, which are the enumeration of possible values for
  48.  * that field. <br>
  49.  * Each option value is represented by a CustomFieldValue instance. There's a
  50.  * 1-N relationship between CustomField - CustomFieldValue. A CustomFieldValue
  51.  * can only belong to 1 CustomField (composition).
  52.  * </p>
  53.  * <p/>
  54.  * <p>
  55.  * A value of a CustomField for a given Issue is represented by an IssueField
  56.  * instance. (CustomField - IssueField is a 1-N relationship).
  57.  * </p>
  58.  *
  59.  * @author ready
  60.  * @see CustomFieldValue
  61.  * @see IssueField
  62.  */
  63. public class CustomField extends AbstractEntity implements Comparable<Entity> {

  64.     private static final Logger logger = Logger.getLogger(CustomField.class);

  65.     /**
  66.      * Dateformat able to parse datepicker generated date string (dd/MM/yyyy)
  67.      */
  68.     public static final SimpleDateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat("dd/MM/yyyy");
  69.     /**
  70.      *
  71.      */
  72.     private static final long serialVersionUID = 1L;

  73.     /**
  74.      * Field value data type.
  75.      */
  76.     private Type type;

  77.     /**
  78.      * Display format to use if <code>fieldType</code> is a Date.
  79.      * <p/>
  80.      * TODO: use type-safe enum CustomField.DateFormat
  81.      */
  82.     private String dateFormat;

  83.     /**
  84.      * Whether this field is mandatory or optional. PENDING: this should be
  85.      * specified when the field is used in a project!
  86.      */
  87.     private boolean required;

  88.     /**
  89.      * List of options for a field of type <code>LIST</code>.
  90.      * <p/>
  91.      * <p>
  92.      * This is the enumeration of possible values for the field.
  93.      * </p>
  94.      * <p/>
  95.      * Note: this field used to be named <code>values</code> is iTracker 2.
  96.      * <p/>
  97.      * <p>
  98.      * PENDING: There's no way to use this as a list of proposed values,
  99.      * allowing the user to enter a value that's not in this list.
  100.      * </p>
  101.      */
  102.     private List<CustomFieldValue> options = new ArrayList<CustomFieldValue>();

  103.     /**
  104.      * Whether the options of a field of type List should be sorted by their
  105.      * name rather than by {@link CustomFieldValue#getSortOrder() }.
  106.      */
  107.     private boolean sortOptionsByName;

  108.     /**
  109.      * Default constructor (required by Hibernate).
  110.      * <p/>
  111.      * <p>
  112.      * PENDING: should be <code>private</code> so that it can only be used by
  113.      * Hibernate, to ensure that the fields which form an instance's identity
  114.      * are always initialized/never <tt>null</tt>.
  115.      * </p>
  116.      */
  117.     public CustomField() {
  118.     }


  119.     public Type getFieldType() {
  120.         return type;
  121.     }

  122.     public void setFieldType(Type type) {
  123.         this.type = type;
  124.     }

  125.     public String getDateFormat() {
  126.         return dateFormat;
  127.     }

  128.     public void setDateFormat(String dateFormat) {
  129.         this.dateFormat = dateFormat;
  130.     }

  131.     public boolean isRequired() {
  132.         return required;
  133.     }

  134.     public void setRequired(boolean required) {
  135.         this.required = required;
  136.     }

  137.     public List<CustomFieldValue> getOptions() {
  138.         return options;
  139.     }

  140.     public void setOptions(List<CustomFieldValue> options) {
  141.         this.options = options;
  142.     }

  143.     /**
  144.      * Adds a new option value/name to the custom field.
  145.      * <p/>
  146.      * <p>
  147.      * New options are put at the end of the list even if they should be sorted.
  148.      * <br>
  149.      * This method is mainly used to build a new custom field so it can be saved
  150.      * later.
  151.      * </p>
  152.      *
  153.      * @param value the option value
  154.      * @param label the label/name for the new option
  155.      * @deprecated this can not be in the entity, replace by Utility or service.
  156.      */
  157.     public void addOption(String value, String label) {
  158.         this.options.add(new CustomFieldValue(this, value));
  159.     }


  160.     public boolean isSortOptionsByName() {
  161.         return sortOptionsByName;
  162.     }

  163.     public void setSortOptionsByName(boolean sortOptionsByName) {
  164.         this.sortOptionsByName = sortOptionsByName;
  165.     }

  166.     @Override
  167.     public String toString() {

  168.         return new ToStringBuilder(this)
  169.                 .append("id", getId())
  170.                 .append("type", getFieldType())
  171.                 .append("sortOptionsByName", isSortOptionsByName()).toString();
  172.     }

  173.     /**
  174.      * Checks if the given value is assignable to this custom field.
  175.      *
  176.      * @param value custom field data
  177.      * @throws org.itracker.IssueException if it isn't
  178.      * @see IssueField#setValue(String, Locale, ResourceBundle)
  179.      */
  180.     public void checkAssignable(String value, Locale locale,
  181.                                 ResourceBundle bundle) throws IssueException {


  182.         if (this.isRequired() && (value == null || value.trim().length() == 0)) {
  183.             throw new IssueException("Value is required.", IssueException.TYPE_CF_REQ_FIELD);
  184.         }

  185.         switch (this.type) {

  186.             case INTEGER:
  187.                 try {
  188.                     Integer.parseInt(value);
  189.                 } catch (NumberFormatException nfe) {
  190.                     throw new IssueException("Invalid integer.",
  191.                             IssueException.TYPE_CF_PARSE_NUM);
  192.                 }
  193.                 break;

  194.             case DATE:
  195.                 if (!CustomFieldUtilities.DATE_FORMAT_UNKNOWN.equals(this.dateFormat)) {
  196.                     SimpleDateFormat format =
  197.                             // DEFAULT_DATE_FORMAT;
  198.                             new SimpleDateFormat(bundle
  199.                                     .getString("itracker.dateformat." + this.dateFormat),
  200.                                     locale);

  201.                     try {
  202.                         format.parse(value);
  203.                     } catch (ParseException ex) {
  204.                         throw new IssueException("Invalid date format.",
  205.                                 IssueException.TYPE_CF_PARSE_DATE);
  206.                     }
  207.                 }
  208.                 break;

  209.             case LIST:
  210.                 for (CustomFieldValue customFieldValue : getOptions()) {
  211.                     if (customFieldValue.getValue().equalsIgnoreCase(value)) {
  212.                         return;
  213.                     }
  214.                 }
  215.                 if (logger.isDebugEnabled()) {
  216.                     logger.debug("checkAssignable: could not assign value to custom field values: " + value + ", " + getOptions());
  217.                 }
  218.                 throw new IssueException("Invalid value.", IssueException.TYPE_CF_INVALID_LIST_OPTION);
  219.             default:
  220.                 // Value is OK
  221.         }
  222.     }

  223.     /**
  224.      * Enumeration of possible data types.
  225.      */
  226.     public static enum Type implements IntCodeEnum<Type> {

  227.         STRING(1), INTEGER(2), DATE(3), LIST(4);

  228.         private final Integer code;

  229.         private Type(Integer code) {
  230.             this.code = code;
  231.         }

  232.         public Integer getCode() {
  233.             return code;
  234.         }

  235.         public Type fromCode(Integer code) {
  236.             return Type.valueOf(code);
  237.         }

  238.         public static Type valueOf(Integer code) {
  239.             for (Type val: values()) {
  240.                 if (val.code.compareTo(code) == 0) {
  241.                     return val;
  242.                 }
  243.             }
  244.             throw new IllegalArgumentException("Unknown code : " + code);
  245.         }


  246.     }

  247.     /**
  248.      * Date format for fields of type DATE.
  249.      * <p/>
  250.      * PENDING: consider replacing the DATE Type with these 3 new data types.
  251.      */
  252.     public static enum DateFormat {

  253.         DATE_TIME("full"), DATE("dateonly"), TIME("timeonly");

  254.         final String code;

  255.         DateFormat(String code) {
  256.             this.code = code;
  257.         }

  258.     }
  259. }