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