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