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