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}