    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}