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