    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005// http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.corelib.base;
    014
    015import org.apache.tapestry5.*;
    016import org.apache.tapestry5.annotations.*;
    017import org.apache.tapestry5.beaneditor.Width;
    018import org.apache.tapestry5.commons.AnnotationProvider;
    019import org.apache.tapestry5.corelib.mixins.RenderDisabled;
    020import org.apache.tapestry5.ioc.annotations.Inject;
    021import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    022
    023import java.lang.annotation.Annotation;
    024import java.util.Locale;
    025
    026/**
    027 * Abstract class for a variety of components that render some variation of a text field. Most of the hooks for user
    028 * input validation are in this class.
    029 *
    030 * In particular, all subclasses support the "toclient" and "parseclient" events. These two events allow the normal
    031 * {@link Translator} (specified by the translate parameter, but often automatically derived by Tapestry) to be
    032 * augmented.
    033 *
    034 * If the component container (i.e., the page) provides an event handler method for the "toclient" event, and that
    035 * handler returns a non-null string, that will be the string value sent to the client. The context passed to the event
    036 * handler method is t he current value of the value parameter.
    037 *
    038 * Likewise, on a form submit, the "parseclient" event handler method will be passed the string provided by the client,
    039 * and may provide a non-null value as the parsed value. Returning null allows the normal translator to operate. The
    040 * event handler may also throw {@link org.apache.tapestry5.ValidationException}.
    041 *
    042 * @tapestrydoc
    043 */
    044@Events(
    045        {EventConstants.TO_CLIENT, EventConstants.VALIDATE, EventConstants.PARSE_CLIENT})
    046public abstract class AbstractTextField extends AbstractField
    047{
    048    /**
    049     * The value to be read and updated. This is not necessarily a string, a translator may be provided to convert
    050     * between client side and server side representations. If not bound, a default binding is made to a property of the
    051     * container matching the component's id. If no such property exists, then you will see a runtime exception due to
    052     * the unbound value parameter.
    053     */
    054    @Parameter(required = true, principal = true, autoconnect = true)
    055    private Object value;
    056
    057    /**
    058     * The object which will perform translation between server-side and client-side representations. If not specified,
    059     * a value will usually be generated based on the type of the value parameter.
    060     */
    061    @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.TRANSLATE)
    062    private FieldTranslator<Object> translate;
    063
    064    /**
    065     * The object that will perform input validation (which occurs after translation). The validate binding prefix is
    066     * generally used to provide this object in a declarative fashion.
    067     */
    068    @Parameter(defaultPrefix = BindingConstants.VALIDATE, allowNull = false)
    069    @SuppressWarnings("unchecked")
    070    private FieldValidator<Object> validate;
    071
    072    /**
    073     * Provider of annotations used for some defaults. Annotation are usually provided in terms of the value parameter
    074     * (i.e., from the getter and/or setter bound to the value parameter).
    075     *
    076     * @see org.apache.tapestry5.beaneditor.Width
    077     */
    078    @Parameter
    079    private AnnotationProvider annotationProvider;
    080
    081    /**
    082     * Defines how nulls on the server side, or sent from the client side, are treated. The selected strategy may
    083     * replace the nulls with some other value. The default strategy leaves nulls alone. Another built-in strategy,
    084     * zero, replaces nulls with the value 0.
    085     */
    086    @Parameter(defaultPrefix = BindingConstants.NULLFIELDSTRATEGY, value = "default")
    087    private NullFieldStrategy nulls;
    088
    089    @Inject
    090    private Locale locale;
    091
    092    @SuppressWarnings("unused")
    093    @Mixin
    094    private RenderDisabled renderDisabled;
    095
    096    /**
    097     * Computes a default value for the "translate" parameter using
    098     * {@link org.apache.tapestry5.services.ComponentDefaultProvider#defaultTranslator(String, org.apache.tapestry5.ComponentResources)}
    099     * .
    100     */
    101    final Binding defaultTranslate()
    102    {
    103        return defaultProvider.defaultTranslatorBinding("value", resources);
    104    }
    105
    106    final AnnotationProvider defaultAnnotationProvider()
    107    {
    108        return new AnnotationProvider()
    109        {
    110            public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
    111            {
    112                return resources.getParameterAnnotation("value", annotationClass);
    113            }
    114        };
    115    }
    116
    117    /**
    118     * Computes a default value for the "validate" parameter using
    119     * {@link org.apache.tapestry5.services.FieldValidatorDefaultSource}.
    120     */
    121    final Binding defaultValidate()
    122    {
    123        return defaultProvider.defaultValidatorBinding("value", resources);
    124    }
    125
    126    @SuppressWarnings(
    127            {"unchecked"})
    128    @BeginRender
    129    void begin(MarkupWriter writer)
    130    {
    131        String value = validationTracker.getInput(this);
    132
    133        // If this is a response to a form submission, and the user provided a value.
    134        // then send that exact value back at them.
    135
    136        if (value == null)
    137        {
    138            // Otherwise, get the value from the parameter ...
    139            // Then let the translator and or various triggered events get it into
    140            // a format ready to be sent to the client.
    141
    142            value = fieldValidationSupport.toClient(this.value, resources, translate, nulls);
    143        }
    144
    145        writeFieldTag(writer, value);
    146
    147        putPropertyNameIntoBeanValidationContext("value");
    148
    149        translate.render(writer);
    150        validate.render(writer);
    151
    152        removePropertyNameFromBeanValidationContext();
    153
    154        resources.renderInformalParameters(writer);
    155
    156        decorateInsideField();
    157    }
    158
    159    /**
    160     * Invoked from {@link #begin(MarkupWriter)} to write out the element and attributes (typically, &lt;input&gt;). The
    161     * {@linkplain AbstractField#getControlName() controlName} and {@linkplain AbstractField#getClientId() clientId}
    162     * properties will already have been set or updated.
    163     *
    164     * Generally, the subclass will invoke {@link MarkupWriter#element(String, Object[])}, and will be responsible for
    165     * including an {@link AfterRender} phase method to invoke {@link MarkupWriter#end()}.
    166     *
    167     * @param writer
    168     *         markup write to send output to
    169     * @param value
    170     *         the value (either obtained and translated from the value parameter, or obtained from the tracker)
    171     */
    172    protected abstract void writeFieldTag(MarkupWriter writer, String value);
    173
    174    @SuppressWarnings(
    175            {"unchecked"})
    176    @Override
    177    protected void processSubmission(String controlName)
    178    {
    179        String rawValue = request.getParameter(controlName);
    180
    181        validationTracker.recordInput(this, rawValue);
    182
    183        try
    184        {
    185            Object translated = fieldValidationSupport.parseClient(rawValue, resources, translate, nulls);
    186
    187            putPropertyNameIntoBeanValidationContext("value");
    188
    189            fieldValidationSupport.validate(translated, resources, validate);
    190
    191            // If the value provided is blank and we're ignoring blank input (i.e. PasswordField),
    192            // then don't update the value parameter.
    193
    194            if (!(ignoreBlankInput() && InternalUtils.isBlank(rawValue)))
    195                value = translated;
    196        } catch (ValidationException ex)
    197        {
    198            validationTracker.recordError(this, ex.getMessage());
    199        }
    200
    201        removePropertyNameFromBeanValidationContext();
    202    }
    203
    204    /**
    205     * Should blank input be ignored (after validation)? This will be true for
    206     * {@link org.apache.tapestry5.corelib.components.PasswordField}.
    207     * @return true if blank input should be ignored, false otherwise
    208     */
    209    protected boolean ignoreBlankInput()
    210    {
    211        return false;
    212    }
    213
    214    @Override
    215    public boolean isRequired()
    216    {
    217        return validate.isRequired();
    218    }
    219
    220    /**
    221     * Looks for a {@link org.apache.tapestry5.beaneditor.Width} annotation and, if present, returns its value as a
    222     * string.
    223     *
    224     * @return the indicated width, or null if the annotation is not present
    225     */
    226    protected final String getWidth()
    227    {
    228        Width width = annotationProvider.getAnnotation(Width.class);
    229
    230        if (width == null)
    231            return null;
    232
    233        return Integer.toString(width.value());
    234    }
    235}