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