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    }