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