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