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