    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.concurrent.ConcurrentHashMap;
    029import java.util.concurrent.ConcurrentMap;
    030import java.util.regex.Matcher;
    031import java.util.regex.Pattern;
    032
    033@SuppressWarnings("rawtypes")
    034public class PlasticInternalUtils
    035{
    036    public static final String[] EMPTY = new String[0];
    037
    038    public static boolean isEmpty(Object[] input)
    039    {
    040        return input == null || input.length == 0;
    041    }
    042
    043    public static String[] orEmpty(String[] input)
    044    {
    045        return input == null ? EMPTY : input;
    046    }
    047
    048    public static boolean isBlank(String input)
    049    {
    050        return input == null || input.length() == 0 || input.trim().length() == 0;
    051    }
    052
    053    public static boolean isNonBlank(String input)
    054    {
    055        return !isBlank(input);
    056    }
    057
    058    public static String toInternalName(String className)
    059    {
    060        assert isNonBlank(className);
    061
    062        return className.replace('.', '/');
    063    }
    064
    065    public static String toClassPath(String className)
    066    {
    067        return toInternalName(className) + ".class";
    068    }
    069
    070    public static String toMessage(Throwable t)
    071    {
    072        String message = t.getMessage();
    073
    074        return isBlank(message) ? t.getClass().getName() : message;
    075    }
    076
    077    public static void close(Closeable closeable)
    078    {
    079        try
    080        {
    081            if (closeable != null)
    082                closeable.close();
    083        } catch (IOException ex)
    084        {
    085            // Ignore it.
    086        }
    087    }
    088
    089    @SuppressWarnings("unchecked")
    090    public static MethodDescription toMethodDescription(MethodNode node)
    091    {
    092        String returnType = Type.getReturnType(node.desc).getClassName();
    093
    094        String[] arguments = toClassNames(Type.getArgumentTypes(node.desc));
    095
    096        List<String> exceptions = node.exceptions;
    097
    098        String[] exceptionClassNames = new String[exceptions.size()];
    099
    100        for (int i = 0; i < exceptionClassNames.length; i++)
    101        {
    102            exceptionClassNames[i] = exceptions.get(i).replace('/', '.');
    103        }
    104
    105        return new MethodDescription(node.access, returnType, node.name, arguments, node.signature, exceptionClassNames);
    106    }
    107
    108    private static String[] toClassNames(Type[] types)
    109    {
    110        if (isEmpty(types))
    111            return EMPTY;
    112
    113        String[] result = new String[types.length];
    114
    115        for (int i = 0; i < result.length; i++)
    116        {
    117            result[i] = types[i].getClassName();
    118        }
    119
    120        return result;
    121    }
    122
    123    /**
    124     * Converts a class's internal name (i.e., using slashes)
    125     * to Java source code format (i.e., using periods).
    126     */
    127    public static String toClassName(String internalName)
    128    {
    129        assert isNonBlank(internalName);
    130
    131        return internalName.replace('/', '.');
    132    }
    133
    134    /**
    135     * Converts a primitive type or fully qualified class name (or array form) to
    136     * a descriptor.
    137     * <ul>
    138     * <li>boolean --&gt; Z
    139     * <li>
    140     * <li>java.lang.Integer --&gt; Ljava/lang/Integer;</li>
    141     * <li>char[] --&gt; [C</li>
    142     * <li>java.lang.String[][] --&gt; [[java/lang/String;
    143     * </ul>
    144     */
    145    public static String toDescriptor(String className)
    146    {
    147        String buffer = className;
    148        int arrayDepth = 0;
    149
    150        while (buffer.endsWith("[]"))
    151        {
    152            arrayDepth++;
    153            buffer = buffer.substring(0, buffer.length() - 2);
    154        }
    155
    156        // Get the description of the base element type, then figure out if and
    157        // how to identify it as an array type.
    158
    159        PrimitiveType type = PrimitiveType.getByName(buffer);
    160
    161        String baseDesc = type == null ? "L" + buffer.replace('.', '/') + ";" : type.descriptor;
    162
    163        if (arrayDepth == 0)
    164            return baseDesc;
    165
    166        StringBuilder b = new StringBuilder();
    167
    168        for (int i = 0; i < arrayDepth; i++)
    169        {
    170            b.append('[');
    171        }
    172
    173        b.append(baseDesc);
    174
    175        return b.toString();
    176    }
    177
    178    private static final Pattern DESC = Pattern.compile("^L(.*);$");
    179
    180    /**
    181     * Converts an object type descriptor (i.e. "Ljava/lang/Object;") to a class name
    182     * ("java.lang.Object").
    183     */
    184    public static String objectDescriptorToClassName(String descriptor)
    185    {
    186        assert descriptor != null;
    187
    188        Matcher matcher = DESC.matcher(descriptor);
    189
    190        if (!matcher.matches())
    191            throw new IllegalArgumentException(String.format("Input '%s' is not an object descriptor.", descriptor));
    192
    193        return toClassName(matcher.group(1));
    194    }
    195
    196    public static <K, V> Map<K, V> newMap()
    197    {
    198        return new HashMap<K, V>();
    199    }
    200
    201    public static <K, V> ConcurrentMap<K, V> newConcurrentMap()
    202    {
    203        return new ConcurrentHashMap<K, V>();
    204    }
    205
    206    public static <T> Set<T> newSet()
    207    {
    208        return new HashSet<T>();
    209    }
    210
    211    public static <T> List<T> newList()
    212    {
    213        return new ArrayList<T>();
    214    }
    215
    216    public static String dissasembleBytecode(ClassNode classNode)
    217    {
    218        StringWriter stringWriter = new StringWriter();
    219        PrintWriter writer = new PrintWriter(stringWriter);
    220
    221        TraceClassVisitor visitor = new TraceClassVisitor(writer);
    222
    223        classNode.accept(visitor);
    224
    225        writer.close();
    226
    227        return stringWriter.toString();
    228    }
    229
    230    private static final Pattern PROPERTY_PATTERN = Pattern.compile("^(m?_+)?(.+?)_*$", Pattern.CASE_INSENSITIVE);
    231
    232    /**
    233     * Strips out leading and trailing underscores, leaving the real property name.
    234     * In addition, "m_foo" is converted to "foo".
    235     *
    236     * @param fieldName to convert
    237     * @return the property name
    238     */
    239    public static String toPropertyName(String fieldName)
    240    {
    241        Matcher matcher = PROPERTY_PATTERN.matcher(fieldName);
    242
    243        if (!matcher.matches())
    244            throw new IllegalArgumentException(String.format(
    245                    "Field name '%s' can not be converted to a property name.", fieldName));
    246
    247        return matcher.group(2);
    248    }
    249
    250    /**
    251     * Capitalizes the input string, converting the first character to upper case.
    252     *
    253     * @param input a non-empty string
    254     * @return the same string if already capitalized, or a capitalized version
    255     */
    256    public static String capitalize(String input)
    257    {
    258        char first = input.charAt(0);
    259
    260        if (Character.isUpperCase(first))
    261            return input;
    262
    263        return String.valueOf(Character.toUpperCase(first)) + input.substring(1);
    264    }
    265
    266    private static final Map<String, Class> PRIMITIVES = new HashMap<String, Class>();
    267
    268    static
    269    {
    270        PRIMITIVES.put("boolean", boolean.class);
    271        PRIMITIVES.put("char", char.class);
    272        PRIMITIVES.put("byte", byte.class);
    273        PRIMITIVES.put("short", short.class);
    274        PRIMITIVES.put("int", int.class);
    275        PRIMITIVES.put("long", long.class);
    276        PRIMITIVES.put("float", float.class);
    277        PRIMITIVES.put("double", double.class);
    278        PRIMITIVES.put("void", void.class);
    279    }
    280
    281    /**
    282     * @param loader   class loader to look up in
    283     * @param javaName java name is Java source format (e.g., "int", "int[]", "java.lang.String", "java.lang.String[]", etc.)
    284     * @return class instance
    285     * @throws ClassNotFoundException
    286     */
    287    public static Class toClass(ClassLoader loader, String javaName) throws ClassNotFoundException
    288    {
    289        int depth = 0;
    290
    291        while (javaName.endsWith("[]"))
    292        {
    293            depth++;
    294            javaName = javaName.substring(0, javaName.length() - 2);
    295        }
    296
    297        Class primitive = PRIMITIVES.get(javaName);
    298
    299        if (primitive != null)
    300        {
    301            Class result = primitive;
    302            for (int i = 0; i < depth; i++)
    303            {
    304                result = Array.newInstance(result, 0).getClass();
    305            }
    306
    307            return result;
    308        }
    309
    310        if (depth == 0)
    311            return Class.forName(javaName, true, loader);
    312
    313        StringBuilder builder = new StringBuilder(20);
    314
    315        for (int i = 0; i < depth; i++)
    316        {
    317            builder.append('[');
    318        }
    319
    320        builder.append('L').append(javaName).append(';');
    321
    322        return Class.forName(builder.toString(), true, loader);
    323    }
    324
    325    public static Object getFromInstanceContext(InstanceContext context, String javaName)
    326    {
    327        ClassLoader loader = context.getInstanceType().getClassLoader();
    328
    329        try
    330        {
    331            Class valueType = toClass(loader, javaName);
    332
    333            return context.get(valueType);
    334        } catch (ClassNotFoundException ex)
    335        {
    336            throw new RuntimeException(ex);
    337        }
    338    }
    339
    340    /**
    341     * Returns true if both objects are the same instance, or both null, or left equals right.
    342     */
    343    public static boolean isEqual(Object left, Object right)
    344    {
    345        return left == right || (left != null && left.equals(right));
    346    }
    347
    348    static byte[] readBytestream(InputStream stream) throws IOException
    349    {
    350        byte[] buffer = new byte[5000];
    351
    352        ByteArrayOutputStream bos = new ByteArrayOutputStream();
    353
    354        while (true)
    355        {
    356            int length = stream.read(buffer);
    357
    358            if (length < 0)
    359                break;
    360
    361            bos.write(buffer, 0, length);
    362        }
    363
    364        bos.close();
    365
    366        return bos.toByteArray();
    367    }
    368
    369    public static byte[] readBytecodeForClass(ClassLoader loader, String className, boolean mustExist)
    370    {
    371        String path = toClassPath(className);
    372        InputStream stream = null;
    373
    374        try
    375        {
    376            stream = getStreamForPath(loader, path);
    377
    378            if (stream == null)
    379            {
    380                if (mustExist)
    381                    throw new RuntimeException(String.format("Unable to locate class file for '%s' in class loader %s.",
    382                            className, loader));
    383
    384                return null;
    385            }
    386
    387            return readBytestream(stream);
    388        } catch (IOException ex)
    389        {
    390            throw new RuntimeException(String.format("Failure reading bytecode for class %s: %s", className,
    391                    toMessage(ex)), ex);
    392        } finally
    393        {
    394            close(stream);
    395        }
    396    }
    397
    398    static InputStream getStreamForPath(ClassLoader loader, String path) throws IOException
    399    {
    400        URL url = loader.getResource(path);
    401
    402        if (url == null)
    403        {
    404            return null;
    405        }
    406
    407        // This *should* handle Tomcat better, where the Tomcat class loader appears to be caching
    408        // the contents of files; this bypasses Tomcat to re-read the files from the disk directly.
    409
    410        if (url.getProtocol().equals("file"))
    411        {
    412            try {
    413                return new FileInputStream(new File(url.toURI()));
    414            } catch (URISyntaxException e)
    415            {
    416                return null;
    417            }
    418        }
    419
    420        return url.openStream();
    421    }
    422
    423    public static ClassNode convertBytecodeToClassNode(byte[] bytecode)
    424    {
    425        ClassReader cr = new ClassReader(bytecode);
    426
    427        ClassNode result = new ClassNode();
    428
    429        ClassVisitor adapter = new ClassVisitor(Opcodes.ASM9, result)
    430        {
    431            @Override
    432            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
    433            {
    434                MethodVisitor delegate = super.visitMethod(access, name, desc, signature, exceptions);
    435
    436                return new JSRInlinerAdapter(delegate, access, name, desc, signature, exceptions);
    437            }
    438        };
    439
    440        cr.accept(adapter, 0);
    441
    442        return result;
    443    }
    444}