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 }