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.commons.lang3.StringEscapeUtils;
016import org.apache.tapestry5.*;
017import org.apache.tapestry5.annotations.*;
018import org.apache.tapestry5.commons.internal.util.TapestryException;
019import org.apache.tapestry5.corelib.mixins.DiscardBody;
020import org.apache.tapestry5.corelib.mixins.RenderInformals;
021import org.apache.tapestry5.http.services.Request;
022import org.apache.tapestry5.internal.BeanValidationContext;
023import org.apache.tapestry5.internal.InternalComponentResources;
024import org.apache.tapestry5.internal.services.FormControlNameManager;
025import org.apache.tapestry5.ioc.annotations.Inject;
026import org.apache.tapestry5.ioc.annotations.Symbol;
027import org.apache.tapestry5.ioc.internal.util.InternalUtils;
028import org.apache.tapestry5.services.ComponentDefaultProvider;
029import org.apache.tapestry5.services.Environment;
030import org.apache.tapestry5.services.FormSupport;
031import org.apache.tapestry5.services.javascript.JavaScriptSupport;
032
033import java.io.Serializable;
034
035/**
036 * Provides initialization of the clientId and elementName properties. In addition, adds the {@link RenderInformals},
037 * and {@link DiscardBody} mixins.
038 *
039 * @tapestrydoc
040 */
041@SuppressWarnings("deprecation")
042@SupportsInformalParameters
043public abstract class AbstractField implements Field
044{
045    /**
046     * The user presentable label for the field. If not provided, a reasonable label is generated from the component's
047     * id, first by looking for a message key named "id-label" (substituting the component's actual id), then by
048     * converting the actual id to a presentable string (for example, "userId" to "User Id").
049     */
050    @Parameter(defaultPrefix = BindingConstants.LITERAL)
051    protected String label;
052
053    /**
054     * If true, then the field will render out with a disabled attribute
055     * (to turn off client-side behavior). When the form is submitted, the
056     * bound value is evaluated again and, if true, the field's value is
057     * ignored (not even validated) and the component's events are not fired.
058     */
059    @Parameter("false")
060    protected boolean disabled;
061
062    @SuppressWarnings("unused")
063    @Mixin
064    private DiscardBody discardBody;
065
066    @Environmental
067    protected ValidationDecorator decorator;
068
069    @Inject
070    protected Environment environment;
071
072    @Inject
073    @Symbol(SymbolConstants.FORM_FIELD_CSS_CLASS)
074    protected String cssClass;
075
076    static class Setup implements ComponentAction<AbstractField>, Serializable
077    {
078        private static final long serialVersionUID = 2690270808212097020L;
079
080        private final String controlName;
081
082        public Setup(String controlName)
083        {
084            this.controlName = controlName;
085        }
086
087        public void execute(AbstractField component)
088        {
089            component.setupControlName(controlName);
090        }
091
092        @Override
093        public String toString()
094        {
095            return String.format("AbstractField.Setup[%s]", controlName);
096        }
097    }
098
099    static class ProcessSubmission implements ComponentAction<AbstractField>, Serializable
100    {
101        private static final long serialVersionUID = -4346426414137434418L;
102
103        public void execute(AbstractField component)
104        {
105            component.processSubmission();
106        }
107
108        @Override
109        public String toString()
110        {
111            return "AbstractField.ProcessSubmission";
112        }
113    }
114
115    /**
116     * Used a shared instance for all types of fields, for efficiency.
117     */
118    private static final ProcessSubmission PROCESS_SUBMISSION_ACTION = new ProcessSubmission();
119
120    /**
121     * Used to explicitly set the client-side id of the element for this component. Normally this is not
122     * bound (or null) and {@link org.apache.tapestry5.services.javascript.JavaScriptSupport#allocateClientId(org.apache.tapestry5.ComponentResources)}
123     * is used to generate a unique client-id based on the component's id. In some cases, when creating client-side
124     * behaviors, it is useful to explicitly set a unique id for an element using this parameter.
125     * 
126     * Certain values, such as "submit", "method", "reset", etc., will cause client-side conflicts and are not allowed; using such will
127     * cause a runtime exception.
128     */
129    @Parameter(defaultPrefix = BindingConstants.LITERAL)
130    private String clientId;
131
132    /**
133     * A rarely used option that indicates that the actual client id should start with the clientId parameter (if non-null)
134     * but should still pass that Id through {@link org.apache.tapestry5.services.javascript.JavaScriptSupport#allocateClientId(String)}
135     * to generate the final id.
136     * 
137     * An example of this are the components used inside a {@link org.apache.tapestry5.corelib.components.BeanEditor} which
138     * will specify a clientId (based on the property name) but still require that it be unique.
139     * 
140     * Defaults to false.
141     *
142     * @since 5.4
143     */
144    @Parameter
145    private boolean ensureClientIdUnique;
146
147
148    private String assignedClientId;
149
150    private String controlName;
151
152    @Environmental(false)
153    protected FormSupport formSupport;
154
155    @Environmental
156    protected JavaScriptSupport javaScriptSupport;
157
158    @Environmental
159    protected ValidationTracker validationTracker;
160
161    @Inject
162    protected ComponentResources resources;
163
164    @Inject
165    protected ComponentDefaultProvider defaultProvider;
166
167    @Inject
168    protected Request request;
169
170    @Inject
171    protected FieldValidationSupport fieldValidationSupport;
172
173    @Inject
174    private FormControlNameManager formControlNameManager;
175
176    final String defaultLabel()
177    {
178        return defaultProvider.defaultLabel(resources);
179    }
180
181    public final String getLabel()
182    {
183        return label;
184    }
185
186    @SetupRender
187    final void setup()
188    {
189        // Often, these controlName and clientId will end up as the same value. There are many
190        // exceptions, including a form that renders inside a loop, or a form inside a component
191        // that is used multiple times.
192
193        if (formSupport == null)
194            throw new RuntimeException(String.format("Component %s must be enclosed by a Form component.",
195                    resources.getCompleteId()));
196
197        assignedClientId = allocateClientId();
198
199        String controlName = formSupport.allocateControlName(assignedClientId);
200
201        formSupport.storeAndExecute(this, new Setup(controlName));
202        formSupport.store(this, PROCESS_SUBMISSION_ACTION);
203    }
204
205    private String allocateClientId()
206    {
207        if (clientId == null)
208        {
209            return javaScriptSupport.allocateClientId(resources);
210        }
211
212
213        if (ensureClientIdUnique)
214        {
215            return javaScriptSupport.allocateClientId(clientId);
216        } else
217        {
218            // See https://issues.apache.org/jira/browse/TAP5-1632
219            // Basically, on the client, there can be a convenience lookup inside a HTMLFormElement
220            // by id OR name; so an id of "submit" (for example) will mask the HTMLFormElement.submit()
221            // function.
222
223            if (formControlNameManager.isReserved(clientId))
224            {
225                throw new TapestryException(String.format(
226                        "The value '%s' for parameter clientId is not allowed as it causes a naming conflict in the client-side DOM. " +
227                                "Select an id not in the list: %s.",
228                        clientId,
229                        InternalUtils.joinSorted(formControlNameManager.getReservedNames())), this, null);
230            }
231        }
232
233        return clientId;
234    }
235
236    public final String getClientId()
237    {
238        return assignedClientId;
239    }
240
241    public final String getControlName()
242    {
243        return controlName;
244    }
245
246    public final boolean isDisabled()
247    {
248        return disabled;
249    }
250
251    /**
252     * Invoked from within a ComponentCommand callback, to restore the component's elementName.
253     */
254    private void setupControlName(String controlName)
255    {
256        this.controlName = controlName;
257    }
258
259    private void processSubmission()
260    {
261        if (!disabled)
262            processSubmission(controlName);
263    }
264
265    /**
266     * Method implemented by subclasses to actually do the work of processing the submission of the form. The element's
267     * controlName property will already have been set. This method is only invoked if the field is <strong>not
268     * {@link #isDisabled() disabled}</strong>.
269     *
270     * @param controlName
271     *         the control name of the rendered element (used to find the correct parameter in the request)
272     */
273    protected abstract void processSubmission(String controlName);
274
275    /**
276     * Allows the validation decorator to write markup before the field itself writes markup.
277     */
278    @BeginRender
279    final void beforeDecorator()
280    {
281        decorator.beforeField(this);
282    }
283
284    /**
285     * Allows the validation decorator to write markup after the field has written all of its markup.
286     * In addition, may invoke the <code>core/fields:showValidationError</code> function to present
287     * the field's error (if it has one) to the user.
288     */
289    @AfterRender
290    final void afterDecorator()
291    {
292        decorator.afterField(this);
293
294        String error = validationTracker.getError(this);
295        error = StringEscapeUtils.escapeHtml4(error);
296
297        if (error != null)
298        {
299            javaScriptSupport.require("t5/core/fields").invoke("showValidationError").with(assignedClientId, error);
300        }
301    }
302
303    /**
304     * Invoked from subclasses after they have written their tag and (where appropriate) their informal parameters
305     * <em>and</em> have allowed their {@link Validator} to write markup as well.
306     */
307    protected final void decorateInsideField()
308    {
309        decorator.insideField(this);
310    }
311
312    protected final void setDecorator(ValidationDecorator decorator)
313    {
314        this.decorator = decorator;
315    }
316
317    protected final void setFormSupport(FormSupport formSupport)
318    {
319        this.formSupport = formSupport;
320    }
321
322    /**
323     * Returns false; most components do not support declarative validation.
324     */
325    public boolean isRequired()
326    {
327        return false;
328    }
329
330    // This is set to true for some unit test.
331    private boolean beanValidationDisabled = false;
332
333    protected void putPropertyNameIntoBeanValidationContext(String parameterName)
334    {
335        if (beanValidationDisabled)
336        {
337            return;
338        }
339
340        String propertyName = ((InternalComponentResources) resources).getPropertyName(parameterName);
341
342        BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
343
344        if (beanValidationContext == null)
345            return;
346
347        // If field is inside BeanEditForm, then property is already set
348        if (beanValidationContext.getCurrentProperty() == null)
349        {
350            beanValidationContext.setCurrentProperty(propertyName);
351        }
352    }
353
354    protected void removePropertyNameFromBeanValidationContext()
355    {
356        if (beanValidationDisabled)
357        {
358            return;
359        }
360
361        BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
362
363        if (beanValidationContext == null)
364            return;
365
366        beanValidationContext.setCurrentProperty(null);
367    }
368}