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}