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 }