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