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