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}