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