package org.gwtoolbox.widget.client.form;
import com.google.gwt.user.client.ui.Widget;
import org.gwtoolbox.widget.client.data.MapRecord;
import org.gwtoolbox.widget.client.data.MutableRecord;
import org.gwtoolbox.widget.client.data.Record;
import org.gwtoolbox.widget.client.form.field.FormField;
import org.gwtoolbox.widget.client.form.field.HasFields;
import org.gwtoolbox.widget.client.form.validation.ValidationResult;
import org.gwtoolbox.widget.client.form.validation.Validator;
import java.util.*;
/**
* <p>Utility class to help creating and handling forms. A form has multiple <code>FormField</code>s, a <code>Validator</code>
* and some utilities that almost every form needs. Rendering the form is a seperate concern from doing validation and
* form submission.</p>
* <p/>
* <p>Most important interactions with the form are described below</p>
* <ul>
* <li>obtaining fields - You can obtain a collection or a single instance by key</li>
* <li>obtain field characteristics poer key like : Label, EditorWidget</li>
* <li>validate the fields in the form. {@see FormField} for more information about the validators of the <code>FormField</code>
* items.</li>
* <li>Submit the form and provide a <code>SubmissionHandler</code> to determine what to do when the submission is valid or not.
* The handler receives the <code>Record</code> containing the fields and their values.</li>
* <li>Reset the form, cleaning all form fields</li>
* <li>Mark the form as mutable or un mutable. A mutable form can be edited, an un mutable form cannot</li>
* </ul>
* <p/>
* <p>This class also specifies the <code>FormValidationResult</code> as an inner class. This interface defines the interaction
* with the validation result. Of course you can ask if the form is valid. You can also ask the error messages. These are split
* up in generic error messages as well as field specific error messages.</p>
* <p/>
* <p>During form submission, a <code>Record</code> is filled with the data of the form fields. The record makes it easier to
* get to the data without the specifics of the widgets. Custom records can be provided using the contructor. By default the
* {@see MapRecord} is used.</p>
* <p/>
*
* @author Uri Boness
* @author Jettro Coenradie
*/
public class Form implements HasFields {
private final InternalFormFields formFields;
private final MutableRecord record;
private Validator<Form> formValidator;
private boolean mutable = true;
/**
* Default constructor of the form.
*/
public Form() {
this(new MapRecord());
}
/**
* Constructor that accepts an implementation of the <code>MutableRecord</code> specification.
*
* @param record a non default implementation of the <code>MutableRecord</code>.
*/
public Form(MutableRecord record) {
formFields = new InternalFormFields();
this.record = record;
}
/**
* Returns a collection with all the <code>FormField</code>s
*
* @return collection with all the FormFields
*/
public FormFields getFields() {
return formFields;
}
/**
* Returns the FormField belonging to the provided key. If the key cannot be found, a NoSuchFormFieldException is thrown
* is returned.
*
* @param key the key to obtain the FormField for
* @return FormField belonging to the provided key
*/
public FormField getField(String key) throws NoSuchFormFieldException {
FormField field = formFields.getField(key);
if (field == null) {
throw new NoSuchFormFieldException("No field with key " + key + " available in the form");
}
return field;
}
/**
* Returns a collection with all available keys
*
* @return Colection containing all keys
*/
public Collection<String> getKeys() {
return formFields.getFieldsKeys();
}
/**
* Return the label registered for the provided key
*
* @param fieldKey the label for the provided key
* @return the label belonging to the provided key
* @throws NoSuchFormFieldException thrown if the provided key is unknown
*/
public String getLabel(String fieldKey) throws NoSuchFormFieldException {
return getField(fieldKey).getLabel();
}
/**
* Returns the editor for the field with the provided key
*
* @param fieldKey key of the field to obtain the editor for
* @return editor for the field with the provided key
* @throws NoSuchFormFieldException thrown if the provided key is unknown
*/
public Widget getEditorWidget(String fieldKey) throws NoSuchFormFieldException {
return getField(fieldKey).getEditor().getWidget();
}
/**
* Provided a form validator with validation rules that are not specific for one of the fields.
*
* @param formValidator validator for the complete form
*/
public void setFormValidator(Validator<Form> formValidator) {
this.formValidator = formValidator;
}
/**
* Removes the form validator
*/
public void clearFormValidator() {
formValidator = null;
}
/**
* Adds the given fields to this form.
*
* @param fields The fields to be added.
*/
public void addFields(FormField... fields) {
for (FormField field : fields) {
field.setEnabled(mutable);
formFields.add(field);
}
}
/**
* Evaluates and validates the input data.
*
* @return The validation result.
*/
public FormValidationResult validate() {
DefaultFormValidationResult result = new DefaultFormValidationResult();
for (Map.Entry<String, FormField> entry : formFields.fieldByKey.entrySet()) {
FormField field = entry.getValue();
if (field.isEnabled()) {
ValidationResult fieldResult = entry.getValue().validate();
result.merge(field.getKey(), fieldResult);
}
}
if (formValidator != null) {
ValidationResult generalValidationResult = formValidator.validate(this);
if (!generalValidationResult.isValid()) {
result.setGeneralValidationResult(generalValidationResult);
}
}
return result;
}
/**
* Returns the record underneath the form. Beware, this is useless before the form is submitted.
*
* @return The record from the form
*/
public MutableRecord getRecord() {
return record;
}
/**
* Submits the form. The submision process is composed of 2 phases:
* <ol>
* <li>Validates the input data in all the fields</li>
* <li>Collects the input data into a Record</li>
* </ol>
* If validation fails, the {@link org.gwtoolbox.widget.client.form.SubmitHandler#invalid(FormValidationResult)}
* will be called. Otherwise, the {@link org.gwtoolbox.widget.client.form.SubmitHandler#success(org.gwtoolbox.widget.client.data.Record)} will be called.
*
* @param handler The handler to handle the form submission.
*/
public void submit(SubmitHandler handler) {
FormValidationResult validationResult = validate();
if (!validationResult.isValid()) {
if (handler != null) {
handler.invalid(validationResult);
}
return;
}
record.setValues(collectData());
if (handler != null) {
handler.success(record);
}
}
/**
* Resets the form. Calls {@link org.gwtoolbox.widget.client.form.field.FormField#reset()} on all the fields.
*/
public void reset() {
for (FormField field : formFields.fieldByKey.values()) {
field.reset();
}
}
/**
* indicate that the fields of the form should be editable in the rendering
*/
public void markMutable() {
mutable = true;
for (FormField field : formFields.fieldByKey.values()) {
field.setEnabled(true);
}
}
/**
* Indicate that the fields of the form should not be editable in the rendering
*/
public void markUnMutable() {
mutable = false;
for (FormField field : formFields.fieldByKey.values()) {
field.setEnabled(false);
}
}
/**
* Returns if the form is currently editable
*
* @return true if editable false elsewise
*/
public boolean isMutable() {
return mutable;
}
//================================================ Helper Methods ==================================================
/**
* Collects all the input data and returns it as a {@link org.gwtoolbox.widget.client.data.Record}. Note that no validation is performed on the
* collected data therefore the returned data may be invalid.
*
* @return A record representing the input data.
*/
private Record collectData() {
Map<String, Object> data = new HashMap<String, Object>();
for (Map.Entry<String, FormField> entry : formFields.fieldByKey.entrySet()) {
FormField field = entry.getValue();
if (field.isEnabled()) {
Object value = entry.getValue().getEditor().getValue();
data.put(entry.getKey(), value);
}
}
return new MapRecord(data);
}
//================================================= Inner Classes ==================================================
public static interface FormValidationResult {
boolean isValid();
boolean isValid(String fieldKey);
List<String> getGeneralErrorMessages();
Collection<String> getInvalidFieldsKeys();
List<String> getErrorMessages(String fieldKey);
}
private static class DefaultFormValidationResult implements FormValidationResult {
private ValidationResult generalValidationResult;
private Map<String, ValidationResult> validationResultByFieldKey;
public boolean isValid() {
return (validationResultByFieldKey == null || validationResultByFieldKey.isEmpty()) &&
(generalValidationResult == null || generalValidationResult.isValid());
}
public boolean isValid(String fieldKey) {
if (generalValidationResult == null && validationResultByFieldKey == null) {
return true;
}
ValidationResult result = validationResultByFieldKey == null ? null : validationResultByFieldKey.get(fieldKey);
return result == null ? true : result.isValid();
}
public Collection<String> getInvalidFieldsKeys() {
return validationResultByFieldKey == null ? Collections.EMPTY_LIST : validationResultByFieldKey.keySet();
}
public List<String> getErrorMessages(String fieldKey) {
if (validationResultByFieldKey == null) {
return null;
}
ValidationResult result = validationResultByFieldKey.get(fieldKey);
return result == null ? null : result.getErrorMessages();
}
public List<String> getGeneralErrorMessages() {
return (generalValidationResult == null) ? Collections.EMPTY_LIST : generalValidationResult.getErrorMessages();
}
/**
* If the ValidationResult is not valid, the result is stored in the list with errors per field
*
* @param key The key to put the ValidationResult in the map with errors
* @param result The ValidationResult to store for the provided key.
*/
private void merge(String key, ValidationResult result) {
if (result == null || result.isValid()) {
return;
}
if (validationResultByFieldKey == null) {
validationResultByFieldKey = new HashMap<String, ValidationResult>();
}
validationResultByFieldKey.put(key, result);
}
private void setGeneralValidationResult(ValidationResult result) {
this.generalValidationResult = result;
}
}
private class InternalFormFields implements FormFields {
private final Map<String, FormField> fieldByKey = new HashMap<String, FormField>();
private final List<String> fieldKeys = new ArrayList<String>();
public List<String> getFieldsKeys() {
return fieldKeys;
}
public FormField getField(String key) {
return fieldByKey.get(key);
}
public void add(FormField field) {
fieldByKey.put(field.getKey(), field);
fieldKeys.add(field.getKey());
}
}
}