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
015package org.apache.tapestry5.internal.structure;
016
017import org.apache.tapestry5.*;
018import org.apache.tapestry5.annotations.*;
019import org.apache.tapestry5.dom.Element;
020import org.apache.tapestry5.internal.AbstractEventContext;
021import org.apache.tapestry5.internal.InternalComponentResources;
022import org.apache.tapestry5.internal.InternalConstants;
023import org.apache.tapestry5.internal.services.ComponentEventImpl;
024import org.apache.tapestry5.internal.services.Instantiator;
025import org.apache.tapestry5.internal.util.NamedSet;
026import org.apache.tapestry5.internal.util.NotificationEventCallback;
027import org.apache.tapestry5.ioc.BaseLocatable;
028import org.apache.tapestry5.ioc.Invokable;
029import org.apache.tapestry5.ioc.Location;
030import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
031import org.apache.tapestry5.ioc.internal.util.InternalUtils;
032import org.apache.tapestry5.ioc.internal.util.Orderer;
033import org.apache.tapestry5.ioc.internal.util.TapestryException;
034import org.apache.tapestry5.ioc.services.PerThreadValue;
035import org.apache.tapestry5.ioc.services.SymbolSource;
036import org.apache.tapestry5.ioc.util.AvailableValues;
037import org.apache.tapestry5.ioc.util.UnknownValueException;
038import org.apache.tapestry5.model.ComponentModel;
039import org.apache.tapestry5.model.ParameterModel;
040import org.apache.tapestry5.runtime.Component;
041import org.apache.tapestry5.runtime.*;
042import org.apache.tapestry5.services.Request;
043import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
044import org.slf4j.Logger;
045
046import 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 */
061public 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}