View Javadoc
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  
19  package org.itracker.model;
20  
21  import org.apache.commons.lang.builder.ToStringBuilder;
22  import org.apache.log4j.Logger;
23  import org.itracker.IssueException;
24  import org.itracker.model.util.CustomFieldUtilities;
25  
26  import java.text.ParseException;
27  import java.text.SimpleDateFormat;
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.ResourceBundle;
32  
33  /**
34   * A custom field that can be added to an Issue.
35   * <p/>
36   * <p>
37   * Allows the user to dynamically extend the set of attributes/properties of the
38   * Issue class.
39   * </p>
40   * <p/>
41   * <p>
42   * A CustomField must be configured to be used in a Project in order to extend
43   * the attributes/properties of all Issues created for that project. A
44   * CustomField may be used in more than 1 project. (Project - CustomField is a
45   * M-N relathionship).
46   * </p>
47   * <p/>
48   * <p>
49   * A CustomField has a type, which indicates the data type of its value. <br>
50   * The special type <code>LIST</code>, allows to associate a list of string
51   * options to a CustomField, which are the enumeration of possible values for
52   * that field. <br>
53   * Each option value is represented by a CustomFieldValue instance. There's a
54   * 1-N relationship between CustomField - CustomFieldValue. A CustomFieldValue
55   * can only belong to 1 CustomField (composition).
56   * </p>
57   * <p/>
58   * <p>
59   * A value of a CustomField for a given Issue is represented by an IssueField
60   * instance. (CustomField - IssueField is a 1-N relationship).
61   * </p>
62   *
63   * @author ready
64   * @see CustomFieldValue
65   * @see IssueField
66   */
67  public class CustomField extends AbstractEntity implements Comparable<Entity> {
68  
69      private static final Logger logger = Logger.getLogger(CustomField.class);
70  
71      /**
72       * Dateformat able to parse datepicker generated date string (dd/MM/yyyy)
73       */
74      public static final SimpleDateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat("dd/MM/yyyy");
75      /**
76       *
77       */
78      private static final long serialVersionUID = 1L;
79  
80      /**
81       * Field value data type.
82       */
83      private Type type;
84  
85      /**
86       * Display format to use if <code>fieldType</code> is a Date.
87       * <p/>
88       * TODO: use type-safe enum CustomField.DateFormat
89       */
90      private String dateFormat;
91  
92      /**
93       * Whether this field is mandatory or optional. PENDING: this should be
94       * specified when the field is used in a project!
95       */
96      private boolean required;
97  
98      /**
99       * List of options for a field of type <code>LIST</code>.
100      * <p/>
101      * <p>
102      * This is the enumeration of possible values for the field.
103      * </p>
104      * <p/>
105      * Note: this field used to be named <code>values</code> is iTracker 2.
106      * <p/>
107      * <p>
108      * PENDING: There's no way to use this as a list of proposed values,
109      * allowing the user to enter a value that's not in this list.
110      * </p>
111      */
112     private List<CustomFieldValue> options = new ArrayList<CustomFieldValue>();
113 
114     /**
115      * Whether the options of a field of type List should be sorted by their
116      * name rather than by {@link CustomFieldValue#getSortOrder() }.
117      */
118     private boolean sortOptionsByName;
119 
120     /**
121      * Default constructor (required by Hibernate).
122      * <p/>
123      * <p>
124      * PENDING: should be <code>private</code> so that it can only be used by
125      * Hibernate, to ensure that the fields which form an instance's identity
126      * are always initialized/never <tt>null</tt>.
127      * </p>
128      */
129     public CustomField() {
130     }
131 
132 
133     public Type getFieldType() {
134         return type;
135     }
136 
137     public void setFieldType(Type type) {
138         this.type = type;
139     }
140 
141     public String getDateFormat() {
142         return dateFormat;
143     }
144 
145     public void setDateFormat(String dateFormat) {
146         this.dateFormat = dateFormat;
147     }
148 
149     public boolean isRequired() {
150         return required;
151     }
152 
153     public void setRequired(boolean required) {
154         this.required = required;
155     }
156 
157     public List<CustomFieldValue> getOptions() {
158         return options;
159     }
160 
161     public void setOptions(List<CustomFieldValue> options) {
162         this.options = options;
163     }
164 
165     /**
166      * Adds a new option value/name to the custom field.
167      * <p/>
168      * <p>
169      * New options are put at the end of the list even if they should be sorted.
170      * <br>
171      * This method is mainly used to build a new custom field so it can be saved
172      * later.
173      * </p>
174      *
175      * @param value the option value
176      * @param label the label/name for the new option
177      * @deprecated this can not be in the entity, replace by Utility or service.
178      */
179     public void addOption(String value, String label) {
180         this.options.add(new CustomFieldValue(this, value));
181     }
182 
183 
184     public boolean isSortOptionsByName() {
185         return sortOptionsByName;
186     }
187 
188     public void setSortOptionsByName(boolean sortOptionsByName) {
189         this.sortOptionsByName = sortOptionsByName;
190     }
191 
192     @Override
193     public String toString() {
194 
195         return new ToStringBuilder(this)
196                 .append("id", getId())
197                 .append("type", getFieldType())
198                 .append("sortOptionsByName", isSortOptionsByName()).toString();
199     }
200 
201     /**
202      * Checks if the given value is assignable to this custom field.
203      *
204      * @param value custom field data
205      * @throws org.itracker.IssueException if it isn't
206      * @see IssueField#setValue(String, Locale, ResourceBundle)
207      */
208     public void checkAssignable(String value, Locale locale,
209                                 ResourceBundle bundle) throws IssueException {
210 
211 
212         if (this.isRequired() && (value == null || value.trim().length() == 0)) {
213             throw new IssueException("Value is required.", IssueException.TYPE_CF_REQ_FIELD);
214         }
215 
216         switch (this.type) {
217 
218             case INTEGER:
219                 try {
220                     Integer.parseInt(value);
221                 } catch (NumberFormatException nfe) {
222                     throw new IssueException("Invalid integer.",
223                             IssueException.TYPE_CF_PARSE_NUM);
224                 }
225                 break;
226 
227             case DATE:
228                 if (!CustomFieldUtilities.DATE_FORMAT_UNKNOWN.equals(this.dateFormat)) {
229                     SimpleDateFormat format =
230                             // DEFAULT_DATE_FORMAT;
231                             new SimpleDateFormat(bundle
232                                     .getString("itracker.dateformat." + this.dateFormat),
233                                     locale);
234 
235                     try {
236                         format.parse(value);
237                     } catch (ParseException ex) {
238                         throw new IssueException("Invalid date format.",
239                                 IssueException.TYPE_CF_PARSE_DATE);
240                     }
241                 }
242                 break;
243 
244             case LIST:
245                 for (CustomFieldValue customFieldValue : getOptions()) {
246                     if (customFieldValue.getValue().equalsIgnoreCase(value)) {
247                         return;
248                     }
249                 }
250                 if (logger.isDebugEnabled()) {
251                     logger.debug("checkAssignable: could not assign value to custom field values: " + value + ", " + getOptions());
252                 }
253                 throw new IssueException("Invalid value.", IssueException.TYPE_CF_INVALID_LIST_OPTION);
254             default:
255                 // Value is OK
256         }
257     }
258 
259     /**
260      * Enumeration of possible data types.
261      */
262     public static enum Type implements IntCodeEnum<Type> {
263 
264         STRING(1), INTEGER(2), DATE(3), LIST(4);
265 
266         private final Integer code;
267 
268         private Type(Integer code) {
269             this.code = code;
270         }
271 
272         public Integer getCode() {
273             return code;
274         }
275 
276         public Type fromCode(Integer code) {
277             return Type.valueOf(code);
278         }
279 
280         public static Type valueOf(Integer code) {
281             for (Type val: values()) {
282                 if (val.code.compareTo(code) == 0) {
283                     return val;
284                 }
285             }
286             throw new IllegalArgumentException("Unknown code : " + code);
287         }
288 
289 
290     }
291 
292     /**
293      * Date format for fields of type DATE.
294      * <p/>
295      * PENDING: consider replacing the DATE Type with these 3 new data types.
296      */
297     public static enum DateFormat {
298 
299         DATE_TIME("full"), DATE("dateonly"), TIME("timeonly");
300 
301         final String code;
302 
303         DateFormat(String code) {
304             this.code = code;
305         }
306 
307     }
308 }