    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}