package javango.forms;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javango.forms.fields.BoundField;
import javango.forms.fields.Field;
import javango.forms.fields.FieldFactory;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;
public class AbstractForm implements Form {
public final static String NON_FIELD_ERRORS = "__all__";
private final static Log log = LogFactory.getLog(AbstractForm.class);
protected Map<String, Field<?>> _fields = new LinkedHashMap<String, Field<?>>(); // use caution when using this directly, user getter (getFields());
protected Map<String, String> errors; // null until isValid is called
protected Map<String, Object> cleanedData; // null until isValid is called
protected Map<String, String[]> data;
protected Map<String, FileItem> fileData;
protected Map<String, Object> initial;
protected String prefix;
protected String id = "id_%s"; // set to null to disable ID.
protected boolean readOnly;
protected boolean init = false;
@Inject(optional = true)
@Named("javango.forms.errorCssClass")
protected String errorCssClass = null;
@Inject(optional = true)
@Named("javango.forms.requiredCssClass")
protected String requiredCssClass = null;
protected FieldFactory fieldFactory;
/**
* if injected clean(class) will use it to create the instance.
*/
@Inject
Injector injector;
@Inject
public AbstractForm(FieldFactory fieldFactory) {
this.fieldFactory = fieldFactory;
}
public Form bind(Map<String, String[]> map) {
this.data = map == null ? null : new HashMap<String, String[]>(map);
return this;
}
public Form bind(Map<String, String[]> map, Map<String, FileItem> fileMap) {
bind(map);
this.fileData = fileMap == null ? null : new HashMap<String, FileItem>(fileMap);
return this;
}
/**
* Set this form's initial values to the cooresponding fields in the provided bean.
* @param bean
* @return
*/
public Form setInitial(Object bean) {
if (bean == null) {
return this;
}
if (bean instanceof Map) {
return setInitial((Map)bean);
}
try {
for (Entry<String, Field<?>> e : getFields().entrySet()) {
if (PropertyUtils.isReadable(bean, e.getKey())) {
this.getInitial().put(e.getKey(), PropertyUtils.getProperty(bean, e.getKey()));
}
}
// TODO what should really be done on these exceptions... continue with the rest, throw something
} catch (IllegalAccessException e) {
log.error(e,e);
} catch (NoSuchMethodException e) {
log.error(e,e);
} catch (InvocationTargetException e) {
log.error(e,e);
}
return this;
}
/**
* Returns true if this is a bound form, bound forms have been associated with input from a user.
* @return
*/
public boolean isBound() {
return this.data != null;
}
/**
* If this form has a prefix, returns the field name with the form's prefix appended.
* @param fieldName
*/
public String addPrefix(String fieldName) {
return getPrefix() == null ? fieldName : String.format("%s-%s", getPrefix(), fieldName);
}
/**
* Cleans all of data and populates errors and cleanedData
*/
protected void fullClean() {
if (cleanedData != null) return; // we have already run full clean I don't think we should run again.
errors = new HashMap<String, String>();
if (!isBound()) return;
cleanedData = new HashMap<String, Object>();
for (Entry<String, Field<?>> e : getFields().entrySet()) {
Field<?> field = e.getValue();
String name = field.getName() == null ? e.getKey() : field.getName();
if (field.getName() == null) { // TODO This is here in case a field does not know its name..
field.setName(name);
}
String[] value = field.getWidget().valueFromMap(data, addPrefix(name));
try {
cleanedData.put(name, field.clean(value, errors));
if (errors.containsKey(field.getName())) {
cleanedData.remove(field.getName());
} else {
try {
Method m = this.getClass().getMethod("clean_" + name);
cleanedData.put(name, m.invoke(this));
} catch (NoSuchMethodException ex) {
// ouch is this the best way to figure out that a clean_ method does not exist, seems an exception might be too slow.
} catch (InvocationTargetException ex) { // TODO should this be logged
if (ex.getCause() instanceof ValidationException) {
throw (ValidationException)ex.getCause();
}
LogFactory.getLog(AbstractForm.class).error(ex,ex);
LogFactory.getLog(AbstractForm.class).error(ex.getCause(),ex.getCause());
throw new RuntimeException(
String.format("Unable to run clean_%s method due to an InvocationTargetException '%s'", name, ex.getCause().getMessage()));
} catch (IllegalAccessException ex) {
LogFactory.getLog(AbstractForm.class).error(ex,ex);
throw new RuntimeException(
String.format("Unable to run clean_%s method due to an IllegalAccessException", name));
}
}
} catch (ValidationException ex) {
errors.put(name, ex.getMessage());
cleanedData.remove(name);
}
}
if (errors.isEmpty()) {
try {
clean();
} catch (ValidationException ex) {
errors.put(NON_FIELD_ERRORS, ex.getMessage()); // TODO allow more than one non-field error??
}
}
if (!errors.isEmpty()) {
cleanedData = null;
}
}
/**
* Hook for doing any additional form-wide cleaning after fullClean has been called, at this point Field.clean will have
* been called for all fields.
*/
protected void clean() throws ValidationException {
}
/**
* Return true if this form is valid.
* @return
*/
public boolean isValid() {
return isBound() && getErrors().isEmpty();
}
/**
* Returns a Map of field errors
* TODO If we go with Hibernate validator, should this return a Map/List of hibernate validators
* @return
*/
public Map<String, String> getErrors() {
if (errors == null) {
fullClean();
}
return errors;
}
/**
* Cleans the data from this form into the specified object, returns null if the form is not valid.
*
* @param object
*/
public <T> T clean(T bean) {
fullClean();
if (!errors.isEmpty()) return null;
for (Entry<String, Object> e : getCleanedData().entrySet()) {
String fieldName = e.getKey();
Field f = getFields().get(fieldName);
if (f == null || f.isEditable()) {
if (log.isDebugEnabled()) log.debug(String.format("Cleaning : '%s'", fieldName));
if (log.isDebugEnabled()) log.debug("Found data : '%s'" + e.getValue());
if (PropertyUtils.isWriteable(bean, fieldName)) {
try {
if (log.isDebugEnabled()) log.debug("Trying to set");
PropertyUtils.setProperty(bean, fieldName, e.getValue());
} catch (InvocationTargetException ex) {
log.error(ex,ex);
} catch (IllegalAccessException ex) {
log.error(ex,ex);
} catch (NoSuchMethodException ex) {
log.error(ex,ex);
}
}
}
}
return bean;
}
/**
* Cleans the data in this form into a new bean of the specified class.
* @param objectClass
* @return
*/
public <T> T clean(Class<T> objectClass) {
try {
T bean = null;
if (injector != null) {
bean = injector.getInstance(objectClass);
} else {
bean = objectClass.newInstance();
}
return clean(bean);
} catch (IllegalAccessException e) {
log.error(e,e);
} catch (InstantiationException e) {
log.error(e,e);
}
return null;
}
/**
*
* @param field
* @return
*/
protected Field loadField(java.lang.reflect.Field field) {
return loadField(field, true);
}
/**
* Loads the specified field, optionally processing annotations. Annotations are optional at this time because a subclass may want
* more fine grained processing
*
* @param field
* @param processAnnotations
* @return
*/
protected Field loadField(java.lang.reflect.Field field, boolean processAnnotations) {
if (!Field.class.isAssignableFrom(field.getType())) {
if (log.isDebugEnabled()) log.debug("Field not extended from Field class, skipping: " + field.getName());
return null;
}
if (_fields.containsKey(field.getName())) {
if (log.isDebugEnabled()) log.debug("Field already in field list, skipping: " + field.getName());
return null;
}
try {
Field<?> baseField = (Field<?>)field.get(this);
if (baseField == null) {
baseField = fieldFactory.newField((Class<? extends Field>)field.getType());
field.set(this, baseField);
}
baseField.setName(field.getName());
_fields.put(field.getName(), baseField);
if (processAnnotations) {
Annotation[] annotations = field.getAnnotations();
for (Annotation annotation: annotations) {
baseField.handleAnnotation(annotation);
}
}
return baseField;
} catch (IllegalAccessException e) {
log.error("Unable to load field into form, change visiblity to public: " + e,e);
return null;
}
}
/**
* Actually setup the form, populate the field list with all Field typed properties in this form or any super classes.
*/
protected void init() {
if (init) return; // no reason to call twice!!
Class<?> cls = this.getClass();
while(cls != null) {
java.lang.reflect.Field[] classFields = cls.getDeclaredFields();
for (int i=0; i<classFields.length; i++) {
loadField(classFields[i]);
}
cls = cls.getSuperclass();
}
init = true;
}
/**
*
*/
public String asTable() {
StringBuilder b = new StringBuilder();
StringBuilder hidden_fields = new StringBuilder();
for (Entry<String, Field<?>> entry : getFields().entrySet()) {
Field<?> field = entry.getValue();
String fieldName = field.getName() == null ? entry.getKey() : field.getName();
BoundField bf = new BoundField(field, this, fieldName);
// any class attributes that should be applied to the table row.
String cssClasses = bf.getCssClasses();
String htmlClassAttr = cssClasses == null ? "" : String.format(" class=\"%s\"", cssClasses);
if (bf.isHidden()) {
hidden_fields.append(bf.toString());
hidden_fields.append("\n");
} else {
StringBuilder errors = new StringBuilder();
if (this.getErrors().get(fieldName) != null) {
errors.append("<ul class=\"errorlist\">");
errors.append("<li>");
errors.append(this.getErrors().get(fieldName)); // when this goes to a list, use bf.geterorrs instead of this.
errors.append("</li>");
errors.append("</ul>");
}
b.append(String.format("<tr%s><th>%s</th><td>%s%s%s</td></tr>\n", htmlClassAttr, bf.getLabelHtml(), errors.toString(), bf.toString(), bf.getHelpText()));
}
}
if (hidden_fields.length() ==0 ) { // no hidden fields
return b.toString();
} else if (b.length() > 11) { // insert hidden field into the last cell of the table.
return b.insert(b.length()-11, hidden_fields).toString();
} else { // must only have hidden fields, TODO this is probably not correct as we should probably create a tr/td to contain the fields.
return b.append(hidden_fields).toString();
}
}
@SuppressWarnings("unchecked") // the cast had better work if the field is doing its job!!
public <T> T getCleanedValue(Field<T> field) {
return (T)getCleanedData().get(field.getName());
}
public Map<String, Object> getCleanedData() {
return cleanedData;
}
public Map<String, Field<?>> getFields() {
if (!init) init();
return _fields;
}
public Map<String, Object> getInitial() {
if (initial == null) {
initial = new HashMap<String, Object>();
}
return initial;
}
public Form setInitial(Map<String, Object> initial) {
this.initial = initial;
return this;
}
public Map<String, String[]> getData() {
return data;
}
public String getPrefix() {
return prefix;
}
public Form setPrefix(String prefix) {
this.prefix = prefix;
return this;
}
/**
* Returns a bound field for the requested field.
* @param field
* @return
*/
public BoundField get(String field) {
if (!getFields().containsKey(field)) {
return null;
}
Field f = getFields().get(field);
if (f.getName() == null) {
f.setName(field);
}
return new BoundField(f, this, f.getName());
}
/**
* Returns an iterator of the BoundFields in the form.
*/
public Iterator<BoundField> iterator() {
List<BoundField> boundFields = new ArrayList<BoundField>();
for (Entry<String, Field<?>> e : getFields().entrySet()) {
Field<?> field = e.getValue();
String fieldName = field.getName() == null ? e.getKey() : field.getName();
BoundField bf = new BoundField(field, this, fieldName);
boundFields.add(bf);
}
return boundFields.iterator();
}
public String getId() {
return id;
}
public AbstractForm setId(String id) {
this.id = id;
return this;
}
public List<String> getNonFieldErrors() {
List<String> nonFieldErrorList = new ArrayList<String>();
if (errors != null && errors.containsKey(NON_FIELD_ERRORS)) {
nonFieldErrorList.add(errors.get(NON_FIELD_ERRORS));
}
return nonFieldErrorList;
}
public Form setReadOnly(boolean readOnly) {
this.readOnly = readOnly;
return this;
}
public boolean isReadOnly() {
return this.readOnly;
}
public String getErrorCssClass() {
return errorCssClass;
}
public void setErrorCssClass(String errorCssClass) {
this.errorCssClass = errorCssClass;
}
public String getRequiredCssClass() {
return requiredCssClass;
}
public void setRequiredCssClass(String requiredCssClass) {
this.requiredCssClass = requiredCssClass;
}
public String getHiddenFieldsHtml() {
StringBuilder hidden_fields = new StringBuilder();
for (Entry<String, Field<?>> entry : getFields().entrySet()) {
Field<?> field = entry.getValue();
if (field.isHidden()) {
String fieldName = field.getName() == null ? entry.getKey() : field.getName();
BoundField bf = new BoundField(field, this, fieldName);
hidden_fields.append(bf.toString());
hidden_fields.append("\n");
}
}
return hidden_fields.toString();
}
}