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