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.plastic; 014 015import org.apache.tapestry5.internal.plastic.asm.*; 016import org.apache.tapestry5.internal.plastic.asm.commons.JSRInlinerAdapter; 017import org.apache.tapestry5.internal.plastic.asm.tree.ClassNode; 018import org.apache.tapestry5.internal.plastic.asm.tree.MethodNode; 019import org.apache.tapestry5.internal.plastic.asm.util.TraceClassVisitor; 020import org.apache.tapestry5.plastic.InstanceContext; 021import org.apache.tapestry5.plastic.MethodDescription; 022 023import java.io.*; 024import java.lang.reflect.Array; 025import java.net.URISyntaxException; 026import java.net.URL; 027import java.util.*; 028import java.util.concurrent.ConcurrentHashMap; 029import java.util.concurrent.ConcurrentMap; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033@SuppressWarnings("rawtypes") 034public class PlasticInternalUtils 035{ 036 public static final String[] EMPTY = new String[0]; 037 038 public static boolean isEmpty(Object[] input) 039 { 040 return input == null || input.length == 0; 041 } 042 043 public static String[] orEmpty(String[] input) 044 { 045 return input == null ? EMPTY : input; 046 } 047 048 public static boolean isBlank(String input) 049 { 050 return input == null || input.length() == 0 || input.trim().length() == 0; 051 } 052 053 public static boolean isNonBlank(String input) 054 { 055 return !isBlank(input); 056 } 057 058 public static String toInternalName(String className) 059 { 060 assert isNonBlank(className); 061 062 return className.replace('.', '/'); 063 } 064 065 public static String toClassPath(String className) 066 { 067 return toInternalName(className) + ".class"; 068 } 069 070 public static String toMessage(Throwable t) 071 { 072 String message = t.getMessage(); 073 074 return isBlank(message) ? t.getClass().getName() : message; 075 } 076 077 public static void close(Closeable closeable) 078 { 079 try 080 { 081 if (closeable != null) 082 closeable.close(); 083 } catch (IOException ex) 084 { 085 // Ignore it. 086 } 087 } 088 089 @SuppressWarnings("unchecked") 090 public static MethodDescription toMethodDescription(MethodNode node) 091 { 092 String returnType = Type.getReturnType(node.desc).getClassName(); 093 094 String[] arguments = toClassNames(Type.getArgumentTypes(node.desc)); 095 096 List<String> exceptions = node.exceptions; 097 098 String[] exceptionClassNames = new String[exceptions.size()]; 099 100 for (int i = 0; i < exceptionClassNames.length; i++) 101 { 102 exceptionClassNames[i] = exceptions.get(i).replace('/', '.'); 103 } 104 105 return new MethodDescription(node.access, returnType, node.name, arguments, node.signature, exceptionClassNames); 106 } 107 108 private static String[] toClassNames(Type[] types) 109 { 110 if (isEmpty(types)) 111 return EMPTY; 112 113 String[] result = new String[types.length]; 114 115 for (int i = 0; i < result.length; i++) 116 { 117 result[i] = types[i].getClassName(); 118 } 119 120 return result; 121 } 122 123 /** 124 * Converts a class's internal name (i.e., using slashes) 125 * to Java source code format (i.e., using periods). 126 */ 127 public static String toClassName(String internalName) 128 { 129 assert isNonBlank(internalName); 130 131 return internalName.replace('/', '.'); 132 } 133 134 /** 135 * Converts a primitive type or fully qualified class name (or array form) to 136 * a descriptor. 137 * <ul> 138 * <li>boolean --> Z 139 * <li> 140 * <li>java.lang.Integer --> Ljava/lang/Integer;</li> 141 * <li>char[] --> [C</li> 142 * <li>java.lang.String[][] --> [[java/lang/String; 143 * </ul> 144 */ 145 public static String toDescriptor(String className) 146 { 147 String buffer = className; 148 int arrayDepth = 0; 149 150 while (buffer.endsWith("[]")) 151 { 152 arrayDepth++; 153 buffer = buffer.substring(0, buffer.length() - 2); 154 } 155 156 // Get the description of the base element type, then figure out if and 157 // how to identify it as an array type. 158 159 PrimitiveType type = PrimitiveType.getByName(buffer); 160 161 String baseDesc = type == null ? "L" + buffer.replace('.', '/') + ";" : type.descriptor; 162 163 if (arrayDepth == 0) 164 return baseDesc; 165 166 StringBuilder b = new StringBuilder(); 167 168 for (int i = 0; i < arrayDepth; i++) 169 { 170 b.append('['); 171 } 172 173 b.append(baseDesc); 174 175 return b.toString(); 176 } 177 178 private static final Pattern DESC = Pattern.compile("^L(.*);$"); 179 180 /** 181 * Converts an object type descriptor (i.e. "Ljava/lang/Object;") to a class name 182 * ("java.lang.Object"). 183 */ 184 public static String objectDescriptorToClassName(String descriptor) 185 { 186 assert descriptor != null; 187 188 Matcher matcher = DESC.matcher(descriptor); 189 190 if (!matcher.matches()) 191 throw new IllegalArgumentException(String.format("Input '%s' is not an object descriptor.", descriptor)); 192 193 return toClassName(matcher.group(1)); 194 } 195 196 public static <K, V> Map<K, V> newMap() 197 { 198 return new HashMap<K, V>(); 199 } 200 201 public static <K, V> ConcurrentMap<K, V> newConcurrentMap() 202 { 203 return new ConcurrentHashMap<K, V>(); 204 } 205 206 public static <T> Set<T> newSet() 207 { 208 return new HashSet<T>(); 209 } 210 211 public static <T> List<T> newList() 212 { 213 return new ArrayList<T>(); 214 } 215 216 public static String dissasembleBytecode(ClassNode classNode) 217 { 218 StringWriter stringWriter = new StringWriter(); 219 PrintWriter writer = new PrintWriter(stringWriter); 220 221 TraceClassVisitor visitor = new TraceClassVisitor(writer); 222 223 classNode.accept(visitor); 224 225 writer.close(); 226 227 return stringWriter.toString(); 228 } 229 230 private static final Pattern PROPERTY_PATTERN = Pattern.compile("^(m?_+)?(.+?)_*$", Pattern.CASE_INSENSITIVE); 231 232 /** 233 * Strips out leading and trailing underscores, leaving the real property name. 234 * In addition, "m_foo" is converted to "foo". 235 * 236 * @param fieldName to convert 237 * @return the property name 238 */ 239 public static String toPropertyName(String fieldName) 240 { 241 Matcher matcher = PROPERTY_PATTERN.matcher(fieldName); 242 243 if (!matcher.matches()) 244 throw new IllegalArgumentException(String.format( 245 "Field name '%s' can not be converted to a property name.", fieldName)); 246 247 return matcher.group(2); 248 } 249 250 /** 251 * Capitalizes the input string, converting the first character to upper case. 252 * 253 * @param input a non-empty string 254 * @return the same string if already capitalized, or a capitalized version 255 */ 256 public static String capitalize(String input) 257 { 258 char first = input.charAt(0); 259 260 if (Character.isUpperCase(first)) 261 return input; 262 263 return String.valueOf(Character.toUpperCase(first)) + input.substring(1); 264 } 265 266 private static final Map<String, Class> PRIMITIVES = new HashMap<String, Class>(); 267 268 static 269 { 270 PRIMITIVES.put("boolean", boolean.class); 271 PRIMITIVES.put("char", char.class); 272 PRIMITIVES.put("byte", byte.class); 273 PRIMITIVES.put("short", short.class); 274 PRIMITIVES.put("int", int.class); 275 PRIMITIVES.put("long", long.class); 276 PRIMITIVES.put("float", float.class); 277 PRIMITIVES.put("double", double.class); 278 PRIMITIVES.put("void", void.class); 279 } 280 281 /** 282 * @param loader class loader to look up in 283 * @param javaName java name is Java source format (e.g., "int", "int[]", "java.lang.String", "java.lang.String[]", etc.) 284 * @return class instance 285 * @throws ClassNotFoundException 286 */ 287 public static Class toClass(ClassLoader loader, String javaName) throws ClassNotFoundException 288 { 289 int depth = 0; 290 291 while (javaName.endsWith("[]")) 292 { 293 depth++; 294 javaName = javaName.substring(0, javaName.length() - 2); 295 } 296 297 Class primitive = PRIMITIVES.get(javaName); 298 299 if (primitive != null) 300 { 301 Class result = primitive; 302 for (int i = 0; i < depth; i++) 303 { 304 result = Array.newInstance(result, 0).getClass(); 305 } 306 307 return result; 308 } 309 310 if (depth == 0) 311 return Class.forName(javaName, true, loader); 312 313 StringBuilder builder = new StringBuilder(20); 314 315 for (int i = 0; i < depth; i++) 316 { 317 builder.append('['); 318 } 319 320 builder.append('L').append(javaName).append(';'); 321 322 return Class.forName(builder.toString(), true, loader); 323 } 324 325 public static Object getFromInstanceContext(InstanceContext context, String javaName) 326 { 327 ClassLoader loader = context.getInstanceType().getClassLoader(); 328 329 try 330 { 331 Class valueType = toClass(loader, javaName); 332 333 return context.get(valueType); 334 } catch (ClassNotFoundException ex) 335 { 336 throw new RuntimeException(ex); 337 } 338 } 339 340 /** 341 * Returns true if both objects are the same instance, or both null, or left equals right. 342 */ 343 public static boolean isEqual(Object left, Object right) 344 { 345 return left == right || (left != null && left.equals(right)); 346 } 347 348 static byte[] readBytestream(InputStream stream) throws IOException 349 { 350 byte[] buffer = new byte[5000]; 351 352 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 353 354 while (true) 355 { 356 int length = stream.read(buffer); 357 358 if (length < 0) 359 break; 360 361 bos.write(buffer, 0, length); 362 } 363 364 bos.close(); 365 366 return bos.toByteArray(); 367 } 368 369 public static byte[] readBytecodeForClass(ClassLoader loader, String className, boolean mustExist) 370 { 371 String path = toClassPath(className); 372 InputStream stream = null; 373 374 try 375 { 376 stream = getStreamForPath(loader, path); 377 378 if (stream == null) 379 { 380 if (mustExist) 381 throw new RuntimeException(String.format("Unable to locate class file for '%s' in class loader %s.", 382 className, loader)); 383 384 return null; 385 } 386 387 return readBytestream(stream); 388 } catch (IOException ex) 389 { 390 throw new RuntimeException(String.format("Failure reading bytecode for class %s: %s", className, 391 toMessage(ex)), ex); 392 } finally 393 { 394 close(stream); 395 } 396 } 397 398 static InputStream getStreamForPath(ClassLoader loader, String path) throws IOException 399 { 400 URL url = loader.getResource(path); 401 402 if (url == null) 403 { 404 return null; 405 } 406 407 // This *should* handle Tomcat better, where the Tomcat class loader appears to be caching 408 // the contents of files; this bypasses Tomcat to re-read the files from the disk directly. 409 410 if (url.getProtocol().equals("file")) 411 { 412 try { 413 return new FileInputStream(new File(url.toURI())); 414 } catch (URISyntaxException e) 415 { 416 return null; 417 } 418 } 419 420 return url.openStream(); 421 } 422 423 public static ClassNode convertBytecodeToClassNode(byte[] bytecode) 424 { 425 ClassReader cr = new ClassReader(bytecode); 426 427 ClassNode result = new ClassNode(); 428 429 ClassVisitor adapter = new ClassVisitor(Opcodes.ASM9, result) 430 { 431 @Override 432 public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) 433 { 434 MethodVisitor delegate = super.visitMethod(access, name, desc, signature, exceptions); 435 436 return new JSRInlinerAdapter(delegate, access, name, desc, signature, exceptions); 437 } 438 }; 439 440 cr.accept(adapter, 0); 441 442 return result; 443 } 444}