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