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