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