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