001    // Copyright 2004, 2005 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.tapestry;
016    
017    import org.apache.hivemind.ApplicationRuntimeException;
018    import org.apache.hivemind.Messages;
019    import org.apache.hivemind.impl.BaseLocatable;
020    import org.apache.hivemind.util.Defense;
021    import org.apache.tapestry.bean.BeanProvider;
022    import org.apache.tapestry.engine.IPageLoader;
023    import org.apache.tapestry.event.BrowserEvent;
024    import org.apache.tapestry.event.PageEvent;
025    import org.apache.tapestry.internal.Component;
026    import org.apache.tapestry.internal.event.IComponentEventInvoker;
027    import org.apache.tapestry.listener.ListenerMap;
028    import org.apache.tapestry.services.ComponentRenderWorker;
029    import org.apache.tapestry.spec.IComponentSpecification;
030    import org.apache.tapestry.spec.IContainedComponent;
031    
032    import java.util.*;
033    
034    /**
035     * Abstract base class implementing the {@link IComponent}interface.
036     * 
037     * @author Howard Lewis Ship
038     */
039    
040    public abstract class AbstractComponent extends BaseLocatable implements IDirectEvent, Component {
041        
042        private static final int MAP_SIZE = 5;
043        
044        private static final int BODY_INIT_SIZE = 5;
045    
046        /**
047         * Used in place of JDK 1.3's Collections.EMPTY_MAP (which is not available in JDK 1.2).
048         */
049    
050        private static final Map EMPTY_MAP = Collections.unmodifiableMap(new HashMap(1));
051        
052        /**
053         * The page that contains the component, possibly itself (if the component is in fact, a page).
054         */
055    
056        private IPage _page;
057    
058        /**
059         * The component which contains the component. This will only be null if the component is
060         * actually a page.
061         */
062    
063        private IComponent _container;
064    
065        /**
066         * The simple id of this component.
067         */
068    
069        private String _id;
070        
071        /**
072         * The fully qualified id of this component. This is calculated the first time it is needed,
073         * then cached for later.
074         */
075        private String _idPath;
076    
077        /**
078         * The html tag name that was used to reference the component.
079         */
080        private String _templateTagName;
081        
082        /**
083         * A {@link Map}of all bindings (for which there isn't a corresponding JavaBeans property); the
084         * keys are the names of formal and informal parameters.
085         */
086    
087        private Map _bindings;
088    
089        private Map _components;
090    
091        private INamespace _namespace;
092    
093        /**
094         * The number of {@link IRender}objects in the body of this component.
095         */
096    
097        protected int _bodyCount = 0;
098    
099        /**
100         * An aray of elements in the body of this component.
101         */
102    
103        protected IRender[] _body;
104    
105        /**
106         * The components' asset map.
107         */
108    
109        private Map _assets;
110    
111        /**
112         * A mapping that allows public instance methods to be dressed up as {@link IActionListener}
113         * listener objects.
114         * 
115         * @since 1.0.2
116         */
117    
118        private ListenerMap _listeners;
119    
120        /**
121         * A bean provider; these are lazily created as needed.
122         * 
123         * @since 1.0.4
124         */
125    
126        private IBeanProvider _beans;
127    
128        /**
129         * Returns true if the component is currently rendering.
130         * 
131         * @see #prepareForRender(IRequestCycle)
132         * @see #cleanupAfterRender(IRequestCycle)
133         * @since 4.0
134         */
135    
136        private boolean _rendering;
137    
138        /**
139         * @since 4.0
140         */
141    
142        private boolean _active;
143    
144        /** @since 4.0 */
145    
146        private IContainedComponent _containedComponent;
147    
148        private boolean _hasEvents;
149        
150        public void addAsset(String name, IAsset asset)
151        {
152            Defense.notNull(name, "name");
153            Defense.notNull(asset, "asset");
154    
155            checkActiveLock();
156    
157            if (_assets == null)
158                _assets = new HashMap(MAP_SIZE);
159    
160            _assets.put(name, asset);
161        }
162    
163        public void addComponent(IComponent component)
164        {
165            Defense.notNull(component, "component");
166    
167            checkActiveLock();
168    
169            if (_components == null)
170                _components = new HashMap(MAP_SIZE);
171            
172            _components.put(component.getId(), component);
173        }
174    
175        /**
176         * Adds an element (which may be static text or a component) as a body element of this
177         * component. Such elements are rendered by {@link #renderBody(IMarkupWriter, IRequestCycle)}.
178         * 
179         * @since 2.2
180         */
181    
182        public void addBody(IRender element)
183        {
184            Defense.notNull(element, "element");
185            
186            // Should check the specification to see if this component
187            // allows body. Curently, this is checked by the component
188            // in render(), which is silly.
189    
190            if (_body == null)
191            {
192                _body = new IRender[BODY_INIT_SIZE];
193                _body[0] = element;
194    
195                _bodyCount = 1;
196                return;
197            }
198    
199            // No more room? Make the array bigger.
200    
201            if (_bodyCount == _body.length)
202            {
203                IRender[] newWrapped;
204    
205                newWrapped = new IRender[_body.length * 2];
206    
207                System.arraycopy(_body, 0, newWrapped, 0, _bodyCount);
208    
209                _body = newWrapped;
210            }
211    
212            _body[_bodyCount++] = element;
213        }
214    
215        
216        public IRender[] getContainedRenderers()
217        {
218            return _body;
219        }
220    
221        public IRender[] getInnerRenderers()
222        {
223            return null;
224        }
225    
226        public boolean hasEvents()
227        {
228            return _hasEvents;
229        }
230    
231        public void setHasEvents(boolean hasEvents)
232        {
233            _hasEvents = hasEvents;
234        }
235    
236        /**
237         * Invokes {@link #finishLoad()}. Subclasses may overide as needed, but must invoke this
238         * implementation. {@link BaseComponent} loads its HTML template.
239         */
240    
241        public void finishLoad(IRequestCycle cycle, IPageLoader loader, IComponentSpecification specification)
242        {
243            finishLoad();
244        }
245    
246        /**
247         * Converts informal parameters into additional attributes on the curently open tag.
248         * <p>
249         * Invoked from subclasses to allow additional attributes to be specified within a tag (this
250         * works best when there is a one-to-one corespondence between an {@link IComponent}and a HTML
251         * element.
252         * <p>
253         * Iterates through the bindings for this component. Filters out bindings for formal parameters.
254         * <p>
255         * For each acceptible key, the value is extracted using {@link IBinding#getObject()}. If the
256         * value is null, no attribute is written.
257         * <p>
258         * If the value is an instance of {@link IAsset}, then {@link IAsset#buildURL()}
259         * is invoked to convert the asset to a URL.
260         * <p>
261         * Finally, {@link IMarkupWriter#attribute(String,String)}is invoked with the value (or the
262         * URL).
263         * <p>
264         * The most common use for informal parameters is to support the HTML class attribute (for use
265         * with cascading style sheets) and to specify JavaScript event handlers.
266         * <p>
267         * Components are only required to generate attributes on the result phase; this can be skipped
268         * during the rewind phase.
269         */
270    
271        protected void renderInformalParameters(IMarkupWriter writer, IRequestCycle cycle)
272        {
273            String attribute;
274    
275            if (_bindings == null)
276                return;
277    
278            Iterator i = _bindings.entrySet().iterator();
279    
280            while (i.hasNext())
281            {
282                Map.Entry entry = (Map.Entry) i.next();
283                String name = (String) entry.getKey();
284    
285                if (isFormalParameter(name))
286                    continue;
287    
288                IBinding binding = (IBinding) entry.getValue();
289    
290                Object value = binding.getObject();
291                if (value == null)
292                    continue;
293    
294                if (value instanceof IAsset)
295                {
296                    IAsset asset = (IAsset) value;
297    
298                    // Get the URL of the asset and insert that.
299    
300                    attribute = asset.buildURL();
301                }
302                else
303                    attribute = value.toString();
304                
305                writer.attribute(name, attribute);
306            }
307        }
308        
309        /**
310         * Renders the (unique) id attribute for this component. 
311         * 
312         * @param writer
313         *          The writer to render attribute in.
314         * @param cycle
315         *          The current request.
316         */
317        protected void renderIdAttribute(IMarkupWriter writer, IRequestCycle cycle)
318        {
319            String id = getClientId();
320            
321            if (id != null)
322                writer.attribute("id", id);
323        }
324        
325        /** @since 4.0 */
326        private boolean isFormalParameter(String name)
327        {
328            Defense.notNull(name, "name");
329            
330            return getSpecification().getParameter(name) != null;
331        }
332        
333        /**
334         * Returns the named binding, or null if it doesn't exist.
335         * <p>
336         * In Tapestry 3.0, it was possible to force a binding to be stored in a component property by
337         * defining a concrete or abstract property named "nameBinding" of type {@link IBinding}. This
338         * has been removed in release 4.0 and bindings are always stored inside a Map of the component.
339         * 
340         * @see #setBinding(String,IBinding)
341         */
342    
343        public IBinding getBinding(String name)
344        {
345            Defense.notNull(name, "name");
346    
347            if (_bindings == null)
348                return null;
349    
350            return (IBinding) _bindings.get(name);
351        }
352    
353        /**
354         * Returns true if the specified parameter is bound.
355         * 
356         * @since 4.0
357         */
358    
359        public boolean isParameterBound(String parameterName)
360        {
361            Defense.notNull(parameterName, "parameterName");
362    
363            return _bindings != null && _bindings.containsKey(parameterName);
364        }
365    
366        public IComponent getComponent(String id)
367        {
368            Defense.notNull(id, "id");
369    
370            IComponent result = null;
371    
372            if (_components != null)
373                result = (IComponent) _components.get(id);
374    
375            if (result == null)
376                throw new ApplicationRuntimeException(Tapestry.format("no-such-component", this, id),
377                        this, null, null);
378    
379            return result;
380        }
381    
382        public IComponent getContainer()
383        {
384            return _container;
385        }
386    
387        public void setContainer(IComponent value)
388        {
389            checkActiveLock();
390    
391            if (_container != null)
392                throw new ApplicationRuntimeException(Tapestry
393                        .getMessage("AbstractComponent.attempt-to-change-container"));
394    
395            _container = value;
396        }
397    
398        /**
399         * Returns the name of the page, a slash, and this component's id path. Pages are different,
400         * they override this method to simply return their page name.
401         * 
402         * @see #getIdPath()
403         */
404    
405        public String getExtendedId()
406        {
407            if (_page == null)
408                return null;
409            
410            return _page.getPageName() + "/" + getIdPath();
411        }
412    
413        /** @since 4.1 */
414        
415        public String getSpecifiedId()
416        {
417            String id = getBoundId();
418            
419            if (id != null)
420                return id;
421            
422            return getId();
423        }
424        
425        public String getId()
426        {
427            return _id;
428        }
429    
430        public void setId(String value)
431        {
432            if (_id != null)
433                throw new ApplicationRuntimeException(Tapestry.getMessage("AbstractComponent.attempt-to-change-component-id"));
434    
435            _id = value;
436        }
437        
438        public String getIdPath()
439        {
440            if (_idPath != null)
441                return _idPath;
442            
443            String containerIdPath;
444            
445            if (_container == null)
446                throw new NullPointerException(Tapestry.format("AbstractComponent.null-container", this));
447            
448            containerIdPath = _container.getIdPath();
449            
450            if (containerIdPath == null)
451                _idPath = _id;
452            else
453                _idPath = containerIdPath + "." + _id;
454    
455            return _idPath;
456        }
457        
458        /**
459         * {@inheritDoc}
460         * @since 4.1
461         */
462        public abstract String getClientId();
463        
464        public abstract void setClientId(String id);
465        
466        /**
467         * {@inheritDoc}
468         */
469        public String peekClientId()
470        {
471            if (getPage() == null)
472                return null;
473            
474            String id = getSpecifiedId();
475            if (id == null)
476                return null;
477            
478            return getPage().getRequestCycle().peekUniqueId(TapestryUtils.convertTapestryIdToNMToken(id));
479        }
480        
481        protected void generateClientId()
482        {
483            String id = getSpecifiedId();
484            
485            if (id != null && getPage() != null && getPage().getRequestCycle() != null)
486                 setClientId(getPage().getRequestCycle().getUniqueId(TapestryUtils.convertTapestryIdToNMToken(id)));
487        }
488        
489        protected String getBoundId()
490        {
491            if (_bindings == null)
492                return null;
493            
494            IBinding id = (IBinding)_bindings.get("id");
495            
496            if (id == null || id.getObject() == null)
497                return null;
498            
499            return id.getObject().toString();
500        }
501        
502        public String getTemplateTagName()
503        {
504            return _templateTagName;
505        }
506        
507        /** 
508         * {@inheritDoc}
509         */
510        public void setTemplateTagName(String tag)
511        {
512            if (_templateTagName != null)
513                throw new ApplicationRuntimeException(Tapestry.getMessage("AbstractComponent.attempt-to-change-template-tag"));
514            
515            _templateTagName = tag;
516        }
517    
518        public IPage getPage()
519        {
520            return _page;
521        }
522    
523        public void setPage(IPage value)
524        {
525            if (_page != null)
526                throw new ApplicationRuntimeException(Tapestry.getMessage("AbstractComponent.attempt-to-change-page"));
527    
528            _page = value;
529        }
530    
531        /**
532         * Renders all elements wrapped by the receiver.
533         */
534    
535        public void renderBody(IMarkupWriter writer, IRequestCycle cycle)
536        {
537            for (int i = 0; i < _bodyCount; i++)
538                cycle.getResponseBuilder().render(writer, _body[i], cycle);
539        }
540    
541        /**
542         * Adds the binding with the given name, replacing any existing binding with that name.
543         * <p>
544         * 
545         * @see #getBinding(String)
546         */
547    
548        public void setBinding(String name, IBinding binding)
549        {
550            Defense.notNull(name, "name");
551            Defense.notNull(binding, "binding");
552    
553            if (_bindings == null)
554                _bindings = new HashMap(MAP_SIZE);
555    
556            _bindings.put(name, binding);
557        }
558    
559        public String toString()
560        {
561            StringBuffer buffer;
562            
563            buffer = new StringBuffer(super.toString());
564    
565            buffer.append('[');
566    
567            buffer.append(getExtendedId());
568    
569            buffer.append(']');
570    
571            return buffer.toString();
572        }
573    
574        /**
575         * Returns an unmodifiable {@link Map}of components, keyed on component id. Never returns null,
576         * but may return an empty map. The returned map is immutable.
577         */
578    
579        public Map getComponents()
580        {
581            if (_components == null)
582                return EMPTY_MAP;
583    
584            return _components;
585    
586        }
587    
588        public Map getAssets()
589        {
590            if (_assets == null)
591                return EMPTY_MAP;
592    
593            return _assets;
594        }
595    
596        public IAsset getAsset(String name)
597        {
598            if (_assets == null)
599                return null;
600    
601            return (IAsset) _assets.get(name);
602        }
603    
604        public Collection getBindingNames()
605        {
606            // If no conainer, i.e. a page, then no bindings.
607    
608            if (_container == null)
609                return null;
610    
611            HashSet result = new HashSet();
612    
613            // All the informal bindings go into the bindings Map.
614    
615            if (_bindings != null)
616                result.addAll(_bindings.keySet());
617    
618            // Now, iterate over the formal parameters and add the formal parameters
619            // that have a binding.
620    
621            List names = getSpecification().getParameterNames();
622    
623            int count = names.size();
624    
625            for (int i = 0; i < count; i++)
626            {
627                String name = (String) names.get(i);
628    
629                if (result.contains(name))
630                    continue;
631    
632                if (getBinding(name) != null)
633                    result.add(name);
634            }
635    
636            return result;
637        }
638    
639        /**
640         * Returns an unmodifiable {@link Map}of all bindings for this component.
641         * 
642         * @since 1.0.5
643         */
644    
645        public Map getBindings()
646        {
647            if (_bindings == null)
648                return EMPTY_MAP;
649    
650            return _bindings;
651        }
652    
653        /**
654         * Returns a {@link ListenerMap}&nbsp;for the component. A ListenerMap contains a number of
655         * synthetic read-only properties that implement the {@link IActionListener}interface, but in
656         * fact, cause public instance methods to be invoked.
657         * 
658         * @since 1.0.2
659         */
660    
661        public ListenerMap getListeners()
662        {
663            // This is what's called a violation of the Law of Demeter!
664            // This should probably be converted over to some kind of injection, as with
665            // getMessages(), etc.
666    
667            if (_listeners == null)
668                _listeners = getPage().getEngine().getInfrastructure().getListenerMapSource().getListenerMapForObject(this);
669    
670            return _listeners;
671        }
672    
673        /**
674         * Returns the {@link IBeanProvider}for this component. This is lazily created the first time
675         * it is needed.
676         * 
677         * @since 1.0.4
678         */
679    
680        public IBeanProvider getBeans()
681        {
682            if (_beans == null)
683                _beans = new BeanProvider(this);
684    
685            return _beans;
686        }
687    
688        /**
689         * Invoked, as a convienience, from
690         * {@link #finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}. This implemenation
691         * does nothing. Subclasses may override without invoking this implementation.
692         * 
693         * @since 1.0.5
694         */
695    
696        protected void finishLoad()
697        {
698        }
699    
700        /**
701         * The main method used to render the component. Invokes
702         * {@link #prepareForRender(IRequestCycle)}, then
703         * {@link #renderComponent(IMarkupWriter, IRequestCycle)}.
704         * {@link #cleanupAfterRender(IRequestCycle)}is invoked in a <code>finally</code> block.
705         * <p>
706         * Subclasses should not override this method; instead they will implement
707         * {@link #renderComponent(IMarkupWriter, IRequestCycle)}.
708         * 
709         * @since 2.0.3
710         */
711    
712        public final void render(IMarkupWriter writer, IRequestCycle cycle)
713        {
714            try
715            {
716                _rendering = true;
717                
718                cycle.renderStackPush(this);
719                
720                generateClientId();
721                
722                prepareForRender(cycle);
723                
724                renderComponent(writer, cycle);
725            }
726            finally
727            {
728                _rendering = false;
729                
730                cleanupAfterRender(cycle);
731                
732                cycle.renderStackPop();
733            }
734        }
735    
736        /**
737         * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to prepare the component to render.
738         * This implementation sets JavaBeans properties from matching bound parameters. The default
739         * implementation of this method is empty.
740         * 
741         * @since 2.0.3
742         */
743    
744        protected void prepareForRender(IRequestCycle cycle)
745        {
746        }
747    
748        /**
749         * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to actually render the component
750         * (with any parameter values already set). This is the method that subclasses must implement.
751         * 
752         * @since 2.0.3
753         */
754    
755        protected abstract void renderComponent(IMarkupWriter writer, IRequestCycle cycle);
756        
757        /**
758         * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}after the component renders.
759         * 
760         * @since 2.0.3
761         */
762    
763        protected void cleanupAfterRender(IRequestCycle cycle)
764        {
765            getRenderWorker().renderComponent(cycle, this);
766        }
767    
768        public INamespace getNamespace()
769        {
770            return _namespace;
771        }
772    
773        public void setNamespace(INamespace namespace)
774        {
775            _namespace = namespace;
776        }
777    
778        /**
779         * Returns the body of the component, the element (which may be static HTML or components) that
780         * the component immediately wraps. May return null. Do not modify the returned array. The array
781         * may be padded with nulls.
782         * 
783         * @since 2.3
784         * @see #getBodyCount()
785         */
786    
787        public IRender[] getBody()
788        {
789            return _body;
790        }
791    
792        /**
793         * Returns the active number of elements in the the body, which may be zero.
794         * 
795         * @since 2.3
796         * @see #getBody()
797         */
798    
799        public int getBodyCount()
800        {
801            return _bodyCount;
802        }
803    
804        /**
805         * Empty implementation of
806         * {@link org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)}. This allows
807         * classes to implement {@link org.apache.tapestry.event.PageRenderListener}and only implement
808         * the {@link org.apache.tapestry.event.PageRenderListener#pageBeginRender(PageEvent)}method.
809         * 
810         * @since 3.0
811         */
812    
813        public void pageEndRender(PageEvent event)
814        {
815        }
816    
817        /**
818         * @since 4.0
819         */
820    
821        public final boolean isRendering()
822        {
823            return _rendering;
824        }
825    
826        /**
827         * Returns true if the component has been transitioned into its active state by invoking
828         * {@link #enterActiveState()}.
829         * 
830         * @since 4.0
831         */
832    
833        protected final boolean isInActiveState()
834        {
835            return _active;
836        }
837    
838        /** @since 4.0 */
839        public final void enterActiveState()
840        {
841            _active = true;
842        }
843    
844        /** @since 4.0 */
845    
846        protected final void checkActiveLock()
847        {
848            if (_active)
849                throw new UnsupportedOperationException(TapestryMessages.componentIsLocked(this));
850        }
851    
852        public Messages getMessages()
853        {
854            throw new IllegalStateException(TapestryMessages.providedByEnhancement("getMessages"));
855        }
856    
857        public IComponentSpecification getSpecification()
858        {
859            throw new IllegalStateException(TapestryMessages.providedByEnhancement("getSpecification"));
860        }
861    
862        /** @since 4.0 */
863        public final IContainedComponent getContainedComponent()
864        {
865            return _containedComponent;
866        }
867    
868        /** @since 4.0 */
869        public final void setContainedComponent(IContainedComponent containedComponent)
870        {
871            Defense.notNull(containedComponent, "containedComponent");
872    
873            if (_containedComponent != null)
874                throw new ApplicationRuntimeException(TapestryMessages
875                        .attemptToChangeContainedComponent(this));
876    
877            _containedComponent = containedComponent;
878        }
879        
880        /**
881         * {@inheritDoc}
882         */
883        public IComponentEventInvoker getEventInvoker()
884        {
885            throw new IllegalStateException(TapestryMessages.providedByEnhancement("getEventInvoker"));
886        }
887        
888        /**
889         * {@inheritDoc}
890         */
891        public void triggerEvent(IRequestCycle cycle, BrowserEvent event)
892        {
893            getEventInvoker().invokeListeners(this, cycle, event);
894        }
895        
896        public ComponentRenderWorker getRenderWorker()
897        {
898            throw new IllegalStateException(TapestryMessages.providedByEnhancement("getRenderWorker"));
899        }
900        
901        /**
902         * {@inheritDoc}
903         */
904        public boolean isStateful()
905        {
906            return false;
907        }
908        
909        /**
910         * {@inheritDoc}
911         */
912        public int hashCode()
913        {
914            final int prime = 31;
915            int result = 1;
916            result = prime * result + ((getClientId() == null) ? 0 : getClientId().hashCode());
917            result = prime * result + ((_id == null) ? 0 : _id.hashCode());
918            return result;
919        }
920        
921        /**
922         * {@inheritDoc}
923         */
924        public boolean equals(Object obj)
925        {
926            if (this == obj) return true;
927            if (obj == null) return false;
928            if (getClass() != obj.getClass()) return false;
929            final AbstractComponent other = (AbstractComponent) obj;
930            if (getClientId() == null) {
931                if (other.getClientId() != null) return false;
932            } else if (!getClientId().equals(other.getClientId())) return false;
933            if (_id == null) {
934                if (other._id != null) return false;
935            } else if (!_id.equals(other._id)) return false;
936            return true;
937        }
938    }