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 = getEmbeddedElementIds(); 846 847 throw new UnknownValueException(String.format("Component %s does not contain embedded component '%s'.", 848 getCompleteId(), embeddedId), new AvailableValues("Embedded components", ids)); 849 } 850 851 return embeddedElement; 852 } 853 854 @Override 855 public Set<String> getEmbeddedElementIds() { 856 Set<String> ids = CollectionFactory.newSet(); 857 858 if (children != null) 859 { 860 for (ComponentPageElement child : children) 861 { 862 ids.add(child.getId()); 863 } 864 } 865 return ids; 866 } 867 868 public String getId() 869 { 870 return id; 871 } 872 873 public Logger getLogger() 874 { 875 return coreResources.getLogger(); 876 } 877 878 public Component getMixinByClassName(String mixinClassName) 879 { 880 Component result = mixinForClassName(mixinClassName); 881 882 if (result == null) 883 throw new TapestryException(StructureMessages.unknownMixin(completeId, mixinClassName), getLocation(), null); 884 885 return result; 886 } 887 888 private Component mixinForClassName(String mixinClassName) 889 { 890 if (mixinIdToComponentResources == null) 891 return null; 892 893 for (InternalComponentResources resources : NamedSet.getValues(mixinIdToComponentResources)) 894 { 895 if (resources.getComponentModel().getComponentClassName().equals(mixinClassName)) 896 { 897 return resources 898 .getComponent(); 899 } 900 } 901 902 return null; 903 } 904 905 public ComponentResources getMixinResources(String mixinId) 906 { 907 ComponentResources result = NamedSet.get(mixinIdToComponentResources, mixinId); 908 909 if (result == null) 910 throw new IllegalArgumentException(String.format("Unable to locate mixin '%s' for component '%s'.", 911 mixinId, completeId)); 912 913 return result; 914 } 915 916 public String getNestedId() 917 { 918 return nestedId; 919 } 920 921 public boolean dispatchEvent(ComponentEvent event) 922 { 923 if (components == null) 924 return coreComponent.dispatchComponentEvent(event); 925 926 // Otherwise, iterate over mixins + core component 927 928 boolean result = false; 929 930 for (Component component : components) 931 { 932 result |= component.dispatchComponentEvent(event); 933 934 if (event.isAborted()) 935 break; 936 } 937 938 return result; 939 } 940 941 /** 942 * Invokes a callback on the component instances (the core component plus any mixins). 943 * 944 * @param reverse 945 * if true, the callbacks are in the reverse of the normal order (this is associated 946 * with AfterXXX 947 * phases) 948 * @param callback 949 * the object to receive each component instance 950 */ 951 private void invoke(boolean reverse, ComponentCallback callback) 952 { 953 try 954 { // Optimization: In the most general case (just the one component, no mixins) 955 // invoke the callback on the component and be done ... no iterators, no nothing. 956 957 if (components == null) 958 { 959 callback.run(coreComponent); 960 return; 961 } 962 963 Iterator<Component> i = reverse ? InternalUtils.reverseIterator(components) : components.iterator(); 964 965 while (i.hasNext()) 966 { 967 callback.run(i.next()); 968 969 if (callback.isEventAborted()) 970 return; 971 } 972 } catch (RuntimeException ex) 973 { 974 throw new TapestryException(ex.getMessage(), getLocation(), ex); 975 } 976 } 977 978 public boolean isLoaded() 979 { 980 return loaded; 981 } 982 983 public boolean isRendering() 984 { 985 return renderingValue.get(false); 986 } 987 988 /** 989 * Generate a toString() for the inner classes that represent render phases. 990 */ 991 private String phaseToString(String phaseName) 992 { 993 return String.format("%s[%s]", phaseName, completeId); 994 } 995 996 /** 997 * Pushes the SetupRender phase state onto the queue. 998 */ 999 public final void render(MarkupWriter writer, RenderQueue queue) 1000 { 1001 // TODO: An error if the render flag is already set (recursive rendering not 1002 // allowed or advisable). 1003 1004 // TODO: Check for recursive rendering. 1005 1006 renderingValue.set(true); 1007 1008 queue.startComponent(coreResources); 1009 1010 queue.push(new PostRenderCleanupPhase(writer.getElement())); 1011 1012 push(queue, setupRenderPhase); 1013 } 1014 1015 @Override 1016 public String toString() 1017 { 1018 return String.format("ComponentPageElement[%s]", completeId); 1019 } 1020 1021 public boolean triggerEvent(String eventType, Object[] contextValues, ComponentEventCallback callback) 1022 { 1023 return triggerContextEvent(eventType, createParameterContext(contextValues == null ? new Object[0] 1024 : contextValues), callback); 1025 } 1026 1027 private EventContext createParameterContext(final Object... values) 1028 { 1029 return new AbstractEventContext() 1030 { 1031 public int getCount() 1032 { 1033 return values.length; 1034 } 1035 1036 public <T> T get(Class<T> desiredType, int index) 1037 { 1038 return elementResources.coerce(values[index], desiredType); 1039 } 1040 }; 1041 } 1042 1043 public boolean triggerContextEvent(final String eventType, final EventContext context, 1044 final ComponentEventCallback callback) 1045 { 1046 assert InternalUtils.isNonBlank(eventType); 1047 assert context != null; 1048 String description = "Triggering event '" + eventType + "' on " + completeId; 1049 1050 return elementResources.invoke(description, new Invokable<Boolean>() 1051 { 1052 public Boolean invoke() 1053 { 1054 return processEventTriggering(eventType, context, callback); 1055 } 1056 }); 1057 } 1058 1059 @SuppressWarnings("all") 1060 private boolean processEventTriggering(String eventType, EventContext context, ComponentEventCallback callback) 1061 { 1062 boolean result = false; 1063 1064 ComponentPageElement component = this; 1065 String componentId = ""; 1066 1067 // Provide a default handler for when the provided handler is null. 1068 final ComponentEventCallback providedHandler = callback == null ? new NotificationEventCallback(eventType, 1069 completeId) : callback; 1070 1071 ComponentEventCallback wrapped = new ComponentEventCallback() 1072 { 1073 public boolean handleResult(Object result) 1074 { 1075 // Boolean value is not passed to the handler; it will be true (abort event) 1076 // or false (continue looking for event handlers). 1077 1078 if (result instanceof Boolean) 1079 return (Boolean) result; 1080 1081 return providedHandler.handleResult(result); 1082 } 1083 }; 1084 1085 RuntimeException rootException = null; 1086 1087 // Because I don't like to reassign parameters. 1088 1089 String currentEventType = eventType; 1090 EventContext currentContext = context; 1091 1092 // Track the location of the original component for the event, even as we work our way up 1093 // the hierarchy. This may not be ideal if we trigger an "exception" event ... or maybe 1094 // it's right (it's the location of the originally thrown exception). 1095 1096 Location location = component.getComponentResources().getLocation(); 1097 1098 while (component != null) 1099 { 1100 try 1101 { 1102 Logger logger = component.getEventLogger(); 1103 1104 ComponentEvent event = new ComponentEventImpl(currentEventType, componentId, currentContext, wrapped, 1105 elementResources, exactParameterCountMatch, coreResources.getComponentModel(), logger); 1106 1107 logger.debug(TapestryMarkers.EVENT_DISPATCH, "Dispatch event: {}", event); 1108 1109 result |= component.dispatchEvent(event); 1110 1111 if (event.isAborted()) 1112 return result; 1113 } 1114 1115 // As with render phase methods, dispatchEvent() can now simply throw arbitrary exceptions 1116 // (the distinction between RuntimeException and checked Exception is entirely in the compiler, 1117 // not the JVM). 1118 catch (Exception ex) 1119 { 1120 // An exception in an event handler method 1121 // while we're trying to handle a previous exception! 1122 1123 if (rootException != null) 1124 throw rootException; 1125 1126 // We know component is not null and therefore has a component resources that 1127 // should have a location. 1128 1129 // Wrap it up to help ensure that a location is available to the event handler 1130 // method or, 1131 // more likely, to the exception report page. 1132 1133 rootException = new ComponentEventException(ex.getMessage(), eventType, context, location, ex); 1134 1135 // Switch over to triggering an "exception" event, starting in the component that 1136 // threw the exception. 1137 1138 currentEventType = "exception"; 1139 currentContext = createParameterContext(rootException); 1140 1141 continue; 1142 } 1143 1144 // On each bubble up, make the event appear to come from the previous component 1145 // in which the event was triggered. 1146 1147 componentId = component.getId(); 1148 1149 component = component.getContainerElement(); 1150 } 1151 1152 // If there was a handler for the exception event, it is required to return a non-null (and 1153 // non-boolean) value 1154 // to tell Tapestry what to do. Since that didn't happen, we have no choice but to rethrow 1155 // the (wrapped) 1156 // exception. 1157 1158 if (rootException != null) 1159 throw rootException; 1160 1161 return result; 1162 } 1163 1164 private void verifyRequiredParametersAreBound() 1165 { 1166 List<String> unbound = CollectionFactory.newList(); 1167 1168 addUnboundParameterNames(null, unbound, coreResources); 1169 1170 List<String> sortedNames = CollectionFactory.newList(NamedSet.getNames(mixinIdToComponentResources)); 1171 1172 Collections.sort(sortedNames); 1173 1174 for (String name : sortedNames) 1175 { 1176 addUnboundParameterNames(name, unbound, mixinIdToComponentResources.get(name)); 1177 } 1178 1179 if (!unbound.isEmpty()) 1180 { 1181 throw new TapestryException(StructureMessages.missingParameters(unbound, this), this, null); 1182 } 1183 } 1184 1185 public Locale getLocale() 1186 { 1187 return page.getSelector().locale; 1188 } 1189 1190 public String getElementName(String defaultElementName) 1191 { 1192 return elementName != null ? elementName : defaultElementName; 1193 } 1194 1195 public Block getBlock(String id) 1196 { 1197 Block result = findBlock(id); 1198 1199 if (result == null) 1200 throw new BlockNotFoundException(StructureMessages.blockNotFound(completeId, id), getLocation()); 1201 1202 return result; 1203 } 1204 1205 public Block findBlock(String id) 1206 { 1207 assert InternalUtils.isNonBlank(id); 1208 1209 return NamedSet.get(blocks, id); 1210 } 1211 1212 public void addBlock(String blockId, Block block) 1213 { 1214 if (blocks == null) 1215 blocks = NamedSet.create(); 1216 1217 if (!blocks.putIfNew(blockId, block)) 1218 throw new TapestryException(StructureMessages.duplicateBlock(this, blockId), block, null); 1219 } 1220 1221 public String getPageName() 1222 { 1223 return page.getName(); 1224 } 1225 1226 public boolean hasBody() 1227 { 1228 return bodyBlock != null; 1229 } 1230 1231 public Block getBody() 1232 { 1233 return bodyBlock == null ? PLACEHOLDER_BLOCK : bodyBlock; 1234 } 1235 1236 public Map<String, Binding> getInformalParameterBindings() 1237 { 1238 return coreResources.getInformalParameterBindings(); 1239 } 1240 1241 public Logger getEventLogger() 1242 { 1243 return eventLogger; 1244 } 1245 1246 public Link createEventLink(String eventType, Object... context) 1247 { 1248 return elementResources.createComponentEventLink(coreResources, eventType, false, context); 1249 } 1250 1251 public Link createFormEventLink(String eventType, Object... context) 1252 { 1253 return elementResources.createComponentEventLink(coreResources, eventType, true, context); 1254 } 1255 1256 protected RenderPhaseEvent createRenderEvent(RenderQueue queue) 1257 { 1258 return new RenderPhaseEvent(new RenderPhaseEventHandler(queue), eventLogger, elementResources); 1259 } 1260 1261 boolean isRenderTracingEnabled() 1262 { 1263 return elementResources.isRenderTracingEnabled(); 1264 } 1265 1266 public ComponentResourceSelector getResourceSelector() 1267 { 1268 return page.getSelector(); 1269 } 1270}