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