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 }