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        /**
464         * @deprecated Please use second render method.
465         */
466        public void render(String method, IRender informalParametersRenderer, ILink link, String scheme)
467        {
468            render(method, informalParametersRenderer, link, scheme, null);
469        }
470        public void render(String method, IRender informalParametersRenderer, ILink link, 
471                    String scheme, Integer port)
472        {
473            String formId = _form.getName();
474    
475            emitEventManagerInitialization(formId);
476    
477            // Convert the link's query parameters into a series of
478            // hidden field values (that will be rendered later).
479    
480            addHiddenFieldsForLinkParameters(link);
481    
482            // Create a hidden field to store the submission mode, in case
483            // client-side JavaScript forces an update.
484    
485            addHiddenValue(SUBMIT_MODE, null);
486    
487            // And another for the name of the component that
488            // triggered the submit.
489    
490            addHiddenValue(FormConstants.SUBMIT_NAME_PARAMETER, null);
491    
492            IMarkupWriter nested = _writer.getNestedWriter();
493    
494            _form.renderBody(nested, _cycle);
495    
496            runDeferredRunnables();
497            
498            int portI = (port == null) ? 0 : port.intValue();
499            writeTag(_writer, method, link.getURL(scheme, null, portI, null, false));
500            
501            // For HTML compatibility
502            _writer.attribute("name", formId);
503    
504            // For XHTML compatibility
505            _writer.attribute("id", formId);
506    
507            if (_encodingType != null)
508                _writer.attribute("enctype", _encodingType);
509    
510            // Write out event handlers collected during the rendering.
511    
512            emitEventHandlers(formId);
513    
514            informalParametersRenderer.render(_writer, _cycle);
515    
516            // Finish the <form> tag
517    
518            _writer.println();
519    
520            writeHiddenFields();
521    
522            // Close the nested writer, inserting its contents.
523    
524            nested.close();
525    
526            // Close the <form> tag.
527    
528            _writer.end();
529    
530            String fieldId = _delegate.getFocusField();
531    
532            if (fieldId == null || _pageRenderSupport == null)
533                return;
534    
535            // If the form doesn't support focus, or the focus has already been set by a different form,
536            // then do nothing.
537    
538            if (!_form.getFocus() || _cycle.getAttribute(FIELD_FOCUS_ATTRIBUTE) != null)
539                return;
540    
541            _pageRenderSupport.addInitializationScript("Tapestry.set_focus('" + fieldId + "');");
542    
543            _cycle.setAttribute(FIELD_FOCUS_ATTRIBUTE, Boolean.TRUE);
544        }
545    
546        /**
547         * Pre-renders the form, setting up some client-side form support. Returns the name of the
548         * client-side form event manager variable.
549         */
550        protected void emitEventManagerInitialization(String formId)
551        {
552            if (_pageRenderSupport == null)
553                return;
554    
555            _pageRenderSupport.addExternalScript(_script);
556    
557            _pageRenderSupport.addInitializationScript("Tapestry.register_form('" + formId + "');");
558        }
559    
560        public String rewind()
561        {
562            _form.getDelegate().clear();
563    
564            String mode = _cycle.getParameter(SUBMIT_MODE);
565    
566            // On a cancel, don't bother rendering the body or anything else at all.
567    
568            if (FormConstants.SUBMIT_CANCEL.equals(mode))
569                return mode;
570    
571            reinitializeIdAllocatorForRewind();
572    
573            _form.renderBody(_writer, _cycle);
574    
575            int expected = _allocatedIds.size();
576    
577            // The other case, _allocatedIdIndex > expected, is
578            // checked for inside getElementId(). Remember that
579            // _allocatedIdIndex is incremented after allocating.
580    
581            if (_allocatedIdIndex < expected)
582            {
583                String nextExpectedId = (String) _allocatedIds.get(_allocatedIdIndex);
584    
585                throw new StaleLinkException(FormMessages.formTooFewIds(_form, expected
586                        - _allocatedIdIndex, nextExpectedId), _form);
587            }
588    
589            runDeferredRunnables();
590    
591            if (_submitModes.contains(mode))
592                return mode;
593    
594            // Either something wacky on the client side, or a client without
595            // javascript enabled.
596    
597            return FormConstants.SUBMIT_NORMAL;
598    
599        }
600    
601        private void runDeferredRunnables()
602        {
603            Iterator i = _deferredRunnables.iterator();
604            while (i.hasNext())
605            {
606                Runnable r = (Runnable) i.next();
607    
608                r.run();
609            }
610        }
611    
612        public void setEncodingType(String encodingType)
613        {
614    
615            if (_encodingType != null && !_encodingType.equals(encodingType))
616                throw new ApplicationRuntimeException(FormMessages.encodingTypeContention(
617                        _form,
618                        _encodingType,
619                        encodingType), _form, null, null);
620    
621            _encodingType = encodingType;
622        }
623    
624        /**
625         * Overwridden by {@link org.apache.tapestry.wml.GoFormSupportImpl} (WML).
626         */
627        protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value)
628        {
629            writer.beginEmpty("input");
630            writer.attribute("type", "hidden");
631            writer.attribute("name", name);
632    
633            if (HiveMind.isNonBlank(id))
634                writer.attribute("id", id);
635    
636            writer.attribute("value", value == null ? "" : value);
637            writer.println();
638        }
639    
640        private void writeHiddenField(String name, String id, String value)
641        {
642            writeHiddenField(_writer, name, id, value);
643        }
644    
645        /**
646         * Writes out all hidden values previously added by
647         * {@link #addHiddenValue(String, String, String)}. Writes a &lt;div&gt; tag around
648         * {@link #writeHiddenFieldList()}. Overriden by
649         * {@link org.apache.tapestry.wml.GoFormSupportImpl}.
650         */
651    
652        protected void writeHiddenFields()
653        {
654            _writer.begin("div");
655            _writer.attribute("style", "display:none;");
656    
657            writeHiddenFieldList();
658    
659            _writer.end();
660        }
661    
662        /**
663         * Writes out all hidden values previously added by
664         * {@link #addHiddenValue(String, String, String)}, plus the allocated id list.
665         */
666    
667        protected void writeHiddenFieldList()
668        {
669            writeHiddenField(FORM_IDS, null, buildAllocatedIdList());
670    
671            Iterator i = _hiddenValues.iterator();
672            while (i.hasNext())
673            {
674                HiddenFieldData data = (HiddenFieldData) i.next();
675    
676                writeHiddenField(data.getName(), data.getId(), data.getValue());
677            }
678        }
679    
680        private void addHiddenFieldsForLinkParameter(ILink link, String parameterName)
681        {
682            String[] values = link.getParameterValues(parameterName);
683    
684            // In some cases, there are no values, but a space is "reserved" for the provided name.
685    
686            if (values == null)
687                return;
688    
689            for (int i = 0; i < values.length; i++)
690            {
691                addHiddenValue(parameterName, values[i]);
692            }
693        }
694    
695        protected void writeTag(IMarkupWriter writer, String method, String url)
696        {
697            writer.begin("form");
698            writer.attribute("method", method);
699            writer.attribute("action", url);
700        }
701    
702        public void prerenderField(IMarkupWriter writer, IComponent field, Location location)
703        {
704            Defense.notNull(writer, "writer");
705            Defense.notNull(field, "field");
706    
707            String key = field.getExtendedId();
708    
709            if (_prerenderMap.containsKey(key))
710                throw new ApplicationRuntimeException(FormMessages.fieldAlreadyPrerendered(field),
711                        field, location, null);
712    
713            NestedMarkupWriter nested = writer.getNestedWriter();
714    
715            field.render(nested, _cycle);
716    
717            _prerenderMap.put(key, nested.getBuffer());
718        }
719    
720        public boolean wasPrerendered(IMarkupWriter writer, IComponent field)
721        {
722            String key = field.getExtendedId();
723    
724            // During a rewind, if the form is pre-rendered, the buffer will be null,
725            // so do the check based on the key, not a non-null value.
726    
727            if (!_prerenderMap.containsKey(key))
728                return false;
729    
730            String buffer = (String) _prerenderMap.get(key);
731    
732            writer.printRaw(buffer);
733    
734            _prerenderMap.remove(key);
735    
736            return true;
737        }
738    
739        public void addDeferredRunnable(Runnable runnable)
740        {
741            Defense.notNull(runnable, "runnable");
742    
743            _deferredRunnables.add(runnable);
744        }
745    
746        public void registerForFocus(IFormComponent field, int priority)
747        {
748            _delegate.registerForFocus(field, priority);
749        }
750    
751    }