Package javango.forms

Source Code of javango.forms.AbstractForm

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();
  }
}
TOP

Related Classes of javango.forms.AbstractForm

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.