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