001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.internal.structure; 014 015import org.apache.tapestry5.*; 016import org.apache.tapestry5.annotations.*; 017import org.apache.tapestry5.dom.Element; 018import org.apache.tapestry5.internal.AbstractEventContext; 019import org.apache.tapestry5.internal.InternalComponentResources; 020import org.apache.tapestry5.internal.InternalConstants; 021import org.apache.tapestry5.internal.services.ComponentEventImpl; 022import org.apache.tapestry5.internal.services.Instantiator; 023import org.apache.tapestry5.internal.util.NamedSet; 024import org.apache.tapestry5.internal.util.NotificationEventCallback; 025import org.apache.tapestry5.ioc.BaseLocatable; 026import org.apache.tapestry5.ioc.Invokable; 027import org.apache.tapestry5.ioc.Location; 028import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 029import org.apache.tapestry5.ioc.internal.util.InternalUtils; 030import org.apache.tapestry5.ioc.internal.util.Orderer; 031import org.apache.tapestry5.ioc.internal.util.TapestryException; 032import org.apache.tapestry5.ioc.services.PerThreadValue; 033import org.apache.tapestry5.ioc.util.AvailableValues; 034import org.apache.tapestry5.ioc.util.UnknownValueException; 035import org.apache.tapestry5.model.ComponentModel; 036import org.apache.tapestry5.model.ParameterModel; 037import org.apache.tapestry5.runtime.Component; 038import org.apache.tapestry5.runtime.*; 039import org.apache.tapestry5.services.pageload.ComponentResourceSelector; 040import org.slf4j.Logger; 041 042import java.util.*; 043 044/** 045 * Implements {@link RenderCommand} and represents a component within an overall page. Much of a 046 * component page 047 * element's behavior is delegated to user code, via a {@link org.apache.tapestry5.runtime.Component} instance. 048 * 049 * Once instantiated, a ComponentPageElement should be registered as a 050 * {@linkplain org.apache.tapestry5.internal.structure.Page#addLifecycleListener(org.apache.tapestry5.runtime.PageLifecycleListener) 051 * lifecycle listener}. This could be done inside the constructors, but that tends to complicate unit tests, so its done 052 * by {@link org.apache.tapestry5.internal.services.PageElementFactoryImpl}. There's still a bit of refactoring in this 053 * class (and its many inner classes) that can improve overall efficiency. 054 * 055 * Modified for Tapestry 5.2 to adjust for the no-pooling approach (shared instances with externalized mutable state). 056 */ 057public class ComponentPageElementImpl extends BaseLocatable implements ComponentPageElement 058{ 059 /** 060 * Placeholder for the body used when the component has no real content. 061 */ 062 private static class PlaceholderBlock implements Block, Renderable, RenderCommand 063 { 064 public void render(MarkupWriter writer) 065 { 066 } 067 068 public void render(MarkupWriter writer, RenderQueue queue) 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 private final boolean exactParameterCountMatch; 505 506 // We know that, at the very least, there will be an element to force the component to render 507 // its body, so there's no reason to wait to initialize the list. 508 509 private final List<RenderCommand> template = CollectionFactory.newList(); 510 511 private RenderCommand setupRenderPhase, beginRenderPhase, beforeRenderTemplatePhase, beforeRenderBodyPhase, 512 afterRenderBodyPhase, afterRenderTemplatePhase, afterRenderPhase, cleanupRenderPhase; 513 514 /** 515 * Constructor for other components embedded within the root component or at deeper levels of 516 * the hierarchy. 517 * 518 * @param page 519 * ultimately containing this component 520 * @param container 521 * component immediately containing this component (may be null for a root component) 522 * @param id 523 * unique (within the container) id for this component (may be null for a root 524 * component) 525 * @param elementName 526 * 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 530 * used to create the new component instance and access the component's model 531 * @param location 532 * location of the element (within a template), used as part of exception reporting 533 * @param elementResources 534 */ 535 ComponentPageElementImpl(Page page, ComponentPageElement container, String id, String nestedId, String completeId, 536 String elementName, Instantiator instantiator, Location location, 537 ComponentPageElementResources elementResources) 538 { 539 super(location); 540 541 this.page = page; 542 this.container = container; 543 this.id = id; 544 this.nestedId = nestedId; 545 this.completeId = completeId; 546 this.elementName = elementName; 547 this.elementResources = elementResources; 548 549 this.exactParameterCountMatch = page.isExactParameterCountMatch(); 550 551 ComponentResources containerResources = container == null ? null : container.getComponentResources(); 552 553 coreResources = new InternalComponentResourcesImpl(this.page, this, containerResources, this.elementResources, 554 completeId, nestedId, instantiator, false); 555 556 coreComponent = coreResources.getComponent(); 557 558 eventLogger = elementResources.getEventLogger(coreResources.getLogger()); 559 560 renderingValue = elementResources.createPerThreadValue(); 561 562 page.addPageLoadedCallback(new Runnable() 563 { 564 public void run() 565 { 566 pageLoaded(); 567 } 568 }); 569 } 570 571 /** 572 * Constructor for the root component of a page. 573 */ 574 public ComponentPageElementImpl(Page page, Instantiator instantiator, 575 ComponentPageElementResources elementResources) 576 { 577 this(page, null, null, null, page.getName(), null, instantiator, null, elementResources); 578 } 579 580 private void initializeRenderPhases() 581 { 582 setupRenderPhase = new SetupRenderPhase(); 583 beginRenderPhase = new BeginRenderPhase(); 584 beforeRenderTemplatePhase = new BeforeRenderTemplatePhase(); 585 beforeRenderBodyPhase = new BeforeRenderBodyPhase(); 586 afterRenderBodyPhase = new AfterRenderBodyPhase(); 587 afterRenderTemplatePhase = new AfterRenderTemplatePhase(); 588 afterRenderPhase = new AfterRenderPhase(); 589 cleanupRenderPhase = new CleanupRenderPhase(); 590 591 // Now the optimization, where we remove, replace and collapse unused phases. We use 592 // the component models to determine which phases have handler methods for the 593 // render phases. 594 595 Set<Class> handled = coreResources.getComponentModel().getHandledRenderPhases(); 596 597 for (ComponentResources r : NamedSet.getValues(mixinIdToComponentResources)) 598 { 599 handled.addAll(r.getComponentModel().getHandledRenderPhases()); 600 } 601 602 if (!handled.contains(CleanupRender.class)) 603 cleanupRenderPhase = null; 604 605 // Now, work back to front. 606 607 if (!handled.contains(AfterRender.class)) 608 afterRenderPhase = cleanupRenderPhase; 609 610 if (!handled.contains(AfterRenderTemplate.class)) 611 afterRenderTemplatePhase = null; 612 613 if (!handled.contains(AfterRenderBody.class)) 614 afterRenderBodyPhase = null; 615 616 if (!handled.contains(BeforeRenderTemplate.class)) 617 beforeRenderTemplatePhase = new RenderTemplatePhase(); 618 619 if (!handled.contains(BeginRender.class)) 620 { 621 RenderCommand replacement = afterRenderPhase != null ? new OptimizedBeginRenderPhase() 622 : beforeRenderTemplatePhase; 623 624 beginRenderPhase = replacement; 625 } 626 627 if (!handled.contains(SetupRender.class)) 628 setupRenderPhase = beginRenderPhase; 629 } 630 631 public ComponentPageElement newChild(String id, String nestedId, String completeId, String elementName, 632 Instantiator instantiator, Location location) 633 { 634 ComponentPageElementImpl child = new ComponentPageElementImpl(page, this, id, nestedId, completeId, 635 elementName, instantiator, location, elementResources); 636 637 addEmbeddedElement(child); 638 639 return child; 640 } 641 642 void push(RenderQueue queue, boolean forward, RenderCommand forwardPhase, RenderCommand backwardPhase) 643 { 644 push(queue, forward ? forwardPhase : backwardPhase); 645 } 646 647 void push(RenderQueue queue, RenderCommand nextPhase) 648 { 649 if (nextPhase != null) 650 queue.push(nextPhase); 651 } 652 653 void addEmbeddedElement(ComponentPageElement child) 654 { 655 if (children == null) 656 children = CollectionFactory.newList(); 657 658 String childId = child.getId(); 659 660 for (ComponentPageElement existing : children) 661 { 662 if (existing.getId().equalsIgnoreCase(childId)) 663 throw new TapestryException(StructureMessages.duplicateChildComponent(this, childId), child, 664 new TapestryException(StructureMessages.originalChildComponent(this, childId, 665 existing.getLocation()), existing, null)); 666 } 667 668 children.add(child); 669 } 670 671 public void addMixin(String mixinId, Instantiator instantiator, String... order) 672 { 673 if (mixinIdToComponentResources == null) 674 { 675 mixinIdToComponentResources = NamedSet.create(); 676 components = CollectionFactory.newList(); 677 } 678 679 String mixinExtension = "$" + mixinId.toLowerCase(); 680 681 InternalComponentResourcesImpl resources = new InternalComponentResourcesImpl(page, this, coreResources, 682 elementResources, completeId + mixinExtension, nestedId + mixinExtension, instantiator, true); 683 684 mixinIdToComponentResources.put(mixinId, resources); 685 // note that since we're using explicit ordering now, 686 // we don't add anything to components until we page load; instead, we add 687 // to the orderers. 688 if (order == null) 689 order = InternalConstants.EMPTY_STRING_ARRAY; 690 691 if (resources.getComponentModel().isMixinAfter()) 692 { 693 if (mixinAfterOrderer == null) 694 mixinAfterOrderer = new Orderer<Component>(getLogger()); 695 mixinAfterOrderer.add(mixinId, resources.getComponent(), order); 696 } else 697 { 698 if (mixinBeforeOrderer == null) 699 mixinBeforeOrderer = new Orderer<Component>(getLogger()); 700 mixinBeforeOrderer.add(mixinId, resources.getComponent(), order); 701 } 702 } 703 704 public void bindMixinParameter(String mixinId, String parameterName, Binding binding) 705 { 706 InternalComponentResources mixinResources = NamedSet.get(mixinIdToComponentResources, mixinId); 707 708 mixinResources.bindParameter(parameterName, binding); 709 } 710 711 public Binding getBinding(String parameterName) 712 { 713 return coreResources.getBinding(parameterName); 714 } 715 716 public void bindParameter(String parameterName, Binding binding) 717 { 718 coreResources.bindParameter(parameterName, binding); 719 } 720 721 public void addToBody(RenderCommand element) 722 { 723 if (bodyBlock == null) 724 bodyBlock = new BlockImpl(getLocation(), "Body of " + getCompleteId()); 725 726 bodyBlock.addToBody(element); 727 } 728 729 public void addToTemplate(RenderCommand element) 730 { 731 template.add(element); 732 } 733 734 private void addUnboundParameterNames(String prefix, List<String> unbound, InternalComponentResources resource) 735 { 736 ComponentModel model = resource.getComponentModel(); 737 738 for (String name : model.getParameterNames()) 739 { 740 if (resource.isBound(name)) 741 continue; 742 743 ParameterModel parameterModel = model.getParameterModel(name); 744 745 if (parameterModel.isRequired()) 746 { 747 String fullName = prefix == null ? name : prefix + "." + name; 748 749 unbound.add(fullName); 750 } 751 } 752 } 753 754 private void pageLoaded() 755 { 756 // If this component has mixins, order them according to: 757 // mixins. 758 759 if (components != null) 760 { 761 List<Component> ordered = CollectionFactory.newList(); 762 763 if (mixinBeforeOrderer != null) 764 ordered.addAll(mixinBeforeOrderer.getOrdered()); 765 766 ordered.add(coreComponent); 767 768 // Add the remaining, late executing mixins 769 if (mixinAfterOrderer != null) 770 ordered.addAll(mixinAfterOrderer.getOrdered()); 771 772 components = ordered; 773 // no need to keep the orderers around. 774 mixinBeforeOrderer = null; 775 mixinAfterOrderer = null; 776 } 777 778 initializeRenderPhases(); 779 780 page.addVerifyCallback(new Runnable() 781 { 782 public void run() 783 { 784 // For some parameters, bindings (from defaults) are provided inside the callback method, so 785 // that is invoked first, before we check for unbound parameters. 786 787 verifyRequiredParametersAreBound(); 788 } 789 }); 790 791 792 loaded = true; 793 } 794 795 public void enqueueBeforeRenderBody(RenderQueue queue) 796 { 797 if (bodyBlock != null) 798 push(queue, beforeRenderBodyPhase); 799 } 800 801 public String getCompleteId() 802 { 803 return completeId; 804 } 805 806 public Component getComponent() 807 { 808 return coreComponent; 809 } 810 811 public InternalComponentResources getComponentResources() 812 { 813 return coreResources; 814 } 815 816 public ComponentPageElement getContainerElement() 817 { 818 return container; 819 } 820 821 public Page getContainingPage() 822 { 823 return page; 824 } 825 826 public ComponentPageElement getEmbeddedElement(String embeddedId) 827 { 828 ComponentPageElement embeddedElement = null; 829 830 if (children != null) 831 { 832 for (ComponentPageElement child : children) 833 { 834 if (child.getId().equalsIgnoreCase(embeddedId)) 835 { 836 embeddedElement = child; 837 break; 838 } 839 } 840 } 841 842 if (embeddedElement == null) 843 { 844 Set<String> ids = CollectionFactory.newSet(); 845 846 if (children != null) 847 { 848 for (ComponentPageElement child : children) 849 { 850 ids.add(child.getId()); 851 } 852 } 853 854 throw new UnknownValueException(String.format("Component %s does not contain embedded component '%s'.", 855 getCompleteId(), embeddedId), new AvailableValues("Embedded components", ids)); 856 } 857 858 return embeddedElement; 859 } 860 861 public String getId() 862 { 863 return id; 864 } 865 866 public Logger getLogger() 867 { 868 return coreResources.getLogger(); 869 } 870 871 public Component getMixinByClassName(String mixinClassName) 872 { 873 Component result = mixinForClassName(mixinClassName); 874 875 if (result == null) 876 throw new TapestryException(StructureMessages.unknownMixin(completeId, mixinClassName), getLocation(), null); 877 878 return result; 879 } 880 881 private Component mixinForClassName(String mixinClassName) 882 { 883 if (mixinIdToComponentResources == null) 884 return null; 885 886 for (InternalComponentResources resources : NamedSet.getValues(mixinIdToComponentResources)) 887 { 888 if (resources.getComponentModel().getComponentClassName().equals(mixinClassName)) 889 { 890 return resources 891 .getComponent(); 892 } 893 } 894 895 return null; 896 } 897 898 public ComponentResources getMixinResources(String mixinId) 899 { 900 ComponentResources result = NamedSet.get(mixinIdToComponentResources, mixinId); 901 902 if (result == null) 903 throw new IllegalArgumentException(String.format("Unable to locate mixin '%s' for component '%s'.", 904 mixinId, completeId)); 905 906 return result; 907 } 908 909 public String getNestedId() 910 { 911 return nestedId; 912 } 913 914 public boolean dispatchEvent(ComponentEvent event) 915 { 916 if (components == null) 917 return coreComponent.dispatchComponentEvent(event); 918 919 // Otherwise, iterate over mixins + core component 920 921 boolean result = false; 922 923 for (Component component : components) 924 { 925 result |= component.dispatchComponentEvent(event); 926 927 if (event.isAborted()) 928 break; 929 } 930 931 return result; 932 } 933 934 /** 935 * Invokes a callback on the component instances (the core component plus any mixins). 936 * 937 * @param reverse 938 * if true, the callbacks are in the reverse of the normal order (this is associated 939 * with AfterXXX 940 * phases) 941 * @param callback 942 * the object to receive each component instance 943 */ 944 private void invoke(boolean reverse, ComponentCallback callback) 945 { 946 try 947 { // Optimization: In the most general case (just the one component, no mixins) 948 // invoke the callback on the component and be done ... no iterators, no nothing. 949 950 if (components == null) 951 { 952 callback.run(coreComponent); 953 return; 954 } 955 956 Iterator<Component> i = reverse ? InternalUtils.reverseIterator(components) : components.iterator(); 957 958 while (i.hasNext()) 959 { 960 callback.run(i.next()); 961 962 if (callback.isEventAborted()) 963 return; 964 } 965 } catch (RuntimeException ex) 966 { 967 throw new TapestryException(ex.getMessage(), getLocation(), ex); 968 } 969 } 970 971 public boolean isLoaded() 972 { 973 return loaded; 974 } 975 976 public boolean isRendering() 977 { 978 return renderingValue.get(false); 979 } 980 981 /** 982 * Generate a toString() for the inner classes that represent render phases. 983 */ 984 private String phaseToString(String phaseName) 985 { 986 return String.format("%s[%s]", phaseName, completeId); 987 } 988 989 /** 990 * Pushes the SetupRender phase state onto the queue. 991 */ 992 public final void render(MarkupWriter writer, RenderQueue queue) 993 { 994 // TODO: An error if the render flag is already set (recursive rendering not 995 // allowed or advisable). 996 997 // TODO: Check for recursive rendering. 998 999 renderingValue.set(true); 1000 1001 queue.startComponent(coreResources); 1002 1003 queue.push(new PostRenderCleanupPhase(writer.getElement())); 1004 1005 push(queue, setupRenderPhase); 1006 } 1007 1008 @Override 1009 public String toString() 1010 { 1011 return String.format("ComponentPageElement[%s]", completeId); 1012 } 1013 1014 public boolean triggerEvent(String eventType, Object[] contextValues, ComponentEventCallback callback) 1015 { 1016 return triggerContextEvent(eventType, createParameterContext(contextValues == null ? new Object[0] 1017 : contextValues), callback); 1018 } 1019 1020 private EventContext createParameterContext(final Object... values) 1021 { 1022 return new AbstractEventContext() 1023 { 1024 public int getCount() 1025 { 1026 return values.length; 1027 } 1028 1029 public <T> T get(Class<T> desiredType, int index) 1030 { 1031 return elementResources.coerce(values[index], desiredType); 1032 } 1033 }; 1034 } 1035 1036 public boolean triggerContextEvent(final String eventType, final EventContext context, 1037 final ComponentEventCallback callback) 1038 { 1039 assert InternalUtils.isNonBlank(eventType); 1040 assert context != null; 1041 String description = "Triggering event '" + eventType + "' on " + completeId; 1042 1043 return elementResources.invoke(description, new Invokable<Boolean>() 1044 { 1045 public Boolean invoke() 1046 { 1047 return processEventTriggering(eventType, context, callback); 1048 } 1049 }); 1050 } 1051 1052 @SuppressWarnings("all") 1053 private boolean processEventTriggering(String eventType, EventContext context, ComponentEventCallback callback) 1054 { 1055 boolean result = false; 1056 1057 ComponentPageElement component = this; 1058 String componentId = ""; 1059 1060 // Provide a default handler for when the provided handler is null. 1061 final ComponentEventCallback providedHandler = callback == null ? new NotificationEventCallback(eventType, 1062 completeId) : callback; 1063 1064 ComponentEventCallback wrapped = new ComponentEventCallback() 1065 { 1066 public boolean handleResult(Object result) 1067 { 1068 // Boolean value is not passed to the handler; it will be true (abort event) 1069 // or false (continue looking for event handlers). 1070 1071 if (result instanceof Boolean) 1072 return (Boolean) result; 1073 1074 return providedHandler.handleResult(result); 1075 } 1076 }; 1077 1078 RuntimeException rootException = null; 1079 1080 // Because I don't like to reassign parameters. 1081 1082 String currentEventType = eventType; 1083 EventContext currentContext = context; 1084 1085 // Track the location of the original component for the event, even as we work our way up 1086 // the hierarchy. This may not be ideal if we trigger an "exception" event ... or maybe 1087 // it's right (it's the location of the originally thrown exception). 1088 1089 Location location = component.getComponentResources().getLocation(); 1090 1091 while (component != null) 1092 { 1093 try 1094 { 1095 Logger logger = component.getEventLogger(); 1096 1097 ComponentEvent event = new ComponentEventImpl(currentEventType, componentId, currentContext, wrapped, 1098 elementResources, exactParameterCountMatch, coreResources.getComponentModel(), logger); 1099 1100 logger.debug(TapestryMarkers.EVENT_DISPATCH, "Dispatch event: {}", event); 1101 1102 result |= component.dispatchEvent(event); 1103 1104 if (event.isAborted()) 1105 return result; 1106 } 1107 1108 // As with render phase methods, dispatchEvent() can now simply throw arbitrary exceptions 1109 // (the distinction between RuntimeException and checked Exception is entirely in the compiler, 1110 // not the JVM). 1111 catch (Exception ex) 1112 { 1113 // An exception in an event handler method 1114 // while we're trying to handle a previous exception! 1115 1116 if (rootException != null) 1117 throw rootException; 1118 1119 // We know component is not null and therefore has a component resources that 1120 // should have a location. 1121 1122 // Wrap it up to help ensure that a location is available to the event handler 1123 // method or, 1124 // more likely, to the exception report page. 1125 1126 rootException = new ComponentEventException(ex.getMessage(), eventType, context, location, ex); 1127 1128 // Switch over to triggering an "exception" event, starting in the component that 1129 // threw the exception. 1130 1131 currentEventType = "exception"; 1132 currentContext = createParameterContext(rootException); 1133 1134 continue; 1135 } 1136 1137 // On each bubble up, make the event appear to come from the previous component 1138 // in which the event was triggered. 1139 1140 componentId = component.getId(); 1141 1142 component = component.getContainerElement(); 1143 } 1144 1145 // If there was a handler for the exception event, it is required to return a non-null (and 1146 // non-boolean) value 1147 // to tell Tapestry what to do. Since that didn't happen, we have no choice but to rethrow 1148 // the (wrapped) 1149 // exception. 1150 1151 if (rootException != null) 1152 throw rootException; 1153 1154 return result; 1155 } 1156 1157 private void verifyRequiredParametersAreBound() 1158 { 1159 List<String> unbound = CollectionFactory.newList(); 1160 1161 addUnboundParameterNames(null, unbound, coreResources); 1162 1163 List<String> sortedNames = CollectionFactory.newList(NamedSet.getNames(mixinIdToComponentResources)); 1164 1165 Collections.sort(sortedNames); 1166 1167 for (String name : sortedNames) 1168 { 1169 addUnboundParameterNames(name, unbound, mixinIdToComponentResources.get(name)); 1170 } 1171 1172 if (!unbound.isEmpty()) 1173 { 1174 throw new TapestryException(StructureMessages.missingParameters(unbound, this), this, null); 1175 } 1176 } 1177 1178 public Locale getLocale() 1179 { 1180 return page.getSelector().locale; 1181 } 1182 1183 public String getElementName(String defaultElementName) 1184 { 1185 return elementName != null ? elementName : defaultElementName; 1186 } 1187 1188 public Block getBlock(String id) 1189 { 1190 Block result = findBlock(id); 1191 1192 if (result == null) 1193 throw new BlockNotFoundException(StructureMessages.blockNotFound(completeId, id), getLocation()); 1194 1195 return result; 1196 } 1197 1198 public Block findBlock(String id) 1199 { 1200 assert InternalUtils.isNonBlank(id); 1201 1202 return NamedSet.get(blocks, id); 1203 } 1204 1205 public void addBlock(String blockId, Block block) 1206 { 1207 if (blocks == null) 1208 blocks = NamedSet.create(); 1209 1210 if (!blocks.putIfNew(blockId, block)) 1211 throw new TapestryException(StructureMessages.duplicateBlock(this, blockId), block, null); 1212 } 1213 1214 public String getPageName() 1215 { 1216 return page.getName(); 1217 } 1218 1219 public boolean hasBody() 1220 { 1221 return bodyBlock != null; 1222 } 1223 1224 public Block getBody() 1225 { 1226 return bodyBlock == null ? PLACEHOLDER_BLOCK : bodyBlock; 1227 } 1228 1229 public Map<String, Binding> getInformalParameterBindings() 1230 { 1231 return coreResources.getInformalParameterBindings(); 1232 } 1233 1234 public Logger getEventLogger() 1235 { 1236 return eventLogger; 1237 } 1238 1239 public Link createEventLink(String eventType, Object... context) 1240 { 1241 return elementResources.createComponentEventLink(coreResources, eventType, false, context); 1242 } 1243 1244 public Link createFormEventLink(String eventType, Object... context) 1245 { 1246 return elementResources.createComponentEventLink(coreResources, eventType, true, context); 1247 } 1248 1249 protected RenderPhaseEvent createRenderEvent(RenderQueue queue) 1250 { 1251 return new RenderPhaseEvent(new RenderPhaseEventHandler(queue), eventLogger, elementResources); 1252 } 1253 1254 boolean isRenderTracingEnabled() 1255 { 1256 return elementResources.isRenderTracingEnabled(); 1257 } 1258 1259 public ComponentResourceSelector getResourceSelector() 1260 { 1261 return page.getSelector(); 1262 } 1263}