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