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