001 // Copyright 2006, 2007, 2008, 2009, 2010, 2011 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.tapestry5.internal; 016 017 import org.apache.tapestry5.*; 018 import org.apache.tapestry5.func.Mapper; 019 import org.apache.tapestry5.internal.util.Holder; 020 import org.apache.tapestry5.ioc.Messages; 021 import org.apache.tapestry5.ioc.OperationTracker; 022 import org.apache.tapestry5.ioc.Orderable; 023 import org.apache.tapestry5.ioc.Resource; 024 import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 025 import org.apache.tapestry5.ioc.internal.util.InternalUtils; 026 import org.apache.tapestry5.services.ComponentEventRequestParameters; 027 import org.apache.tapestry5.services.LinkCreationListener; 028 import org.apache.tapestry5.services.LinkCreationListener2; 029 import org.apache.tapestry5.services.PageRenderRequestParameters; 030 import org.apache.tapestry5.services.javascript.StylesheetLink; 031 032 import java.io.IOException; 033 import java.io.InputStream; 034 import java.io.OutputStream; 035 import java.lang.annotation.Annotation; 036 import java.lang.ref.Reference; 037 import java.util.List; 038 import java.util.Map; 039 import java.util.regex.Pattern; 040 041 /** 042 * Shared utility methods used by various implementation classes. 043 */ 044 @SuppressWarnings("all") 045 public class TapestryInternalUtils 046 { 047 private static final String SLASH = "/"; 048 049 private static final Pattern SLASH_PATTERN = Pattern.compile(SLASH); 050 051 private static final Pattern NON_WORD_PATTERN = Pattern.compile("[^\\w]"); 052 053 private static final Pattern COMMA_PATTERN = Pattern.compile("\\s*,\\s*"); 054 055 private static final int BUFFER_SIZE = 5000; 056 057 /** 058 * Capitalizes the string, and inserts a space before each upper case character (or sequence of upper case 059 * characters). Thus "userId" becomes "User Id", etc. Also, converts underscore into space (and capitalizes the 060 * following word), thus "user_id" also becomes "User Id". 061 */ 062 public static String toUserPresentable(String id) 063 { 064 StringBuilder builder = new StringBuilder(id.length() * 2); 065 066 char[] chars = id.toCharArray(); 067 boolean postSpace = true; 068 boolean upcaseNext = true; 069 070 for (char ch : chars) 071 { 072 if (upcaseNext) 073 { 074 builder.append(Character.toUpperCase(ch)); 075 upcaseNext = false; 076 077 continue; 078 } 079 080 if (ch == '_') 081 { 082 builder.append(' '); 083 upcaseNext = true; 084 continue; 085 } 086 087 boolean upperCase = Character.isUpperCase(ch); 088 089 if (upperCase && !postSpace) 090 builder.append(' '); 091 092 builder.append(ch); 093 094 postSpace = upperCase; 095 } 096 097 return builder.toString(); 098 } 099 100 public static Map<String, String> mapFromKeysAndValues(String... keysAndValues) 101 { 102 Map<String, String> result = CollectionFactory.newMap(); 103 104 int i = 0; 105 while (i < keysAndValues.length) 106 { 107 String key = keysAndValues[i++]; 108 String value = keysAndValues[i++]; 109 110 result.put(key, value); 111 } 112 113 return result; 114 } 115 116 /** 117 * Converts a string to an {@link OptionModel}. The string is of the form "value=label". If the equals sign is 118 * omitted, then the same value is used for both value and label. 119 */ 120 public static OptionModel toOptionModel(String input) 121 { 122 assert input != null; 123 int equalsx = input.indexOf('='); 124 125 if (equalsx < 0) 126 return new OptionModelImpl(input); 127 128 String value = input.substring(0, equalsx); 129 String label = input.substring(equalsx + 1); 130 131 return new OptionModelImpl(label, value); 132 } 133 134 /** 135 * Parses a string input into a series of value=label pairs compatible with {@link #toOptionModel(String)}. Splits 136 * on commas. Ignores whitespace around commas. 137 * 138 * @param input comma seperated list of terms 139 * @return list of option models 140 */ 141 public static List<OptionModel> toOptionModels(String input) 142 { 143 assert input != null; 144 List<OptionModel> result = CollectionFactory.newList(); 145 146 for (String term : input.split(",")) 147 result.add(toOptionModel(term.trim())); 148 149 return result; 150 } 151 152 /** 153 * Wraps the result of {@link #toOptionModels(String)} as a {@link SelectModel} (with no option groups). 154 */ 155 public static SelectModel toSelectModel(String input) 156 { 157 List<OptionModel> options = toOptionModels(input); 158 159 return new SelectModelImpl(null, options); 160 } 161 162 /** 163 * Converts a map entry to an {@link OptionModel}. 164 */ 165 public static OptionModel toOptionModel(Map.Entry input) 166 { 167 assert input != null; 168 String label = input.getValue() != null ? String.valueOf(input.getValue()) : ""; 169 170 return new OptionModelImpl(label, input.getKey()); 171 } 172 173 /** 174 * Processes a map input into a series of map entries compatible with {@link #toOptionModel(Map.Entry)}. 175 * 176 * @param input map of elements 177 * @return list of option models 178 */ 179 public static <K, V> List<OptionModel> toOptionModels(Map<K, V> input) 180 { 181 assert input != null; 182 List<OptionModel> result = CollectionFactory.newList(); 183 184 for (Map.Entry entry : input.entrySet()) 185 result.add(toOptionModel(entry)); 186 187 return result; 188 } 189 190 /** 191 * Wraps the result of {@link #toOptionModels(Map)} as a {@link SelectModel} (with no option groups). 192 */ 193 public static <K, V> SelectModel toSelectModel(Map<K, V> input) 194 { 195 List<OptionModel> options = toOptionModels(input); 196 197 return new SelectModelImpl(null, options); 198 } 199 200 /** 201 * Converts an object to an {@link OptionModel}. 202 */ 203 public static OptionModel toOptionModel(Object input) 204 { 205 String label = (input != null ? String.valueOf(input) : ""); 206 207 return new OptionModelImpl(label, input); 208 } 209 210 /** 211 * Processes a list input into a series of objects compatible with {@link #toOptionModel(Object)}. 212 * 213 * @param input list of elements 214 * @return list of option models 215 */ 216 public static <E> List<OptionModel> toOptionModels(List<E> input) 217 { 218 assert input != null; 219 List<OptionModel> result = CollectionFactory.newList(); 220 221 for (E element : input) 222 result.add(toOptionModel(element)); 223 224 return result; 225 } 226 227 /** 228 * Wraps the result of {@link #toOptionModels(List)} as a {@link SelectModel} (with no option groups). 229 */ 230 public static <E> SelectModel toSelectModel(List<E> input) 231 { 232 List<OptionModel> options = toOptionModels(input); 233 234 return new SelectModelImpl(null, options); 235 } 236 237 /** 238 * Parses a key/value pair where the key and the value are seperated by an equals sign. The key and value are 239 * trimmed of leading and trailing whitespace, and returned as a {@link KeyValue}. 240 */ 241 public static KeyValue parseKeyValue(String input) 242 { 243 int pos = input.indexOf('='); 244 245 if (pos < 1) 246 throw new IllegalArgumentException(InternalMessages.badKeyValue(input)); 247 248 String key = input.substring(0, pos); 249 String value = input.substring(pos + 1); 250 251 return new KeyValue(key.trim(), value.trim()); 252 } 253 254 /** 255 * Used to convert a property expression into a key that can be used to locate various resources (Blocks, messages, 256 * etc.). Strips out any punctuation characters, leaving just words characters (letters, number and the 257 * underscore). 258 * 259 * @param expression a property expression 260 * @return the expression with punctuation removed 261 */ 262 public static String extractIdFromPropertyExpression(String expression) 263 { 264 return replace(expression, NON_WORD_PATTERN, ""); 265 } 266 267 /** 268 * Looks for a label within the messages based on the id. If found, it is used, otherwise the name is converted to a 269 * user presentable form. 270 */ 271 public static String defaultLabel(String id, Messages messages, String propertyExpression) 272 { 273 String key = id + "-label"; 274 275 if (messages.contains(key)) 276 return messages.get(key); 277 278 return toUserPresentable(extractIdFromPropertyExpression(lastTerm(propertyExpression))); 279 } 280 281 /** 282 * Strips a dotted sequence (such as a property expression, or a qualified class name) down to the last term of that 283 * expression, by locating the last period ('.') in the string. 284 */ 285 public static String lastTerm(String input) 286 { 287 int dotx = input.lastIndexOf('.'); 288 289 return input.substring(dotx + 1); 290 } 291 292 /** 293 * Converts an list of strings into a space-separated string combining them all, suitable for use as an HTML class 294 * attribute value. 295 * 296 * @param classes classes to combine 297 * @return the joined classes, or null if classes is empty 298 */ 299 public static String toClassAttributeValue(List<String> classes) 300 { 301 if (classes.isEmpty()) 302 return null; 303 304 return InternalUtils.join(classes, " "); 305 } 306 307 /** 308 * Converts an enum to a label string, allowing for overrides from a message catalog. 309 * <p/> 310 * <ul> 311 * <li>As key <em>prefix</em>.<em>name</em> if present. Ex: "ElementType.LOCAL_VARIABLE" 312 * <li>As key <em>name</em> if present, i.e., "LOCAL_VARIABLE". 313 * <li>As a user-presentable version of the name, i.e., "Local Variable". 314 * </ul> 315 * 316 * @param messages the messages to search for the label 317 * @param prefix prepended to key 318 * @param value to get a label for 319 * @return the label 320 */ 321 public static String getLabelForEnum(Messages messages, String prefix, Enum value) 322 { 323 String name = value.name(); 324 325 String key = prefix + "." + name; 326 327 if (messages.contains(key)) 328 return messages.get(key); 329 330 if (messages.contains(name)) 331 return messages.get(name); 332 333 return toUserPresentable(name.toLowerCase()); 334 } 335 336 public static String getLabelForEnum(Messages messages, Enum value) 337 { 338 String prefix = lastTerm(value.getClass().getName()); 339 340 return getLabelForEnum(messages, prefix, value); 341 } 342 343 private static String replace(String input, Pattern pattern, String replacement) 344 { 345 return pattern.matcher(input).replaceAll(replacement); 346 } 347 348 /** 349 * Determines if the two values are equal. They are equal if they are the exact same value (including if they are 350 * both null). Otherwise standard equals() comparison is used. 351 * 352 * @param left value to compare, possibly null 353 * @param right value to compare, possibly null 354 * @return true if same value, both null, or equal 355 */ 356 public static <T> boolean isEqual(T left, T right) 357 { 358 if (left == right) 359 return true; 360 361 if (left == null) 362 return false; 363 364 return left.equals(right); 365 } 366 367 /** 368 * Splits a path at each slash. 369 */ 370 public static String[] splitPath(String path) 371 { 372 return SLASH_PATTERN.split(path); 373 } 374 375 /** 376 * Splits a value around commas. Whitespace around the commas is removed, as is leading and trailing whitespace. 377 * 378 * @since 5.1.0.0 379 */ 380 public static String[] splitAtCommas(String value) 381 { 382 if (InternalUtils.isBlank(value)) 383 return InternalConstants.EMPTY_STRING_ARRAY; 384 385 return COMMA_PATTERN.split(value.trim()); 386 } 387 388 /** 389 * Copies some content from an input stream to an output stream. It is the caller's responsibility to close the 390 * streams. 391 * 392 * @param in source of data 393 * @param out sink of data 394 * @throws IOException 395 * @since 5.1.0.0 396 */ 397 public static void copy(InputStream in, OutputStream out) throws IOException 398 { 399 byte[] buffer = new byte[BUFFER_SIZE]; 400 401 while (true) 402 { 403 int length = in.read(buffer); 404 405 if (length < 0) 406 break; 407 408 out.write(buffer, 0, length); 409 } 410 411 // TAPESTRY-2415: WebLogic needs this flush() call. 412 out.flush(); 413 } 414 415 public static boolean isEqual(EventContext left, EventContext right) 416 { 417 if (left == right) 418 return true; 419 420 int count = left.getCount(); 421 422 if (count != right.getCount()) 423 return false; 424 425 for (int i = 0; i < count; i++) 426 { 427 if (!left.get(Object.class, i).equals(right.get(Object.class, i))) 428 return false; 429 } 430 431 return true; 432 } 433 434 /** 435 * Converts an Asset to an Asset2 if necessary. When actually wrapping an Asset as an Asset2, the asset is assumed 436 * to be variant (i.e., not cacheable). 437 * 438 * @since 5.1.0.0 439 */ 440 public static Asset2 toAsset2(final Asset asset) 441 { 442 if (asset instanceof Asset2) 443 return (Asset2) asset; 444 445 return new Asset2() 446 { 447 /** Returns false. */ 448 public boolean isInvariant() 449 { 450 return false; 451 } 452 453 public Resource getResource() 454 { 455 return asset.getResource(); 456 } 457 458 public String toClientURL() 459 { 460 return asset.toClientURL(); 461 } 462 463 @Override 464 public String toString() 465 { 466 return asset.toString(); 467 } 468 }; 469 } 470 471 public static InternalPropertyConduit toInternalPropertyConduit(final PropertyConduit conduit) 472 { 473 if (conduit instanceof InternalPropertyConduit) 474 return (InternalPropertyConduit) conduit; 475 476 return new InternalPropertyConduit() 477 { 478 479 public <T extends Annotation> T getAnnotation(Class<T> annotationClass) 480 { 481 return conduit.getAnnotation(annotationClass); 482 } 483 484 public void set(Object instance, Object value) 485 { 486 conduit.set(instance, value); 487 } 488 489 public Class getPropertyType() 490 { 491 return conduit.getPropertyType(); 492 } 493 494 public Object get(Object instance) 495 { 496 return conduit.get(instance); 497 } 498 499 public String getPropertyName() 500 { 501 return null; 502 } 503 }; 504 } 505 506 /** 507 * @param mixinDef the original mixin definition. 508 * @return an Orderable whose id is the mixin name. 509 */ 510 public static Orderable<String> mixinTypeAndOrder(String mixinDef) 511 { 512 int idx = mixinDef.indexOf("::"); 513 if (idx == -1) 514 { 515 return new Orderable<String>(mixinDef, mixinDef); 516 } 517 String type = mixinDef.substring(0, idx); 518 String[] constraints = splitMixinConstraints(mixinDef.substring(idx + 2)); 519 520 return new Orderable<String>(type, type, constraints); 521 } 522 523 public static String[] splitMixinConstraints(String s) 524 { 525 return InternalUtils.isBlank(s) ? null : s.split(";"); 526 } 527 528 /** 529 * Common mapper, used primarily with {@link org.apache.tapestry5.func.Flow#map(org.apache.tapestry5.func.Mapper)} 530 * 531 * @since 5.2.0 532 */ 533 public static Mapper<Asset, StylesheetLink> assetToStylesheetLink = new Mapper<Asset, StylesheetLink>() 534 { 535 public StylesheetLink map(Asset input) 536 { 537 return new StylesheetLink(input); 538 } 539 }; 540 541 public static LinkCreationListener2 toLinkCreationListener2(final LinkCreationListener delegate) 542 { 543 return new LinkCreationListener2() 544 { 545 546 public void createdPageRenderLink(Link link, PageRenderRequestParameters parameters) 547 { 548 delegate.createdPageRenderLink(link); 549 } 550 551 public void createdComponentEventLink(Link link, ComponentEventRequestParameters parameters) 552 { 553 delegate.createdComponentEventLink(link); 554 } 555 }; 556 } 557 558 /** 559 * @since 5.3 560 */ 561 public static String toFileSuffix(String fileName) 562 { 563 int dotx = fileName.lastIndexOf('.'); 564 565 return dotx < 0 ? "" : fileName.substring(dotx + 1); 566 } 567 568 /** 569 * Performs an operation and re-throws the IOException that may occur. 570 */ 571 public static void performIO(OperationTracker tracker, String description, final IOOperation operation) 572 throws IOException 573 { 574 final Holder<IOException> exceptionHolder = Holder.create(); 575 576 tracker.run(description, new Runnable() 577 { 578 public void run() 579 { 580 try 581 { 582 operation.perform(); 583 } catch (IOException ex) 584 { 585 exceptionHolder.put(ex); 586 } 587 } 588 }); 589 590 if (exceptionHolder.hasValue()) 591 throw exceptionHolder.get(); 592 } 593 594 /** 595 * Extracts a value from a map of references. Handles the case where the reference does not exist, 596 * and the case where the reference itself now contains null. 597 * 598 * @since 5.3 599 */ 600 public static <K, V> V getAndDeref(Map<K, ? extends Reference<V>> map, K key) 601 { 602 Reference<V> ref = map.get(key); 603 604 return ref == null ? null : ref.get(); 605 } 606 } 607