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