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