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 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 }