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 }