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