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