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