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
390                            .size(), 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            
412            component.setClientId(result);
413            
414            return result;
415        }
416    
417        public String peekClientId(IFormComponent comp)
418        {
419            String id = comp.getSpecifiedId();
420            if (id == null)
421                return null;
422            
423            return _elementIdAllocator.peekNextId(id);
424        }
425        
426        public boolean isRewinding()
427        {
428            return _rewinding;
429        }
430    
431        private void preallocateReservedIds()
432        {
433            for (int i = 0; i < ServiceConstants.RESERVED_IDS.length; i++)
434                _elementIdAllocator.allocateId(ServiceConstants.RESERVED_IDS[i]);
435        }
436    
437        /**
438         * Invoked when rewinding a form to re-initialize the _allocatedIds and _elementIdAllocator.
439         * Converts a string passed as a parameter (and containing a comma separated list of ids) back
440         * into the allocateIds property. In addition, return the state of the ID allocater back to
441         * where it was at the start of the render.
442         * 
443         * @see #buildAllocatedIdList()
444         * @since 3.0
445         */
446    
447        private void reinitializeIdAllocatorForRewind()
448        {
449            String allocatedFormIds = _cycle.getParameter(FORM_IDS);
450            
451            String[] ids = TapestryUtils.split(allocatedFormIds);
452            
453            for (int i = 0; i < ids.length; i++)
454                _allocatedIds.add(ids[i]);
455    
456            // Now, reconstruct the initial state of the
457            // id allocator.
458    
459            preallocateReservedIds();
460    
461            String extraReservedIds = _cycle.getParameter(RESERVED_FORM_IDS);
462    
463            ids = TapestryUtils.split(extraReservedIds);
464    
465            for (int i = 0; i < ids.length; i++)
466                _elementIdAllocator.allocateId(ids[i]);
467        }
468        
469        int convertSeedToId(String input)
470        {
471            int index = input.lastIndexOf("_");
472            
473            if (index < 0)
474                throw new ApplicationRuntimeException("Unable to convert seedId of " + input + " to integer.");
475            
476            return Integer.parseInt(input.substring(index, input.length()));
477        }
478        
479        public void render(String method, IRender informalParametersRenderer, ILink link, String scheme, Integer port)
480        {
481            String formId = _form.getName();
482    
483            emitEventManagerInitialization(formId);
484    
485            // Convert the link's query parameters into a series of
486            // hidden field values (that will be rendered later).
487    
488            addHiddenFieldsForLinkParameters(link);
489    
490            // Create a hidden field to store the submission mode, in case
491            // client-side JavaScript forces an update.
492    
493            addHiddenValue(SUBMIT_MODE, null);
494            
495            // And another for the name of the component that
496            // triggered the submit.
497    
498            addHiddenValue(FormConstants.SUBMIT_NAME_PARAMETER, null);
499            
500            IMarkupWriter nested = _writer.getNestedWriter();
501    
502            _form.renderBody(nested, _cycle);
503    
504            runDeferredRunnables();
505            
506            int portI = (port == null) ? 0 : port.intValue();
507            
508            writeTag(_writer, method, link.getURL(scheme, null, portI, null, false));
509            
510            // For XHTML compatibility
511            _writer.attribute("id", formId);
512            
513            if (_encodingType != null)
514                _writer.attribute("enctype", _encodingType);
515    
516            // Write out event handlers collected during the rendering.
517    
518            emitEventHandlers(formId);
519    
520            informalParametersRenderer.render(_writer, _cycle);
521    
522            // Finish the <form> tag
523    
524            _writer.println();
525    
526            writeHiddenFields();
527            
528            // Close the nested writer, inserting its contents.
529            
530            nested.close();
531            
532            // Close the <form> tag.
533            
534            _writer.end();
535            
536            String fieldId = _delegate.getFocusField();
537            
538            if (_pageRenderSupport == null)
539                return;
540            
541            // If the form doesn't support focus, or the focus has already been set by a different form,
542            // then do nothing.
543            
544            if (!_cycle.isFocusDisabled() && fieldId != null && _form.getFocus() 
545                    && _cycle.getAttribute(FIELD_FOCUS_ATTRIBUTE) == null)
546            {
547                _pageRenderSupport.addInitializationScript(_form, "dojo.require(\"tapestry.form\");tapestry.form.focusField('" + fieldId + "');");
548                _cycle.setAttribute(FIELD_FOCUS_ATTRIBUTE, Boolean.TRUE);
549            }
550            
551            // register the validation profile with client side form manager
552            
553            if (_form.isClientValidationEnabled())
554            {
555                IPage page = _form.getPage();
556    
557                // only include dojo widget layer if it's not already been included
558    
559                if (!page.hasWidgets()) {
560                    IAsset clientScript = _form.getAsset("clientValidationScript");
561                    if (clientScript != null){
562    
563                        _pageRenderSupport.addExternalScript(_form, clientScript.getResourceLocation());
564                    }
565                }
566                
567                _pageRenderSupport.addInitializationScript(_form, "dojo.require(\"tapestry.form\");tapestry.form.clearProfiles('"
568                        + formId + "'); tapestry.form.registerProfile('" + formId + "'," 
569                        + _profile.toString() + ");");
570            }
571        }
572    
573        /**
574         * Pre-renders the form, setting up some client-side form support. Returns the name of the
575         * client-side form event manager variable.
576         *
577         * @param formId
578         *          The client id of the form.
579         */
580        protected void emitEventManagerInitialization(String formId)
581        {
582            if (_pageRenderSupport == null)
583                return;
584            
585            StringBuffer str = new StringBuffer("dojo.require(\"tapestry.form\");");
586            str.append("tapestry.form.registerForm(\"").append(formId).append("\"");
587            
588            if (_form.isAsync()) {
589                
590                str.append(", true");
591                
592                if (_form.isJson()) {
593                    str.append(", true");
594                }
595            }
596            
597            str.append(");");
598            
599            _pageRenderSupport.addInitializationScript(_form, str.toString());
600        }
601        
602        public String rewind()
603        {
604            _form.getDelegate().clear();
605    
606            String mode = _cycle.getParameter(SUBMIT_MODE);
607            
608            // On a cancel, don't bother rendering the body or anything else at all.
609            
610            if (FormConstants.SUBMIT_CANCEL.equals(mode))
611                return mode;
612            
613            reinitializeIdAllocatorForRewind();
614            
615            _form.renderBody(_writer, _cycle);
616            
617            // New, handles cases where an eventlistener
618            // causes a form submission.
619            
620            BrowserEvent event = new BrowserEvent(_cycle);
621            
622            _form.getEventInvoker().invokeFormListeners(this, _cycle, event);
623    
624            int expected = _allocatedIds.size();
625            
626            // The other case, _allocatedIdIndex > expected, is
627            // checked for inside getElementId(). Remember that
628            // _allocatedIdIndex is incremented after allocating.
629            
630            if (_allocatedIdIndex < expected)
631            {
632                String nextExpectedId = (String) _allocatedIds.get(_allocatedIdIndex);
633                
634                throw new StaleLinkException(FormMessages.formTooFewIds(_form, expected
635                        - _allocatedIdIndex, nextExpectedId), _form);
636            }
637            
638            runDeferredRunnables();
639    
640            if (_submitModes.contains(mode)) {
641    
642                // clear errors during refresh
643                
644                if (FormConstants.SUBMIT_REFRESH.equals(mode)) {
645    
646                    _form.getDelegate().clearErrors();
647                }
648    
649                return mode;
650            }
651            
652            // Either something wacky on the client side, or a client without
653            // javascript enabled.
654            
655            return FormConstants.SUBMIT_NORMAL;
656        }
657    
658        private void runDeferredRunnables()
659        {
660            Iterator i = _deferredRunnables.iterator();
661            while (i.hasNext())
662            {
663                Runnable r = (Runnable) i.next();
664                
665                r.run();
666            }
667        }
668    
669        public void setEncodingType(String encodingType)
670        {
671    
672            if (_encodingType != null && !_encodingType.equals(encodingType))
673                throw new ApplicationRuntimeException(FormMessages.encodingTypeContention(
674                        _form,
675                        _encodingType,
676                        encodingType), _form, null, null);
677    
678            _encodingType = encodingType;
679        }
680    
681        /**
682         * Overwridden by {@link org.apache.tapestry.wml.GoFormSupportImpl} (WML).
683         */
684        protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value)
685        {
686            writer.beginEmpty("input");
687            writer.attribute("type", "hidden");
688            writer.attribute("name", name);
689            
690            if (HiveMind.isNonBlank(id))
691                writer.attribute("id", id);
692            
693            writer.attribute("value", value == null ? "" : value);
694            writer.println();
695        }
696    
697        /**
698         * Writes out all hidden values previously added by
699         * {@link #addHiddenValue(String, String, String)}. Writes a &lt;div&gt; tag around
700         * {@link #writeHiddenFieldList(IMarkupWriter)}. Overriden by
701         * {@link org.apache.tapestry.wml.GoFormSupportImpl}.
702         */
703        
704        protected void writeHiddenFields()
705        {
706            IMarkupWriter writer = getHiddenFieldWriter();
707            
708            writer.begin("div");
709            writer.attribute("style", "display:none;");
710            writer.attribute("id", _form.getName() + "hidden");
711            
712            writeHiddenFieldList(writer);
713            
714            writer.end();
715        }
716        
717        /**
718         * Writes out all hidden values previously added by
719         * {@link #addHiddenValue(String, String, String)}, plus the allocated id list.
720         */
721        
722        protected void writeHiddenFieldList(IMarkupWriter writer)
723        {
724            writeHiddenField(writer, FORM_IDS, null, buildAllocatedIdList());
725            
726            Iterator i = _hiddenValues.iterator();
727            while (i.hasNext())
728            {
729                HiddenFieldData data = (HiddenFieldData) i.next();
730                
731                writeHiddenField(writer, data.getName(), data.getId(), data.getValue());
732            }
733        }
734        
735        /**
736         * Determines if a hidden field change has occurred, which would require
737         * that we write hidden form fields using the {@link ResponseBuilder} 
738         * writer.
739         * 
740         * @return The default {@link IMarkupWriter} if not doing a managed ajax/json
741         *          response, else whatever is returned from {@link ResponseBuilder}.
742         */
743        protected IMarkupWriter getHiddenFieldWriter()
744        {
745            if (_cycle.getResponseBuilder().contains(_form) 
746                    || (!_fieldUpdating || !_cycle.getResponseBuilder().isDynamic()) ) {
747                return _writer;
748            }
749            
750            return _cycle.getResponseBuilder().getWriter(_form.getName() + "hidden", 
751                    ResponseBuilder.ELEMENT_TYPE);
752        }
753        
754        private void addHiddenFieldsForLinkParameter(ILink link, String parameterName)
755        {
756            String[] values = link.getParameterValues(parameterName);
757    
758            // In some cases, there are no values, but a space is "reserved" for the provided name.
759    
760            if (values == null)
761                return;
762    
763            for (int i = 0; i < values.length; i++)
764            {
765                addHiddenValue(parameterName, values[i]);
766            }
767        }
768    
769        protected void writeTag(IMarkupWriter writer, String method, String url)
770        {
771            writer.begin("form");
772            writer.attribute("method", method);
773            writer.attribute("action", url);
774        }
775    
776        public void prerenderField(IMarkupWriter writer, IComponent field, Location location)
777        {
778            Defense.notNull(writer, "writer");
779            Defense.notNull(field, "field");
780    
781            String key = field.getExtendedId();
782    
783            if (_prerenderMap.containsKey(key))
784                throw new ApplicationRuntimeException(FormMessages.fieldAlreadyPrerendered(field), field, location, null);
785            
786            NestedMarkupWriter nested = writer.getNestedWriter();
787            
788            TapestryUtils.storePrerender(_cycle, field);
789            
790            _cycle.getResponseBuilder().render(nested, field, _cycle);
791            
792            TapestryUtils.removePrerender(_cycle);
793            
794            _prerenderMap.put(key, nested.getBuffer());
795        }
796        
797        public boolean wasPrerendered(IMarkupWriter writer, IComponent field)
798        {
799            String key = field.getExtendedId();
800    
801            // During a rewind, if the form is pre-rendered, the buffer will be null,
802            // so do the check based on the key, not a non-null value.
803    
804            if (!_prerenderMap.containsKey(key))
805                return false;
806    
807            String buffer = (String) _prerenderMap.get(key);
808    
809            writer.printRaw(buffer);
810    
811            _prerenderMap.remove(key);
812    
813            return true;
814        }
815        
816        public void addDeferredRunnable(Runnable runnable)
817        {
818            Defense.notNull(runnable, "runnable");
819    
820            _deferredRunnables.add(runnable);
821        }
822    
823        public void registerForFocus(IFormComponent field, int priority)
824        {
825            _delegate.registerForFocus(field, priority);
826        }
827    
828        /** 
829         * {@inheritDoc}
830         */
831        public JSONObject getProfile()
832        {
833            return _profile;
834        }
835    
836        /** 
837         * {@inheritDoc}
838         */
839        public boolean isFormFieldUpdating()
840        {
841            return _fieldUpdating;
842        }
843        
844        /** 
845         * {@inheritDoc}
846         */
847        public void setFormFieldUpdating(boolean value)
848        {
849            _fieldUpdating = value;
850        }
851    }