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 }