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 java.io.Serializable;
22 import java.text.ParseException;
23 import java.text.SimpleDateFormat;
24 import java.util.ArrayList;
25 import java.util.Comparator;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.ResourceBundle;
30
31 import org.apache.commons.lang.builder.CompareToBuilder;
32 import org.apache.commons.lang.builder.ToStringBuilder;
33 import org.apache.log4j.Logger;
34 import org.itracker.services.exceptions.IssueException;
35 import org.itracker.services.util.CustomFieldUtilities;
36 import org.itracker.web.taglib.FormatCustomFieldTag;
37 import org.jfree.util.Log;
38
39 /**
40 * A custom field that can be added to an Issue.
41 *
42 * <p>
43 * Allows the user to dynamically extend the set of attributes/properties of the
44 * Issue class.
45 * </p>
46 *
47 * <p>
48 * A CustomField must be configured to be used in a Project in order to extend
49 * the attributes/properties of all Issues created for that project. A
50 * CustomField may be used in more than 1 project. (Project - CustomField is a
51 * M-N relathionship).
52 * </p>
53 *
54 * <p>
55 * A CustomField has a type, which indicates the data type of its value. <br>
56 * The special type <code>LIST</code>, allows to associate a list of string
57 * options to a CustomField, which are the enumeration of possible values for
58 * that field. <br>
59 * Each option value is represented by a CustomFieldValue instance. There's a
60 * 1-N relationship between CustomField - CustomFieldValue. A CustomFieldValue
61 * can only belong to 1 CustomField (composition).
62 * </p>
63 *
64 * <p>
65 * A value of a CustomField for a given Issue is represented by an IssueField
66 * instance. (CustomField - IssueField is a 1-N relationship).
67 * </p>
68 *
69 * @author ready
70 * @see CustomFieldValue
71 * @see IssueField
72 */
73 public class CustomField extends AbstractEntity implements Comparable<Entity> {
74
75 private static final Logger logger = Logger.getLogger(CustomField.class);
76
77 public static final Comparator<CustomField> NAME_COMPARATOR = new NameComparator();
78 /**
79 * Dateformat able to parse datepicker generated date string (dd/MM/yyyy)
80 */
81 public static final SimpleDateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat("dd/MM/yyyy");
82 /**
83 *
84 */
85 private static final long serialVersionUID = 1L;
86
87
88 /* Is a CustomField instance locale-specific ? */
89 // private Locale locale;
90 /**
91 * Field value data type.
92 */
93 private Type type;
94
95 /**
96 * Display format to use if <code>fieldType</code> is a Date.
97 *
98 * TODO: use type-safe enum CustomField.DateFormat
99 */
100 private String dateFormat;
101
102 /**
103 * Whether this field is mandatory or optional. PENDING: this should be
104 * specified when the field is used in a project!
105 */
106 private boolean required;
107
108 /**
109 * List of options for a field of type <code>LIST</code>.
110 *
111 * <p>
112 * This is the enumeration of possible values for the field.
113 * </p>
114 *
115 * Note: this field used to be named <code>values</code> is iTracker 2.
116 *
117 * <p>
118 * PENDING: There's no way to use this as a list of proposed values,
119 * allowing the user to enter a value that's not in this list.
120 * </p>
121 */
122 private List<CustomFieldValue> options = new ArrayList<CustomFieldValue>();
123
124 /**
125 * Whether the options of a field of type List should be sorted by their
126 * name rather than by {@link CustomFieldValue#getSortOrder() }.
127 */
128 private boolean sortOptionsByName;
129
130 /*
131 * This class used to have a <code>fields</code> attribute, which was a
132 * Collection<IssueField>. This has been removed because the association
133 * CustomField - IssueField doesn't need to be navigatable in this
134 * direction.
135 */
136
137 /**
138 * Default constructor (required by Hibernate).
139 *
140 * <p>
141 * PENDING: should be <code>private</code> so that it can only be used by
142 * Hibernate, to ensure that the fields which form an instance's identity
143 * are always initialized/never <tt>null</tt>.
144 * </p>
145 */
146 public CustomField() {
147 }
148
149 @Deprecated
150 public CustomField(String name, Type type) {
151 // setName(name);
152 setFieldType(type);
153 }
154
155 // /**
156 // *
157 // * @deprecated this can not be in the entity, replace by Utility or service.
158 // * @return
159 // */
160 // public String getName() {
161 // return name;
162 // }
163 //
164 // public void setName(String name) {
165 // this.name = name;
166 // }
167
168 public Type getFieldType() {
169 return type;
170 }
171
172 public void setFieldType(Type type) {
173 this.type = type;
174 }
175
176 public String getDateFormat() {
177 return dateFormat;
178 }
179
180 public void setDateFormat(String dateFormat) {
181 this.dateFormat = dateFormat;
182 }
183
184 public boolean isRequired() {
185 return required;
186 }
187
188 public void setRequired(boolean required) {
189 this.required = required;
190 }
191
192 public List<CustomFieldValue> getOptions() {
193 return options;
194 }
195
196 public void setOptions(List<CustomFieldValue> options) {
197 this.options = options;
198 }
199
200 /**
201 * Adds a new option value/name to the custom field.
202 *
203 * <p>
204 * New options are put at the end of the list even if they should be sorted.
205 * <br>
206 * This method is mainly used to build a new custom field so it can be saved
207 * later.
208 * </p>
209 *
210 * @param value
211 * the option value
212 * @param label
213 * the label/name for the new option
214 *
215 * @deprecated this can not be in the entity, replace by Utility or service.
216 */
217 public void addOption(String value, String label) {
218 this.options.add(new CustomFieldValue(this, value));
219 }
220
221 /**
222 * Returns the name for a particular option value.
223 *
224 * @deprecated this can not be in the entity, replace by Utility or service.
225 * FIXME: Don't know, this seems not to be working. Removed use from {@link FormatCustomFieldTag}
226 * @param optionValue
227 * the value to lookup the name for
228 * @return the localized name for the supplied value
229 */
230 public String getOptionNameByValue(String optionValue) {
231 final Iterator<CustomFieldValue> iter = this.options.iterator();
232
233 if (logger.isDebugEnabled()) {
234 logger.warn("getOptionNameByValue: called deprecated api", new RuntimeException());
235 }
236 while (iter.hasNext()) {
237 CustomFieldValue option = iter.next();
238
239 if (option.getValue().equalsIgnoreCase(optionValue)) {
240 return CustomFieldUtilities.getCustomFieldOptionName(option, null);
241 }
242 }
243 return "";
244 }
245
246 public boolean isSortOptionsByName() {
247 return sortOptionsByName;
248 }
249
250 /**
251 * @param sortOptionsByName
252 */
253 public void setSortOptionsByName(boolean sortOptionsByName) {
254 this.sortOptionsByName = sortOptionsByName;
255 }
256
257 // /**
258 // * Sets this custom fields names based on the supplied local string.
259 // *
260 // * @deprecated this can not be in the entity, replace by Utility or service.
261 // * @param locale
262 // * the name of the locale to use for the names
263 // */
264 // public void setLabels(String locale) {
265 // Locale loc = ITrackerResources.getLocale(locale);
266 // setLabels(loc);
267 // }
268
269 // /**
270 // * Sets this custom fields names based on the supplied locale.
271 // * @deprecated this can not be in the entity, replace by Utility or service.
272 // * @param locale
273 // * the locale to use for the names
274 // */
275 // public void setLabels(Locale locale) {
276 // setName(CustomFieldUtilities.getCustomFieldName(getId(), locale));
277 //
278 // final Iterator<CustomFieldValue> iter = this.getOptions().iterator();
279 //
280 // while (iter.hasNext()) {
281 // CustomFieldValue option = iter.next();
282 //
283 // option.setName(CustomFieldUtilities.getCustomFieldOptionName(
284 // this.getId(), option.getId(), locale));
285 // }
286 //
287 // if (isSortOptionsByName()) {
288 // // Specify ordering other than the natural ordering of
289 // // CustomFieldValue.
290 // Collections.sort(this.getOptions(), CustomFieldValue.NAME_COMPARATOR);
291 // } else {
292 // Collections.sort(this.getOptions(), CustomFieldValue.SORT_ORDER_COMPARATOR);
293 // }
294 // }
295
296 @Override
297 public String toString() {
298
299 return new ToStringBuilder(this)
300 .append("id", getId())
301 .append("type", getFieldType())
302 .append("sortOptionsByName", isSortOptionsByName()).toString();
303 }
304
305 //
306 // public int compareTo(CustomField other) {
307 // return this.name.compareTo(other.name);
308 // }
309 //
310 // @Override
311 // public boolean equals(Object obj) {
312 // if (this == obj) {
313 // return true;
314 // }
315 //
316 // if (obj instanceof CustomField) {
317 // final CustomField other = (CustomField)obj;
318 //
319 // return this.name.equals(other.name);
320 // }
321 // return false;
322 // }
323 //
324 // @Override
325 // public int hashCode() {
326 // return this.name.hashCode();
327 // }
328 //
329 /**
330 * Checks if the given value is assignable to this custom field.
331 *
332 * @param value
333 * custom field data
334 * @param locale
335 * @param bundle
336 * @throws IssueException
337 * if it isn't
338 * @see IssueField#setValue(String, Locale, ResourceBundle)
339 */
340 public void checkAssignable(String value, Locale locale,
341 ResourceBundle bundle) throws IssueException {
342
343
344 if (this.isRequired() && (value == null || value.trim().length() == 0)) {
345 throw new IssueException("Value is required.", IssueException.TYPE_CF_REQ_FIELD);
346 }
347
348 switch (this.type) {
349
350 case INTEGER:
351 try {
352 Integer.parseInt(value);
353 } catch (NumberFormatException nfe) {
354 throw new IssueException("Invalid integer.",
355 IssueException.TYPE_CF_PARSE_NUM);
356 }
357 break;
358
359 case DATE:
360 if (this.dateFormat != CustomFieldUtilities.DATE_FORMAT_UNKNOWN) {
361 SimpleDateFormat format = DEFAULT_DATE_FORMAT;
362 // new SimpleDateFormat(bundle
363 // .getString("itracker.dateformat." + this.dateFormat),
364 // locale);
365
366 try {
367 format.parse(value);
368 } catch (ParseException ex) {
369 throw new IssueException("Invalid date format.",
370 IssueException.TYPE_CF_PARSE_DATE);
371 }
372 }
373 break;
374
375 case LIST:
376 Iterator<CustomFieldValue> it = getOptions().iterator();
377 while (it.hasNext()) {
378 CustomFieldValue customFieldValue = (CustomFieldValue) it
379 .next();
380 if (customFieldValue.getValue().equalsIgnoreCase(value)) {
381 return;
382 }
383 }
384 if (Log.isDebugEnabled()) {
385 Log.debug("checkAssignable: could not assign value to custom field values: " + value + ", " + getOptions());
386 }
387 throw new IssueException("Invalid value.", IssueException.TYPE_CF_INVALID_LIST_OPTION);
388 default:
389 // Value is OK
390 }
391 }
392
393 /**
394 * Enumeration of possible data types.
395 */
396 public static enum Type implements IntCodeEnum<Type> {
397
398 STRING(1), INTEGER(2), DATE(3), LIST(4);
399
400 private final int code;
401
402 private Type(int code) {
403 this.code = code;
404 }
405
406 public int getCode() {
407 return code;
408 }
409
410 public Type fromCode(int code) {
411 return Type.valueOf(code);
412 }
413
414 public static Type valueOf(int code) {
415 switch (code) {
416 case 1:
417 return STRING;
418 case 2:
419 return INTEGER;
420 case 3:
421 return DATE;
422 case 4:
423 return LIST;
424 default:
425 throw new IllegalArgumentException("Unknown code : " + code);
426 }
427 }
428
429 }
430
431 /**
432 * Date format for fields of type DATE.
433 *
434 * PENDING: consider replacing the DATE Type with these 3 new data types.
435 */
436 public static enum DateFormat {
437
438 DATE_TIME("full"), DATE("dateonly"), TIME("timeonly");
439
440 final String code;
441
442 DateFormat(String code) {
443 this.code = code;
444 }
445
446 }
447
448 public static final class NameComparator implements Comparator<CustomField>, Serializable {
449 /**
450 *
451 */
452 private static final long serialVersionUID = 1L;
453
454 private final Locale locale;
455
456 public NameComparator() {
457 this(null);
458 }
459 public NameComparator(Locale locale) {
460 this.locale = locale;
461 }
462
463 public int compare(CustomField o1, CustomField o2) {
464 return new CompareToBuilder().append(
465 CustomFieldUtilities.getCustomFieldName(o1.getId(), locale),
466 CustomFieldUtilities.getCustomFieldName(o2.getId(), locale))
467 .append(o1.getId(), o2.getId())
468 .toComparison();
469 }
470
471 }
472
473 }