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