CustomField.java
/*
* This software was designed and created by Jason Carroll.
* Copyright (c) 2002, 2003, 2004 Jason Carroll.
* The author can be reached at jcarroll@cowsultants.com
* ITracker website: http://www.cowsultants.com
* ITracker forums: http://www.cowsultants.com/phpBB/index.php
*
* This program is free software; you can redistribute it and/or modify
* it only under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
package org.itracker.model;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.log4j.Logger;
import org.itracker.IssueException;
import org.itracker.model.util.CustomFieldUtilities;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
/**
* A custom field that can be added to an Issue.
* <p/>
* <p>
* Allows the user to dynamically extend the set of attributes/properties of the
* Issue class.
* </p>
* <p/>
* <p>
* A CustomField must be configured to be used in a Project in order to extend
* the attributes/properties of all Issues created for that project. A
* CustomField may be used in more than 1 project. (Project - CustomField is a
* M-N relathionship).
* </p>
* <p/>
* <p>
* A CustomField has a type, which indicates the data type of its value. <br>
* The special type <code>LIST</code>, allows to associate a list of string
* options to a CustomField, which are the enumeration of possible values for
* that field. <br>
* Each option value is represented by a CustomFieldValue instance. There's a
* 1-N relationship between CustomField - CustomFieldValue. A CustomFieldValue
* can only belong to 1 CustomField (composition).
* </p>
* <p/>
* <p>
* A value of a CustomField for a given Issue is represented by an IssueField
* instance. (CustomField - IssueField is a 1-N relationship).
* </p>
*
* @author ready
* @see CustomFieldValue
* @see IssueField
*/
public class CustomField extends AbstractEntity implements Comparable<Entity> {
private static final Logger logger = Logger.getLogger(CustomField.class);
/**
* Dateformat able to parse datepicker generated date string (dd/MM/yyyy)
*/
public static final SimpleDateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat("dd/MM/yyyy");
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* Field value data type.
*/
private Type type;
/**
* Display format to use if <code>fieldType</code> is a Date.
* <p/>
* TODO: use type-safe enum CustomField.DateFormat
*/
private String dateFormat;
/**
* Whether this field is mandatory or optional. PENDING: this should be
* specified when the field is used in a project!
*/
private boolean required;
/**
* List of options for a field of type <code>LIST</code>.
* <p/>
* <p>
* This is the enumeration of possible values for the field.
* </p>
* <p/>
* Note: this field used to be named <code>values</code> is iTracker 2.
* <p/>
* <p>
* PENDING: There's no way to use this as a list of proposed values,
* allowing the user to enter a value that's not in this list.
* </p>
*/
private List<CustomFieldValue> options = new ArrayList<CustomFieldValue>();
/**
* Whether the options of a field of type List should be sorted by their
* name rather than by {@link CustomFieldValue#getSortOrder() }.
*/
private boolean sortOptionsByName;
/**
* Default constructor (required by Hibernate).
* <p/>
* <p>
* PENDING: should be <code>private</code> so that it can only be used by
* Hibernate, to ensure that the fields which form an instance's identity
* are always initialized/never <tt>null</tt>.
* </p>
*/
public CustomField() {
}
public Type getFieldType() {
return type;
}
public void setFieldType(Type type) {
this.type = type;
}
public String getDateFormat() {
return dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
public boolean isRequired() {
return required;
}
public void setRequired(boolean required) {
this.required = required;
}
public List<CustomFieldValue> getOptions() {
return options;
}
public void setOptions(List<CustomFieldValue> options) {
this.options = options;
}
/**
* Adds a new option value/name to the custom field.
* <p/>
* <p>
* New options are put at the end of the list even if they should be sorted.
* <br>
* This method is mainly used to build a new custom field so it can be saved
* later.
* </p>
*
* @param value the option value
* @param label the label/name for the new option
* @deprecated this can not be in the entity, replace by Utility or service.
*/
public void addOption(String value, String label) {
this.options.add(new CustomFieldValue(this, value));
}
public boolean isSortOptionsByName() {
return sortOptionsByName;
}
public void setSortOptionsByName(boolean sortOptionsByName) {
this.sortOptionsByName = sortOptionsByName;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("id", getId())
.append("type", getFieldType())
.append("sortOptionsByName", isSortOptionsByName()).toString();
}
/**
* Checks if the given value is assignable to this custom field.
*
* @param value custom field data
* @throws org.itracker.IssueException if it isn't
* @see IssueField#setValue(String, Locale, ResourceBundle)
*/
public void checkAssignable(String value, Locale locale,
ResourceBundle bundle) throws IssueException {
if (this.isRequired() && (value == null || value.trim().length() == 0)) {
throw new IssueException("Value is required.", IssueException.TYPE_CF_REQ_FIELD);
}
switch (this.type) {
case INTEGER:
try {
Integer.parseInt(value);
} catch (NumberFormatException nfe) {
throw new IssueException("Invalid integer.",
IssueException.TYPE_CF_PARSE_NUM);
}
break;
case DATE:
if (!CustomFieldUtilities.DATE_FORMAT_UNKNOWN.equals(this.dateFormat)) {
SimpleDateFormat format =
// DEFAULT_DATE_FORMAT;
new SimpleDateFormat(bundle
.getString("itracker.dateformat." + this.dateFormat),
locale);
try {
format.parse(value);
} catch (ParseException ex) {
throw new IssueException("Invalid date format.",
IssueException.TYPE_CF_PARSE_DATE);
}
}
break;
case LIST:
for (CustomFieldValue customFieldValue : getOptions()) {
if (customFieldValue.getValue().equalsIgnoreCase(value)) {
return;
}
}
if (logger.isDebugEnabled()) {
logger.debug("checkAssignable: could not assign value to custom field values: " + value + ", " + getOptions());
}
throw new IssueException("Invalid value.", IssueException.TYPE_CF_INVALID_LIST_OPTION);
default:
// Value is OK
}
}
/**
* Enumeration of possible data types.
*/
public static enum Type implements IntCodeEnum<Type> {
STRING(1), INTEGER(2), DATE(3), LIST(4);
private final Integer code;
private Type(Integer code) {
this.code = code;
}
public Integer getCode() {
return code;
}
public Type fromCode(Integer code) {
return Type.valueOf(code);
}
public static Type valueOf(Integer code) {
for (Type val: values()) {
if (val.code.compareTo(code) == 0) {
return val;
}
}
throw new IllegalArgumentException("Unknown code : " + code);
}
}
/**
* Date format for fields of type DATE.
* <p/>
* PENDING: consider replacing the DATE Type with these 3 new data types.
*/
public static enum DateFormat {
DATE_TIME("full"), DATE("dateonly"), TIME("timeonly");
final String code;
DateFormat(String code) {
this.code = code;
}
}
}