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.spec;
016    
017    import org.apache.hivemind.ApplicationRuntimeException;
018    import org.apache.hivemind.HiveMind;
019    import org.apache.hivemind.Resource;
020    import org.apache.hivemind.util.Defense;
021    import org.apache.hivemind.util.ToStringBuilder;
022    import org.apache.tapestry.IComponent;
023    import org.apache.tapestry.IForm;
024    import org.apache.tapestry.event.BrowserEvent;
025    import org.apache.tapestry.internal.event.ComponentEventProperty;
026    import org.apache.tapestry.internal.event.EventBoundListener;
027    
028    import java.util.*;
029    
030    /**
031     * A specification for a component, as read from an XML specification file.
032     * <p>
033     * A specification consists of
034     * <ul>
035     * <li>An implementing class
036     * <li>An optional description
037     * <li>A set of contained components
038     * <li>Bindings for the properties of each contained component
039     * <li>A set of named assets
040     * <li>Definitions for managed beans
041     * <li>Any reserved names (used for HTML attributes)
042     * <li>Declared properties
043     * <li>Property injections
044     * </ul>
045     * <p>
046     * From this information, an actual component may be instantiated and initialized. Instantiating a
047     * component is usually a recursive process, since to initialize a container component, it is
048     * necessary to instantiate and initialize its contained components as well.
049     * 
050     * @see org.apache.tapestry.IComponent
051     * @see IContainedComponent
052     * @see org.apache.tapestry.engine.IPageLoader
053     * @author Howard Lewis Ship
054     */
055    
056    public class ComponentSpecification extends LocatablePropertyHolder implements IComponentSpecification
057    {
058        /**
059         * Keyed on component id, value is {@link IContainedComponent}.
060         */
061    
062        protected Map _components;
063    
064        /**
065         * Keyed on asset name, value is {@link IAssetSpecification}.
066         */
067    
068        protected Map _assets;
069    
070        /**
071         * Defines all formal parameters. Keyed on parameter name, value is
072         * {@link IParameterSpecification}.
073         */
074    
075        protected Map _parameters;
076    
077        /**
078         * Defines all helper beans. Keyed on name, value is {@link IBeanSpecification}.
079         * 
080         * @since 1.0.4
081         */
082    
083        protected Map _beans;
084    
085        /**
086         * The names of all reserved informal parameter names (as lower-case). This allows the page
087         * loader to filter out any informal parameters during page load, rather than during render.
088         * 
089         * @since 1.0.5
090         */
091    
092        protected Set _reservedParameterNames;
093    
094        private String _componentClassName;
095    
096        /** @since 1.0.9 * */
097    
098        private String _description;
099        
100        /**
101         * Is the component allowed to have a body (that is, wrap other elements?).
102         */
103    
104        private boolean _allowBody = true;
105    
106        /**
107         * Is the component allow to have informal parameter specified.
108         */
109    
110        private boolean _allowInformalParameters = true;
111    
112        /**
113         * The XML Public Id used when the page or component specification was read (if applicable).
114         * 
115         * @since 2.2
116         */
117    
118        private String _publicId;
119    
120        /**
121         * Indicates that the specification is for a page, not a component.
122         * 
123         * @since 2.2
124         */
125    
126        private boolean _pageSpecification;
127    
128        /**
129         * The location from which the specification was obtained.
130         * 
131         * @since 3.0
132         */
133    
134        private Resource _specificationLocation;
135    
136        /**
137         * A Map of {@link IPropertySpecification}keyed on the name of the property.
138         * 
139         * @since 3.0
140         */
141    
142        private Map _propertySpecifications;
143    
144        /**
145         * List of {@link InjectSpecification}.
146         * 
147         * @since 4.0
148         */
149    
150        private List _injectSpecifications;
151    
152        /**
153         * Keyed on property name, value is some other object (such as an IAssetSpecification) that has
154         * claimed a property of the page.
155         * 
156         * @since 4.0
157         */
158    
159        private Map _claimedProperties;
160    
161        /**
162         * @since 4.0
163         */
164    
165        private boolean _deprecated = false;
166        
167        private Map _componentEvents = new HashMap();
168        private Map _elementEvents = new HashMap();
169    
170        /**
171         * @throws ApplicationRuntimeException
172         *             if the name already exists.
173         */
174    
175        public void addAsset(String name, IAssetSpecification asset)
176        {
177            if (_assets == null)
178                _assets = new HashMap();
179    
180            IAssetSpecification existing = (IAssetSpecification) _assets.get(name);
181    
182            if (existing != null)
183                throw new ApplicationRuntimeException(SpecMessages.duplicateAsset(name, existing),
184                        asset.getLocation(), null);
185    
186            claimProperty(asset.getPropertyName(), asset);
187    
188            _assets.put(name, asset);
189        }
190    
191        /**
192         * @throws ApplicationRuntimeException if the id is already defined.
193         */
194    
195        public void addComponent(String id, IContainedComponent component)
196        {
197            if (_components == null)
198                _components = new HashMap();
199    
200            IContainedComponent existing = (IContainedComponent) _components.get(id);
201    
202            if (existing != null)
203                throw new ApplicationRuntimeException(SpecMessages.duplicateComponent(id, existing),
204                        component.getLocation(), null);
205    
206            _components.put(id, component);
207    
208            claimProperty(component.getPropertyName(), component);
209        }
210    
211        /**
212         * Adds the parameter. The name is added as a reserved name.
213         * 
214         * @throws ApplicationRuntimeException if the name already exists.
215         */
216    
217        public void addParameter(IParameterSpecification spec)
218        {
219            if (_parameters == null)
220                _parameters = new HashMap();
221    
222            String name = spec.getParameterName();
223    
224            addParameterByName(name, spec);
225    
226            Iterator i = spec.getAliasNames().iterator();
227            while (i.hasNext())
228            {
229                String alias = (String) i.next();
230    
231                addParameterByName(alias, spec);
232            }
233    
234            claimProperty(spec.getPropertyName(), spec);
235        }
236    
237        private void addParameterByName(String name, IParameterSpecification spec)
238        {
239            IParameterSpecification existing = (IParameterSpecification) _parameters.get(name);
240    
241            if (existing != null)
242                throw new ApplicationRuntimeException(SpecMessages.duplicateParameter(name, existing),
243                        spec.getLocation(), null);
244    
245            _parameters.put(name, spec);
246    
247            addReservedParameterName(name);
248        }
249    
250        /**
251         * Returns true if the component is allowed to wrap other elements (static HTML or other
252         * components). The default is true.
253         * 
254         * @see #setAllowBody(boolean)
255         */
256    
257        public boolean getAllowBody()
258        {
259            return _allowBody;
260        }
261    
262        /**
263         * Returns true if the component allows informal parameters (parameters not formally defined).
264         * Informal parameters are generally used to create additional HTML attributes for an HTML tag
265         * rendered by the component. This is often used to specify JavaScript event handlers or the
266         * class of the component (for Cascarding Style Sheets).
267         * <p>
268         * The default value is true.
269         * 
270         * @see #setAllowInformalParameters(boolean)
271         */
272    
273        public boolean getAllowInformalParameters()
274        {
275            return _allowInformalParameters;
276        }
277    
278        /**
279         * Returns the {@link IAssetSpecification}with the given name, or null if no such specification
280         * exists.
281         * 
282         * @see #addAsset(String,IAssetSpecification)
283         */
284    
285        public IAssetSpecification getAsset(String name)
286        {
287            return (IAssetSpecification) get(_assets, name);
288        }
289    
290        /**
291         * Returns a <code>List</code> of the String names of all assets, in alphabetical order.
292         */
293    
294        public List getAssetNames()
295        {
296            return sortedKeys(_assets);
297        }
298    
299        /**
300         * Returns the specification of a contained component with the given id, or null if no such
301         * contained component exists.
302         * 
303         * @see #addComponent(String, IContainedComponent)
304         */
305    
306        public IContainedComponent getComponent(String id)
307        {
308            return (IContainedComponent) get(_components, id);
309        }
310    
311        
312        public String getComponentClassName()
313        {
314            return _componentClassName;
315        }
316    
317        /**
318         * Returns an <code>List</code> of the String names of the {@link IContainedComponent}s for
319         * this component.
320         * 
321         * @see #addComponent(String, IContainedComponent)
322         */
323    
324        public List getComponentIds()
325        {
326            return sortedKeys(_components);
327        }
328    
329        /**
330         * Returns the specification of a parameter with the given name, or null if no such parameter
331         * exists.
332         * 
333         * @see #addParameterByName(String, IParameterSpecification)
334         */
335    
336        public IParameterSpecification getParameter(String name)
337        {
338            return (IParameterSpecification) get(_parameters, name);
339        }
340    
341        public Collection getRequiredParameters()
342        {
343            if (_parameters == null)
344                return Collections.EMPTY_LIST;
345    
346            Collection result = new ArrayList();
347    
348            Iterator i = _parameters.entrySet().iterator();
349            while (i.hasNext())
350            {
351                Map.Entry entry = (Map.Entry) i.next();
352                String name = (String) entry.getKey();
353                IParameterSpecification spec = (IParameterSpecification) entry.getValue();
354    
355                if (!spec.isRequired())
356                    continue;
357    
358                if (!name.equals(spec.getParameterName()))
359                    continue;
360    
361                result.add(spec);
362            }
363    
364            return result;
365        }
366    
367        /**
368         * Returns a List of of String names of all parameters. This list is in alphabetical order.
369         * 
370         * @see #addParameterByName(String, IParameterSpecification)
371         */
372    
373        public List getParameterNames()
374        {
375            return sortedKeys(_parameters);
376        }
377    
378        public void setAllowBody(boolean value)
379        {
380            _allowBody = value;
381        }
382    
383        public void setAllowInformalParameters(boolean value)
384        {
385            _allowInformalParameters = value;
386        }
387    
388        public void setComponentClassName(String value)
389        {
390            _componentClassName = value;
391        }
392    
393        /**
394         * @since 1.0.4
395         * @throws ApplicationRuntimeException
396         *             if the bean already has a specification.
397         */
398    
399        public void addBeanSpecification(String name, IBeanSpecification specification)
400        {
401            if (_beans == null)
402                _beans = new HashMap();
403    
404            IBeanSpecification existing = (IBeanSpecification) _beans.get(name);
405    
406            if (existing != null)
407                throw new ApplicationRuntimeException(SpecMessages.duplicateBean(name, existing),
408                        specification.getLocation(), null);
409    
410            claimProperty(specification.getPropertyName(), specification);
411    
412            _beans.put(name, specification);
413        }
414    
415        /**
416         * Returns the {@link IBeanSpecification}for the given name, or null if not such specification
417         * exists.
418         * 
419         * @since 1.0.4
420         */
421    
422        public IBeanSpecification getBeanSpecification(String name)
423        {
424            return (IBeanSpecification) get(_beans, name);
425        }
426    
427        /**
428         * Returns an unmodifiable collection of the names of all beans.
429         */
430    
431        public Collection getBeanNames()
432        {
433            if (_beans == null)
434                return Collections.EMPTY_LIST;
435    
436            return Collections.unmodifiableCollection(_beans.keySet());
437        }
438    
439        /**
440         * Adds the value as a reserved name. Reserved names are not allowed as the names of informal
441         * parameters. Since the comparison is caseless, the value is converted to lowercase before
442         * being stored.
443         * 
444         * @since 1.0.5
445         */
446    
447        public void addReservedParameterName(String value)
448        {
449            if (_reservedParameterNames == null)
450                _reservedParameterNames = new HashSet();
451    
452            _reservedParameterNames.add(value.toLowerCase());
453        }
454    
455        /**
456         * Returns true if the value specified is in the reserved name list. The comparison is caseless.
457         * All formal parameters are automatically in the reserved name list, as well as any additional
458         * reserved names specified in the component specification. The latter refer to HTML attributes
459         * generated directly by the component.
460         * 
461         * @since 1.0.5
462         */
463    
464        public boolean isReservedParameterName(String value)
465        {
466            if (_reservedParameterNames == null)
467                return false;
468    
469            return _reservedParameterNames.contains(value.toLowerCase());
470        }
471    
472        public String toString()
473        {
474            ToStringBuilder builder = new ToStringBuilder(this);
475    
476            builder.append("componentClassName", _componentClassName);
477            builder.append("pageSpecification", _pageSpecification);
478            builder.append("specificationLocation", _specificationLocation);
479            builder.append("allowBody", _allowBody);
480            builder.append("allowInformalParameter", _allowInformalParameters);
481    
482            return builder.toString();
483        }
484    
485        /**
486         * Returns the documentation for this component.
487         * 
488         * @since 1.0.9
489         */
490    
491        public String getDescription()
492        {
493            return _description;
494        }
495    
496        /**
497         * Sets the documentation for this component.
498         * 
499         * @since 1.0.9
500         */
501    
502        public void setDescription(String description)
503        {
504            _description = description;
505        }
506    
507        /**
508         * Returns the XML Public Id for the specification file, or null if not applicable.
509         * <p>
510         * This method exists as a convienience for the Spindle plugin. A previous method used an
511         * arbitrary version string, the public id is more useful and less ambiguous.
512         * 
513         * @since 2.2
514         */
515    
516        public String getPublicId()
517        {
518            return _publicId;
519        }
520    
521        /** @since 2.2 * */
522    
523        public void setPublicId(String publicId)
524        {
525            _publicId = publicId;
526        }
527    
528        /**
529         * Returns true if the specification is known to be a page specification and not a component
530         * specification. Earlier versions of the framework did not distinguish between the two, but
531         * starting in 2.2, there are seperate XML entities for pages and components. Pages omit several
532         * attributes and entities related to parameters, as parameters only make sense for components.
533         * 
534         * @since 2.2
535         */
536    
537        public boolean isPageSpecification()
538        {
539            return _pageSpecification;
540        }
541    
542        /** @since 2.2 * */
543    
544        public void setPageSpecification(boolean pageSpecification)
545        {
546            _pageSpecification = pageSpecification;
547        }
548    
549        /** @since 2.2 * */
550    
551        private List sortedKeys(Map input)
552        {
553            if (input == null)
554                return Collections.EMPTY_LIST;
555    
556            List result = new ArrayList(input.keySet());
557    
558            Collections.sort(result);
559    
560            return result;
561        }
562    
563        /** @since 2.2 * */
564    
565        private Object get(Map map, Object key)
566        {
567            if (map == null)
568                return null;
569    
570            return map.get(key);
571        }
572    
573        /** @since 3.0 * */
574    
575        public Resource getSpecificationLocation()
576        {
577            return _specificationLocation;
578        }
579    
580        /** @since 3.0 * */
581    
582        public void setSpecificationLocation(Resource specificationLocation)
583        {
584            _specificationLocation = specificationLocation;
585        }
586    
587        /**
588         * Adds a new property specification. The name of the property must not already be defined (and
589         * must not change after being added).
590         * 
591         * @since 3.0
592         */
593    
594        public void addPropertySpecification(IPropertySpecification spec)
595        {
596            if (_propertySpecifications == null)
597                _propertySpecifications = new HashMap();
598    
599            String name = spec.getName();
600            IPropertySpecification existing = (IPropertySpecification) _propertySpecifications
601                    .get(name);
602    
603            if (existing != null)
604                throw new ApplicationRuntimeException(SpecMessages.duplicateProperty(name, existing), spec.getLocation(), null);
605    
606            claimProperty(name, spec);
607    
608            _propertySpecifications.put(name, spec);
609        }
610    
611        /**
612         * Returns a sorted, immutable list of the names of all
613         * {@link org.apache.tapestry.spec.IPropertySpecification}s.
614         * 
615         * @since 3.0
616         */
617    
618        public List getPropertySpecificationNames()
619        {
620            return sortedKeys(_propertySpecifications);
621        }
622    
623        /**
624         * Returns the named {@link org.apache.tapestry.spec.IPropertySpecification}, or null if no
625         * such specification exist.
626         * 
627         * @since 3.0
628         * @see #addPropertySpecification(IPropertySpecification)
629         */
630    
631        public IPropertySpecification getPropertySpecification(String name)
632        {
633            return (IPropertySpecification) get(_propertySpecifications, name);
634        }
635    
636        public void addInjectSpecification(InjectSpecification spec)
637        {
638            if (_injectSpecifications == null)
639                _injectSpecifications = new ArrayList();
640    
641            claimProperty(spec.getProperty(), spec);
642    
643            _injectSpecifications.add(spec);
644        }
645    
646        public List getInjectSpecifications()
647        {
648            return safeList(_injectSpecifications);
649        }
650    
651        private List safeList(List input)
652        {
653            if (input == null)
654                return Collections.EMPTY_LIST;
655    
656            return Collections.unmodifiableList(input);
657        }
658    
659        private void claimProperty(String propertyName, Object subSpecification)
660        {
661            if (propertyName == null)
662                return;
663    
664            if (_claimedProperties == null)
665                _claimedProperties = new HashMap();
666    
667            Object existing = _claimedProperties.get(propertyName);
668    
669            if (existing != null)
670                throw new ApplicationRuntimeException(SpecMessages.claimedProperty(
671                        propertyName,
672                        existing), HiveMind.getLocation(subSpecification), null);
673    
674            _claimedProperties.put(propertyName, subSpecification);
675        }
676    
677        /** @since 4.0 */
678        public boolean isDeprecated()
679        {
680            return _deprecated;
681        }
682    
683        /** @since 4.0 */
684        public void setDeprecated(boolean deprecated)
685        {
686            _deprecated = deprecated;
687        }
688    
689        public Set getReservedParameterNames()
690        {
691            if (_reservedParameterNames == null)
692                return Collections.EMPTY_SET;
693    
694            return Collections.unmodifiableSet(_reservedParameterNames);
695        }
696        
697        /**
698         * {@inheritDoc}
699         */
700        public void addEventListener(String componentId, String[] events, 
701                String methodName, String formId, boolean validateForm, boolean async, boolean focus, boolean autoSubmit)
702        {
703            ComponentEventProperty property = getComponentEvents(componentId);
704            if (property == null) {
705                property = new ComponentEventProperty(componentId);
706                _componentEvents.put(componentId, property);
707            }
708    
709            property.addListener(events, methodName, formId, validateForm, async, focus, autoSubmit);
710        }
711        
712        /**
713         * {@inheritDoc}
714         */
715        public void addElementEventListener(String elementId, String[] events, 
716                String methodName, String formId, boolean validateForm, boolean async, boolean focus)
717        {
718            ComponentEventProperty property = getElementEvents(elementId);
719            if (property == null) {
720                property = new ComponentEventProperty(elementId);
721                _elementEvents.put(elementId, property);
722            }
723    
724            property.addListener(events, methodName, formId, validateForm, async, focus, true);
725        }
726    
727        public void connectAutoSubmitEvents(IComponent component, IForm form)
728        {
729            Defense.notNull(form, "form");
730            
731            ComponentEventProperty property = getComponentEvents(component.getExtendedId());
732            
733            if (property == null)
734                return;
735    
736            property.connectAutoSubmitEvents(form.getExtendedId());
737        }
738    
739        public void rewireComponentId(String componentId, String extendedId, String idPath)
740        {
741            ComponentEventProperty prop = getComponentEvents(componentId);
742            if (prop == null)
743                return;
744    
745            if (_componentEvents.containsKey(extendedId))
746                return;
747    
748            try {
749    
750                ComponentEventProperty clone = (ComponentEventProperty) prop.clone();
751    
752                clone.rewireComponentId(extendedId, idPath);
753    
754                _componentEvents.put(extendedId, clone);
755    
756            } catch (CloneNotSupportedException e) {
757    
758                throw new ApplicationRuntimeException(e);
759            }
760        }
761    
762        /**
763         * {@inheritDoc}
764         */
765        public ComponentEventProperty getComponentEvents(String id)
766        {
767            return (ComponentEventProperty)_componentEvents.get(id);
768        }
769    
770        public Map getComponentEvents()
771        {
772            return _componentEvents;
773        }
774    
775        /**
776         * {@inheritDoc}
777         */
778        public ComponentEventProperty getElementEvents(String id)
779        {
780            return (ComponentEventProperty)_elementEvents.get(id);
781        }
782        
783        /**
784         * {@inheritDoc}
785         */
786        public Map getElementEvents()
787        {
788            return _elementEvents;
789        }
790        
791        /**
792         * {@inheritDoc}
793         */
794        public EventBoundListener[] getFormEvents(String formId, BrowserEvent event)
795        {
796            List ret = new ArrayList();
797            
798            Iterator it = _componentEvents.keySet().iterator();
799            while (it.hasNext()) {
800                
801                String compId = (String)it.next();
802                ComponentEventProperty prop = (ComponentEventProperty)_componentEvents.get(compId);
803    
804                ret.addAll(prop.getFormEventListeners(formId, event, null));
805            }
806            
807            it = _elementEvents.keySet().iterator();
808            while (it.hasNext()) {
809                
810                String compId = (String)it.next();
811                ComponentEventProperty prop = (ComponentEventProperty)_elementEvents.get(compId);
812    
813                ret.addAll(prop.getFormEventListeners(formId, event, null));
814            }
815            
816            return (EventBoundListener[])ret.toArray(new EventBoundListener[ret.size()]);
817        }
818        
819        /**
820         * {@inheritDoc}
821         */
822        public boolean hasElementEvents()
823        {
824            return _elementEvents.size() > 0;
825        }
826    
827        /**
828         * {@inheritDoc}
829         */
830        public int hashCode()
831        {
832            final int prime = 31;
833            int result = 1;
834            result = prime * result + ((_componentClassName == null) ? 0 : _componentClassName.hashCode());
835            result = prime * result + ((_description == null) ? 0 : _description.hashCode());
836            result = prime * result + (_pageSpecification ? 1231 : 1237);
837            result = prime * result + ((_publicId == null) ? 0 : _publicId.hashCode());
838            return result;
839        }
840    
841        /**
842         * {@inheritDoc}
843         */
844        public boolean equals(Object obj)
845        {
846            if (this == obj) return true;
847            if (obj == null) return false;
848            if (getClass() != obj.getClass()) return false;
849            final ComponentSpecification other = (ComponentSpecification) obj;
850            if (_componentClassName == null) {
851                if (other._componentClassName != null) return false;
852            } else if (!_componentClassName.equals(other._componentClassName)) return false;
853            if (_description == null) {
854                if (other._description != null) return false;
855            } else if (!_description.equals(other._description)) return false;
856            if (_pageSpecification != other._pageSpecification) return false;
857            if (_publicId == null) {
858                if (other._publicId != null) return false;
859            } else if (!_publicId.equals(other._publicId)) return false;
860            if (_specificationLocation == null) {
861                if (other._specificationLocation != null) return false;
862            } else if (!_specificationLocation.getPath().equals(other._specificationLocation.getPath())) return false;
863            return true;
864        }
865        
866        
867    }