001    // Copyright 2006, 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    // http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry5.internal.structure;
016    
017    import org.apache.tapestry5.*;
018    import org.apache.tapestry5.annotations.*;
019    import org.apache.tapestry5.dom.Element;
020    import org.apache.tapestry5.internal.AbstractEventContext;
021    import org.apache.tapestry5.internal.InternalComponentResources;
022    import org.apache.tapestry5.internal.InternalConstants;
023    import org.apache.tapestry5.internal.services.ComponentEventImpl;
024    import org.apache.tapestry5.internal.services.Instantiator;
025    import org.apache.tapestry5.internal.util.NamedSet;
026    import org.apache.tapestry5.internal.util.NotificationEventCallback;
027    import org.apache.tapestry5.ioc.BaseLocatable;
028    import org.apache.tapestry5.ioc.Invokable;
029    import org.apache.tapestry5.ioc.Location;
030    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
031    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
032    import org.apache.tapestry5.ioc.internal.util.Orderer;
033    import org.apache.tapestry5.ioc.internal.util.TapestryException;
034    import org.apache.tapestry5.ioc.services.PerThreadValue;
035    import org.apache.tapestry5.ioc.services.SymbolSource;
036    import org.apache.tapestry5.ioc.util.AvailableValues;
037    import org.apache.tapestry5.ioc.util.UnknownValueException;
038    import org.apache.tapestry5.model.ComponentModel;
039    import org.apache.tapestry5.model.ParameterModel;
040    import org.apache.tapestry5.runtime.Component;
041    import org.apache.tapestry5.runtime.*;
042    import org.apache.tapestry5.services.Request;
043    import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
044    import org.slf4j.Logger;
045    
046    import java.util.*;
047    
048    /**
049     * Implements {@link RenderCommand} and represents a component within an overall page. Much of a
050     * component page
051     * element's behavior is delegated to user code, via a {@link org.apache.tapestry5.runtime.Component} instance.
052     * <p/>
053     * Once instantiated, a ComponentPageElement should be registered as a
054     * {@linkplain org.apache.tapestry5.internal.structure.Page#addLifecycleListener(org.apache.tapestry5.runtime.PageLifecycleListener)
055     * lifecycle listener}. This could be done inside the constructors, but that tends to complicate unit tests, so its done
056     * by {@link org.apache.tapestry5.internal.services.PageElementFactoryImpl}. There's still a bit of refactoring in this
057     * class (and its many inner classes) that can improve overall efficiency.
058     * <p/>
059     * Modified for Tapestry 5.2 to adjust for the no-pooling approach (shared instances with externalized mutable state).
060     */
061    public class ComponentPageElementImpl extends BaseLocatable implements ComponentPageElement
062    {
063        /**
064         * Placeholder for the body used when the component has no real content.
065         */
066        private static class PlaceholderBlock implements Block, Renderable
067        {
068            public void render(MarkupWriter writer)
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        // should be okay since it's a shadow service object
505        private final Request request;
506        private final SymbolSource symbolSource;
507        private final boolean productionMode;
508        private final boolean componentTracingEnabled;
509    
510        // We know that, at the very least, there will be an element to force the component to render
511        // its body, so there's no reason to wait to initialize the list.
512    
513        private final List<RenderCommand> template = CollectionFactory.newList();
514    
515        private RenderCommand setupRenderPhase, beginRenderPhase, beforeRenderTemplatePhase, beforeRenderBodyPhase,
516                afterRenderBodyPhase, afterRenderTemplatePhase, afterRenderPhase, cleanupRenderPhase;
517    
518        /**
519         * Constructor for other components embedded within the root component or at deeper levels of
520         * the hierarchy.
521         *
522         * @param page             ultimately containing this component
523         * @param container        component immediately containing this component (may be null for a root component)
524         * @param id               unique (within the container) id for this component (may be null for a root
525         *                         component)
526         * @param elementName      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     used to create the new component instance and access the component's model
530         * @param location         location of the element (within a template), used as part of exception reporting
531         * @param elementResources Provides access to common methods of various services
532         */
533        ComponentPageElementImpl(Page page, ComponentPageElement container, String id, String nestedId, String completeId,
534                                 String elementName, Instantiator instantiator, Location location,
535                                 ComponentPageElementResources elementResources, Request request, SymbolSource symbolSource)
536        {
537            super(location);
538    
539            this.page = page;
540            this.container = container;
541            this.id = id;
542            this.nestedId = nestedId;
543            this.completeId = completeId;
544            this.elementName = elementName;
545            this.elementResources = elementResources;
546            this.request = request;
547            this.symbolSource = symbolSource;
548    
549            // evaluate this once because it gets referenced a lot during rendering
550            this.productionMode = "true".equals(symbolSource.valueForSymbol(SymbolConstants.PRODUCTION_MODE));
551            this.componentTracingEnabled = "true".equals(symbolSource
552                    .valueForSymbol(SymbolConstants.COMPONENT_RENDER_TRACING_ENABLED));
553    
554            ComponentResources containerResources = container == null ? null : container.getComponentResources();
555    
556            coreResources = new InternalComponentResourcesImpl(this.page, this, containerResources, this.elementResources,
557                    completeId, nestedId, instantiator, false);
558    
559            coreComponent = coreResources.getComponent();
560    
561            eventLogger = elementResources.getEventLogger(coreResources.getLogger());
562    
563            renderingValue = elementResources.createPerThreadValue();
564    
565            page.addLifecycleListener(new PageLifecycleAdapter()
566            {
567                @Override
568                public void containingPageDidLoad()
569                {
570                    pageLoaded();
571    
572                    ComponentPageElementImpl.this.page.removeLifecycleListener(this);
573                }
574            });
575        }
576    
577        /**
578         * Constructor for the root component of a page.
579         */
580        public ComponentPageElementImpl(Page page, Instantiator instantiator,
581                                        ComponentPageElementResources elementResources, Request request, SymbolSource symbolSource)
582        {
583            this(page, null, null, null, page.getName(), null, instantiator, null, elementResources, request, symbolSource);
584        }
585    
586        private void initializeRenderPhases()
587        {
588            setupRenderPhase = new SetupRenderPhase();
589            beginRenderPhase = new BeginRenderPhase();
590            beforeRenderTemplatePhase = new BeforeRenderTemplatePhase();
591            beforeRenderBodyPhase = new BeforeRenderBodyPhase();
592            afterRenderBodyPhase = new AfterRenderBodyPhase();
593            afterRenderTemplatePhase = new AfterRenderTemplatePhase();
594            afterRenderPhase = new AfterRenderPhase();
595            cleanupRenderPhase = new CleanupRenderPhase();
596    
597            // Now the optimization, where we remove, replace and collapse unused phases. We use
598            // the component models to determine which phases have handler methods for the
599            // render phases.
600    
601            Set<Class> handled = coreResources.getComponentModel().getHandledRenderPhases();
602    
603            for (ComponentResources r : NamedSet.getValues(mixinIdToComponentResources))
604            {
605                handled.addAll(r.getComponentModel().getHandledRenderPhases());
606            }
607    
608            if (!handled.contains(CleanupRender.class))
609                cleanupRenderPhase = null;
610    
611            // Now, work back to front.
612    
613            if (!handled.contains(AfterRender.class))
614                afterRenderPhase = cleanupRenderPhase;
615    
616            if (!handled.contains(AfterRenderTemplate.class))
617                afterRenderTemplatePhase = null;
618    
619            if (!handled.contains(AfterRenderBody.class))
620                afterRenderBodyPhase = null;
621    
622            if (!handled.contains(BeforeRenderTemplate.class))
623                beforeRenderTemplatePhase = new RenderTemplatePhase();
624    
625            if (!handled.contains(BeginRender.class))
626            {
627                RenderCommand replacement = afterRenderPhase != null ? new OptimizedBeginRenderPhase()
628                        : beforeRenderTemplatePhase;
629    
630                beginRenderPhase = replacement;
631            }
632    
633            if (!handled.contains(SetupRender.class))
634                setupRenderPhase = beginRenderPhase;
635        }
636    
637        public ComponentPageElement newChild(String id, String nestedId, String completeId, String elementName,
638                                             Instantiator instantiator, Location location)
639        {
640            ComponentPageElementImpl child = new ComponentPageElementImpl(page, this, id, nestedId, completeId,
641                    elementName, instantiator, location, elementResources, request, symbolSource);
642    
643            addEmbeddedElement(child);
644    
645            return child;
646        }
647    
648        void push(RenderQueue queue, boolean forward, RenderCommand forwardPhase, RenderCommand backwardPhase)
649        {
650            push(queue, forward ? forwardPhase : backwardPhase);
651        }
652    
653        void push(RenderQueue queue, RenderCommand nextPhase)
654        {
655            if (nextPhase != null)
656                queue.push(nextPhase);
657        }
658    
659        void addEmbeddedElement(ComponentPageElement child)
660        {
661            if (children == null)
662                children = CollectionFactory.newList();
663    
664            String childId = child.getId();
665    
666            for (ComponentPageElement existing : children)
667            {
668                if (existing.getId().equalsIgnoreCase(childId))
669                    throw new TapestryException(StructureMessages.duplicateChildComponent(this, childId), child,
670                            new TapestryException(StructureMessages.originalChildComponent(this, childId,
671                                    existing.getLocation()), existing, null));
672            }
673    
674            children.add(child);
675        }
676    
677        public void addMixin(String mixinId, Instantiator instantiator, String... order)
678        {
679            if (mixinIdToComponentResources == null)
680            {
681                mixinIdToComponentResources = NamedSet.create();
682                components = CollectionFactory.newList();
683            }
684    
685            String mixinExtension = "$" + mixinId.toLowerCase();
686    
687            InternalComponentResourcesImpl resources = new InternalComponentResourcesImpl(page, this, coreResources,
688                    elementResources, completeId + mixinExtension, nestedId + mixinExtension, instantiator, true);
689    
690            mixinIdToComponentResources.put(mixinId, resources);
691            // note that since we're using explicit ordering now,
692            // we don't add anything to components until we page load; instead, we add
693            // to the orderers.
694            if (order == null)
695                order = InternalConstants.EMPTY_STRING_ARRAY;
696    
697            if (resources.getComponentModel().isMixinAfter())
698            {
699                if (mixinAfterOrderer == null)
700                    mixinAfterOrderer = new Orderer<Component>(getLogger());
701                mixinAfterOrderer.add(mixinId, resources.getComponent(), order);
702            } else
703            {
704                if (mixinBeforeOrderer == null)
705                    mixinBeforeOrderer = new Orderer<Component>(getLogger());
706                mixinBeforeOrderer.add(mixinId, resources.getComponent(), order);
707            }
708        }
709    
710        public void bindMixinParameter(String mixinId, String parameterName, Binding binding)
711        {
712            InternalComponentResources mixinResources = NamedSet.get(mixinIdToComponentResources, mixinId);
713    
714            mixinResources.bindParameter(parameterName, binding);
715        }
716    
717        public Binding getBinding(String parameterName)
718        {
719            return coreResources.getBinding(parameterName);
720        }
721    
722        public void bindParameter(String parameterName, Binding binding)
723        {
724            coreResources.bindParameter(parameterName, binding);
725        }
726    
727        public void addToBody(RenderCommand element)
728        {
729            if (bodyBlock == null)
730                bodyBlock = new BlockImpl(getLocation(), "Body of " + getCompleteId());
731    
732            bodyBlock.addToBody(element);
733        }
734    
735        public void addToTemplate(RenderCommand element)
736        {
737            template.add(element);
738        }
739    
740        private void addUnboundParameterNames(String prefix, List<String> unbound, InternalComponentResources resource)
741        {
742            ComponentModel model = resource.getComponentModel();
743    
744            for (String name : model.getParameterNames())
745            {
746                if (resource.isBound(name))
747                    continue;
748    
749                ParameterModel parameterModel = model.getParameterModel(name);
750    
751                if (parameterModel.isRequired())
752                {
753                    String fullName = prefix == null ? name : prefix + "." + name;
754    
755                    unbound.add(fullName);
756                }
757            }
758        }
759    
760        private void pageLoaded()
761        {
762            // If this component has mixins, order them according to:
763            // mixins.
764    
765            if (components != null)
766            {
767                List<Component> ordered = CollectionFactory.newList();
768    
769                if (mixinBeforeOrderer != null)
770                    ordered.addAll(mixinBeforeOrderer.getOrdered());
771    
772                ordered.add(coreComponent);
773    
774                // Add the remaining, late executing mixins
775                if (mixinAfterOrderer != null)
776                    ordered.addAll(mixinAfterOrderer.getOrdered());
777    
778                components = ordered;
779                // no need to keep the orderers around.
780                mixinBeforeOrderer = null;
781                mixinAfterOrderer = null;
782            }
783    
784            initializeRenderPhases();
785    
786            page.addVerifyListener(new Runnable()
787            {
788                public void run()
789                {
790                    // For some parameters, bindings (from defaults) are provided inside the callback method, so
791                    // that is invoked first, before we check for unbound parameters.
792    
793                    verifyRequiredParametersAreBound();
794                }
795            });
796    
797    
798            loaded = true;
799        }
800    
801        public void enqueueBeforeRenderBody(RenderQueue queue)
802        {
803            if (bodyBlock != null)
804                push(queue, beforeRenderBodyPhase);
805        }
806    
807        public String getCompleteId()
808        {
809            return completeId;
810        }
811    
812        public Component getComponent()
813        {
814            return coreComponent;
815        }
816    
817        public InternalComponentResources getComponentResources()
818        {
819            return coreResources;
820        }
821    
822        public ComponentPageElement getContainerElement()
823        {
824            return container;
825        }
826    
827        public Page getContainingPage()
828        {
829            return page;
830        }
831    
832        public ComponentPageElement getEmbeddedElement(String embeddedId)
833        {
834            ComponentPageElement embeddedElement = null;
835    
836            if (children != null)
837            {
838                for (ComponentPageElement child : children)
839                {
840                    if (child.getId().equalsIgnoreCase(embeddedId))
841                    {
842                        embeddedElement = child;
843                        break;
844                    }
845                }
846            }
847    
848            if (embeddedElement == null)
849            {
850                Set<String> ids = CollectionFactory.newSet();
851    
852                if (children != null)
853                {
854                    for (ComponentPageElement child : children)
855                    {
856                        ids.add(child.getId());
857                    }
858                }
859    
860                throw new UnknownValueException(String.format("Component %s does not contain embedded component '%s'.",
861                        getCompleteId(), embeddedId), new AvailableValues("Embedded components", ids));
862            }
863    
864            return embeddedElement;
865        }
866    
867        public String getId()
868        {
869            return id;
870        }
871    
872        public Logger getLogger()
873        {
874            return coreResources.getLogger();
875        }
876    
877        public Component getMixinByClassName(String mixinClassName)
878        {
879            Component result = mixinForClassName(mixinClassName);
880    
881            if (result == null)
882                throw new TapestryException(StructureMessages.unknownMixin(completeId, mixinClassName), getLocation(), null);
883    
884            return result;
885        }
886    
887        private Component mixinForClassName(String mixinClassName)
888        {
889            if (mixinIdToComponentResources == null)
890                return null;
891    
892            for (InternalComponentResources resources : NamedSet.getValues(mixinIdToComponentResources))
893            {
894                if (resources.getComponentModel().getComponentClassName().equals(mixinClassName))
895                {
896                    return resources
897                            .getComponent();
898                }
899            }
900    
901            return null;
902        }
903    
904        public ComponentResources getMixinResources(String mixinId)
905        {
906            ComponentResources result = NamedSet.get(mixinIdToComponentResources, mixinId);
907    
908            if (result == null)
909                throw new IllegalArgumentException(String.format("Unable to locate mixin '%s' for component '%s'.",
910                        mixinId, completeId));
911    
912            return result;
913        }
914    
915        public String getNestedId()
916        {
917            return nestedId;
918        }
919    
920        public boolean dispatchEvent(ComponentEvent event)
921        {
922            if (components == null)
923                return coreComponent.dispatchComponentEvent(event);
924    
925            // Otherwise, iterate over mixins + core component
926    
927            boolean result = false;
928    
929            for (Component component : components)
930            {
931                result |= component.dispatchComponentEvent(event);
932    
933                if (event.isAborted())
934                    break;
935            }
936    
937            return result;
938        }
939    
940        /**
941         * Invokes a callback on the component instances (the core component plus any mixins).
942         *
943         * @param reverse  if true, the callbacks are in the reverse of the normal order (this is associated
944         *                 with AfterXXX
945         *                 phases)
946         * @param callback the object to receive each component instance
947         */
948        private void invoke(boolean reverse, ComponentCallback callback)
949        {
950            try
951            { // Optimization: In the most general case (just the one component, no mixins)
952                // invoke the callback on the component and be done ... no iterators, no nothing.
953    
954                if (components == null)
955                {
956                    callback.run(coreComponent);
957                    return;
958                }
959    
960                Iterator<Component> i = reverse ? InternalUtils.reverseIterator(components) : components.iterator();
961    
962                while (i.hasNext())
963                {
964                    callback.run(i.next());
965    
966                    if (callback.isEventAborted())
967                        return;
968                }
969            } catch (RuntimeException ex)
970            {
971                throw new TapestryException(ex.getMessage(), getLocation(), ex);
972            }
973        }
974    
975        public boolean isLoaded()
976        {
977            return loaded;
978        }
979    
980        public boolean isRendering()
981        {
982            return renderingValue.get(false);
983        }
984    
985        /**
986         * Generate a toString() for the inner classes that represent render phases.
987         */
988        private String phaseToString(String phaseName)
989        {
990            return String.format("%s[%s]", phaseName, completeId);
991        }
992    
993        /**
994         * Pushes the SetupRender phase state onto the queue.
995         */
996        public final void render(MarkupWriter writer, RenderQueue queue)
997        {
998            // TODO: An error if the render flag is already set (recursive rendering not
999            // allowed or advisable).
1000    
1001            // TODO: Check for recursive rendering.
1002    
1003            renderingValue.set(true);
1004    
1005            queue.startComponent(coreResources);
1006    
1007            queue.push(new PostRenderCleanupPhase(writer.getElement()));
1008    
1009            push(queue, setupRenderPhase);
1010        }
1011    
1012        @Override
1013        public String toString()
1014        {
1015            return String.format("ComponentPageElement[%s]", completeId);
1016        }
1017    
1018        public boolean triggerEvent(String eventType, Object[] contextValues, ComponentEventCallback callback)
1019        {
1020            return triggerContextEvent(eventType, createParameterContext(contextValues == null ? new Object[0]
1021                    : contextValues), callback);
1022        }
1023    
1024        private EventContext createParameterContext(final Object... values)
1025        {
1026            return new AbstractEventContext()
1027            {
1028                public int getCount()
1029                {
1030                    return values.length;
1031                }
1032    
1033                public <T> T get(Class<T> desiredType, int index)
1034                {
1035                    return elementResources.coerce(values[index], desiredType);
1036                }
1037            };
1038        }
1039    
1040        public boolean triggerContextEvent(final String eventType, final EventContext context,
1041                                           final ComponentEventCallback callback)
1042        {
1043            assert InternalUtils.isNonBlank(eventType);
1044            assert context != null;
1045            String description = String.format("Triggering event '%s' on %s", eventType, completeId);
1046    
1047            return elementResources.invoke(description, new Invokable<Boolean>()
1048            {
1049                public Boolean invoke()
1050                {
1051                    return processEventTriggering(eventType, context, callback);
1052                }
1053            });
1054        }
1055    
1056        @SuppressWarnings("all")
1057        private boolean processEventTriggering(String eventType, EventContext context, ComponentEventCallback callback)
1058        {
1059            boolean result = false;
1060    
1061            ComponentPageElement component = this;
1062            String componentId = "";
1063    
1064            // Provide a default handler for when the provided handler is null.
1065            final ComponentEventCallback providedHandler = callback == null ? new NotificationEventCallback(eventType,
1066                    completeId) : callback;
1067    
1068            ComponentEventCallback wrapped = new ComponentEventCallback()
1069            {
1070                public boolean handleResult(Object result)
1071                {
1072                    // Boolean value is not passed to the handler; it will be true (abort event)
1073                    // or false (continue looking for event handlers).
1074    
1075                    if (result instanceof Boolean)
1076                        return (Boolean) result;
1077    
1078                    return providedHandler.handleResult(result);
1079                }
1080            };
1081    
1082            RuntimeException rootException = null;
1083    
1084            // Because I don't like to reassign parameters.
1085    
1086            String currentEventType = eventType;
1087            EventContext currentContext = context;
1088    
1089            // Track the location of the original component for the event, even as we work our way up
1090            // the hierarchy. This may not be ideal if we trigger an "exception" event ... or maybe
1091            // it's right (it's the location of the originally thrown exception).
1092    
1093            Location location = component.getComponentResources().getLocation();
1094    
1095            while (component != null)
1096            {
1097                try
1098                {
1099                    Logger logger = component.getEventLogger();
1100    
1101                    ComponentEvent event = new ComponentEventImpl(currentEventType, componentId, currentContext, wrapped,
1102                            elementResources, logger);
1103    
1104                    logger.debug(TapestryMarkers.EVENT_DISPATCH, "Dispatch event: {}", event);
1105    
1106                    result |= component.dispatchEvent(event);
1107    
1108                    if (event.isAborted())
1109                        return result;
1110                }
1111    
1112                // As with render phase methods, dispatchEvent() can now simply throw arbitrary exceptions
1113                // (the distinction between RuntimeException and checked Exception is entirely in the compiler,
1114                // not the JVM).
1115                catch (Exception ex)
1116                {
1117                    // An exception in an event handler method
1118                    // while we're trying to handle a previous exception!
1119    
1120                    if (rootException != null)
1121                        throw rootException;
1122    
1123                    // We know component is not null and therefore has a component resources that
1124                    // should have a location.
1125    
1126                    // Wrap it up to help ensure that a location is available to the event handler
1127                    // method or,
1128                    // more likely, to the exception report page.
1129    
1130                    rootException = new ComponentEventException(ex.getMessage(), eventType, context, location, ex);
1131    
1132                    // Switch over to triggering an "exception" event, starting in the component that
1133                    // threw the exception.
1134    
1135                    currentEventType = "exception";
1136                    currentContext = createParameterContext(rootException);
1137    
1138                    continue;
1139                }
1140    
1141                // On each bubble up, make the event appear to come from the previous component
1142                // in which the event was triggered.
1143    
1144                componentId = component.getId();
1145    
1146                component = component.getContainerElement();
1147            }
1148    
1149            // If there was a handler for the exception event, it is required to return a non-null (and
1150            // non-boolean) value
1151            // to tell Tapestry what to do. Since that didn't happen, we have no choice but to rethrow
1152            // the (wrapped)
1153            // exception.
1154    
1155            if (rootException != null)
1156                throw rootException;
1157    
1158            return result;
1159        }
1160    
1161        private void verifyRequiredParametersAreBound()
1162        {
1163            List<String> unbound = CollectionFactory.newList();
1164    
1165            addUnboundParameterNames(null, unbound, coreResources);
1166    
1167            List<String> sortedNames = CollectionFactory.newList(NamedSet.getNames(mixinIdToComponentResources));
1168    
1169            Collections.sort(sortedNames);
1170    
1171            for (String name : sortedNames)
1172            {
1173                addUnboundParameterNames(name, unbound, mixinIdToComponentResources.get(name));
1174            }
1175    
1176            if (!unbound.isEmpty())
1177            {
1178                throw new TapestryException(StructureMessages.missingParameters(unbound, this), this, null);
1179            }
1180        }
1181    
1182        public Locale getLocale()
1183        {
1184            return page.getSelector().locale;
1185        }
1186    
1187        public String getElementName(String defaultElementName)
1188        {
1189            return elementName != null ? elementName : defaultElementName;
1190        }
1191    
1192        public Block getBlock(String id)
1193        {
1194            Block result = findBlock(id);
1195    
1196            if (result == null)
1197                throw new BlockNotFoundException(StructureMessages.blockNotFound(completeId, id), getLocation());
1198    
1199            return result;
1200        }
1201    
1202        public Block findBlock(String id)
1203        {
1204            assert InternalUtils.isNonBlank(id);
1205    
1206            return NamedSet.get(blocks, id);
1207        }
1208    
1209        public void addBlock(String blockId, Block block)
1210        {
1211            if (blocks == null)
1212                blocks = NamedSet.create();
1213    
1214            if (!blocks.putIfNew(blockId, block))
1215                throw new TapestryException(StructureMessages.duplicateBlock(this, blockId), block, null);
1216        }
1217    
1218        public String getPageName()
1219        {
1220            return page.getName();
1221        }
1222    
1223        public boolean hasBody()
1224        {
1225            return bodyBlock != null;
1226        }
1227    
1228        public Block getBody()
1229        {
1230            return bodyBlock == null ? PLACEHOLDER_BLOCK : bodyBlock;
1231        }
1232    
1233        public Map<String, Binding> getInformalParameterBindings()
1234        {
1235            return coreResources.getInformalParameterBindings();
1236        }
1237    
1238        public Logger getEventLogger()
1239        {
1240            return eventLogger;
1241        }
1242    
1243        public Link createEventLink(String eventType, Object... context)
1244        {
1245            return elementResources.createComponentEventLink(coreResources, eventType, false, context);
1246        }
1247    
1248        public Link createActionLink(String eventType, boolean forForm, Object... context)
1249        {
1250            return elementResources.createComponentEventLink(coreResources, eventType, forForm, context);
1251        }
1252    
1253        public Link createFormEventLink(String eventType, Object... context)
1254        {
1255            return elementResources.createComponentEventLink(coreResources, eventType, true, context);
1256        }
1257    
1258        public Link createPageLink(String pageName, boolean override, Object... context)
1259        {
1260            return elementResources.createPageRenderLink(pageName, override, context);
1261        }
1262    
1263        public Link createPageLink(Class pageClass, boolean override, Object... context)
1264        {
1265            return elementResources.createPageRenderLink(pageClass, override, context);
1266        }
1267    
1268        protected RenderPhaseEvent createRenderEvent(RenderQueue queue)
1269        {
1270            return new RenderPhaseEvent(new RenderPhaseEventHandler(queue), eventLogger, elementResources);
1271        }
1272    
1273        boolean isRenderTracingEnabled()
1274        {
1275            return !productionMode && (componentTracingEnabled || "true".equals(request.getParameter("t:component-trace")));
1276        }
1277    
1278        public ComponentResourceSelector getResourceSelector()
1279        {
1280            return page.getSelector();
1281        }
1282    }