001// Copyright 2006-2013 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.corelib.mixins.DiscardBody;
020import org.apache.tapestry5.corelib.mixins.RenderInformals;
021import org.apache.tapestry5.internal.BeanValidationContext;
022import org.apache.tapestry5.internal.InternalComponentResources;
023import org.apache.tapestry5.ioc.annotations.Inject;
024import org.apache.tapestry5.ioc.annotations.Symbol;
025import org.apache.tapestry5.services.ComponentDefaultProvider;
026import org.apache.tapestry5.services.Environment;
027import org.apache.tapestry5.services.FormSupport;
028import org.apache.tapestry5.services.Request;
029import org.apache.tapestry5.services.javascript.JavaScriptSupport;
030
031import java.io.Serializable;
032
033/**
034 * Provides initialization of the clientId and elementName properties. In addition, adds the {@link RenderInformals},
035 * and {@link DiscardBody} mixins.
036 *
037 * @tapestrydoc
038 */
039@SupportsInformalParameters
040public abstract class AbstractField implements Field
041{
042    /**
043     * The user presentable label for the field. If not provided, a reasonable label is generated from the component's
044     * id, first by looking for a message key named "id-label" (substituting the component's actual id), then by
045     * converting the actual id to a presentable string (for example, "userId" to "User Id").
046     */
047    @Parameter(defaultPrefix = BindingConstants.LITERAL)
048    protected String label;
049
050    /**
051     * If true, then the field will render out with a disabled attribute
052     * (to turn off client-side behavior). When the form is submitted, the
053     * bound value is evaluated again and, if true, the field's value is
054     * ignored (not even validated) and the component's events are not fired.
055     */
056    @Parameter("false")
057    protected boolean disabled;
058
059    @SuppressWarnings("unused")
060    @Mixin
061    private DiscardBody discardBody;
062
063    @Environmental
064    protected ValidationDecorator decorator;
065
066    @Inject
067    protected Environment environment;
068    
069    @Inject
070    @Symbol(SymbolConstants.FORM_FIELD_CSS_CLASS)
071    protected String cssClass;
072
073    static class Setup implements ComponentAction<AbstractField>, Serializable
074    {
075        private static final long serialVersionUID = 2690270808212097020L;
076
077        private final String controlName;
078
079        public Setup(String controlName)
080        {
081            this.controlName = controlName;
082        }
083
084        public void execute(AbstractField component)
085        {
086            component.setupControlName(controlName);
087        }
088
089        @Override
090        public String toString()
091        {
092            return String.format("AbstractField.Setup[%s]", controlName);
093        }
094    }
095
096    static class ProcessSubmission implements ComponentAction<AbstractField>, Serializable
097    {
098        private static final long serialVersionUID = -4346426414137434418L;
099
100        public void execute(AbstractField component)
101        {
102            component.processSubmission();
103        }
104
105        @Override
106        public String toString()
107        {
108            return "AbstractField.ProcessSubmission";
109        }
110    }
111
112    /**
113     * Used a shared instance for all types of fields, for efficiency.
114     */
115    private static final ProcessSubmission PROCESS_SUBMISSION_ACTION = new ProcessSubmission();
116
117    /**
118     * The id used to generate a page-unique client-side identifier for the component. If a component renders multiple
119     * times, a suffix will be appended to the to id to ensure uniqueness. The uniqued value may be accessed via the
120     * {@link #getClientId() clientId property}.
121     */
122    @Parameter(value = "prop:componentResources.id", defaultPrefix = BindingConstants.LITERAL)
123    protected String clientId;
124
125    private String assignedClientId;
126
127    private String controlName;
128
129    @Environmental(false)
130    protected FormSupport formSupport;
131
132    @Environmental
133    protected JavaScriptSupport javaScriptSupport;
134
135    @Environmental
136    protected ValidationTracker validationTracker;
137
138    @Inject
139    protected ComponentResources resources;
140
141    @Inject
142    protected ComponentDefaultProvider defaultProvider;
143
144    @Inject
145    protected Request request;
146
147    @Inject
148    protected FieldValidationSupport fieldValidationSupport;
149
150
151    final String defaultLabel()
152    {
153        return defaultProvider.defaultLabel(resources);
154    }
155
156    public final String getLabel()
157    {
158        return label;
159    }
160
161    @SetupRender
162    final void setup()
163    {
164        // By default, use the component id as the (base) client id. If the clientid
165        // parameter is bound, then that is the value to use.
166
167        String id = clientId;
168
169        // Often, these controlName and clientId will end up as the same value. There are many
170        // exceptions, including a form that renders inside a loop, or a form inside a component
171        // that is used multiple times.
172
173        if (formSupport == null)
174            throw new RuntimeException(String.format("Component %s must be enclosed by a Form component.",
175                    resources.getCompleteId()));
176
177        assignedClientId = javaScriptSupport.allocateClientId(id);
178        String controlName = formSupport.allocateControlName(id);
179
180        formSupport.storeAndExecute(this, new Setup(controlName));
181        formSupport.store(this, PROCESS_SUBMISSION_ACTION);
182    }
183
184    public final String getClientId()
185    {
186        return assignedClientId;
187    }
188
189    public final String getControlName()
190    {
191        return controlName;
192    }
193
194    public final boolean isDisabled()
195    {
196        return disabled;
197    }
198
199    /**
200     * Invoked from within a ComponentCommand callback, to restore the component's elementName.
201     */
202    private void setupControlName(String controlName)
203    {
204        this.controlName = controlName;
205    }
206
207    private void processSubmission()
208    {
209        if (!disabled)
210            processSubmission(controlName);
211    }
212
213    /**
214     * Method implemented by subclasses to actually do the work of processing the submission of the form. The element's
215     * controlName property will already have been set. This method is only invoked if the field is <strong>not
216     * {@link #isDisabled() disabled}</strong>.
217     *
218     * @param controlName
219     *         the control name of the rendered element (used to find the correct parameter in the request)
220     */
221    protected abstract void processSubmission(String controlName);
222
223    /**
224     * Allows the validation decorator to write markup before the field itself writes markup.
225     */
226    @BeginRender
227    final void beforeDecorator()
228    {
229        decorator.beforeField(this);
230    }
231
232    /**
233     * Allows the validation decorator to write markup after the field has written all of its markup.
234     * In addition, may invoke the <code>core/fields:showValidationError</code> function to present
235     * the field's error (if it has one) to the user.
236     */
237    @AfterRender
238    final void afterDecorator()
239    {
240        decorator.afterField(this);
241
242        String error = validationTracker.getError(this);
243
244        if (error != null)
245        {
246            javaScriptSupport.require("t5/core/fields").invoke("showValidationError").with(assignedClientId, error);
247        }
248    }
249
250    /**
251     * Invoked from subclasses after they have written their tag and (where appropriate) their informal parameters
252     * <em>and</em> have allowed their {@link Validator} to write markup as well.
253     */
254    protected final void decorateInsideField()
255    {
256        decorator.insideField(this);
257    }
258
259    protected final void setDecorator(ValidationDecorator decorator)
260    {
261        this.decorator = decorator;
262    }
263
264    protected final void setFormSupport(FormSupport formSupport)
265    {
266        this.formSupport = formSupport;
267    }
268
269    /**
270     * Returns false; most components do not support declarative validation.
271     */
272    public boolean isRequired()
273    {
274        return false;
275    }
276
277    // This is set to true for some unit test.
278    private boolean beanValidationDisabled = false;
279
280    protected void putPropertyNameIntoBeanValidationContext(String parameterName)
281    {
282        if (beanValidationDisabled) { return; }
283
284        String propertyName = ((InternalComponentResources) resources).getPropertyName(parameterName);
285
286        BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
287
288        if (beanValidationContext == null)
289            return;
290
291        // If field is inside BeanEditForm, then property is already set
292        if (beanValidationContext.getCurrentProperty() == null)
293        {
294            beanValidationContext.setCurrentProperty(propertyName);
295        }
296    }
297
298    protected void removePropertyNameFromBeanValidationContext()
299    {
300        if (beanValidationDisabled) { return; }
301
302        BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
303
304        if (beanValidationContext == null)
305            return;
306
307        beanValidationContext.setCurrentProperty(null);
308    }
309}