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
015package org.apache.tapestry5.internal.plastic;
016
017import java.io.ByteArrayOutputStream;
018import java.io.Closeable;
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.PrintWriter;
024import java.io.StringWriter;
025import java.lang.reflect.Array;
026import java.net.URISyntaxException;
027import java.net.URL;
028import java.util.ArrayList;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037import org.apache.tapestry5.internal.plastic.asm.ClassReader;
038import org.apache.tapestry5.internal.plastic.asm.ClassVisitor;
039import org.apache.tapestry5.internal.plastic.asm.MethodVisitor;
040import org.apache.tapestry5.internal.plastic.asm.Opcodes;
041import org.apache.tapestry5.internal.plastic.asm.Type;
042import org.apache.tapestry5.internal.plastic.asm.commons.JSRInlinerAdapter;
043import org.apache.tapestry5.internal.plastic.asm.tree.ClassNode;
044import org.apache.tapestry5.internal.plastic.asm.tree.MethodNode;
045import org.apache.tapestry5.internal.plastic.asm.util.TraceClassVisitor;
046import org.apache.tapestry5.plastic.InstanceContext;
047import org.apache.tapestry5.plastic.MethodDescription;
048
049@SuppressWarnings("rawtypes")
050public 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 --&gt; Z
155     * <li>
156     * <li>java.lang.Integer --&gt; Ljava/lang/Integer;</li>
157     * <li>char[] -->&gt; [C</li>
158     * <li>java.lang.String[][] --&gt; [[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}