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