001    // Copyright 2004, 2005 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry;
016    
017    import org.apache.hivemind.ApplicationRuntimeException;
018    import org.apache.hivemind.Location;
019    import org.apache.tapestry.event.ChangeObserver;
020    import org.apache.tapestry.event.ObservedChangeEvent;
021    import org.apache.tapestry.multipart.IMultipartDecoder;
022    import org.apache.tapestry.spec.IComponentSpecification;
023    import org.apache.tapestry.util.StringSplitter;
024    
025    import java.io.IOException;
026    import java.io.InputStream;
027    import java.text.MessageFormat;
028    import java.util.*;
029    
030    /**
031     * A placeholder for a number of (static) methods that don't belong elsewhere, as well as a global
032     * location for static constants.
033     *
034     * @since 1.0.1
035     * @author Howard Lewis Ship
036     */
037    
038    public final class Tapestry
039    {
040        /**
041         * The name ("direct") of a service that allows stateless behavior for an {@link
042         * org.apache.tapestry.link.DirectLink} component.
043         * <p>
044         * This service rolls back the state of the page but doesn't rewind the the dynamic state of the
045         * page the was the action service does, which is more efficient but less powerful.
046         * <p>
047         * An array of String parameters may be included with the service URL; these will be made
048         * available to the {@link org.apache.tapestry.link.DirectLink} component's listener.
049         */
050    
051        public static final String DIRECT_SERVICE = "direct";
052    
053        /**
054         * Almost identical to the direct service, except specifically for handling
055         * browser level events.
056         *
057         * @since 4.1
058         */
059    
060        public static final String DIRECT_EVENT_SERVICE = "directevent";
061    
062        /**
063         * The name ("external") of a service that a allows {@link IExternalPage} to be selected.
064         * Associated with a {@link org.apache.tapestry.link.ExternalLink} component.
065         * <p>
066         * This service enables {@link IExternalPage}s to be accessed via a URL. External pages may be
067         * booked marked using their URL for future reference.
068         * <p>
069         * An array of Object parameters may be included with the service URL; these will be passed to
070         * the {@link IExternalPage#activateExternalPage(Object[], IRequestCycle)} method.
071         */
072    
073        public static final String EXTERNAL_SERVICE = "external";
074    
075        /**
076         * The name ("page") of a service that allows a new page to be selected. Associated with a
077         * {@link org.apache.tapestry.link.PageLink} component.
078         * <p>
079         * The service requires a single parameter: the name of the target page.
080         */
081    
082        public static final String PAGE_SERVICE = "page";
083    
084        /**
085         * The name ("home") of a service that jumps to the home page. A stand-in for when no service is
086         * provided, which is typically the entrypoint to the application.
087         */
088    
089        public static final String HOME_SERVICE = "home";
090    
091        /**
092         * The name ("restart") of a service that invalidates the session and restarts the application.
093         * Typically used just to recover from an exception.
094         */
095    
096        public static final String RESTART_SERVICE = "restart";
097    
098        /**
099         * The name ("asset") of a service used to access internal assets.
100         */
101    
102        public static final String ASSET_SERVICE = "asset";
103    
104        /**
105         * The name ("reset") of a service used to clear cached template and specification data and
106         * remove all pooled pages. This is only used when debugging as a quick way to clear the out
107         * cached data, to allow updated versions of specifications and templates to be loaded (without
108         * stopping and restarting the servlet container).
109         * <p>
110         * This service is only available if the Java system property
111         * <code>org.apache.tapestry.enable-reset-service</code> is set to <code>true</code>.
112         */
113    
114        public static final String RESET_SERVICE = "reset";
115    
116        /**
117         * Property name used to get the extension used for templates. This may be set in the page or
118         * component specification, or in the page (or component's) immediate container (library or
119         * application specification). Unlike most properties, value isn't inherited all the way up the
120         * chain. The default template extension is "html".
121         *
122         * @since 3.0
123         */
124    
125        public static final String TEMPLATE_EXTENSION_PROPERTY = "org.apache.tapestry.template-extension";
126    
127        /**
128         * The name of an {@link org.apache.tapestry.IRequestCycle} attribute in which the currently
129         * rendering {@link org.apache.tapestry.components.ILinkComponent} is stored. Link components do
130         * not nest.
131         */
132    
133        public static final String LINK_COMPONENT_ATTRIBUTE_NAME = "org.apache.tapestry.active-link-component";
134    
135        /**
136         * Suffix appended to a parameter name to form the name of a property that stores the binding
137         * for the parameter.
138         *
139         * @since 3.0
140         */
141    
142        public static final String PARAMETER_PROPERTY_NAME_SUFFIX = "Binding";
143    
144        /**
145         * Key used to obtain an extension from the application specification. The extension, if it
146         * exists, implements {@link org.apache.tapestry.request.IRequestDecoder}.
147         *
148         * @since 2.2
149         */
150    
151        public static final String REQUEST_DECODER_EXTENSION_NAME = "org.apache.tapestry.request-decoder";
152    
153        /**
154         * Name of optional application extension for the multipart decoder used by the application. The
155         * extension must implement {@link org.apache.tapestry.multipart.IMultipartDecoder} (and is
156         * generally a configured instance of
157         * {@link IMultipartDecoder}).
158         *
159         * @since 3.0
160         */
161    
162        public static final String MULTIPART_DECODER_EXTENSION_NAME = "org.apache.tapestry.multipart-decoder";
163    
164        /**
165         * Method id used to check that {@link IPage#validate(IRequestCycle)} is invoked.
166         *
167         * @see #checkMethodInvocation(Object, String, Object)
168         * @since 3.0
169         */
170    
171        public static final String ABSTRACTPAGE_VALIDATE_METHOD_ID = "AbstractPage.validate()";
172    
173        /**
174         * Method id used to check that {@link IPage#detach()} is invoked.
175         *
176         * @see #checkMethodInvocation(Object, String, Object)
177         * @since 3.0
178         */
179    
180        public static final String ABSTRACTPAGE_DETACH_METHOD_ID = "AbstractPage.detach()";
181    
182        /**
183         * Regular expression defining a simple property name. Used by several different parsers. Simple
184         * property names match Java variable names; a leading letter (or underscore), followed by
185         * letters, numbers and underscores.
186         *
187         * @since 3.0
188         */
189    
190        public static final String SIMPLE_PROPERTY_NAME_PATTERN = "^_?[a-zA-Z]\\w*$";
191    
192        /**
193         * Class name of an {@link ognl.TypeConverter}implementing class to use as a type converter for
194         * {@link org.apache.tapestry.binding.ExpressionBinding}.
195         */
196        public static final String OGNL_TYPE_CONVERTER = "org.apache.tapestry.ognl-type-converter";
197    
198        /**
199         * The version of the framework; this is updated for major releases.
200         */
201    
202        public static final String VERSION = readVersion();
203    
204        private static final String UNKNOWN_VERSION = "Unknown";
205    
206        /**
207         * Contains strings loaded from TapestryStrings.properties.
208         *
209         * @since 1.0.8
210         */
211    
212        private static ResourceBundle _strings;
213    
214        /**
215         * A {@link Map}that links Locale names (as in {@link Locale#toString()}to {@link Locale}
216         * instances. This prevents needless duplication of Locales.
217         */
218    
219        private static final Map _localeMap = new HashMap();
220    
221        static
222        {
223            Locale[] locales = Locale.getAvailableLocales();
224            for (int i = 0; i < locales.length; i++)
225            {
226                _localeMap.put(locales[i].toString(), locales[i]);
227            }
228        }
229    
230        /**
231         * Used for tracking if a particular super-class method has been invoked.
232         */
233    
234        private static final ThreadLocal _invokedMethodIds = new ThreadLocal();
235    
236    
237        /**
238         * Prevent instantiation.
239         */
240    
241        private Tapestry()
242        {
243        }
244    
245        /**
246         * Copys all informal {@link IBinding bindings}from a source component to the destination
247         * component. Informal bindings are bindings for informal parameters. This will overwrite
248         * parameters (formal or informal) in the destination component if there is a naming conflict.
249         */
250    
251        public static void copyInformalBindings(IComponent source, IComponent destination)
252        {
253            Collection names = source.getBindingNames();
254    
255            if (names == null)
256                return;
257    
258            IComponentSpecification specification = source.getSpecification();
259            Iterator i = names.iterator();
260    
261            while (i.hasNext())
262            {
263                String name = (String) i.next();
264    
265                // If not a formal parameter, then copy it over.
266    
267                if (specification.getParameter(name) == null)
268                {
269                    IBinding binding = source.getBinding(name);
270    
271                    destination.setBinding(name, binding);
272                }
273            }
274        }
275    
276        /**
277         * Gets the {@link Locale}for the given string, which is the result of
278         * {@link Locale#toString()}. If no such locale is already registered, a new instance is
279         * created, registered and returned.
280         */
281    
282        public static Locale getLocale(String s)
283        {
284            Locale result = null;
285    
286            synchronized (_localeMap)
287            {
288                result = (Locale) _localeMap.get(s);
289            }
290    
291            if (result == null)
292            {
293                StringSplitter splitter = new StringSplitter('_');
294                String[] terms = splitter.splitToArray(s);
295    
296                switch (terms.length)
297                {
298                    case 1:
299    
300                        result = new Locale(terms[0], "");
301                        break;
302    
303                    case 2:
304    
305                        result = new Locale(terms[0], terms[1]);
306                        break;
307    
308                    case 3:
309    
310                        result = new Locale(terms[0], terms[1], terms[2]);
311                        break;
312    
313                    default:
314    
315                        throw new IllegalArgumentException("Unable to convert '" + s + "' to a Locale.");
316                }
317    
318                synchronized (_localeMap)
319                {
320                    _localeMap.put(s, result);
321                }
322    
323            }
324    
325            return result;
326    
327        }
328    
329        /**
330         * Closes the stream (if not null), ignoring any {@link IOException}thrown.
331         *
332         * @since 1.0.2
333         */
334    
335        public static void close(InputStream stream)
336        {
337            if (stream != null)
338            {
339                try
340                {
341                    stream.close();
342                }
343                catch (IOException ex)
344                {
345                    // Ignore.
346                }
347            }
348        }
349    
350        /**
351         * Gets a string from the TapestryStrings resource bundle. The string in the bundle is treated
352         * as a pattern for {@link MessageFormat#format(java.lang.String, java.lang.Object[])}.
353         *
354         * @since 1.0.8
355         */
356    
357        public static String format(String key, Object[] args)
358        {
359            if (_strings == null)
360                _strings = ResourceBundle.getBundle("org.apache.tapestry.TapestryStrings");
361    
362            String pattern = _strings.getString(key);
363    
364            if (args == null)
365                return pattern;
366    
367            return MessageFormat.format(pattern, args);
368        }
369    
370        /**
371         * Convienience method for invoking {@link #format(String, Object[])}.
372         *
373         * @since 3.0
374         */
375    
376        public static String getMessage(String key)
377        {
378            return format(key, null);
379        }
380    
381        /**
382         * Convienience method for invoking {@link #format(String, Object[])}.
383         *
384         * @since 3.0
385         */
386    
387        public static String format(String key, Object arg)
388        {
389            return format(key, new Object[]
390              { arg });
391        }
392    
393        /**
394         * Convienience method for invoking {@link #format(String, Object[])}.
395         *
396         * @since 3.0
397         */
398    
399        public static String format(String key, Object arg1, Object arg2)
400        {
401            return format(key, new Object[]
402              { arg1, arg2 });
403        }
404    
405        /**
406         * Convienience method for invoking {@link #format(String, Object[])}.
407         *
408         * @since 3.0
409         */
410    
411        public static String format(String key, Object arg1, Object arg2, Object arg3)
412        {
413            return format(key, new Object[]
414              { arg1, arg2, arg3 });
415        }
416    
417        /**
418         * Invoked when the class is initialized to read the current version file.
419         */
420    
421        private static String readVersion()
422        {
423            Properties props = new Properties();
424    
425            try
426            {
427                InputStream in = Tapestry.class.getResourceAsStream("version.properties");
428    
429                if (in == null)
430                    return UNKNOWN_VERSION;
431    
432                props.load(in);
433    
434                in.close();
435    
436                return props.getProperty("project.version", UNKNOWN_VERSION);
437            }
438            catch (IOException ex)
439            {
440                return UNKNOWN_VERSION;
441            }
442    
443        }
444    
445        /**
446         * Returns the size of a collection, or zero if the collection is null.
447         *
448         * @since 2.2
449         */
450    
451        public static int size(Collection c)
452        {
453            if (c == null)
454                return 0;
455    
456            return c.size();
457        }
458    
459        /**
460         * Returns the length of the array, or 0 if the array is null.
461         *
462         * @since 2.2
463         */
464    
465        public static int size(Object[] array)
466        {
467            if (array == null)
468                return 0;
469    
470            return array.length;
471        }
472    
473        /**
474         * Returns true if the Map is null or empty.
475         *
476         * @since 3.0
477         */
478    
479        public static boolean isEmpty(Map map)
480        {
481            return map == null || map.isEmpty();
482        }
483    
484        /**
485         * Returns true if the Collection is null or empty.
486         *
487         * @since 3.0
488         */
489    
490        public static boolean isEmpty(Collection c)
491        {
492            return c == null || c.isEmpty();
493        }
494    
495        /**
496         * Converts a {@link Map} to an even-sized array of key/value pairs. This may be useful when
497         * using a Map as service parameters (with {@link org.apache.tapestry.link.DirectLink}.
498         * Assuming the keys and values are simple objects (String, Boolean, Integer, etc.), then the
499         * representation as an array will encode more efficiently (via
500         * {@link org.apache.tapestry.util.io.DataSqueezerImpl} than serializing the Map and its
501         * contents.
502         *
503         * @return the array of keys and values, or null if the input Map is null or empty
504         * @since 2.2
505         */
506    
507        public static Object[] convertMapToArray(Map map)
508        {
509            if (isEmpty(map))
510                return null;
511    
512            Set entries = map.entrySet();
513    
514            Object[] result = new Object[2 * entries.size()];
515            int x = 0;
516    
517            Iterator i = entries.iterator();
518            while (i.hasNext())
519            {
520                Map.Entry entry = (Map.Entry) i.next();
521    
522                result[x++] = entry.getKey();
523                result[x++] = entry.getValue();
524            }
525    
526            return result;
527        }
528    
529        /**
530         * Converts an even-sized array of objects back into a {@link Map}.
531         *
532         * @see #convertMapToArray(Map)
533         * @return a Map, or null if the array is null or empty
534         * @since 2.2
535         */
536    
537        public static Map convertArrayToMap(Object[] array)
538        {
539            if (array == null || array.length == 0)
540                return null;
541    
542            if (array.length % 2 != 0)
543                throw new IllegalArgumentException(getMessage("Tapestry.even-sized-array"));
544    
545            Map result = new HashMap();
546    
547            int x = 0;
548            while (x < array.length)
549            {
550                Object key = array[x++];
551                Object value = array[x++];
552    
553                result.put(key, value);
554            }
555    
556            return result;
557        }
558    
559        /**
560         * Creates an exception indicating the binding value is null.
561         *
562         * @since 3.0
563         */
564    
565        public static BindingException createNullBindingException(IBinding binding)
566        {
567            return new BindingException(getMessage("null-value-for-binding"), binding);
568        }
569    
570        /** @since 3.0 * */
571    
572        public static ApplicationRuntimeException createNoSuchComponentException(IComponent component,
573                                                                                 String id, Location location)
574        {
575            return new ApplicationRuntimeException(format("no-such-component", component.getExtendedId(), id),
576                                                   component, location, null);
577        }
578    
579        /** @since 3.0 * */
580    
581        public static BindingException createRequiredParameterException(IComponent component,
582                                                                        String parameterName)
583        {
584            return new BindingException(format("required-parameter", parameterName, component.getExtendedId()),
585                                        component, null, component.getBinding(parameterName), null);
586        }
587    
588        /** @since 3.0 * */
589    
590        public static ApplicationRuntimeException createRenderOnlyPropertyException(
591          IComponent component, String propertyName)
592        {
593            return new ApplicationRuntimeException(format("render-only-property",
594                                                          propertyName,
595                                                          component.getExtendedId()), component, null, null);
596        }
597    
598        /**
599         * Clears the list of method invocations.
600         *
601         * @see #checkMethodInvocation(Object, String, Object)
602         * @since 3.0
603         */
604    
605        public static void clearMethodInvocations()
606        {
607            _invokedMethodIds.set(null);
608        }
609    
610        /**
611         * Adds a method invocation to the list of invocations. This is done in a super-class
612         * implementations.
613         *
614         * @see #checkMethodInvocation(Object, String, Object)
615         * @since 3.0
616         */
617    
618        public static void addMethodInvocation(Object methodId)
619        {
620            List methodIds = (List) _invokedMethodIds.get();
621    
622            if (methodIds == null)
623            {
624                methodIds = new ArrayList();
625                _invokedMethodIds.set(methodIds);
626            }
627    
628            methodIds.add(methodId);
629        }
630    
631        /**
632         * Checks to see if a particular method has been invoked. The method is identified by a methodId
633         * (usually a String). The methodName and object are used to create an error message.
634         * <p>
635         * The caller should invoke {@link #clearMethodInvocations()}, then invoke a method on the
636         * object. The super-class implementation should invoke {@link #addMethodInvocation(Object)} to
637         * indicate that it was, in fact, invoked. The caller then invokes this method to validate that
638         * the super-class implementation was invoked.
639         * <p>
640         * The list of method invocations is stored in a {@link ThreadLocal} variable.
641         *
642         * @since 3.0
643         */
644    
645        public static void checkMethodInvocation(Object methodId, String methodName, Object object)
646        {
647            List methodIds = (List) _invokedMethodIds.get();
648    
649            if (methodIds != null && methodIds.contains(methodId))
650                return;
651    
652            throw new ApplicationRuntimeException(Tapestry.format("Tapestry.missing-method-invocation",
653                                                                  object.getClass().getName(),
654                                                                  methodName));
655        }
656    
657        /**
658         * Method used by pages and components to send notifications about property changes.
659         *
660         * @param component
661         *            the component containing the property
662         * @param propertyName
663         *            the name of the property which changed
664         * @param newValue
665         *            the new value for the property
666         * @since 3.0
667         */
668        public static void fireObservedChange(IComponent component, String propertyName, Object newValue)
669        {
670            ChangeObserver observer = component.getPage().getChangeObserver();
671    
672            if (observer == null)
673                return;
674    
675            ObservedChangeEvent event = new ObservedChangeEvent(component, propertyName, newValue);
676    
677            observer.observeChange(event);
678        }
679    }