001// Copyright 2004, 2005 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007//     http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry;
016
017import java.io.IOException;
018import java.io.InputStream;
019import java.text.MessageFormat;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027import java.util.Properties;
028import java.util.ResourceBundle;
029import java.util.Set;
030
031import org.apache.hivemind.ApplicationRuntimeException;
032import org.apache.hivemind.HiveMind;
033import org.apache.hivemind.Location;
034import org.apache.hivemind.service.ClassFabUtils;
035import org.apache.tapestry.event.ChangeObserver;
036import org.apache.tapestry.event.ObservedChangeEvent;
037import org.apache.tapestry.services.ServiceConstants;
038import org.apache.tapestry.spec.IComponentSpecification;
039import 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
049public 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}