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