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