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 --&gt; Z
137     * <li>
138     * <li>java.lang.Integer --&gt; Ljava/lang/Integer;</li>
139     * <li>char[] --&gt; [C</li>
140     * <li>java.lang.String[][] --&gt; [[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}