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