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