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