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