Package org.apache.cocoon.forms.formmodel

Source Code of org.apache.cocoon.forms.formmodel.Field

/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License.  You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cocoon.forms.formmodel;

import java.util.Locale;

import org.apache.cocoon.environment.Request;
import org.apache.cocoon.forms.FormContext;
import org.apache.cocoon.forms.FormsConstants;
import org.apache.cocoon.forms.FormsRuntimeException;
import org.apache.cocoon.forms.datatype.Datatype;
import org.apache.cocoon.forms.datatype.SelectionList;
import org.apache.cocoon.forms.datatype.convertor.ConversionResult;
import org.apache.cocoon.forms.event.DeferredValueChangedEvent;
import org.apache.cocoon.forms.event.ValueChangedEvent;
import org.apache.cocoon.forms.event.ValueChangedListener;
import org.apache.cocoon.forms.event.ValueChangedListenerEnabled;
import org.apache.cocoon.forms.event.WidgetEvent;
import org.apache.cocoon.forms.event.WidgetEventMulticaster;
import org.apache.cocoon.forms.util.I18nMessage;
import org.apache.cocoon.forms.validation.ValidationError;
import org.apache.cocoon.forms.validation.ValidationErrorAware;
import org.apache.cocoon.xml.AttributesImpl;
import org.apache.cocoon.xml.XMLUtils;

import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

