001 // Copyright 2006, 2007, 2008, 2009, 2010, 2011 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.tapestry5.internal.structure;
016
017 import org.apache.tapestry5.*;
018 import org.apache.tapestry5.annotations.*;
019 import org.apache.tapestry5.dom.Element;
020 import org.apache.tapestry5.internal.AbstractEventContext;
021 import org.apache.tapestry5.internal.InternalComponentResources;
022 import org.apache.tapestry5.internal.InternalConstants;
023 import org.apache.tapestry5.internal.services.ComponentEventImpl;
024 import org.apache.tapestry5.internal.services.Instantiator;
025 import org.apache.tapestry5.internal.util.NamedSet;
026 import org.apache.tapestry5.internal.util.NotificationEventCallback;
027 import org.apache.tapestry5.ioc.BaseLocatable;
028 import org.apache.tapestry5.ioc.Invokable;
029 import org.apache.tapestry5.ioc.Location;
030 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
031 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
032 import org.apache.tapestry5.ioc.internal.util.Orderer;
033 import org.apache.tapestry5.ioc.internal.util.TapestryException;
034 import org.apache.tapestry5.ioc.services.PerThreadValue;
035 import org.apache.tapestry5.ioc.services.SymbolSource;
036 import org.apache.tapestry5.ioc.util.AvailableValues;
037 import org.apache.tapestry5.ioc.util.UnknownValueException;
038 import org.apache.tapestry5.model.ComponentModel;
039 import org.apache.tapestry5.model.ParameterModel;
040 import org.apache.tapestry5.runtime.Component;
041 import org.apache.tapestry5.runtime.*;
042 import org.apache.tapestry5.services.Request;
043 import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
044 import org.slf4j.Logger;
045
046 import java.util.*;
047
048 /**
049 * Implements {@link RenderCommand} and represents a component within an overall page. Much of a
050 * component page
051 * element's behavior is delegated to user code, via a {@link org.apache.tapestry5.runtime.Component} instance.
052 * <p/>
053 * Once instantiated, a ComponentPageElement should be registered as a
054 * {@linkplain org.apache.tapestry5.internal.structure.Page#addLifecycleListener(org.apache.tapestry5.runtime.PageLifecycleListener)
055 * lifecycle listener}. This could be done inside the constructors, but that tends to complicate unit tests, so its done
056 * by {@link org.apache.tapestry5.internal.services.PageElementFactoryImpl}. There's still a bit of refactoring in this
057 * class (and its many inner classes) that can improve overall efficiency.
058 * <p/>
059 * Modified for Tapestry 5.2 to adjust for the no-pooling approach (shared instances with externalized mutable state).
060 */
061 public class ComponentPageElementImpl extends BaseLocatable implements ComponentPageElement
062 {
063 /**
064 * Placeholder for the body used when the component has no real content.
065 */
066 private static class PlaceholderBlock implements Block, Renderable
067 {
068 public void render(MarkupWriter writer)
069 {
070 }
071
072 @Override
073 public String toString()
074 {
075 return "<PlaceholderBlock>";
076 }
077 }
078
079 private static final Block PLACEHOLDER_BLOCK = new PlaceholderBlock();
080
081 private static final ComponentCallback POST_RENDER_CLEANUP = new LifecycleNotificationComponentCallback()
082 {
083 public void run(Component component)
084 {
085 component.postRenderCleanup();
086 }
087 };
088
089 // For the moment, every component will have a template, even if it consists of
090 // just a page element to queue up a BeforeRenderBody phase.
091
092 private static void pushElements(RenderQueue queue, List<RenderCommand> list)
093 {
094 int count = size(list);
095 for (int i = count - 1; i >= 0; i--)
096 queue.push(list.get(i));
097 }
098
099 private static int size(List<?> list)
100 {
101 return list == null ? 0 : list.size();
102 }
103
104 private abstract class AbstractPhase implements RenderCommand
105 {
106 private final String name;
107
108 private final boolean reverse;
109
110 AbstractPhase(String name)
111 {
112 this(name, false);
113 }
114
115 AbstractPhase(String name, boolean reverse)
116 {
117 this.name = name;
118 this.reverse = reverse;
119 }
120
121 @Override
122 public String toString()
123 {
124 return phaseToString(name);
125 }
126
127 void invoke(MarkupWriter writer, Event event)
128 {
129 try
130 {
131 if (components == null)
132 {
133 invokeComponent(coreComponent, writer, event);
134 return;
135 }
136
137 // Multiple components (i.e., some mixins).
138
139 Iterator<Component> i = reverse ? InternalUtils.reverseIterator(components) : components.iterator();
140
141 while (i.hasNext())
142 {
143 invokeComponent(i.next(), writer, event);
144
145 if (event.isAborted())
146 break;
147 }
148 }
149 // This used to be RuntimeException, but with TAP5-1508 changes to RenderPhaseMethodWorker, we now
150 // let ordinary exceptions bubble up as well.
151 catch (Exception ex)
152 {
153 throw new TapestryException(ex.getMessage(), getLocation(), ex);
154 }
155
156 }
157
158 /**
159 * Each concrete class implements this method to branch to the corresponding method
160 * of {@link Component}.
161 */
162 protected abstract void invokeComponent(Component component, MarkupWriter writer, Event event);
163 }
164
165 private class SetupRenderPhase extends AbstractPhase
166 {
167 public SetupRenderPhase()
168 {
169 super("SetupRender");
170 }
171
172 protected void invokeComponent(Component component, MarkupWriter writer, Event event)
173 {
174 component.setupRender(writer, event);
175 }
176
177 public void render(MarkupWriter writer, RenderQueue queue)
178 {
179 RenderPhaseEvent event = createRenderEvent(queue);
180
181 invoke(writer, event);
182
183 push(queue, event.getResult(), beginRenderPhase, cleanupRenderPhase);
184
185 event.enqueueSavedRenderCommands();
186 }
187 }
188
189 private class BeginRenderPhase extends AbstractPhase
190 {
191 private BeginRenderPhase()
192 {
193 super("BeginRender");
194 }
195
196 protected void invokeComponent(Component component, MarkupWriter writer, Event event)
197 {
198 if (isRenderTracingEnabled())
199 writer.comment("BEGIN " + component.getComponentResources().getCompleteId() + " (" + getLocation()
200 + ")");
201
202 component.beginRender(writer, event);
203 }
204
205 public void render(final MarkupWriter writer, final RenderQueue queue)
206 {
207 RenderPhaseEvent event = createRenderEvent(queue);
208
209 invoke(writer, event);
210
211 push(queue, afterRenderPhase);
212 push(queue, event.getResult(), beforeRenderTemplatePhase, null);
213
214 event.enqueueSavedRenderCommands();
215 }
216 }
217
218 /**
219 * Replaces {@link org.apache.tapestry5.internal.structure.ComponentPageElementImpl.BeginRenderPhase} when there is
220 * a handler for AfterRender but not BeginRender.
221 */
222 private class OptimizedBeginRenderPhase implements RenderCommand
223 {
224 public void render(MarkupWriter writer, RenderQueue queue)
225 {
226 push(queue, afterRenderPhase);
227 push(queue, beforeRenderTemplatePhase);
228 }
229
230 @Override
231 public String toString()
232 {
233 return phaseToString("OptimizedBeginRenderPhase");
234 }
235 }
236
237 /**
238 * Reponsible for rendering the component's template. Even a component that doesn't have a
239 * template goes through
240 * this phase, as a synthetic template (used to trigger the rendering of the component's body)
241 * will be supplied.
242 */
243 private class BeforeRenderTemplatePhase extends AbstractPhase
244 {
245 private BeforeRenderTemplatePhase()
246 {
247 super("BeforeRenderTemplate");
248 }
249
250 protected void invokeComponent(Component component, MarkupWriter writer, Event event)
251 {
252 component.beforeRenderTemplate(writer, event);
253 }
254
255 public void render(final MarkupWriter writer, final RenderQueue queue)
256 {
257 RenderPhaseEvent event = createRenderEvent(queue);
258
259 invoke(writer, event);
260
261 push(queue, afterRenderTemplatePhase);
262
263 if (event.getResult())
264 pushElements(queue, template);
265
266 event.enqueueSavedRenderCommands();
267 }
268 }
269
270 /**
271 * Alternative version of BeforeRenderTemplatePhase used when the BeforeRenderTemplate render
272 * phase is not handled.
273 */
274 private class RenderTemplatePhase implements RenderCommand
275 {
276 public void render(MarkupWriter writer, RenderQueue queue)
277 {
278 push(queue, afterRenderTemplatePhase);
279
280 pushElements(queue, template);
281 }
282
283 @Override
284 public String toString()
285 {
286 return phaseToString("RenderTemplate");
287 }
288 }
289
290 private class BeforeRenderBodyPhase extends AbstractPhase
291 {
292 private BeforeRenderBodyPhase()
293 {
294 super("BeforeRenderBody");
295 }
296
297 protected void invokeComponent(Component component, MarkupWriter writer, Event event)
298 {
299 component.beforeRenderBody(writer, event);
300 }
301
302 public void render(final MarkupWriter writer, RenderQueue queue)
303 {
304 RenderPhaseEvent event = createRenderEvent(queue);
305
306 invoke(writer, event);
307
308 push(queue, afterRenderBodyPhase);
309
310 if (event.getResult() && bodyBlock != null)
311 queue.push(bodyBlock);
312
313 event.enqueueSavedRenderCommands();
314 }
315 }
316
317 private class AfterRenderBodyPhase extends AbstractPhase
318 {
319
320 private AfterRenderBodyPhase()
321 {
322 super("AfterRenderBody", true);
323 }
324
325 protected void invokeComponent(Component component, MarkupWriter writer, Event event)
326 {
327 component.afterRenderBody(writer, event);
328 }
329
330 public void render(final MarkupWriter writer, RenderQueue queue)
331 {
332 RenderPhaseEvent event = createRenderEvent(queue);
333
334 invoke(writer, event);
335
336 push(queue, event.getResult(), null, beforeRenderBodyPhase);
337
338 event.enqueueSavedRenderCommands();
339 }
340 }
341
342 private class AfterRenderTemplatePhase extends AbstractPhase
343 {
344 private AfterRenderTemplatePhase()
345 {
346 super("AfterRenderTemplate", true);
347 }
348
349 protected void invokeComponent(Component component, MarkupWriter writer, Event event)
350 {
351 component.afterRenderTemplate(writer, event);
352 }
353
354 public void render(final MarkupWriter writer, final RenderQueue queue)
355 {
356 RenderPhaseEvent event = createRenderEvent(queue);
357
358 invoke(writer, event);
359
360 push(queue, event.getResult(), null, beforeRenderTemplatePhase);
361
362 event.enqueueSavedRenderCommands();
363 }
364 }
365
366 private class AfterRenderPhase extends AbstractPhase
367 {
368 private AfterRenderPhase()
369 {
370 super("AfterRender", true);
371 }
372
373 protected void invokeComponent(Component component, MarkupWriter writer, Event event)
374 {
375 component.afterRender(writer, event);
376
377 if (isRenderTracingEnabled())
378 writer.comment("END " + component.getComponentResources().getCompleteId());
379 }
380
381 public void render(final MarkupWriter writer, RenderQueue queue)
382 {
383 RenderPhaseEvent event = createRenderEvent(queue);
384
385 invoke(writer, event);
386
387 push(queue, event.getResult(), cleanupRenderPhase, beginRenderPhase);
388
389 event.enqueueSavedRenderCommands();
390 }
391 }
392
393 private class CleanupRenderPhase extends AbstractPhase
394 {
395 private CleanupRenderPhase()
396 {
397 super("CleanupRender", true);
398 }
399
400 protected void invokeComponent(Component component, MarkupWriter writer, Event event)
401 {
402 component.cleanupRender(writer, event);
403 }
404
405 public void render(final MarkupWriter writer, RenderQueue queue)
406 {
407 RenderPhaseEvent event = createRenderEvent(queue);
408
409 invoke(writer, event);
410
411 push(queue, event.getResult(), null, setupRenderPhase);
412
413 event.enqueueSavedRenderCommands();
414 }
415 }
416
417 private class PostRenderCleanupPhase implements RenderCommand
418 {
419 /**
420 * Used to detect mismatches calls to {@link MarkupWriter#element(String, Object[])} and
421 * {@link org.apache.tapestry5.MarkupWriter#end()}. The expectation is that any element(s)
422 * begun by this component
423 * during rendering will be balanced by end() calls, resulting in the current element
424 * reverting to its initial
425 * value.
426 */
427 private final Element expectedElementAtCompletion;
428
429 PostRenderCleanupPhase(Element expectedElementAtCompletion)
430 {
431 this.expectedElementAtCompletion = expectedElementAtCompletion;
432 }
433
434 public void render(MarkupWriter writer, RenderQueue queue)
435 {
436 renderingValue.set(false);
437
438 Element current = writer.getElement();
439
440 if (current != expectedElementAtCompletion)
441 throw new TapestryException(StructureMessages.unbalancedElements(completeId), getLocation(), null);
442
443 invoke(false, POST_RENDER_CLEANUP);
444
445 queue.endComponent();
446 }
447
448 @Override
449 public String toString()
450 {
451 return phaseToString("PostRenderCleanup");
452 }
453 }
454
455 private NamedSet<Block> blocks;
456
457 private BlockImpl bodyBlock;
458
459 private List<ComponentPageElement> children;
460
461 private final String elementName;
462
463 private final Logger eventLogger;
464
465 private final String completeId;
466
467 // The user-provided class, with runtime code enhancements. In a component with mixins, this
468 // is the component to which the mixins are attached.
469 private final Component coreComponent;
470
471 /**
472 * Component lifecycle instances for all mixins; the core component is added to this list during
473 * page load. This is only used in the case that a component has mixins (in which case, the core component is
474 * listed last).
475 */
476 private List<Component> components = null;
477
478 private final ComponentPageElementResources elementResources;
479
480 private final ComponentPageElement container;
481
482 private final InternalComponentResources coreResources;
483
484 private final String id;
485
486 private Orderer<Component> mixinBeforeOrderer;
487
488 private Orderer<Component> mixinAfterOrderer;
489
490 private boolean loaded;
491
492 /**
493 * Map from mixin id (the simple name of the mixin class) to resources for the mixin. Created
494 * when first mixin is added.
495 */
496 private NamedSet<InternalComponentResources> mixinIdToComponentResources;
497
498 private final String nestedId;
499
500 private final Page page;
501
502 private final PerThreadValue<Boolean> renderingValue;
503
504 // should be okay since it's a shadow service object
505 private final Request request;
506 private final SymbolSource symbolSource;
507 private final boolean productionMode;
508 private final boolean componentTracingEnabled;
509
510 // We know that, at the very least, there will be an element to force the component to render
511 // its body, so there's no reason to wait to initialize the list.
512
513 private final List<RenderCommand> template = CollectionFactory.newList();
514
515 private RenderCommand setupRenderPhase, beginRenderPhase, beforeRenderTemplatePhase, beforeRenderBodyPhase,
516 afterRenderBodyPhase, afterRenderTemplatePhase, afterRenderPhase, cleanupRenderPhase;
517
518 /**
519 * Constructor for other components embedded within the root component or at deeper levels of
520 * the hierarchy.
521 *
522 * @param page ultimately containing this component
523 * @param container component immediately containing this component (may be null for a root component)
524 * @param id unique (within the container) id for this component (may be null for a root
525 * component)
526 * @param elementName the name of the element which represents this component in the template, or null
527 * for
528 * <comp> element or a page component
529 * @param instantiator used to create the new component instance and access the component's model
530 * @param location location of the element (within a template), used as part of exception reporting
531 * @param elementResources Provides access to common methods of various services
532 */
533 ComponentPageElementImpl(Page page, ComponentPageElement container, String id, String nestedId, String completeId,
534 String elementName, Instantiator instantiator, Location location,
535 ComponentPageElementResources elementResources, Request request, SymbolSource symbolSource)
536 {
537 super(location);
538
539 this.page = page;
540 this.container = container;
541 this.id = id;
542 this.nestedId = nestedId;
543 this.completeId = completeId;
544 this.elementName = elementName;
545 this.elementResources = elementResources;
546 this.request = request;
547 this.symbolSource = symbolSource;
548
549 // evaluate this once because it gets referenced a lot during rendering
550 this.productionMode = "true".equals(symbolSource.valueForSymbol(SymbolConstants.PRODUCTION_MODE));
551 this.componentTracingEnabled = "true".equals(symbolSource
552 .valueForSymbol(SymbolConstants.COMPONENT_RENDER_TRACING_ENABLED));
553
554 ComponentResources containerResources = container == null ? null : container.getComponentResources();
555
556 coreResources = new InternalComponentResourcesImpl(this.page, this, containerResources, this.elementResources,
557 completeId, nestedId, instantiator, false);
558
559 coreComponent = coreResources.getComponent();
560
561 eventLogger = elementResources.getEventLogger(coreResources.getLogger());
562
563 renderingValue = elementResources.createPerThreadValue();
564
565 page.addLifecycleListener(new PageLifecycleAdapter()
566 {
567 @Override
568 public void containingPageDidLoad()
569 {
570 pageLoaded();
571
572 ComponentPageElementImpl.this.page.removeLifecycleListener(this);
573 }
574 });
575 }
576
577 /**
578 * Constructor for the root component of a page.
579 */
580 public ComponentPageElementImpl(Page page, Instantiator instantiator,
581 ComponentPageElementResources elementResources, Request request, SymbolSource symbolSource)
582 {
583 this(page, null, null, null, page.getName(), null, instantiator, null, elementResources, request, symbolSource);
584 }
585
586 private void initializeRenderPhases()
587 {
588 setupRenderPhase = new SetupRenderPhase();
589 beginRenderPhase = new BeginRenderPhase();
590 beforeRenderTemplatePhase = new BeforeRenderTemplatePhase();
591 beforeRenderBodyPhase = new BeforeRenderBodyPhase();
592 afterRenderBodyPhase = new AfterRenderBodyPhase();
593 afterRenderTemplatePhase = new AfterRenderTemplatePhase();
594 afterRenderPhase = new AfterRenderPhase();
595 cleanupRenderPhase = new CleanupRenderPhase();
596
597 // Now the optimization, where we remove, replace and collapse unused phases. We use
598 // the component models to determine which phases have handler methods for the
599 // render phases.
600
601 Set<Class> handled = coreResources.getComponentModel().getHandledRenderPhases();
602
603 for (ComponentResources r : NamedSet.getValues(mixinIdToComponentResources))
604 {
605 handled.addAll(r.getComponentModel().getHandledRenderPhases());
606 }
607
608 if (!handled.contains(CleanupRender.class))
609 cleanupRenderPhase = null;
610
611 // Now, work back to front.
612
613 if (!handled.contains(AfterRender.class))
614 afterRenderPhase = cleanupRenderPhase;
615
616 if (!handled.contains(AfterRenderTemplate.class))
617 afterRenderTemplatePhase = null;
618
619 if (!handled.contains(AfterRenderBody.class))
620 afterRenderBodyPhase = null;
621
622 if (!handled.contains(BeforeRenderTemplate.class))
623 beforeRenderTemplatePhase = new RenderTemplatePhase();
624
625 if (!handled.contains(BeginRender.class))
626 {
627 RenderCommand replacement = afterRenderPhase != null ? new OptimizedBeginRenderPhase()
628 : beforeRenderTemplatePhase;
629
630 beginRenderPhase = replacement;
631 }
632
633 if (!handled.contains(SetupRender.class))
634 setupRenderPhase = beginRenderPhase;
635 }
636
637 public ComponentPageElement newChild(String id, String nestedId, String completeId, String elementName,
638 Instantiator instantiator, Location location)
639 {
640 ComponentPageElementImpl child = new ComponentPageElementImpl(page, this, id, nestedId, completeId,
641 elementName, instantiator, location, elementResources, request, symbolSource);
642
643 addEmbeddedElement(child);
644
645 return child;
646 }
647
648 void push(RenderQueue queue, boolean forward, RenderCommand forwardPhase, RenderCommand backwardPhase)
649 {
650 push(queue, forward ? forwardPhase : backwardPhase);
651 }
652
653 void push(RenderQueue queue, RenderCommand nextPhase)
654 {
655 if (nextPhase != null)
656 queue.push(nextPhase);
657 }
658
659 void addEmbeddedElement(ComponentPageElement child)
660 {
661 if (children == null)
662 children = CollectionFactory.newList();
663
664 String childId = child.getId();
665
666 for (ComponentPageElement existing : children)
667 {
668 if (existing.getId().equalsIgnoreCase(childId))
669 throw new TapestryException(StructureMessages.duplicateChildComponent(this, childId), child,
670 new TapestryException(StructureMessages.originalChildComponent(this, childId,
671 existing.getLocation()), existing, null));
672 }
673
674 children.add(child);
675 }
676
677 public void addMixin(String mixinId, Instantiator instantiator, String... order)
678 {
679 if (mixinIdToComponentResources == null)
680 {
681 mixinIdToComponentResources = NamedSet.create();
682 components = CollectionFactory.newList();
683 }
684
685 String mixinExtension = "$" + mixinId.toLowerCase();
686
687 InternalComponentResourcesImpl resources = new InternalComponentResourcesImpl(page, this, coreResources,
688 elementResources, completeId + mixinExtension, nestedId + mixinExtension, instantiator, true);
689
690 mixinIdToComponentResources.put(mixinId, resources);
691 // note that since we're using explicit ordering now,
692 // we don't add anything to components until we page load; instead, we add
693 // to the orderers.
694 if (order == null)
695 order = InternalConstants.EMPTY_STRING_ARRAY;
696
697 if (resources.getComponentModel().isMixinAfter())
698 {
699 if (mixinAfterOrderer == null)
700 mixinAfterOrderer = new Orderer<Component>(getLogger());
701 mixinAfterOrderer.add(mixinId, resources.getComponent(), order);
702 } else
703 {
704 if (mixinBeforeOrderer == null)
705 mixinBeforeOrderer = new Orderer<Component>(getLogger());
706 mixinBeforeOrderer.add(mixinId, resources.getComponent(), order);
707 }
708 }
709
710 public void bindMixinParameter(String mixinId, String parameterName, Binding binding)
711 {
712 InternalComponentResources mixinResources = NamedSet.get(mixinIdToComponentResources, mixinId);
713
714 mixinResources.bindParameter(parameterName, binding);
715 }
716
717 public Binding getBinding(String parameterName)
718 {
719 return coreResources.getBinding(parameterName);
720 }
721
722 public void bindParameter(String parameterName, Binding binding)
723 {
724 coreResources.bindParameter(parameterName, binding);
725 }
726
727 public void addToBody(RenderCommand element)
728 {
729 if (bodyBlock == null)
730 bodyBlock = new BlockImpl(getLocation(), "Body of " + getCompleteId());
731
732 bodyBlock.addToBody(element);
733 }
734
735 public void addToTemplate(RenderCommand element)
736 {
737 template.add(element);
738 }
739
740 private void addUnboundParameterNames(String prefix, List<String> unbound, InternalComponentResources resource)
741 {
742 ComponentModel model = resource.getComponentModel();
743
744 for (String name : model.getParameterNames())
745 {
746 if (resource.isBound(name))
747 continue;
748
749 ParameterModel parameterModel = model.getParameterModel(name);
750
751 if (parameterModel.isRequired())
752 {
753 String fullName = prefix == null ? name : prefix + "." + name;
754
755 unbound.add(fullName);
756 }
757 }
758 }
759
760 private void pageLoaded()
761 {
762 // If this component has mixins, order them according to:
763 // mixins.
764
765 if (components != null)
766 {
767 List<Component> ordered = CollectionFactory.newList();
768
769 if (mixinBeforeOrderer != null)
770 ordered.addAll(mixinBeforeOrderer.getOrdered());
771
772 ordered.add(coreComponent);
773
774 // Add the remaining, late executing mixins
775 if (mixinAfterOrderer != null)
776 ordered.addAll(mixinAfterOrderer.getOrdered());
777
778 components = ordered;
779 // no need to keep the orderers around.
780 mixinBeforeOrderer = null;
781 mixinAfterOrderer = null;
782 }
783
784 initializeRenderPhases();
785
786 page.addVerifyListener(new Runnable()
787 {
788 public void run()
789 {
790 // For some parameters, bindings (from defaults) are provided inside the callback method, so
791 // that is invoked first, before we check for unbound parameters.
792
793 verifyRequiredParametersAreBound();
794 }
795 });
796
797
798 loaded = true;
799 }
800
801 public void enqueueBeforeRenderBody(RenderQueue queue)
802 {
803 if (bodyBlock != null)
804 push(queue, beforeRenderBodyPhase);
805 }
806
807 public String getCompleteId()
808 {
809 return completeId;
810 }
811
812 public Component getComponent()
813 {
814 return coreComponent;
815 }
816
817 public InternalComponentResources getComponentResources()
818 {
819 return coreResources;
820 }
821
822 public ComponentPageElement getContainerElement()
823 {
824 return container;
825 }
826
827 public Page getContainingPage()
828 {
829 return page;
830 }
831
832 public ComponentPageElement getEmbeddedElement(String embeddedId)
833 {
834 ComponentPageElement embeddedElement = null;
835
836 if (children != null)
837 {
838 for (ComponentPageElement child : children)
839 {
840 if (child.getId().equalsIgnoreCase(embeddedId))
841 {
842 embeddedElement = child;
843 break;
844 }
845 }
846 }
847
848 if (embeddedElement == null)
849 {
850 Set<String> ids = CollectionFactory.newSet();
851
852 if (children != null)
853 {
854 for (ComponentPageElement child : children)
855 {
856 ids.add(child.getId());
857 }
858 }
859
860 throw new UnknownValueException(String.format("Component %s does not contain embedded component '%s'.",
861 getCompleteId(), embeddedId), new AvailableValues("Embedded components", ids));
862 }
863
864 return embeddedElement;
865 }
866
867 public String getId()
868 {
869 return id;
870 }
871
872 public Logger getLogger()
873 {
874 return coreResources.getLogger();
875 }
876
877 public Component getMixinByClassName(String mixinClassName)
878 {
879 Component result = mixinForClassName(mixinClassName);
880
881 if (result == null)
882 throw new TapestryException(StructureMessages.unknownMixin(completeId, mixinClassName), getLocation(), null);
883
884 return result;
885 }
886
887 private Component mixinForClassName(String mixinClassName)
888 {
889 if (mixinIdToComponentResources == null)
890 return null;
891
892 for (InternalComponentResources resources : NamedSet.getValues(mixinIdToComponentResources))
893 {
894 if (resources.getComponentModel().getComponentClassName().equals(mixinClassName))
895 {
896 return resources
897 .getComponent();
898 }
899 }
900
901 return null;
902 }
903
904 public ComponentResources getMixinResources(String mixinId)
905 {
906 ComponentResources result = NamedSet.get(mixinIdToComponentResources, mixinId);
907
908 if (result == null)
909 throw new IllegalArgumentException(String.format("Unable to locate mixin '%s' for component '%s'.",
910 mixinId, completeId));
911
912 return result;
913 }
914
915 public String getNestedId()
916 {
917 return nestedId;
918 }
919
920 public boolean dispatchEvent(ComponentEvent event)
921 {
922 if (components == null)
923 return coreComponent.dispatchComponentEvent(event);
924
925 // Otherwise, iterate over mixins + core component
926
927 boolean result = false;
928
929 for (Component component : components)
930 {
931 result |= component.dispatchComponentEvent(event);
932
933 if (event.isAborted())
934 break;
935 }
936
937 return result;
938 }
939
940 /**
941 * Invokes a callback on the component instances (the core component plus any mixins).
942 *
943 * @param reverse if true, the callbacks are in the reverse of the normal order (this is associated
944 * with AfterXXX
945 * phases)
946 * @param callback the object to receive each component instance
947 */
948 private void invoke(boolean reverse, ComponentCallback callback)
949 {
950 try
951 { // Optimization: In the most general case (just the one component, no mixins)
952 // invoke the callback on the component and be done ... no iterators, no nothing.
953
954 if (components == null)
955 {
956 callback.run(coreComponent);
957 return;
958 }
959
960 Iterator<Component> i = reverse ? InternalUtils.reverseIterator(components) : components.iterator();
961
962 while (i.hasNext())
963 {
964 callback.run(i.next());
965
966 if (callback.isEventAborted())
967 return;
968 }
969 } catch (RuntimeException ex)
970 {
971 throw new TapestryException(ex.getMessage(), getLocation(), ex);
972 }
973 }
974
975 public boolean isLoaded()
976 {
977 return loaded;
978 }
979
980 public boolean isRendering()
981 {
982 return renderingValue.get(false);
983 }
984
985 /**
986 * Generate a toString() for the inner classes that represent render phases.
987 */
988 private String phaseToString(String phaseName)
989 {
990 return String.format("%s[%s]", phaseName, completeId);
991 }
992
993 /**
994 * Pushes the SetupRender phase state onto the queue.
995 */
996 public final void render(MarkupWriter writer, RenderQueue queue)
997 {
998 // TODO: An error if the render flag is already set (recursive rendering not
999 // allowed or advisable).
1000
1001 // TODO: Check for recursive rendering.
1002
1003 renderingValue.set(true);
1004
1005 queue.startComponent(coreResources);
1006
1007 queue.push(new PostRenderCleanupPhase(writer.getElement()));
1008
1009 push(queue, setupRenderPhase);
1010 }
1011
1012 @Override
1013 public String toString()
1014 {
1015 return String.format("ComponentPageElement[%s]", completeId);
1016 }
1017
1018 public boolean triggerEvent(String eventType, Object[] contextValues, ComponentEventCallback callback)
1019 {
1020 return triggerContextEvent(eventType, createParameterContext(contextValues == null ? new Object[0]
1021 : contextValues), callback);
1022 }
1023
1024 private EventContext createParameterContext(final Object... values)
1025 {
1026 return new AbstractEventContext()
1027 {
1028 public int getCount()
1029 {
1030 return values.length;
1031 }
1032
1033 public <T> T get(Class<T> desiredType, int index)
1034 {
1035 return elementResources.coerce(values[index], desiredType);
1036 }
1037 };
1038 }
1039
1040 public boolean triggerContextEvent(final String eventType, final EventContext context,
1041 final ComponentEventCallback callback)
1042 {
1043 assert InternalUtils.isNonBlank(eventType);
1044 assert context != null;
1045 String description = String.format("Triggering event '%s' on %s", eventType, completeId);
1046
1047 return elementResources.invoke(description, new Invokable<Boolean>()
1048 {
1049 public Boolean invoke()
1050 {
1051 return processEventTriggering(eventType, context, callback);
1052 }
1053 });
1054 }
1055
1056 @SuppressWarnings("all")
1057 private boolean processEventTriggering(String eventType, EventContext context, ComponentEventCallback callback)
1058 {
1059 boolean result = false;
1060
1061 ComponentPageElement component = this;
1062 String componentId = "";
1063
1064 // Provide a default handler for when the provided handler is null.
1065 final ComponentEventCallback providedHandler = callback == null ? new NotificationEventCallback(eventType,
1066 completeId) : callback;
1067
1068 ComponentEventCallback wrapped = new ComponentEventCallback()
1069 {
1070 public boolean handleResult(Object result)
1071 {
1072 // Boolean value is not passed to the handler; it will be true (abort event)
1073 // or false (continue looking for event handlers).
1074
1075 if (result instanceof Boolean)
1076 return (Boolean) result;
1077
1078 return providedHandler.handleResult(result);
1079 }
1080 };
1081
1082 RuntimeException rootException = null;
1083
1084 // Because I don't like to reassign parameters.
1085
1086 String currentEventType = eventType;
1087 EventContext currentContext = context;
1088
1089 // Track the location of the original component for the event, even as we work our way up
1090 // the hierarchy. This may not be ideal if we trigger an "exception" event ... or maybe
1091 // it's right (it's the location of the originally thrown exception).
1092
1093 Location location = component.getComponentResources().getLocation();
1094
1095 while (component != null)
1096 {
1097 try
1098 {
1099 Logger logger = component.getEventLogger();
1100
1101 ComponentEvent event = new ComponentEventImpl(currentEventType, componentId, currentContext, wrapped,
1102 elementResources, logger);
1103
1104 logger.debug(TapestryMarkers.EVENT_DISPATCH, "Dispatch event: {}", event);
1105
1106 result |= component.dispatchEvent(event);
1107
1108 if (event.isAborted())
1109 return result;
1110 }
1111
1112 // As with render phase methods, dispatchEvent() can now simply throw arbitrary exceptions
1113 // (the distinction between RuntimeException and checked Exception is entirely in the compiler,
1114 // not the JVM).
1115 catch (Exception ex)
1116 {
1117 // An exception in an event handler method
1118 // while we're trying to handle a previous exception!
1119
1120 if (rootException != null)
1121 throw rootException;
1122
1123 // We know component is not null and therefore has a component resources that
1124 // should have a location.
1125
1126 // Wrap it up to help ensure that a location is available to the event handler
1127 // method or,
1128 // more likely, to the exception report page.
1129
1130 rootException = new ComponentEventException(ex.getMessage(), eventType, context, location, ex);
1131
1132 // Switch over to triggering an "exception" event, starting in the component that
1133 // threw the exception.
1134
1135 currentEventType = "exception";
1136 currentContext = createParameterContext(rootException);
1137
1138 continue;
1139 }
1140
1141 // On each bubble up, make the event appear to come from the previous component
1142 // in which the event was triggered.
1143
1144 componentId = component.getId();
1145
1146 component = component.getContainerElement();
1147 }
1148
1149 // If there was a handler for the exception event, it is required to return a non-null (and
1150 // non-boolean) value
1151 // to tell Tapestry what to do. Since that didn't happen, we have no choice but to rethrow
1152 // the (wrapped)
1153 // exception.
1154
1155 if (rootException != null)
1156 throw rootException;
1157
1158 return result;
1159 }
1160
1161 private void verifyRequiredParametersAreBound()
1162 {
1163 List<String> unbound = CollectionFactory.newList();
1164
1165 addUnboundParameterNames(null, unbound, coreResources);
1166
1167 List<String> sortedNames = CollectionFactory.newList(NamedSet.getNames(mixinIdToComponentResources));
1168
1169 Collections.sort(sortedNames);
1170
1171 for (String name : sortedNames)
1172 {
1173 addUnboundParameterNames(name, unbound, mixinIdToComponentResources.get(name));
1174 }
1175
1176 if (!unbound.isEmpty())
1177 {
1178 throw new TapestryException(StructureMessages.missingParameters(unbound, this), this, null);
1179 }
1180 }
1181
1182 public Locale getLocale()
1183 {
1184 return page.getSelector().locale;
1185 }
1186
1187 public String getElementName(String defaultElementName)
1188 {
1189 return elementName != null ? elementName : defaultElementName;
1190 }
1191
1192 public Block getBlock(String id)
1193 {
1194 Block result = findBlock(id);
1195
1196 if (result == null)
1197 throw new BlockNotFoundException(StructureMessages.blockNotFound(completeId, id), getLocation());
1198
1199 return result;
1200 }
1201
1202 public Block findBlock(String id)
1203 {
1204 assert InternalUtils.isNonBlank(id);
1205
1206 return NamedSet.get(blocks, id);
1207 }
1208
1209 public void addBlock(String blockId, Block block)
1210 {
1211 if (blocks == null)
1212 blocks = NamedSet.create();
1213
1214 if (!blocks.putIfNew(blockId, block))
1215 throw new TapestryException(StructureMessages.duplicateBlock(this, blockId), block, null);
1216 }
1217
1218 public String getPageName()
1219 {
1220 return page.getName();
1221 }
1222
1223 public boolean hasBody()
1224 {
1225 return bodyBlock != null;
1226 }
1227
1228 public Block getBody()
1229 {
1230 return bodyBlock == null ? PLACEHOLDER_BLOCK : bodyBlock;
1231 }
1232
1233 public Map<String, Binding> getInformalParameterBindings()
1234 {
1235 return coreResources.getInformalParameterBindings();
1236 }
1237
1238 public Logger getEventLogger()
1239 {
1240 return eventLogger;
1241 }
1242
1243 public Link createEventLink(String eventType, Object... context)
1244 {
1245 return elementResources.createComponentEventLink(coreResources, eventType, false, context);
1246 }
1247
1248 public Link createActionLink(String eventType, boolean forForm, Object... context)
1249 {
1250 return elementResources.createComponentEventLink(coreResources, eventType, forForm, context);
1251 }
1252
1253 public Link createFormEventLink(String eventType, Object... context)
1254 {
1255 return elementResources.createComponentEventLink(coreResources, eventType, true, context);
1256 }
1257
1258 public Link createPageLink(String pageName, boolean override, Object... context)
1259 {
1260 return elementResources.createPageRenderLink(pageName, override, context);
1261 }
1262
1263 public Link createPageLink(Class pageClass, boolean override, Object... context)
1264 {
1265 return elementResources.createPageRenderLink(pageClass, override, context);
1266 }
1267
1268 protected RenderPhaseEvent createRenderEvent(RenderQueue queue)
1269 {
1270 return new RenderPhaseEvent(new RenderPhaseEventHandler(queue), eventLogger, elementResources);
1271 }
1272
1273 boolean isRenderTracingEnabled()
1274 {
1275 return !productionMode && (componentTracingEnabled || "true".equals(request.getParameter("t:component-trace")));
1276 }
1277
1278 public ComponentResourceSelector getResourceSelector()
1279 {
1280 return page.getSelector();
1281 }
1282 }