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    
015    package org.apache.tapestry5.corelib.base;
016    
017    import org.apache.tapestry5.*;
018    import org.apache.tapestry5.annotations.*;
019    import org.apache.tapestry5.corelib.mixins.DiscardBody;
020    import org.apache.tapestry5.corelib.mixins.RenderDisabled;
021    import org.apache.tapestry5.corelib.mixins.RenderInformals;
022    import org.apache.tapestry5.internal.BeanValidationContext;
023    import org.apache.tapestry5.internal.InternalComponentResources;
024    import org.apache.tapestry5.ioc.annotations.Inject;
025    import org.apache.tapestry5.services.ComponentDefaultProvider;
026    import org.apache.tapestry5.services.Environment;
027    import org.apache.tapestry5.services.FormSupport;
028    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
029    
030    import 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
037    public 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    }