/**
* A general-purpose Widget that can hold one value. A Field widget can be associated
* with a {@link org.apache.cocoon.forms.datatype.Datatype Datatype}, and thus
* a Field widget can be used to edit different kinds of data, such as strings,
* numbers and dates. A Datatype can also have an associated SelectionList, so
* that the value for the Field can be selected from a list, rather than being
* entered in a textbox. The validation of the field is delegated to its associated
* Datatype.
*
* @version $Id: Field.java 618345 2008-02-04 17:47:58Z antonio $
*/
public class Field extends AbstractWidget
                   implements ValidationErrorAware, DataWidget, SelectableWidget,
                              ValueChangedListenerEnabled {

    /**
     * If the field was rendered as a suggestion-list and the user chose one of the suggestions,
     * the field's value is the chosen item's value and the <code>SUGGESTED_LABEL_ATTR</code> field
     * attribute contains the chosen item's label.
     *
     * @see #isSuggested()
     * @since 2.1.9
     */
    public static final String SUGGESTED_LABEL_ATTR = "suggested-label";

    /**
     * Value state indicating that a new value has been read from the request,
     * but has not yet been parsed.
     */
    protected static final int VALUE_UNPARSED = 0;

    /**
     * Value state indicating that a value has been parsed, but needs to be
     * validated (that must occur before the value is given to the application)
     */
    protected static final int VALUE_PARSED = 1;

    /**
     * Value state indicating that a parse error was encountered but should not
     * yet be displayed.
     */
    protected static final int VALUE_PARSE_ERROR = 2;

    /**
     * Value state indicating that validate() has been called when state was
     * VALUE_PARSE_ERROR. This makes the error visible on output.
     */
    protected static final int VALUE_DISPLAY_PARSE_ERROR = 3;

    /**
     * Transient value state indicating that validation is going on.
     *
     * @see #validate()
     */
    protected static final int VALUE_VALIDATING = 4;

    /**
     * Value state indicating that validation has occured, but that any error should not
     * yet be displayed.
     */
    protected static final int VALUE_VALIDATED = 5;

    /**
     * Value state indicating that value validation has occured, and the
     * validation error, if any, should be displayed.
     */
    protected static final int VALUE_DISPLAY_VALIDATION = 6;

    private static final String FIELD_EL = "field";
    private static final String VALUE_EL = "value";
    private static final String VALIDATION_MSG_EL = "validation-message";


    /**
     * Definition of the field.
     */
    private final FieldDefinition definition;

    /**
     * Overrides selection list defined in FieldDefinition, if any.
     */
    protected SelectionList selectionList;
    /**
     * Overrides sugestion list defined in FieldDefinition, if any.
     */
    protected SelectionList suggestionList;

    /**
     * Additional listeners to those defined as part of the widget definition (if any).
     */
    private ValueChangedListener listener;

    protected String enteredValue;
    protected Object value;

    protected boolean required;

    /**
     * Transient widget processing state indicating that the widget is currently validating
     * (used to avoid endless loops when a validator calls getValue).
     */
    protected int valueState = VALUE_PARSED;

    protected ValidationError validationError;


    public Field(FieldDefinition fieldDefinition) {
        super(fieldDefinition);

        this.definition = fieldDefinition;
        this.listener = fieldDefinition.getValueChangedListener();
        /*
         * At startup, we have no value to parse (both enteredValue and value are null),
         * but still need to validate (e.g. error if field is required), so initial value
         * is set to {@link #VALUE_PARSED}.
         */
        this.valueState = VALUE_PARSED;
    }

    public WidgetDefinition getDefinition() {
        return this.definition;
    }

    public final FieldDefinition getFieldDefinition() {
        return this.definition;
    }

    public void initialize() {
        Object value = this.definition.getInitialValue();
        if (value != null) {
            setValue(value);
        }
        this.selectionList = this.definition.getSelectionList();
        this.suggestionList = this.definition.getSuggestionList();
        this.required = this.definition.isRequired();
        super.initialize();
    }

    /**
     * If this field has a selection-list, indicates if the value comes from that list
     * or if a new value was input by the user.
     *
     * @since 2.1.9
     * @return true if the user has chosen a suggested value
     */
    public boolean isSuggested() {
        return this.getAttribute(SUGGESTED_LABEL_ATTR) != null;
    }

    /**
     * Set the suggestion label associated to the widget's current value. This is used to initialize
     * a combobox's rendering. If not such label exists, the widget's value is used.
     *
     * @since 2.1.9
     */
    public void setSuggestionLabel(String label) {
        if (this.definition.getSuggestionList() == null) {
            throw new FormsRuntimeException("Field '" + getRequestParameterName() + "' has no suggestion list.",
                                            getLocation());
        }
        setAttribute(SUGGESTED_LABEL_ATTR, label);
    }

    /**
     * If the user has chosen an item in a suggestion list, returns that item's label.
     *
     * @since 2.1.9
     * @return the item's label, or <code>null</code> if the user entered a new value or
     *         if there's not suggestion list.
     */
    public String getSuggestionLabel() {
        return (String) getAttribute(SUGGESTED_LABEL_ATTR);
    }

    public Object getValue() {
        // if getValue() is called on this field while we're validating, then it's because a validation
        // rule called getValue(), so then we just return the parsed (but not VALUE_VALIDATED) value to avoid an endless loop
        if (this.valueState == VALUE_VALIDATING) {
            return this.value;
        }

        ValidationError oldError = this.validationError;

        // Parse the value
        if (this.valueState == VALUE_UNPARSED) {
            doParse();
        }

        // Validate the value if it was successfully parsed
        if (this.valueState == VALUE_PARSED) {
            doValidate();
        }

        if (oldError != null && this.validationError == null) {
            // The parsing process removed an existing validation error. This happens
            // mainly when a required field is given a value.
            getForm().addWidgetUpdate(this);
        }

        return this.validationError == null ? this.value : null;
    }

    public void setValue(Object newValue) {
        if (newValue != null && !getDatatype().getTypeClass().isAssignableFrom(newValue.getClass())) {
            throw new FormsRuntimeException("Incorrect value type for '" + getRequestParameterName() +
                                            "'. Expected " + getDatatype().getTypeClass() + ", got " + newValue.getClass() + ").",
                                            getLocation());
        }
        // Is it a new value?
        boolean changed;
        if (this.valueState == VALUE_UNPARSED) {
            // Current value was not parsed
            changed = true;
        } else if (this.value == null) {
            // Is current value not null?
            changed = (newValue != null);
        } else {
            // Is current value different?
            changed = !this.value.equals(newValue);
        }

        // Do something only if value is different or null
        // (null allows to reset validation error)
        if (changed || newValue == null) {
            // Do we need to call listeners? If yes, keep (and parse if needed) old value.
            boolean callListeners = changed && (hasValueChangedListeners() || this.getForm().hasFormHandler());
            Object oldValue = callListeners ? getValue() : null;

            this.value = newValue;
            this.validationError = null;
            // Force validation, even if set by the application
            this.valueState = VALUE_PARSED;
            if (newValue != null) {
                this.enteredValue = getDatatype().convertToString(newValue, getForm().getLocale());
            } else {
                this.enteredValue = null;
            }

            if (callListeners) {
                getForm().addWidgetEvent(new ValueChangedEvent(this, oldValue, newValue));
            }
            getForm().addWidgetUpdate(this);
        }
    }

    public void readFromRequest(FormContext formContext) {
        if (!getCombinedState().isAcceptingInputs()) {
            return;
        }

        String paramName = getRequestParameterName();
        Request request = formContext.getRequest();

        String newEnteredValue = request.getParameter(paramName);

        if (this.definition.getSuggestionList() != null) {
            // The Dojo ComboBox sends the typed value or the chosen item's label in the
            // request parameter and sends an additional "*_selected" parameter containing
            // the value of the chosen item (if any).
            // So if *_selected exists, use
            String selectedValue = request.getParameter(paramName + "_selected");
            if (StringUtils.isNotEmpty(selectedValue)) {
                setSuggestionLabel(newEnteredValue);
                newEnteredValue = selectedValue;
            } else {
                this.removeAttribute(SUGGESTED_LABEL_ATTR);
            }
        }

        // FIXME: Should we consider only non-null values?
        // Although distinguishing an empty value (input present but blank) from a null value
        // (input not present in the form) is possible, this distinction is not possible for
        // several other kinds of widgets such as BooleanField or MultiValueField. So we keep
        // it consistent with other widgets.
        //if (newEnteredValue != null) {
        readFromRequest(newEnteredValue);
        //}
    }

    protected void readFromRequest(String newEnteredValue) {
        // whitespace & empty field handling
        newEnteredValue = applyWhitespaceTrim(newEnteredValue);

        // Only convert if the text value actually changed. Otherwise, keep the old value
        // and/or the old validation error (allows to keep errors when clicking on actions)
        boolean changed;
        if (enteredValue == null) {
            changed = (newEnteredValue != null);
        } else {
            changed = !enteredValue.equals(newEnteredValue);
        }

        if (changed) {
            ValidationError oldError = this.validationError;

            // If we have some value-changed listeners, we must make sure the current value has been
            // parsed, to fill the event. Otherwise, we don't need to spend that extra CPU time.
            boolean hasListeners = hasValueChangedListeners() || this.getForm().hasFormHandler();
            Object oldValue = hasListeners ? getValue() : null;

            enteredValue = newEnteredValue;
            validationError = null;
            value = null;
            this.valueState = VALUE_UNPARSED;

            if (hasListeners) {
                // Throw an event that will hold the old value and
                // will lazily compute the new value only if needed.
                getForm().addWidgetEvent(new DeferredValueChangedEvent(this, oldValue));
            }

            if (oldError != null) {
                // There was a validation error, and the user entered a new value: refresh
                // the widget, because the previous error was cleared
                getForm().addWidgetUpdate(this);
            }
        }
    }

    protected String applyWhitespaceTrim(String value) {
        if (value != null) {
            Whitespace trim = this.definition.getWhitespaceTrim();
            if(trim == null || trim == Whitespace.TRIM) {
              value = value.trim();
            } else if(trim == Whitespace.PRESERVE) {
                // do nothing.
            } else if(trim == Whitespace.TRIM_START) {
                value = StringUtils.stripStart(value, null);
            } else if(trim == Whitespace.TRIM_END) {
                value = StringUtils.stripEnd(value, null);
            }

            // treat empty strings as null
            if (value.length() == 0) {
                value = null;
            }
        }
        return value;
    }

    /**
     * @see org.apache.cocoon.forms.formmodel.Widget#validate()
     */
    public boolean validate() {
        if (!getCombinedState().isValidatingValues()) {
            this.wasValid = true;
            return true;
        }

        if (this.valueState == VALUE_UNPARSED) {
            doParse();
        }

        // Force validation on already validated values (but keep invalid parsings)
        if (this.valueState >= VALUE_VALIDATED) {
            this.valueState = VALUE_PARSED;
        }

        if (this.valueState == VALUE_PARSED) {
            doValidate();
            this.valueState = VALUE_DISPLAY_VALIDATION;
            if (this.validationError != null) {
                getForm().addWidgetUpdate(this);
            }
        } else if (this.valueState == VALUE_PARSE_ERROR) {
            this.valueState = VALUE_DISPLAY_PARSE_ERROR;
            getForm().addWidgetUpdate(this);
        }

        this.wasValid = this.validationError == null;
        return this.wasValid;
    }

    /**
     * Parse the value that has been read from the request.
     * Should be called when valueState is VALUE_UNPARSED.
     * On exit, valueState is set to either:
     * - VALUE_PARSED: successful parsing or null value. Value is set and ValidationError
     *   is cleared.
     * - VALUE_PARSE_ERROR: datatype parsing error. In that case, value is null and
     *   validationError is set.
     */
    private void doParse() {
        if (this.valueState != VALUE_UNPARSED) {
            throw new IllegalStateException("Field is not in UNPARSED state (" + this.valueState + ")");
        }

        // Clear value, it will be recomputed
        this.value = null;
        this.validationError = null;

        if (this.enteredValue != null) {
            // Parse the value
            ConversionResult conversionResult = getDatatype().convertFromString(this.enteredValue, getForm().getLocale());
            if (conversionResult.isSuccessful()) {
                this.value = conversionResult.getResult();
                this.valueState = VALUE_PARSED;
            } else {
                // Conversion failed
                this.validationError = conversionResult.getValidationError();
                // No need for further validation (and need to keep the above error)
                this.valueState = VALUE_PARSE_ERROR;
            }
        } else {
            // No value: needs to be validated
            this.valueState = VALUE_PARSED;
        }
    }

    /**
     * Validate the value once it has been parsed.
     * Should be called when valueState is VALUE_PARSED.
     * On exit, valueState is set to VALUE_VALIDATED, and validationError is set if
     * validation failed.
     */
    private void doValidate() {
        if (this.valueState != VALUE_PARSED) {
            throw new IllegalStateException("Field is not in PARSED state (" + this.valueState + ")");
        }

        // Go to transient validating state
        this.valueState = VALUE_VALIDATING;

        // reset validation errot
        this.validationError = null;

        try {
            if (this.value == null && this.required) {
                // Field is required
                this.validationError = new ValidationError(new I18nMessage("general.field-required", FormsConstants.I18N_CATALOGUE));
            } else if (!super.validate()) {
                // New-style validators failed.
            } else if (this.value != null) {
                // Check the old-style ones.
                this.validationError = getDatatype().validate(this.value, new ExpressionContextImpl(this));
            }
        } finally {
            // Consider validation finished even in case of exception
            this.valueState = VALUE_VALIDATED;
        }
    }

    /**
     * Returns the validation error, if any. There will always be a validation error in case the
     * {@link #validate} method returned false.
     *
     * <br>This method does not cause parsing to take effect, use {@link #getValue} if value
     * is not parsed yet.
     */
    public ValidationError getValidationError() {
        return this.validationError;
    }

    /**
     * Set a validation error on this field. This allows fields to be externally marked as invalid by
     * application logic.
     *
     * @param error the validation error
     */
    public void setValidationError(ValidationError error) {
        if (this.valueState >= VALUE_VALIDATED) {
            this.valueState = VALUE_DISPLAY_VALIDATION;
        }

        if (!ObjectUtils.equals(this.validationError, error)) {
            this.validationError = error;
            getForm().addWidgetUpdate(this);
        }
    }

    public boolean isRequired() {
        return this.required;
    }

    public void setRequired(boolean required) {
        this.required = required;
        getForm().addWidgetUpdate(this);
    }

    /**
     * @return "field"
     */
    public String getXMLElementName() {
        return FIELD_EL;
    }

    /**
     * Adds the @required attribute
     */
    public AttributesImpl getXMLElementAttributes() {
        AttributesImpl attrs = super.getXMLElementAttributes();
        attrs.addCDATAAttribute("required", String.valueOf(isRequired()));
        return attrs;
    }

    public void generateItemSaxFragment(ContentHandler contentHandler, Locale locale) throws SAXException {
        if (locale == null) {
            locale = getForm().getLocale();
        }

        if (enteredValue != null || value != null) {
            contentHandler.startElement(FormsConstants.INSTANCE_NS, VALUE_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALUE_EL, XMLUtils.EMPTY_ATTRIBUTES);
            String stringValue;
            if (value != null) {
                stringValue = getDatatype().convertToString(value, locale);
            } else {
                stringValue = enteredValue;
            }
            contentHandler.characters(stringValue.toCharArray(), 0, stringValue.length());
            contentHandler.endElement(FormsConstants.INSTANCE_NS, VALUE_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALUE_EL);
        }

        // Suggested label, if any
        String suggestedLabel = getSuggestionLabel();
        if (suggestedLabel != null) {
            contentHandler.startElement(FormsConstants.INSTANCE_NS, "suggestion", FormsConstants.INSTANCE_PREFIX_COLON + "suggestion", XMLUtils.EMPTY_ATTRIBUTES);
            contentHandler.characters(suggestedLabel.toCharArray(), 0, suggestedLabel.length());
            contentHandler.endElement(FormsConstants.INSTANCE_NS, "suggestion", FormsConstants.INSTANCE_PREFIX_COLON + "suggestion");
        }

        // validation message element: only present if the value is not valid
        if (validationError != null && (this.valueState == VALUE_DISPLAY_VALIDATION || this.valueState == VALUE_DISPLAY_PARSE_ERROR)) {
            contentHandler.startElement(FormsConstants.INSTANCE_NS, VALIDATION_MSG_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALIDATION_MSG_EL, XMLUtils.EMPTY_ATTRIBUTES);
            validationError.generateSaxFragment(contentHandler);
            contentHandler.endElement(FormsConstants.INSTANCE_NS, VALIDATION_MSG_EL, FormsConstants.INSTANCE_PREFIX_COLON + VALIDATION_MSG_EL);
        }

        // generate selection list, if any
        if (selectionList != null) {
            selectionList.generateSaxFragment(contentHandler, locale);
        }

        // include some info about the datatype
        definition.getDatatype().generateSaxFragment(contentHandler, locale);
    }


    /**
     * Set this field's selection list.
     * @param selectionList The new selection list.
     */
    public void setSelectionList(SelectionList selectionList) {
        if (selectionList != null &&
            selectionList.getDatatype() != null &&
            selectionList.getDatatype() != getDatatype()) {
            throw new RuntimeException("Tried to assign a SelectionList that is not associated with this widget's datatype.");
        }
        this.selectionList = selectionList;
        getForm().addWidgetUpdate(this);
    }

    /**
     * Set this field's suggestion list.
     * @param suggestionList The new suggestion list.
     *
     * @since 2.1.12
     */
    public void setSuggestionList(SelectionList suggestionList) {
        if (suggestionList != null &&
            suggestionList.getDatatype() != null &&
            suggestionList.getDatatype() != getDatatype()) {
            throw new RuntimeException("Tried to assign a SuggestionList that is not associated with this widget's datatype.");
        }
        this.suggestionList = suggestionList;
        getForm().addWidgetUpdate(this);
    }

    /**
     * Read this field's selection list from an external source.
     * All Cocoon-supported protocols can be used.
     * The format of the XML produced by the source should be the
     * same as in case of inline specification of the selection list,
     * thus the root element should be a <code>fd:selection-list</code>
     * element.
     * @param uri The URI of the source.
     */
    public void setSelectionList(String uri) {
        setSelectionList(getFieldDefinition().buildSelectionList(uri));
    }

    /**
     * Set this field's selection list using values from an in-memory
     * object. The <code>object</code> parameter should point to a collection
     * (Java collection or array, or Javascript array) of objects. Each object
     * belonging to the collection should have a <em>value</em> property and a
     * <em>label</em> property, whose values are used to specify the <code>value</code>
     * attribute and the contents of the <code>fd:label</code> child element
     * of every <code>fd:item</code> in the list.
     * <p>Access to the values of the above mentioned properties is done
     * via <a href="http://jakarta.apache.org/commons/jxpath/users-guide.html">XPath</a> expressions.
     * @param model The collection used as a model for the selection list.
     * @param valuePath An XPath expression referring to the attribute used
     * to populate the values of the list's items.
     * @param labelPath An XPath expression referring to the attribute used
     * to populate the labels of the list's items.
     */
    public void setSelectionList(Object model, String valuePath, String labelPath) {
        setSelectionList(getFieldDefinition().buildSelectionListFromModel(model, valuePath, labelPath));
    }

    /**
     * Read this field's selection list from an external source.
     * All Cocoon-supported protocols can be used.
     * The format of the XML produced by the source should be the
     * same as in case of inline specification of the selection list,
     * thus the root element should be a <code>fd:selection-list</code>
     * element.
     * @param uri The URI of the source.
     *
     * @since 2.1.12
     *
     */
    public void setSuggestionList(String uri) {
        setSuggestionList(getFieldDefinition().buildSelectionList(uri));
    }

    public SelectionList getSuggestionList() {
        return this.suggestionList;
    }

    public Datatype getDatatype() {
        return getFieldDefinition().getDatatype();
    }

    /**
     * Adds a ValueChangedListener to this widget instance. Listeners defined
     * on the widget instance will be executed in addtion to any listeners
     * that might have been defined in the widget definition.
     */
    public void addValueChangedListener(ValueChangedListener listener) {
        this.listener = WidgetEventMulticaster.add(this.listener, listener);
    }

    public void removeValueChangedListener(ValueChangedListener listener) {
        this.listener = WidgetEventMulticaster.remove(this.listener, listener);
    }

    public boolean hasValueChangedListeners() {
        return this.listener != null;
    }

    public void broadcastEvent(WidgetEvent event) {
        if (event instanceof ValueChangedEvent) {
            if (this.listener != null) {
                this.listener.valueChanged((ValueChangedEvent)event);
            }
        } else {
            // Other kinds of events
            super.broadcastEvent(event);
        }
    }
}
TOP

Related Classes of org.apache.cocoon.forms.formmodel.Field

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.