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