001    // Copyright 2005 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.tapestry.form;
016    
017    import java.util.ArrayList;
018    import java.util.Arrays;
019    import java.util.Collections;
020    import java.util.HashMap;
021    import java.util.HashSet;
022    import java.util.Iterator;
023    import java.util.List;
024    import java.util.Map;
025    import java.util.Set;
026    
027    import org.apache.hivemind.ApplicationRuntimeException;
028    import org.apache.hivemind.HiveMind;
029    import org.apache.hivemind.Location;
030    import org.apache.hivemind.Resource;
031    import org.apache.hivemind.util.ClasspathResource;
032    import org.apache.hivemind.util.Defense;
033    import org.apache.tapestry.IComponent;
034    import org.apache.tapestry.IForm;
035    import org.apache.tapestry.IMarkupWriter;
036    import org.apache.tapestry.IRender;
037    import org.apache.tapestry.IRequestCycle;
038    import org.apache.tapestry.NestedMarkupWriter;
039    import org.apache.tapestry.PageRenderSupport;
040    import org.apache.tapestry.StaleLinkException;
041    import org.apache.tapestry.Tapestry;
042    import org.apache.tapestry.TapestryUtils;
043    import org.apache.tapestry.engine.ILink;
044    import org.apache.tapestry.services.ServiceConstants;
045    import org.apache.tapestry.util.IdAllocator;
046    import org.apache.tapestry.valid.IValidationDelegate;
047    
048    /**
049     * Encapsulates most of the behavior of a Form component.
050     * 
051     * @author Howard M. Lewis Ship
052     * @since 4.0
053     */
054    public class FormSupportImpl implements FormSupport
055    {
056        /**
057         * Name of query parameter storing the ids alloocated while rendering the form, as a comma
058         * seperated list. This information is used when the form is submitted, to ensure that the
059         * rewind allocates the exact same sequence of ids.
060         */
061    
062        public static final String FORM_IDS = "formids";
063    
064        /**
065         * Names of additional ids that were pre-reserved, as a comma-sepereated list. These are names
066         * beyond that standard set. Certain engine services include extra parameter values that must be
067         * accounted for, and page properties may be encoded as additional query parameters.
068         */
069    
070        public static final String RESERVED_FORM_IDS = "reservedids";
071    
072        /**
073         * Indicates why the form was submitted: whether for normal ("submit"), refresh, or because the
074         * form was canceled.
075         */
076    
077        public static final String SUBMIT_MODE = "submitmode";
078    
079        public static final String SCRIPT = "/org/apache/tapestry/form/Form.js";
080    
081        private final static Set _standardReservedIds;
082    
083        /**
084         * Attribute set to true when a field has been focused; used to prevent conflicting JavaScript
085         * for field focusing from being emitted.
086         */
087    
088        public static final String FIELD_FOCUS_ATTRIBUTE = "org.apache.tapestry.field-focused";
089    
090        static
091        {
092            Set set = new HashSet();
093    
094            set.addAll(Arrays.asList(ServiceConstants.RESERVED_IDS));
095            set.add(FORM_IDS);
096            set.add(RESERVED_FORM_IDS);
097            set.add(SUBMIT_MODE);
098    
099            _standardReservedIds = Collections.unmodifiableSet(set);
100        }
101    
102        private final static Set _submitModes;
103    
104        static
105        {
106            Set set = new HashSet();
107            set.add(FormConstants.SUBMIT_CANCEL);
108            set.add(FormConstants.SUBMIT_NORMAL);
109            set.add(FormConstants.SUBMIT_REFRESH);
110    
111            _submitModes = Collections.unmodifiableSet(set);
112        }
113    
114        /**
115         * Used when rewinding the form to figure to match allocated ids (allocated during the rewind)
116         * against expected ids (allocated in the previous request cycle, when the form was rendered).
117         */
118    
119        private int _allocatedIdIndex;
120    
121        /**
122         * The list of allocated ids for form elements within this form. This list is constructed when a
123         * form renders, and is validated against when the form is rewound.
124         */
125    
126        private final List _allocatedIds = new ArrayList();
127    
128        private final IRequestCycle _cycle;
129    
130        private final IdAllocator _elementIdAllocator = new IdAllocator();
131    
132        private String _encodingType;
133    
134        private final List _deferredRunnables = new ArrayList();
135    
136        /**
137         * Map keyed on extended component id, value is the pre-rendered markup for that component.
138         */
139    
140        private final Map _prerenderMap = new HashMap();
141    
142        /**
143         * {@link Map}, keyed on {@link FormEventType}. Values are either a String (the function name
144         * of a single event handler), or a List of Strings (a sequence of event handler function
145         * names).
146         */
147    
148        private Map _events;
149    
150        private final IForm _form;
151    
152        private final List _hiddenValues = new ArrayList();
153    
154        private final boolean _rewinding;
155    
156        private final IMarkupWriter _writer;
157    
158        private final Resource _script;
159    
160        private final IValidationDelegate _delegate;
161    
162        private final PageRenderSupport _pageRenderSupport;
163    
164        public FormSupportImpl(IMarkupWriter writer, IRequestCycle cycle, IForm form)
165        {
166            Defense.notNull(writer, "writer");
167            Defense.notNull(cycle, "cycle");
168            Defense.notNull(form, "form");
169    
170            _writer = writer;
171            _cycle = cycle;
172            _form = form;
173            _delegate = form.getDelegate();
174    
175            _rewinding = cycle.isRewound(form);
176            _allocatedIdIndex = 0;
177    
178            _script = new ClasspathResource(cycle.getEngine().getClassResolver(), SCRIPT);
179    
180            _pageRenderSupport = TapestryUtils.getOptionalPageRenderSupport(cycle);
181        }
182    
183        /**
184         * Adds an event handler for the form, of the given type.
185         */
186    
187        public void addEventHandler(FormEventType type, String functionName)
188        {
189            if (_events == null)
190                _events = new HashMap();
191    
192            List functionList = (List) _events.get(type);
193    
194            // The value can either be a String, or a List of String. Since
195            // it is rare for there to be more than one event handling function,
196            // we start with just a String.
197    
198            if (functionList == null)
199            {
200                functionList = new ArrayList();
201    
202                _events.put(type, functionList);
203            }
204    
205            functionList.add(functionName);
206        }
207    
208        /**
209         * Adds hidden fields for parameters provided by the {@link ILink}. These parameters define the
210         * information needed to dispatch the request, plus state information. The names of these
211         * parameters must be reserved so that conflicts don't occur that could disrupt the request
212         * processing. For example, if the id 'page' is not reserved, then a conflict could occur with a
213         * component whose id is 'page'. A certain number of ids are always reserved, and we find any
214         * additional ids beyond that set.
215         */
216    
217        private void addHiddenFieldsForLinkParameters(ILink link)
218        {
219            String[] names = link.getParameterNames();
220            int count = Tapestry.size(names);
221    
222            StringBuffer extraIds = new StringBuffer();
223            String sep = "";
224            boolean hasExtra = false;
225    
226            // All the reserved ids, which are essential for
227            // dispatching the request, are automatically reserved.
228            // Thus, if you have a component with an id of 'service', its element id
229            // will likely be 'service$0'.
230    
231            preallocateReservedIds();
232    
233            for (int i = 0; i < count; i++)
234            {
235                String name = names[i];
236    
237                // Reserve the name.
238    
239                if (!_standardReservedIds.contains(name))
240                {
241                    _elementIdAllocator.allocateId(name);
242    
243                    extraIds.append(sep);
244                    extraIds.append(name);
245    
246                    sep = ",";
247                    hasExtra = true;
248                }
249    
250                addHiddenFieldsForLinkParameter(link, name);
251            }
252    
253            if (hasExtra)
254                addHiddenValue(RESERVED_FORM_IDS, extraIds.toString());
255        }
256    
257        public void addHiddenValue(String name, String value)
258        {
259            _hiddenValues.add(new HiddenFieldData(name, value));
260        }
261    
262        public void addHiddenValue(String name, String id, String value)
263        {
264            _hiddenValues.add(new HiddenFieldData(name, id, value));
265        }
266    
267        /**
268         * Converts the allocateIds property into a string, a comma-separated list of ids. This is
269         * included as a hidden field in the form and is used to identify discrepencies when the form is
270         * submitted.
271         */
272    
273        private String buildAllocatedIdList()
274        {
275            StringBuffer buffer = new StringBuffer();
276            int count = _allocatedIds.size();
277    
278            for (int i = 0; i < count; i++)
279            {
280                if (i > 0)
281                    buffer.append(',');
282    
283                buffer.append(_allocatedIds.get(i));
284            }
285    
286            return buffer.toString();
287        }
288    
289        private void emitEventHandlers(String formId)
290        {
291            if (_events == null || _events.isEmpty())
292                return;
293    
294            StringBuffer buffer = new StringBuffer();
295    
296            Iterator i = _events.entrySet().iterator();
297    
298            while (i.hasNext())
299            {
300                Map.Entry entry = (Map.Entry) i.next();
301                FormEventType type = (FormEventType) entry.getKey();
302                Object value = entry.getValue();
303    
304                buffer.append("Tapestry.");
305                buffer.append(type.getAddHandlerFunctionName());
306                buffer.append("('");
307                buffer.append(formId);
308                buffer.append("', function (event)\n{");
309    
310                List l = (List) value;
311                int count = l.size();
312    
313                for (int j = 0; j < count; j++)
314                {
315                    String functionName = (String) l.get(j);
316    
317                    if (j > 0)
318                    {
319                        buffer.append(";");
320                    }
321    
322                    buffer.append("\n  ");
323                    buffer.append(functionName);
324    
325                    // It's supposed to be function names, but some of Paul's validation code
326                    // adds inline code to be executed instead.
327    
328                    if (!functionName.endsWith(")"))
329                    {
330                        buffer.append("()");
331                    }
332                }
333    
334                buffer.append(";\n});\n");
335            }
336    
337            // TODO: If PRS is null ...
338    
339            _pageRenderSupport.addInitializationScript(buffer.toString());
340        }
341    
342        /**
343         * Constructs a unique identifier (within the Form). The identifier consists of the component's
344         * id, with an index number added to ensure uniqueness.
345         * <p>
346         * Simply invokes
347         * {@link #getElementId(org.apache.tapestry.form.IFormComponent, java.lang.String)}with the
348         * component's id.
349         */
350    
351        public String getElementId(IFormComponent component)
352        {
353            return getElementId(component, component.getId());
354        }
355    
356        /**
357         * Constructs a unique identifier (within the Form). The identifier consists of the component's
358         * id, with an index number added to ensure uniqueness.
359         * <p>
360         * Simply invokes
361         * {@link #getElementId(org.apache.tapestry.form.IFormComponent, java.lang.String)}with the
362         * component's id.
363         */
364    
365        public String getElementId(IFormComponent component, String baseId)
366        {
367            // $ is not a valid character in an XML/XHTML id, so convert it to an underscore.
368    
369            String filteredId = TapestryUtils.convertTapestryIdToNMToken(baseId);
370    
371            String result = _elementIdAllocator.allocateId(filteredId);
372    
373            if (_rewinding)
374            {
375                if (_allocatedIdIndex >= _allocatedIds.size())
376                {
377                    throw new StaleLinkException(FormMessages.formTooManyIds(_form, _allocatedIds
378                            .size(), component), component);
379                }
380    
381                String expected = (String) _allocatedIds.get(_allocatedIdIndex);
382    
383                if (!result.equals(expected))
384                    throw new StaleLinkException(FormMessages.formIdMismatch(
385                            _form,
386                            _allocatedIdIndex,
387                            expected,
388                            result,
389                            component), component);
390            }
391            else
392            {
393                _allocatedIds.add(result);
394            }
395    
396            _allocatedIdIndex++;
397    
398            component.setName(result);
399    
400            return result;
401        }
402    
403        public boolean isRewinding()
404        {
405            return _rewinding;
406        }
407    
408        private void preallocateReservedIds()
409        {
410            for (int i = 0; i < ServiceConstants.RESERVED_IDS.length; i++)
411                _elementIdAllocator.allocateId(ServiceConstants.RESERVED_IDS[i]);
412        }
413    
414        /**
415         * Invoked when rewinding a form to re-initialize the _allocatedIds and _elementIdAllocator.
416         * Converts a string passed as a parameter (and containing a comma separated list of ids) back
417         * into the allocateIds property. In addition, return the state of the ID allocater back to
418         * where it was at the start of the render.
419         * 
420         * @see #buildAllocatedIdList()
421         * @since 3.0
422         */
423    
424        private void reinitializeIdAllocatorForRewind()
425        {
426            String allocatedFormIds = _cycle.getParameter(FORM_IDS);
427    
428            String[] ids = TapestryUtils.split(allocatedFormIds);
429    
430            for (int i = 0; i < ids.length; i++)
431                _allocatedIds.add(ids[i]);
432    
433            // Now, reconstruct the the initial state of the
434            // id allocator.
435    
436            preallocateReservedIds();
437    
438            String extraReservedIds = _cycle.getParameter(RESERVED_FORM_IDS);
439    
440            ids = TapestryUtils.split(extraReservedIds);
441    
442            for (int i = 0; i < ids.length; i++)
443                _elementIdAllocator.allocateId(ids[i]);
444        }
445    
446        public void render(String method, IRender informalParametersRenderer, ILink link)
447        {
448            String formId = _form.getName();
449    
450            emitEventManagerInitialization(formId);
451    
452            // Convert the link's query parameters into a series of
453            // hidden field values (that will be rendered later).
454    
455            addHiddenFieldsForLinkParameters(link);
456    
457            // Create a hidden field to store the submission mode, in case
458            // client-side JavaScript forces an update.
459    
460            addHiddenValue(SUBMIT_MODE, null);
461    
462            IMarkupWriter nested = _writer.getNestedWriter();
463    
464            _form.renderBody(nested, _cycle);
465    
466            runDeferredRunnables();
467    
468            writeTag(_writer, method, link.getURL(null, false));
469    
470            // For HTML compatibility
471            _writer.attribute("name", formId);
472    
473            // For XHTML compatibility
474            _writer.attribute("id", formId);
475    
476            if (_encodingType != null)
477                _writer.attribute("enctype", _encodingType);
478    
479            // Write out event handlers collected during the rendering.
480    
481            emitEventHandlers(formId);
482    
483            informalParametersRenderer.render(_writer, _cycle);
484    
485            // Finish the <form> tag
486    
487            _writer.println();
488    
489            writeHiddenFields();
490    
491            // Close the nested writer, inserting its contents.
492    
493            nested.close();
494    
495            // Close the <form> tag.
496    
497            _writer.end();
498    
499            String fieldId = _delegate.getFocusField();
500    
501            if (fieldId == null || _pageRenderSupport == null)
502                return;
503    
504            // If the form doesn't support focus, or the focus has already been set by a different form,
505            // then do nothing.
506    
507            if (!_form.getFocus() || _cycle.getAttribute(FIELD_FOCUS_ATTRIBUTE) != null)
508                return;
509    
510            _pageRenderSupport.addInitializationScript("Tapestry.set_focus('" + fieldId + "');");
511    
512            _cycle.setAttribute(FIELD_FOCUS_ATTRIBUTE, Boolean.TRUE);
513        }
514    
515        /**
516         * Pre-renders the form, setting up some client-side form support. Returns the name of the
517         * client-side form event manager variable.
518         */
519        protected void emitEventManagerInitialization(String formId)
520        {
521            if (_pageRenderSupport == null)
522                return;
523    
524            _pageRenderSupport.addExternalScript(_script);
525    
526            _pageRenderSupport.addInitializationScript("Tapestry.register_form('" + formId + "');");
527        }
528    
529        public String rewind()
530        {
531            _form.getDelegate().clear();
532    
533            String mode = _cycle.getParameter(SUBMIT_MODE);
534    
535            // On a cancel, don't bother rendering the body or anything else at all.
536    
537            if (FormConstants.SUBMIT_CANCEL.equals(mode))
538                return mode;
539    
540            reinitializeIdAllocatorForRewind();
541    
542            _form.renderBody(_writer, _cycle);
543    
544            int expected = _allocatedIds.size();
545    
546            // The other case, _allocatedIdIndex > expected, is
547            // checked for inside getElementId(). Remember that
548            // _allocatedIdIndex is incremented after allocating.
549    
550            if (_allocatedIdIndex < expected)
551            {
552                String nextExpectedId = (String) _allocatedIds.get(_allocatedIdIndex);
553    
554                throw new StaleLinkException(FormMessages.formTooFewIds(_form, expected
555                        - _allocatedIdIndex, nextExpectedId), _form);
556            }
557    
558            runDeferredRunnables();
559    
560            if (_submitModes.contains(mode))
561                return mode;
562    
563            // Either something wacky on the client side, or a client without
564            // javascript enabled.
565    
566            return FormConstants.SUBMIT_NORMAL;
567    
568        }
569    
570        private void runDeferredRunnables()
571        {
572            Iterator i = _deferredRunnables.iterator();
573            while (i.hasNext())
574            {
575                Runnable r = (Runnable) i.next();
576    
577                r.run();
578            }
579        }
580    
581        public void setEncodingType(String encodingType)
582        {
583    
584            if (_encodingType != null && !_encodingType.equals(encodingType))
585                throw new ApplicationRuntimeException(FormMessages.encodingTypeContention(
586                        _form,
587                        _encodingType,
588                        encodingType), _form, null, null);
589    
590            _encodingType = encodingType;
591        }
592    
593        /**
594         * Overwridden by {@link org.apache.tapestry.wml.GoFormSupportImpl} (WML).
595         */
596        protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value)
597        {
598            writer.beginEmpty("input");
599            writer.attribute("type", "hidden");
600            writer.attribute("name", name);
601    
602            if (HiveMind.isNonBlank(id))
603                writer.attribute("id", id);
604    
605            writer.attribute("value", value == null ? "" : value);
606            writer.println();
607        }
608    
609        private void writeHiddenField(String name, String id, String value)
610        {
611            writeHiddenField(_writer, name, id, value);
612        }
613    
614        /**
615         * Writes out all hidden values previously added by
616         * {@link #addHiddenValue(String, String, String)}. Writes a &lt;div&gt; tag around
617         * {@link #writeHiddenFieldList()}. Overriden by
618         * {@link org.apache.tapestry.wml.GoFormSupportImpl}.
619         */
620    
621        protected void writeHiddenFields()
622        {
623            _writer.begin("div");
624    
625            writeHiddenFieldList();
626    
627            _writer.end();
628        }
629    
630        /**
631         * Writes out all hidden values previously added by
632         * {@link #addHiddenValue(String, String, String)}, plus the allocated id list.
633         */
634    
635        protected void writeHiddenFieldList()
636        {
637            writeHiddenField(FORM_IDS, null, buildAllocatedIdList());
638    
639            Iterator i = _hiddenValues.iterator();
640            while (i.hasNext())
641            {
642                HiddenFieldData data = (HiddenFieldData) i.next();
643    
644                writeHiddenField(data.getName(), data.getId(), data.getValue());
645            }
646        }
647    
648        private void addHiddenFieldsForLinkParameter(ILink link, String parameterName)
649        {
650            String[] values = link.getParameterValues(parameterName);
651    
652            // In some cases, there are no values, but a space is "reserved" for the provided name.
653    
654            if (values == null)
655                return;
656    
657            for (int i = 0; i < values.length; i++)
658            {
659                addHiddenValue(parameterName, values[i]);
660            }
661        }
662    
663        protected void writeTag(IMarkupWriter writer, String method, String url)
664        {
665            writer.begin("form");
666            writer.attribute("method", method);
667            writer.attribute("action", url);
668        }
669    
670        public void prerenderField(IMarkupWriter writer, IComponent field, Location location)
671        {
672            Defense.notNull(writer, "writer");
673            Defense.notNull(field, "field");
674    
675            String key = field.getExtendedId();
676    
677            if (_prerenderMap.containsKey(key))
678                throw new ApplicationRuntimeException(FormMessages.fieldAlreadyPrerendered(field),
679                        location, null);
680    
681            NestedMarkupWriter nested = writer.getNestedWriter();
682    
683            field.render(nested, _cycle);
684    
685            _prerenderMap.put(key, nested.getBuffer());
686        }
687    
688        public boolean wasPrerendered(IMarkupWriter writer, IComponent field)
689        {
690            String key = field.getExtendedId();
691    
692            // During a rewind, if the form is pre-rendered, the buffer will be null,
693            // so do the check based on the key, not a non-null value.
694    
695            if (!_prerenderMap.containsKey(key))
696                return false;
697    
698            String buffer = (String) _prerenderMap.get(key);
699    
700            writer.printRaw(buffer);
701    
702            _prerenderMap.remove(key);
703    
704            return true;
705        }
706    
707        public void addDeferredRunnable(Runnable runnable)
708        {
709            Defense.notNull(runnable, "runnable");
710    
711            _deferredRunnables.add(runnable);
712        }
713    
714        public void registerForFocus(IFormComponent field, int priority)
715        {
716            _delegate.registerForFocus(field, priority);
717        }
718    
719    }