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.Resource;
031 import org.apache.hivemind.util.ClasspathResource;
032 import org.apache.hivemind.util.Defense;
033 import org.apache.tapestry.IComponent;
034 import org.apache.tapestry.IForm;
035 import org.apache.tapestry.IMarkupWriter;
036 import org.apache.tapestry.IRender;
037 import org.apache.tapestry.IRequestCycle;
038 import org.apache.tapestry.NestedMarkupWriter;
039 import org.apache.tapestry.PageRenderSupport;
040 import org.apache.tapestry.StaleLinkException;
041 import org.apache.tapestry.Tapestry;
042 import org.apache.tapestry.TapestryUtils;
043 import org.apache.tapestry.engine.ILink;
044 import org.apache.tapestry.services.ServiceConstants;
045 import org.apache.tapestry.util.IdAllocator;
046 import 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 */
054 public 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 <div> 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 }