001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.internal; 014 015import org.apache.tapestry5.*; 016import org.apache.tapestry5.func.Mapper; 017import org.apache.tapestry5.ioc.Messages; 018import org.apache.tapestry5.ioc.Orderable; 019import org.apache.tapestry5.ioc.Resource; 020import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 021import org.apache.tapestry5.ioc.internal.util.InternalUtils; 022import org.apache.tapestry5.services.ComponentEventRequestParameters; 023import org.apache.tapestry5.services.LinkCreationListener; 024import org.apache.tapestry5.services.LinkCreationListener2; 025import org.apache.tapestry5.services.PageRenderRequestParameters; 026import org.apache.tapestry5.services.javascript.StylesheetLink; 027 028import java.io.IOException; 029import java.io.InputStream; 030import java.io.OutputStream; 031import java.lang.annotation.Annotation; 032import java.lang.ref.Reference; 033import java.lang.reflect.Type; 034import java.util.Arrays; 035import java.util.List; 036import java.util.Map; 037import java.util.regex.Pattern; 038 039/** 040 * Shared utility methods used by various implementation classes. 041 */ 042@SuppressWarnings("all") 043public class TapestryInternalUtils 044{ 045 private static final String SLASH = "/"; 046 047 private static final Pattern SLASH_PATTERN = Pattern.compile(SLASH); 048 049 private static final Pattern NON_WORD_PATTERN = Pattern.compile("[^\\w]"); 050 051 private static final Pattern COMMA_PATTERN = Pattern.compile("\\s*,\\s*"); 052 053 private static final int BUFFER_SIZE = 5000; 054 055 /** 056 * Capitalizes the string, and inserts a space before each upper case character (or sequence of upper case 057 * characters). Thus "userId" becomes "User Id", etc. Also, converts underscore into space (and capitalizes the 058 * following word), thus "user_id" also becomes "User Id". 059 */ 060 public static String toUserPresentable(String id) 061 { 062 StringBuilder builder = new StringBuilder(id.length() * 2); 063 064 char[] chars = id.toCharArray(); 065 boolean postSpace = true; 066 boolean upcaseNext = true; 067 068 for (char ch : chars) 069 { 070 if (upcaseNext) 071 { 072 builder.append(Character.toUpperCase(ch)); 073 upcaseNext = false; 074 075 continue; 076 } 077 078 if (ch == '_') 079 { 080 builder.append(' '); 081 upcaseNext = true; 082 continue; 083 } 084 085 boolean upperCase = Character.isUpperCase(ch); 086 087 if (upperCase && !postSpace) 088 builder.append(' '); 089 090 builder.append(ch); 091 092 postSpace = upperCase; 093 } 094 095 return builder.toString(); 096 } 097 098 public static Map<String, String> mapFromKeysAndValues(String... keysAndValues) 099 { 100 Map<String, String> result = CollectionFactory.newMap(); 101 102 int i = 0; 103 while (i < keysAndValues.length) 104 { 105 String key = keysAndValues[i++]; 106 String value = keysAndValues[i++]; 107 108 result.put(key, value); 109 } 110 111 return result; 112 } 113 114 /** 115 * Converts a string to an {@link OptionModel}. The string is of the form "value=label". If the equals sign is 116 * omitted, then the same value is used for both value and label. 117 */ 118 public static OptionModel toOptionModel(String input) 119 { 120 assert input != null; 121 int equalsx = input.indexOf('='); 122 123 if (equalsx < 0) 124 return new OptionModelImpl(input); 125 126 String value = input.substring(0, equalsx); 127 String label = input.substring(equalsx + 1); 128 129 return new OptionModelImpl(label, value); 130 } 131 132 /** 133 * Parses a string input into a series of value=label pairs compatible with {@link #toOptionModel(String)}. Splits 134 * on commas. Ignores whitespace around commas. 135 * 136 * @param input comma seperated list of terms 137 * @return list of option models 138 */ 139 public static List<OptionModel> toOptionModels(String input) 140 { 141 assert input != null; 142 List<OptionModel> result = CollectionFactory.newList(); 143 144 for (String term : input.split(",")) 145 result.add(toOptionModel(term.trim())); 146 147 return result; 148 } 149 150 /** 151 * Wraps the result of {@link #toOptionModels(String)} as a {@link SelectModel} (with no option groups). 152 * 153 * See TAP5-2184 for why this ends up causing some trouble! 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(String.format("Key/value pair '%s' is not properly formatted (it does not contain an equals sign).", 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 Type getPropertyGenericType() 495 { 496 if (conduit instanceof PropertyConduit2) 497 { 498 return ((PropertyConduit2) conduit).getPropertyGenericType(); 499 } 500 return conduit.getPropertyType(); 501 } 502 503 public Object get(Object instance) 504 { 505 return conduit.get(instance); 506 } 507 508 public String getPropertyName() 509 { 510 return null; 511 } 512 }; 513 } 514 515 /** 516 * @param mixinDef the original mixin definition. 517 * @return an Orderable whose id is the mixin name. 518 */ 519 public static Orderable<String> mixinTypeAndOrder(String mixinDef) 520 { 521 int idx = mixinDef.indexOf("::"); 522 if (idx == -1) 523 { 524 return new Orderable<String>(mixinDef, mixinDef); 525 } 526 String type = mixinDef.substring(0, idx); 527 String[] constraints = splitMixinConstraints(mixinDef.substring(idx + 2)); 528 529 return new Orderable<String>(type, type, constraints); 530 } 531 532 public static String[] splitMixinConstraints(String s) 533 { 534 return InternalUtils.isBlank(s) ? null : s.split(";"); 535 } 536 537 /** 538 * Common mapper, used primarily with {@link org.apache.tapestry5.func.Flow#map(org.apache.tapestry5.func.Mapper)} 539 * 540 * @since 5.2.0 541 */ 542 public static Mapper<Asset, StylesheetLink> assetToStylesheetLink = new Mapper<Asset, StylesheetLink>() 543 { 544 public StylesheetLink map(Asset input) 545 { 546 return new StylesheetLink(input); 547 } 548 }; 549 550 public static LinkCreationListener2 toLinkCreationListener2(final LinkCreationListener delegate) 551 { 552 return new LinkCreationListener2() 553 { 554 555 public void createdPageRenderLink(Link link, PageRenderRequestParameters parameters) 556 { 557 delegate.createdPageRenderLink(link); 558 } 559 560 public void createdComponentEventLink(Link link, ComponentEventRequestParameters parameters) 561 { 562 delegate.createdComponentEventLink(link); 563 } 564 }; 565 } 566 567 /** 568 * @since 5.3 569 */ 570 public static String toFileSuffix(String fileName) 571 { 572 int dotx = fileName.lastIndexOf('.'); 573 574 return dotx < 0 ? "" : fileName.substring(dotx + 1); 575 } 576 577 /** 578 * Extracts a value from a map of references. Handles the case where the reference does not exist, 579 * and the case where the reference itself now contains null. 580 * 581 * @since 5.3 582 */ 583 public static <K, V> V getAndDeref(Map<K, ? extends Reference<V>> map, K key) 584 { 585 Reference<V> ref = map.get(key); 586 587 return ref == null ? null : ref.get(); 588 } 589 590 /** 591 * Gathers together an array containing all the threads. 592 * @since 5.4 */ 593 public static Thread[] getAllThreads() { 594 ThreadGroup rootGroup = Thread.currentThread().getThreadGroup(); 595 596 while (true) { 597 ThreadGroup parentGroup = rootGroup.getParent(); 598 if (parentGroup == null) { 599 break; 600 } 601 rootGroup = parentGroup; 602 } 603 604 Thread[] threads = new Thread[rootGroup.activeCount()]; 605 606 while (true) { 607 // A really ugly API. threads.length must be larger than 608 // the actual number of threads, just so we can determine 609 // if we're done. 610 int count = rootGroup.enumerate(threads, true); 611 if (count < threads.length) { 612 return Arrays.copyOf(threads, count); 613 } 614 threads = new Thread[threads.length * 2]; 615 } 616 } 617} 618