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 }