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
434                        .getMessage("AbstractComponent.attempt-to-change-component-id"));
435    
436            _id = value;
437        }
438        
439        public String getIdPath()
440        {
441            if (_idPath != null)
442                return _idPath;
443            
444            String containerIdPath;
445            
446            if (_container == null)
447                throw new NullPointerException(Tapestry.format("AbstractComponent.null-container", this));
448            
449            containerIdPath = _container.getIdPath();
450            
451            if (containerIdPath == null)
452                _idPath = _id;
453            else
454                _idPath = containerIdPath + "." + _id;
455    
456            return _idPath;
457        }
458        
459        /**
460         * {@inheritDoc}
461         * @since 4.1
462         */
463        public abstract String getClientId();
464        
465        public abstract void setClientId(String id);
466        
467        /**
468         * {@inheritDoc}
469         */
470        public String peekClientId()
471        {
472            if (getPage() == null)
473                return null;
474            
475            String id = getSpecifiedId();
476            if (id == null)
477                return null;
478            
479            return getPage().getRequestCycle().peekUniqueId(TapestryUtils.convertTapestryIdToNMToken(id));
480        }
481        
482        protected void generateClientId()
483        {
484            String id = getSpecifiedId();
485            
486            if (id != null && getPage() != null && getPage().getRequestCycle() != null)
487                 setClientId(getPage().getRequestCycle().getUniqueId(TapestryUtils.convertTapestryIdToNMToken(id)));
488        }
489        
490        protected String getBoundId()
491        {
492            if (_bindings == null)
493                return null;
494            
495            IBinding id = (IBinding)_bindings.get("id");
496            
497            if (id == null || id.getObject() == null)
498                return null;
499            
500            return id.getObject().toString();
501        }
502        
503        public String getTemplateTagName()
504        {
505            return _templateTagName;
506        }
507        
508        /** 
509         * {@inheritDoc}
510         */
511        public void setTemplateTagName(String tag)
512        {
513            if (_templateTagName != null)
514                throw new ApplicationRuntimeException(Tapestry.getMessage("AbstractComponent.attempt-to-change-template-tag"));
515            
516            _templateTagName = tag;
517        }
518    
519        public IPage getPage()
520        {
521            return _page;
522        }
523    
524        public void setPage(IPage value)
525        {
526            if (_page != null)
527                throw new ApplicationRuntimeException(Tapestry.getMessage("AbstractComponent.attempt-to-change-page"));
528    
529            _page = value;
530        }
531    
532        /**
533         * Renders all elements wrapped by the receiver.
534         */
535    
536        public void renderBody(IMarkupWriter writer, IRequestCycle cycle)
537        {
538            for (int i = 0; i < _bodyCount; i++)
539                cycle.getResponseBuilder().render(writer, _body[i], cycle);
540        }
541    
542        /**
543         * Adds the binding with the given name, replacing any existing binding with that name.
544         * <p>
545         * 
546         * @see #getBinding(String)
547         */
548    
549        public void setBinding(String name, IBinding binding)
550        {
551            Defense.notNull(name, "name");
552            Defense.notNull(binding, "binding");
553    
554            if (_bindings == null)
555                _bindings = new HashMap(MAP_SIZE);
556    
557            _bindings.put(name, binding);
558        }
559    
560        public String toString()
561        {
562            StringBuffer buffer;
563            
564            buffer = new StringBuffer(super.toString());
565    
566            buffer.append('[');
567    
568            buffer.append(getExtendedId());
569    
570            buffer.append(']');
571    
572            return buffer.toString();
573        }
574    
575        /**
576         * Returns an unmodifiable {@link Map}of components, keyed on component id. Never returns null,
577         * but may return an empty map. The returned map is immutable.
578         */
579    
580        public Map getComponents()
581        {
582            if (_components == null)
583                return EMPTY_MAP;
584    
585            return _components;
586    
587        }
588    
589        public Map getAssets()
590        {
591            if (_assets == null)
592                return EMPTY_MAP;
593    
594            return _assets;
595        }
596    
597        public IAsset getAsset(String name)
598        {
599            if (_assets == null)
600                return null;
601    
602            return (IAsset) _assets.get(name);
603        }
604    
605        public Collection getBindingNames()
606        {
607            // If no conainer, i.e. a page, then no bindings.
608    
609            if (_container == null)
610                return null;
611    
612            HashSet result = new HashSet();
613    
614            // All the informal bindings go into the bindings Map.
615    
616            if (_bindings != null)
617                result.addAll(_bindings.keySet());
618    
619            // Now, iterate over the formal parameters and add the formal parameters
620            // that have a binding.
621    
622            List names = getSpecification().getParameterNames();
623    
624            int count = names.size();
625    
626            for (int i = 0; i < count; i++)
627            {
628                String name = (String) names.get(i);
629    
630                if (result.contains(name))
631                    continue;
632    
633                if (getBinding(name) != null)
634                    result.add(name);
635            }
636    
637            return result;
638        }
639    
640        /**
641         * Returns an unmodifiable {@link Map}of all bindings for this component.
642         * 
643         * @since 1.0.5
644         */
645    
646        public Map getBindings()
647        {
648            if (_bindings == null)
649                return EMPTY_MAP;
650    
651            return _bindings;
652        }
653    
654        /**
655         * Returns a {@link ListenerMap}&nbsp;for the component. A ListenerMap contains a number of
656         * synthetic read-only properties that implement the {@link IActionListener}interface, but in
657         * fact, cause public instance methods to be invoked.
658         * 
659         * @since 1.0.2
660         */
661    
662        public ListenerMap getListeners()
663        {
664            // This is what's called a violation of the Law of Demeter!
665            // This should probably be converted over to some kind of injection, as with
666            // getMessages(), etc.
667    
668            if (_listeners == null)
669                _listeners = getPage().getEngine().getInfrastructure().getListenerMapSource().getListenerMapForObject(this);
670    
671            return _listeners;
672        }
673    
674        /**
675         * Returns the {@link IBeanProvider}for this component. This is lazily created the first time
676         * it is needed.
677         * 
678         * @since 1.0.4
679         */
680    
681        public IBeanProvider getBeans()
682        {
683            if (_beans == null)
684                _beans = new BeanProvider(this);
685    
686            return _beans;
687        }
688    
689        /**
690         * Invoked, as a convienience, from
691         * {@link #finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}. This implemenation
692         * does nothing. Subclasses may override without invoking this implementation.
693         * 
694         * @since 1.0.5
695         */
696    
697        protected void finishLoad()
698        {
699        }
700    
701        /**
702         * The main method used to render the component. Invokes
703         * {@link #prepareForRender(IRequestCycle)}, then
704         * {@link #renderComponent(IMarkupWriter, IRequestCycle)}.
705         * {@link #cleanupAfterRender(IRequestCycle)}is invoked in a <code>finally</code> block.
706         * <p>
707         * Subclasses should not override this method; instead they will implement
708         * {@link #renderComponent(IMarkupWriter, IRequestCycle)}.
709         * 
710         * @since 2.0.3
711         */
712    
713        public final void render(IMarkupWriter writer, IRequestCycle cycle)
714        {
715            try
716            {
717                _rendering = true;
718                
719                cycle.renderStackPush(this);
720                
721                generateClientId();
722                
723                prepareForRender(cycle);
724                
725                renderComponent(writer, cycle);
726            }
727            finally
728            {
729                _rendering = false;
730                
731                cleanupAfterRender(cycle);
732                
733                cycle.renderStackPop();
734            }
735        }
736    
737        /**
738         * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to prepare the component to render.
739         * This implementation sets JavaBeans properties from matching bound parameters. The default
740         * implementation of this method is empty.
741         * 
742         * @since 2.0.3
743         */
744    
745        protected void prepareForRender(IRequestCycle cycle)
746        {
747        }
748    
749        /**
750         * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to actually render the component
751         * (with any parameter values already set). This is the method that subclasses must implement.
752         * 
753         * @since 2.0.3
754         */
755    
756        protected abstract void renderComponent(IMarkupWriter writer, IRequestCycle cycle);
757        
758        /**
759         * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}after the component renders.
760         * 
761         * @since 2.0.3
762         */
763    
764        protected void cleanupAfterRender(IRequestCycle cycle)
765        {
766            getRenderWorker().renderComponent(cycle, this);
767        }
768    
769        public INamespace getNamespace()
770        {
771            return _namespace;
772        }
773    
774        public void setNamespace(INamespace namespace)
775        {
776            _namespace = namespace;
777        }
778    
779        /**
780         * Returns the body of the component, the element (which may be static HTML or components) that
781         * the component immediately wraps. May return null. Do not modify the returned array. The array
782         * may be padded with nulls.
783         * 
784         * @since 2.3
785         * @see #getBodyCount()
786         */
787    
788        public IRender[] getBody()
789        {
790            return _body;
791        }
792    
793        /**
794         * Returns the active number of elements in the the body, which may be zero.
795         * 
796         * @since 2.3
797         * @see #getBody()
798         */
799    
800        public int getBodyCount()
801        {
802            return _bodyCount;
803        }
804    
805        /**
806         * Empty implementation of
807         * {@link org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)}. This allows
808         * classes to implement {@link org.apache.tapestry.event.PageRenderListener}and only implement
809         * the {@link org.apache.tapestry.event.PageRenderListener#pageBeginRender(PageEvent)}method.
810         * 
811         * @since 3.0
812         */
813    
814        public void pageEndRender(PageEvent event)
815        {
816        }
817    
818        /**
819         * @since 4.0
820         */
821    
822        public final boolean isRendering()
823        {
824            return _rendering;
825        }
826    
827        /**
828         * Returns true if the component has been transitioned into its active state by invoking
829         * {@link #enterActiveState()}.
830         * 
831         * @since 4.0
832         */
833    
834        protected final boolean isInActiveState()
835        {
836            return _active;
837        }
838    
839        /** @since 4.0 */
840        public final void enterActiveState()
841        {
842            _active = true;
843        }
844    
845        /** @since 4.0 */
846    
847        protected final void checkActiveLock()
848        {
849            if (_active)
850                throw new UnsupportedOperationException(TapestryMessages.componentIsLocked(this));
851        }
852    
853        public Messages getMessages()
854        {
855            throw new IllegalStateException(TapestryMessages.providedByEnhancement("getMessages"));
856        }
857    
858        public IComponentSpecification getSpecification()
859        {
860            throw new IllegalStateException(TapestryMessages.providedByEnhancement("getSpecification"));
861        }
862    
863        /** @since 4.0 */
864        public final IContainedComponent getContainedComponent()
865        {
866            return _containedComponent;
867        }
868    
869        /** @since 4.0 */
870        public final void setContainedComponent(IContainedComponent containedComponent)
871        {
872            Defense.notNull(containedComponent, "containedComponent");
873    
874            if (_containedComponent != null)
875                throw new ApplicationRuntimeException(TapestryMessages
876                        .attemptToChangeContainedComponent(this));
877    
878            _containedComponent = containedComponent;
879        }
880        
881        /**
882         * {@inheritDoc}
883         */
884        public IComponentEventInvoker getEventInvoker()
885        {
886            throw new IllegalStateException(TapestryMessages.providedByEnhancement("getEventInvoker"));
887        }
888        
889        /**
890         * {@inheritDoc}
891         */
892        public void triggerEvent(IRequestCycle cycle, BrowserEvent event)
893        {
894            getEventInvoker().invokeListeners(this, cycle, event);
895        }
896        
897        public ComponentRenderWorker getRenderWorker()
898        {
899            throw new IllegalStateException(TapestryMessages.providedByEnhancement("getRenderWorker"));
900        }
901        
902        /**
903         * {@inheritDoc}
904         */
905        public boolean isStateful()
906        {
907            return false;
908        }
909        
910        /**
911         * {@inheritDoc}
912         */
913        public int hashCode()
914        {
915            final int prime = 31;
916            int result = 1;
917            result = prime * result + ((getClientId() == null) ? 0 : getClientId().hashCode());
918            result = prime * result + ((_id == null) ? 0 : _id.hashCode());
919            return result;
920        }
921        
922        /**
923         * {@inheritDoc}
924         */
925        public boolean equals(Object obj)
926        {
927            if (this == obj) return true;
928            if (obj == null) return false;
929            if (getClass() != obj.getClass()) return false;
930            final AbstractComponent other = (AbstractComponent) obj;
931            if (getClientId() == null) {
932                if (other.getClientId() != null) return false;
933            } else if (!getClientId().equals(other.getClientId())) return false;
934            if (_id == null) {
935                if (other._id != null) return false;
936            } else if (!_id.equals(other._id)) return false;
937            return true;
938        }
939    }