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