001    // Copyright 2006, 2007, 2008, 2009 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.components;
016    
017    import org.apache.tapestry5.*;
018    import org.apache.tapestry5.annotations.*;
019    import org.apache.tapestry5.corelib.internal.ComponentActionSink;
020    import org.apache.tapestry5.corelib.internal.FormSupportImpl;
021    import org.apache.tapestry5.corelib.internal.InternalFormSupport;
022    import org.apache.tapestry5.corelib.mixins.RenderInformals;
023    import org.apache.tapestry5.dom.Element;
024    import org.apache.tapestry5.internal.services.ComponentResultProcessorWrapper;
025    import org.apache.tapestry5.internal.services.HeartbeatImpl;
026    import org.apache.tapestry5.internal.util.AutofocusValidationDecorator;
027    import org.apache.tapestry5.ioc.Location;
028    import org.apache.tapestry5.ioc.Messages;
029    import org.apache.tapestry5.ioc.annotations.Inject;
030    import org.apache.tapestry5.ioc.annotations.Symbol;
031    import org.apache.tapestry5.ioc.internal.util.IdAllocator;
032    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
033    import org.apache.tapestry5.ioc.internal.util.TapestryException;
034    import org.apache.tapestry5.ioc.util.ExceptionUtils;
035    import org.apache.tapestry5.runtime.Component;
036    import org.apache.tapestry5.services.*;
037    import org.slf4j.Logger;
038    
039    import java.io.EOFException;
040    import java.io.IOException;
041    import java.io.ObjectInputStream;
042    
043    /**
044     * An HTML form, which will enclose other components to render out the various types of fields.
045     * <p/>
046     * A Form emits many notification events. When it renders, it fires a {@link org.apache.tapestry5.EventConstants#PREPARE_FOR_RENDER}
047     * notification, followed by a {@link org.apache.tapestry5.EventConstants#PREPARE} notification.
048     * <p/>
049     * When the form is submitted, the component emits several notifications: first a {@link
050     * org.apache.tapestry5.EventConstants#PREPARE_FOR_SUBMIT}, then a {@link org.apache.tapestry5.EventConstants#PREPARE}:
051     * these allow the page to update its state as necessary to prepare for the form submission, then (after components
052     * enclosed by the form have operated), a {@link org.apache.tapestry5.EventConstants#VALIDATE_FORM} event is emitted, to
053     * allow for cross-form validation. After that, either a {@link org.apache.tapestry5.EventConstants#SUCCESS} OR {@link
054     * org.apache.tapestry5.EventConstants#FAILURE} event (depending on whether the {@link ValidationTracker} has recorded
055     * any errors). Lastly, a {@link org.apache.tapestry5.EventConstants#SUBMIT} event, for any listeners that care only
056     * about form submission, regardless of success or failure.
057     * <p/>
058     * For all of these notifications, the event context is derived from the <strong>context</strong> parameter. This
059     * context is encoded into the form's action URI (the parameter is not read when the form is submitted, instead the
060     * values encoded into the form are used).
061     */
062    @Events({ EventConstants.PREPARE_FOR_RENDER, EventConstants.PREPARE, EventConstants.PREPARE_FOR_SUBMIT,
063            EventConstants.VALIDATE_FORM,
064            EventConstants.SUBMIT, EventConstants.FAILURE, EventConstants.SUCCESS })
065    public class Form implements ClientElement, FormValidationControl
066    {
067        /**
068         * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
069         */
070        public static final String PREPARE_FOR_RENDER = EventConstants.PREPARE_FOR_RENDER;
071    
072        /**
073         * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
074         */
075        public static final String PREPARE_FOR_SUBMIT = EventConstants.PREPARE_FOR_SUBMIT;
076    
077        /**
078         * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
079         */
080        public static final String PREPARE = EventConstants.PREPARE;
081    
082        /**
083         * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
084         */
085        public static final String SUBMIT = EventConstants.SUBMIT;
086    
087        /**
088         * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
089         */
090        public static final String VALIDATE_FORM = EventConstants.VALIDATE_FORM;
091    
092        /**
093         * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
094         */
095        public static final String SUCCESS = EventConstants.SUCCESS;
096    
097        /**
098         * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
099         */
100        public static final String FAILURE = EventConstants.FAILURE;
101    
102        /**
103         * Query parameter name storing form data (the serialized commands needed to process a form submission).
104         */
105        public static final String FORM_DATA = "t:formdata";
106    
107        /**
108         * The context for the link (optional parameter). This list of values will be converted into strings and included in
109         * the URI. The strings will be coerced back to whatever their values are and made available to event handler
110         * methods.
111         */
112        @Parameter
113        private Object[] context;
114    
115        /**
116         * The object which will record user input and validation errors. The object must be persistent between requests
117         * (since the form submission and validation occurs in an component event request and the subsequent render occurs
118         * in a render request). The default is a persistent property of the Form component and this is sufficient for
119         * nearly all purposes (except when a Form is rendered inside a loop).
120         */
121        @Parameter("defaultTracker")
122        private ValidationTracker tracker;
123    
124        @Inject
125        @Symbol(SymbolConstants.FORM_CLIENT_LOGIC_ENABLED)
126        private boolean clientLogicDefaultEnabled;
127    
128        /**
129         * If true (the default) then client validation is enabled for the form, and the default set of JavaScript libraries
130         * (Prototype, Scriptaculous and the Tapestry library) will be added to the rendered page, and the form will
131         * register itself for validation. This may be turned off when client validation is not desired; for example, when
132         * many validations are used that do not operate on the client side at all.
133         */
134        @Parameter
135        private boolean clientValidation = clientLogicDefaultEnabled;
136    
137        /**
138         * If true (the default), then the JavaScript will be added to position the cursor into the form. The field to
139         * receive focus is the first rendered field that is in error, or required, or present (in that order of priority).
140         *
141         * @see SymbolConstants#FORM_CLIENT_LOGIC_ENABLED
142         */
143        @Parameter
144        private boolean autofocus = clientLogicDefaultEnabled;
145    
146        /**
147         * Binding the zone parameter will cause the form submission to be handled as an Ajax request that updates the
148         * indicated zone.  Often a Form will update the same zone that contains it.
149         */
150        @Parameter(defaultPrefix = BindingConstants.LITERAL)
151        private String zone;
152    
153        /**
154         * Prefix value used when searching for validation messages and constraints.  The default is the Form component's
155         * id. This is overriden by {@link org.apache.tapestry5.corelib.components.BeanEditForm}.
156         *
157         * @see org.apache.tapestry5.services.FormSupport#getFormValidationId()
158         */
159        @Parameter
160        private String validationId;
161    
162        @Inject
163        private Logger logger;
164    
165        @Inject
166        private Environment environment;
167    
168        @Inject
169        private ComponentResources resources;
170    
171        @Inject
172        private Messages messages;
173    
174        @Environmental
175        private RenderSupport renderSupport;
176    
177        @Inject
178        private Request request;
179    
180        @Inject
181        private ComponentSource source;
182    
183        @Persist(PersistenceConstants.FLASH)
184        private ValidationTracker defaultTracker;
185    
186        private InternalFormSupport formSupport;
187    
188        private Element form;
189    
190        private Element div;
191    
192        // Collects a stream of component actions. Each action goes in as a UTF string (the component
193        // component id), followed by a ComponentAction
194    
195        private ComponentActionSink actionSink;
196    
197        @Mixin
198        private RenderInformals renderInformals;
199    
200        /**
201         * Set up via the traditional or Ajax component event request handler
202         */
203        @Environmental
204        private ComponentEventResultProcessor componentEventResultProcessor;
205    
206        @Environmental
207        private ClientBehaviorSupport clientBehaviorSupport;
208    
209        @Inject
210        private ClientDataEncoder clientDataEncoder;
211    
212        private String name;
213    
214        String defaultValidationId()
215        {
216            return resources.getId();
217        }
218    
219        public ValidationTracker getDefaultTracker()
220        {
221            if (defaultTracker == null) defaultTracker = new ValidationTrackerImpl();
222    
223            return defaultTracker;
224        }
225    
226        public void setDefaultTracker(ValidationTracker defaultTracker)
227        {
228            this.defaultTracker = defaultTracker;
229        }
230    
231        void setupRender()
232        {
233            FormSupport existing = environment.peek(FormSupport.class);
234    
235            if (existing != null)
236                throw new TapestryException(messages.get("nesting-not-allowed"), existing, null);
237        }
238    
239        void beginRender(MarkupWriter writer)
240        {
241            Link link = resources.createFormEventLink(EventConstants.ACTION, context);
242    
243            actionSink = new ComponentActionSink(logger, clientDataEncoder);
244    
245            name = renderSupport.allocateClientId(resources);
246    
247            formSupport = createRenderTimeFormSupport(name, actionSink, new IdAllocator());
248    
249            if (zone != null) clientBehaviorSupport.linkZone(name, zone, link);
250    
251            // TODO: Forms should not allow to nest. Perhaps a set() method instead of a push() method
252            // for this kind of check?  
253    
254            environment.push(FormSupport.class, formSupport);
255            environment.push(ValidationTracker.class, tracker);
256    
257            if (autofocus)
258            {
259                ValidationDecorator autofocusDecorator = new AutofocusValidationDecorator(environment.peek(
260                        ValidationDecorator.class), tracker, renderSupport);
261                environment.push(ValidationDecorator.class, autofocusDecorator);
262            }
263    
264            // Now that the environment is setup, inform the component or other listeners that the form
265            // is about to render.  
266    
267            resources.triggerEvent(EventConstants.PREPARE_FOR_RENDER, context, null);
268    
269            resources.triggerEvent(EventConstants.PREPARE, context, null);
270    
271            // Save the form element for later, in case we want to write an encoding type attribute.
272    
273            form = writer.element("form",
274                                  "name", name,
275                                  "id", name,
276                                  "method", "post",
277                                  "action", link);
278    
279            if ((zone != null || clientValidation) && !request.isXHR())
280                writer.attributes("onsubmit", MarkupConstants.WAIT_FOR_PAGE);
281    
282            resources.renderInformalParameters(writer);
283    
284            div = writer.element("div", "class", CSSClassConstants.INVISIBLE);
285    
286            for (String parameterName : link.getParameterNames())
287            {
288                String value = link.getParameterValue(parameterName);
289    
290                writer.element("input",
291                               "type", "hidden",
292                               "name", parameterName,
293                               "value", value);
294                writer.end();
295            }
296    
297            writer.end(); // div
298    
299            environment.peek(Heartbeat.class).begin();
300        }
301    
302        /**
303         * Creates an {@link org.apache.tapestry5.corelib.internal.InternalFormSupport} for this Form. This method is used
304         * by {@link org.apache.tapestry5.corelib.components.FormInjector}.
305         *
306         * @param name       the client-side name and client id for the rendered form element
307         * @param actionSink used to collect component actions that will, ultimately, be written as the t:formdata hidden
308         *                   field
309         * @param allocator  used to allocate unique ids
310         * @return form support object
311         */
312        InternalFormSupport createRenderTimeFormSupport(String name, ComponentActionSink actionSink, IdAllocator allocator)
313        {
314            return new FormSupportImpl(resources, name, actionSink, clientBehaviorSupport,
315                                       clientValidation, allocator, validationId);
316        }
317    
318        void afterRender(MarkupWriter writer)
319        {
320            environment.peek(Heartbeat.class).end();
321    
322            formSupport.executeDeferred();
323    
324            String encodingType = formSupport.getEncodingType();
325    
326            if (encodingType != null) form.forceAttributes("enctype", encodingType);
327    
328            writer.end(); // form
329    
330            div.element("input",
331                        "type", "hidden",
332                        "name", FORM_DATA,
333                        "value", actionSink.getClientData());
334    
335            if (autofocus)
336                environment.pop(ValidationDecorator.class);
337        }
338    
339        void cleanupRender()
340        {
341            environment.pop(FormSupport.class);
342    
343            formSupport = null;
344    
345            environment.pop(ValidationTracker.class);
346        }
347    
348        @SuppressWarnings({ "unchecked", "InfiniteLoopStatement" })
349        @Log
350        Object onAction(EventContext context) throws IOException
351        {
352            tracker.clear();
353    
354            formSupport = new FormSupportImpl(resources, validationId);
355    
356            environment.push(ValidationTracker.class, tracker);
357            environment.push(FormSupport.class, formSupport);
358    
359            Heartbeat heartbeat = new HeartbeatImpl();
360    
361            environment.push(Heartbeat.class, heartbeat);
362    
363            heartbeat.begin();
364    
365            try
366            {
367                ComponentResultProcessorWrapper callback = new ComponentResultProcessorWrapper(
368                        componentEventResultProcessor);
369    
370                resources.triggerContextEvent(EventConstants.PREPARE_FOR_SUBMIT, context, callback);
371    
372                if (callback.isAborted()) return true;
373    
374                resources.triggerContextEvent(EventConstants.PREPARE, context, callback);
375    
376                if (callback.isAborted()) return true;
377    
378                executeStoredActions();
379    
380                heartbeat.end();
381    
382                formSupport.executeDeferred();
383    
384                fireValidateFormEvent(context, callback);
385    
386                if (callback.isAborted()) return true;
387    
388                // Let the listeners know about overall success or failure. Most listeners fall into
389                // one of those two camps.
390    
391                // If the tracker has no errors, then clear it of any input values
392                // as well, so that the next page render will be "clean" and show
393                // true persistent data, not value from the previous form submission.
394    
395                if (!tracker.getHasErrors())
396                    tracker.clear();
397    
398                resources.triggerContextEvent(tracker.getHasErrors() ? EventConstants.FAILURE : EventConstants.SUCCESS,
399                                              context, callback);
400    
401                // Lastly, tell anyone whose interested that the form is completely submitted.
402    
403                if (callback.isAborted()) return true;
404    
405                resources.triggerContextEvent(EventConstants.SUBMIT, context, callback);
406    
407                return callback.isAborted();
408            }
409            finally
410            {
411                environment.pop(Heartbeat.class);
412                environment.pop(FormSupport.class);
413    
414                // This forces an update that feeds through the system and gets the updated
415                // state of the tracker (if using the Form's defaultTracker property, which is flash persisted)
416                // stored back into the session.
417    
418                tracker = environment.pop(ValidationTracker.class);
419            }
420        }
421    
422        private void fireValidateFormEvent(EventContext context, ComponentResultProcessorWrapper callback)
423        {
424            try
425            {
426                resources.triggerContextEvent(EventConstants.VALIDATE_FORM, context, callback);
427            }
428            catch (RuntimeException ex)
429            {
430                ValidationException ve = ExceptionUtils.findCause(ex, ValidationException.class);
431    
432                if (ve != null)
433                {
434                    recordError(ve.getMessage());
435                    return;
436                }
437    
438                throw ex;
439            }
440        }
441    
442        /**
443         * Pulls the stored actions out of the request, converts them from MIME stream back to object stream and then
444         * objects, and executes them.
445         */
446        private void executeStoredActions()
447        {
448            String[] values = request.getParameters(FORM_DATA);
449    
450            if (!request.getMethod().equals("POST") || values == null)
451                throw new RuntimeException(messages.format("invalid-request", FORM_DATA));
452    
453            // Due to Ajax (FormInjector) there may be multiple values here, so handle each one individually.
454    
455            for (String clientEncodedActions : values)
456            {
457                if (InternalUtils.isBlank(clientEncodedActions)) continue;
458    
459                logger.debug("Processing actions: {}", clientEncodedActions);
460    
461                ObjectInputStream ois = null;
462    
463                Component component = null;
464    
465                try
466                {
467                    ois = clientDataEncoder.decodeClientData(clientEncodedActions);
468    
469                    while (true)
470                    {
471                        String componentId = ois.readUTF();
472                        ComponentAction action = (ComponentAction) ois.readObject();
473    
474                        component = source.getComponent(componentId);
475    
476                        logger.debug("Processing: {} {}", componentId, action);
477    
478                        action.execute(component);
479    
480                        component = null;
481                    }
482                }
483                catch (EOFException ex)
484                {
485                    // Expected
486                }
487                catch (Exception ex)
488                {
489                    Location location = component == null ? null : component.getComponentResources().getLocation();
490    
491                    throw new TapestryException(ex.getMessage(), location, ex);
492                }
493                finally
494                {
495                    InternalUtils.close(ois);
496                }
497            }
498        }
499    
500        public void recordError(String errorMessage)
501        {
502            tracker.recordError(errorMessage);
503        }
504    
505        public void recordError(Field field, String errorMessage)
506        {
507            tracker.recordError(field, errorMessage);
508        }
509    
510        public boolean getHasErrors()
511        {
512            return tracker.getHasErrors();
513        }
514    
515        public boolean isValid()
516        {
517            return !tracker.getHasErrors();
518        }
519    
520        // For testing:
521    
522        void setTracker(ValidationTracker tracker)
523        {
524            this.tracker = tracker;
525        }
526    
527        public void clearErrors()
528        {
529            tracker.clear();
530        }
531    
532        /**
533         * Forms use the same value for their name and their id attribute.
534         */
535        public String getClientId()
536        {
537            return name;
538        }
539    }