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 }