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
015package org.apache.tapestry.spec;
016
017import java.util.ArrayList;
018import java.util.Collections;
019import java.util.HashMap;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.hivemind.ApplicationRuntimeException;
025import org.apache.hivemind.Location;
026import org.apache.hivemind.Resource;
027import org.apache.hivemind.util.ToStringBuilder;
028import org.apache.tapestry.Tapestry;
029
030/**
031 * Specification for a library. {@link org.apache.tapestry.spec.ApplicationSpecification}is a
032 * specialized kind of library.
033 * 
034 * @author Howard Lewis Ship
035 * @since 2.2bv
036 */
037
038public class LibrarySpecification extends LocatablePropertyHolder implements ILibrarySpecification
039{
040    /**
041     * Map of page name to page specification path.
042     */
043
044    private Map _pages;
045
046    /**
047     * Map of component alias to component specification path.
048     */
049    private Map _components;
050
051    /**
052     * Map of library id to library specification path.
053     */
054
055    private Map _libraries;
056
057    private String _description;
058
059    /**
060     * Map of extension name to {@link IExtensionSpecification}.
061     */
062
063    private Map _extensions;
064
065    /**
066     * Map of extension name to Object for instantiated extensions.
067     */
068
069    private Map _instantiatedExtensions;
070
071    /**
072     * The XML Public Id used when the library specification was read (if applicable).
073     * 
074     * @since 2.2
075     */
076
077    private String _publicId;
078
079    /**
080     * The location of the specification.
081     */
082
083    private Resource _specificationLocation;
084
085    public String getLibrarySpecificationPath(String id)
086    {
087        return (String) get(_libraries, id);
088    }
089
090    /**
091     * Sets the specification path for an embedded library.
092     * 
093     * @throws IllegalArgumentException
094     *             if a library with the given id already exists
095     */
096
097    public void setLibrarySpecificationPath(String id, String path)
098    {
099        if (_libraries == null)
100            _libraries = new HashMap();
101
102        if (_libraries.containsKey(id))
103            throw new IllegalArgumentException(Tapestry.format(
104                    "LibrarySpecification.duplicate-child-namespace-id",
105                    id));
106
107        _libraries.put(id, path);
108    }
109
110    public List getLibraryIds()
111    {
112        return sortedKeys(_libraries);
113    }
114
115    public String getPageSpecificationPath(String name)
116    {
117        return (String) get(_pages, name);
118    }
119
120    public void setPageSpecificationPath(String name, String path)
121    {
122        if (_pages == null)
123            _pages = new HashMap();
124
125        if (_pages.containsKey(name))
126            throw new IllegalArgumentException(Tapestry.format(
127                    "LibrarySpecification.duplicate-page-name",
128                    name));
129
130        _pages.put(name, path);
131    }
132
133    public List getPageNames()
134    {
135        return sortedKeys(_pages);
136    }
137
138    public void setComponentSpecificationPath(String alias, String path)
139    {
140        if (_components == null)
141            _components = new HashMap();
142
143        if (_components.containsKey(alias))
144            throw new IllegalArgumentException(Tapestry.format(
145                    "LibrarySpecification.duplicate-component-alias",
146                    alias));
147
148        _components.put(alias, path);
149    }
150
151    public String getComponentSpecificationPath(String alias)
152    {
153        return (String) get(_components, alias);
154    }
155
156    /**
157     * @since 3.0
158     */
159
160    public List getComponentTypes()
161    {
162        return sortedKeys(_components);
163    }
164
165    public String getServiceClassName(String name)
166    {
167        throw new UnsupportedOperationException();
168    }
169
170    public List getServiceNames()
171    {
172        return Collections.EMPTY_LIST;
173    }
174
175    public void setServiceClassName(String name, String className)
176    {
177        throw new UnsupportedOperationException();
178    }
179
180    private List sortedKeys(Map map)
181    {
182        if (map == null)
183            return Collections.EMPTY_LIST;
184
185        List result = new ArrayList(map.keySet());
186
187        Collections.sort(result);
188
189        return result;
190    }
191
192    private Object get(Map map, Object key)
193    {
194        if (map == null)
195            return null;
196
197        return map.get(key);
198    }
199
200    /**
201     * Returns the documentation for this library..
202     */
203
204    public String getDescription()
205    {
206        return _description;
207    }
208
209    /**
210     * Sets the documentation for this library.
211     */
212
213    public void setDescription(String description)
214    {
215        _description = description;
216    }
217
218    /**
219     * Returns a Map of extensions; key is extension name, value is
220     * {@link org.apache.tapestry.spec.IExtensionSpecification}. May return null. The returned Map
221     * is immutable.
222     */
223
224    public Map getExtensionSpecifications()
225    {
226        if (_extensions == null)
227            return null;
228
229        return Collections.unmodifiableMap(_extensions);
230    }
231
232    /**
233     * Adds another extension specification.
234     * 
235     * @throws IllegalArgumentException
236     *             if an extension with the given name already exists.
237     */
238
239    public void addExtensionSpecification(String name, IExtensionSpecification extension)
240    {
241        if (_extensions == null)
242            _extensions = new HashMap();
243
244        if (_extensions.containsKey(name))
245            throw new IllegalArgumentException(Tapestry.format(
246                    "LibrarySpecification.duplicate-extension-name",
247                    this,
248                    name));
249
250        _extensions.put(name, extension);
251    }
252
253    /**
254     * Returns a sorted List of the names of all extensions. May return the empty list, but won't
255     * return null.
256     */
257
258    public synchronized List getExtensionNames()
259    {
260        return sortedKeys(_instantiatedExtensions);
261    }
262
263    /**
264     * Returns the named IExtensionSpecification, or null if it doesn't exist.
265     */
266
267    public IExtensionSpecification getExtensionSpecification(String name)
268    {
269        if (_extensions == null)
270            return null;
271
272        return (IExtensionSpecification) _extensions.get(name);
273    }
274
275    /**
276     * Returns true if this library specification has a specification for the named extension.
277     */
278
279    public boolean checkExtension(String name)
280    {
281        if (_extensions == null)
282            return false;
283
284        return _extensions.containsKey(name);
285    }
286
287    /**
288     * Returns an instantiated extension. Extensions are created as needed and cached for later use.
289     * 
290     * @throws IllegalArgumentException
291     *             if no extension specification exists for the given name.
292     */
293
294    public synchronized Object getExtension(String name)
295    {
296        return getExtension(name, null);
297    }
298
299    /** @since 3.0 * */
300
301    public synchronized Object getExtension(String name, Class typeConstraint)
302    {
303        if (_instantiatedExtensions == null)
304            _instantiatedExtensions = new HashMap();
305
306        Object result = _instantiatedExtensions.get(name);
307        IExtensionSpecification spec = getExtensionSpecification(name);
308
309        if (spec == null)
310            throw new IllegalArgumentException(Tapestry.format(
311                    "LibrarySpecification.no-such-extension",
312                    name));
313
314        if (result == null)
315        {
316
317            result = spec.instantiateExtension();
318
319            _instantiatedExtensions.put(name, result);
320        }
321
322        if (typeConstraint != null)
323            applyTypeConstraint(name, result, typeConstraint, spec.getLocation());
324
325        return result;
326    }
327
328    /**
329     * Checks that an extension conforms to the supplied type constraint.
330     * 
331     * @throws IllegalArgumentException
332     *             if the extension fails the check.
333     * @since 3.0
334     */
335
336    protected void applyTypeConstraint(String name, Object extension, Class typeConstraint,
337            Location location)
338    {
339        Class extensionClass = extension.getClass();
340
341        // Can you assign an instance of the extension to a variable
342        // of type typeContraint legally?
343
344        if (typeConstraint.isAssignableFrom(extensionClass))
345            return;
346
347        String key = typeConstraint.isInterface() ? "LibrarySpecification.extension-does-not-implement-interface"
348                : "LibrarySpecification.extension-not-a-subclass";
349
350        throw new ApplicationRuntimeException(Tapestry.format(
351                key,
352                name,
353                extensionClass.getName(),
354                typeConstraint.getName()), location, null);
355    }
356
357    /**
358     * Invoked after the entire specification has been constructed to instantiate any extensions
359     * marked immediate.
360     */
361
362    public synchronized void instantiateImmediateExtensions()
363    {
364        if (_extensions == null)
365            return;
366
367        Iterator i = _extensions.entrySet().iterator();
368
369        while (i.hasNext())
370        {
371            Map.Entry entry = (Map.Entry) i.next();
372
373            IExtensionSpecification spec = (IExtensionSpecification) entry.getValue();
374
375            if (!spec.isImmediate())
376                continue;
377
378            String name = (String) entry.getKey();
379
380            getExtension(name);
381        }
382
383    }
384
385    /**
386     * Returns the extensions map.
387     * 
388     * @return Map of objects.
389     */
390
391    protected Map getExtensions()
392    {
393        return _extensions;
394    }
395
396    /**
397     * Updates the extension map.
398     * 
399     * @param extension
400     *            A Map of extension specification paths keyed on extension id.
401     *            <p>
402     *            The map is retained, not copied.
403     */
404
405    protected void setExtensions(Map extension)
406    {
407        _extensions = extension;
408    }
409
410    /**
411     * Returns the libraries map.
412     * 
413     * @return Map of {@link LibrarySpecification}.
414     */
415
416    protected Map getLibraries()
417    {
418        return _libraries;
419    }
420
421    /**
422     * Updates the library map.
423     * 
424     * @param libraries
425     *            A Map of library specification paths keyed on library id.
426     *            <p>
427     *            The map is retained, not copied.
428     */
429
430    protected void setLibraries(Map libraries)
431    {
432        _libraries = libraries;
433    }
434
435    /**
436     * Returns the pages map.
437     * 
438     * @return Map of {@link IComponentSpecification}.
439     */
440
441    protected Map getPages()
442    {
443        return _pages;
444    }
445
446    /**
447     * Updates the page map.
448     * 
449     * @param pages
450     *            A Map of page specification paths keyed on page id.
451     *            <p>
452     *            The map is retained, not copied.
453     */
454
455    protected void setPages(Map pages)
456    {
457        _pages = pages;
458    }
459
460    /**
461     * Returns the services.
462     * 
463     * @return Map of service class names.
464     * @deprecated To be removed in release 4.1.
465     */
466
467    protected Map getServices()
468    {
469        return Collections.EMPTY_MAP;
470    }
471
472    /**
473     * Updates the services map.
474     * 
475     * @param services
476     *            A Map of the fully qualified names of classes which implement
477     *            {@link org.apache.tapestry.engine.IEngineService}keyed on service id.
478     *            <p>
479     *            The map is retained, not copied.
480     * @deprecated To be removed in release 4.1.
481     */
482
483    protected void setServices(Map services)
484    {
485    }
486
487    /**
488     * Returns the components map.
489     * 
490     * @return Map of {@link IContainedComponent}.
491     */
492
493    protected Map getComponents()
494    {
495        return _components;
496    }
497
498    /**
499     * Updates the components map.
500     * 
501     * @param components
502     *            A Map of {@link IContainedComponent}keyed on component id. The map is retained,
503     *            not copied.
504     */
505
506    protected void setComponents(Map components)
507    {
508        _components = components;
509    }
510
511    /**
512     * Returns the XML Public Id for the library file, or null if not applicable.
513     * <p>
514     * This method exists as a convienience for the Spindle plugin. A previous method used an
515     * arbitrary version string, the public id is more useful and less ambiguous.
516     */
517
518    public String getPublicId()
519    {
520        return _publicId;
521    }
522
523    public void setPublicId(String publicId)
524    {
525        _publicId = publicId;
526    }
527
528    /** @since 3.0 * */
529
530    public Resource getSpecificationLocation()
531    {
532        return _specificationLocation;
533    }
534
535    /** @since 3.0 * */
536
537    public void setSpecificationLocation(Resource specificationLocation)
538    {
539        _specificationLocation = specificationLocation;
540    }
541
542    /** @since 3.0 * */
543
544    public synchronized String toString()
545    {
546        ToStringBuilder builder = new ToStringBuilder(this);
547
548        builder.append("components", _components);
549        builder.append("description", _description);
550        builder.append("instantiatedExtensions", _instantiatedExtensions);
551        builder.append("libraries", _libraries);
552        builder.append("pages", _pages);
553        builder.append("publicId", _publicId);
554        builder.append("specificationLocation", _specificationLocation);
555
556        extendDescription(builder);
557
558        return builder.toString();
559    }
560
561    /**
562     * Does nothing, subclasses may override to add additional description.
563     * 
564     * @see #toString()
565     * @since 3.0
566     */
567
568    protected void extendDescription(ToStringBuilder builder)
569    {
570    }
571
572}