001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.weaver.normalizer;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.lang.annotation.ElementType;
023import java.lang.annotation.Target;
024import java.nio.charset.Charset;
025import java.security.MessageDigest;
026import java.security.NoSuchAlgorithmException;
027import java.text.MessageFormat;
028import java.util.HashMap;
029import java.util.LinkedHashMap;
030import java.util.LinkedHashSet;
031import java.util.Map;
032import java.util.Set;
033
034import javax.activation.DataSource;
035
036import org.apache.commons.io.IOUtils;
037import org.apache.commons.lang3.ArrayUtils;
038import org.apache.commons.lang3.CharEncoding;
039import org.apache.commons.lang3.Conversion;
040import org.apache.commons.lang3.Validate;
041import org.apache.commons.lang3.mutable.MutableBoolean;
042import org.apache.commons.lang3.tuple.MutablePair;
043import org.apache.commons.lang3.tuple.Pair;
044import org.apache.commons.weaver.model.ScanRequest;
045import org.apache.commons.weaver.model.ScanResult;
046import org.apache.commons.weaver.model.Scanner;
047import org.apache.commons.weaver.model.WeavableClass;
048import org.apache.commons.weaver.model.WeaveEnvironment;
049import org.apache.commons.weaver.spi.Weaver;
050import org.objectweb.asm.AnnotationVisitor;
051import org.objectweb.asm.ClassReader;
052import org.objectweb.asm.ClassVisitor;
053import org.objectweb.asm.ClassWriter;
054import org.objectweb.asm.MethodVisitor;
055import org.objectweb.asm.Opcodes;
056import org.objectweb.asm.Type;
057import org.objectweb.asm.commons.GeneratorAdapter;
058import org.objectweb.asm.commons.Method;
059import org.objectweb.asm.commons.Remapper;
060import org.objectweb.asm.commons.RemappingClassAdapter;
061import org.objectweb.asm.commons.SimpleRemapper;
062
063/**
064 * Handles the work of "normalizing" anonymous class definitions.
065 */
066public class Normalizer {
067    private static final String INIT = "<init>";
068
069    private static final Type OBJECT_TYPE = Type.getType(Object.class);
070
071    /**
072     * Marker annotation.
073     */
074    @Target(ElementType.TYPE)
075    private @interface Marker {
076    }
077
078    private static class ClassWrapper {
079        final Class<?> wrapped;
080        final boolean mustRewriteConstructor;
081
082        ClassWrapper(final Class<?> wrapped, final boolean mustRewriteConstructor) {
083            this.wrapped = wrapped;
084            this.mustRewriteConstructor = mustRewriteConstructor;
085        }
086    }
087
088    private class WriteClass extends ClassVisitor {
089        private String className;
090
091        WriteClass() {
092            super(Opcodes.ASM4, new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS));
093        }
094
095        WriteClass(final ClassReader reader) {
096            super(Opcodes.ASM4, new ClassWriter(reader, 0));
097        }
098
099        @Override
100        public void visit(final int version, final int access, final String name, final String signature,
101            final String superName, final String[] intrfces) {
102            super.visit(version, access, name, signature, superName, intrfces);
103            className = name;
104        }
105
106        @Override
107        public void visitEnd() {
108            super.visitEnd();
109            final byte[] bytecode = ((ClassWriter) cv).toByteArray();
110
111            final DataSource classfile = env.getClassfile(className);
112            env.debug("Writing class %s to %s", className, classfile.getName());
113            OutputStream outputStream = null;
114            try {
115                outputStream = classfile.getOutputStream();
116                IOUtils.write(bytecode, outputStream);
117            } catch (final IOException e) {
118                throw new RuntimeException(e);
119            } finally {
120                IOUtils.closeQuietly(outputStream);
121            }
122        }
123    }
124
125    private enum IneligibilityReason {
126        NOT_ANONYMOUS, TOO_MANY_CONSTRUCTORS, IMPLEMENTS_METHODS, TOO_BUSY_CONSTRUCTOR;
127    }
128
129    /**
130     * Configuration prefix for this {@link Weaver}.
131     */
132    public static final String CONFIG_WEAVER = "normalizer.";
133
134    /**
135     * Property name referencing a comma-delimited list of types whose subclasses/implementations should be normalized,
136     * e.g. {@code javax.enterprise.util.TypeLiteral}.
137     */
138    public static final String CONFIG_SUPER_TYPES = CONFIG_WEAVER + "superTypes";
139
140    /**
141     * Property name referencing a package name to which merged types should be added.
142     */
143    public static final String CONFIG_TARGET_PACKAGE = CONFIG_WEAVER + "targetPackage";
144
145    private static final Charset UTF8 = Charset.forName(CharEncoding.UTF_8);
146
147    private final WeaveEnvironment env;
148
149    private final Set<Class<?>> normalizeTypes;
150    private final String targetPackage;
151
152    /**
153     * Create a new {@link Normalizer} instance.
154     * @param env {@link WeaveEnvironment}
155     */
156    public Normalizer(final WeaveEnvironment env) {
157        this.env = env;
158
159        this.targetPackage =
160            Utils.validatePackageName(Validate.notBlank(env.config.getProperty(CONFIG_TARGET_PACKAGE),
161                "missing target package name"));
162        this.normalizeTypes =
163            Utils.parseTypes(
164                Validate.notEmpty(env.config.getProperty(CONFIG_SUPER_TYPES), "no types specified for normalization"),
165                env.classLoader);
166    }
167
168    /**
169     * Normalize the classes found using the specified {@link Scanner}.
170     * @param scanner to scan with
171     * @return whether any work was done
172     */
173    public boolean normalize(final Scanner scanner) {
174        boolean result = false;
175        for (final Class<?> supertype : normalizeTypes) {
176            final Set<Class<?>> subtypes = getBroadlyEligibleSubclasses(supertype, scanner);
177            try {
178                final Map<Pair<String, String>, Set<ClassWrapper>> segregatedSubtypes = segregate(subtypes);
179                for (final Map.Entry<Pair<String, String>, Set<ClassWrapper>> entry : segregatedSubtypes.entrySet()) {
180                    final Set<ClassWrapper> likeTypes = entry.getValue();
181                    if (likeTypes.size() > 1) {
182                        result = true;
183                        rewrite(entry.getKey(), likeTypes);
184                    }
185                }
186            } catch (final RuntimeException e) {
187                throw e;
188            } catch (final Exception e) {
189                throw new RuntimeException(e);
190            }
191        }
192        return result;
193    }
194
195    /**
196     * Map a set of classes by their enclosing class.
197     * @param sort values
198     * @return {@link Map} of enclosing classname to {@link Map} of internal name to {@link ClassWrapper}
199     */
200    private Map<String, Map<String, ClassWrapper>> byEnclosingClass(final Set<ClassWrapper> sort) {
201        final Map<String, Map<String, ClassWrapper>> result = new HashMap<String, Map<String, ClassWrapper>>();
202        for (final ClassWrapper wrapper : sort) {
203            final String outer = wrapper.wrapped.getEnclosingClass().getName();
204            Map<String, ClassWrapper> map = result.get(outer);
205            if (map == null) {
206                map = new LinkedHashMap<String, Normalizer.ClassWrapper>();
207                result.put(outer, map);
208            }
209            map.put(wrapper.wrapped.getName().replace('.', '/'), wrapper);
210        }
211        return result;
212    }
213
214    /**
215     * Rewrite classes as indicated by one entry of {@link #segregate(Iterable)}.
216     * @param key {@link String} {@link Pair} indicating supertype and constructor signature
217     * @param toMerge matching classes
218     * @throws IOException on I/O error
219     * @throws ClassNotFoundException if class not found
220     */
221    private void rewrite(final Pair<String, String> key, final Set<ClassWrapper> toMerge) throws IOException,
222        ClassNotFoundException {
223        final String target = copy(key, toMerge.iterator().next());
224        env.info("Merging %s identical %s implementations with constructor %s to type %s", toMerge.size(),
225            key.getLeft(), key.getRight(), target);
226
227        final Map<String, Map<String, ClassWrapper>> byEnclosingClass = byEnclosingClass(toMerge);
228        for (final Map.Entry<String, Map<String, ClassWrapper>> entry : byEnclosingClass.entrySet()) {
229            final String outer = entry.getKey();
230            env.debug("Normalizing %s inner classes of %s", entry.getValue().size(), outer);
231            final Map<String, String> classMap = new HashMap<String, String>();
232            for (final String merged : entry.getValue().keySet()) {
233                classMap.put(merged, target);
234            }
235            final Remapper remapper = new SimpleRemapper(classMap);
236
237            InputStream enclosingBytecode = null;
238            try {
239                enclosingBytecode = env.getClassfile(outer).getInputStream();
240                final ClassReader reader = new ClassReader(enclosingBytecode);
241
242                final ClassVisitor cv = // NOPMD
243                        new RemappingClassAdapter(new WriteClass(reader), remapper) {
244
245                    @Override
246                    public void visitInnerClass(final String name, final String outerName, final String innerName,
247                        final int access) {
248                        if (!classMap.containsKey(name)) {
249                            super.visitInnerClass(name, outerName, innerName, access);
250                        }
251                    }
252
253                    @Override
254                    public MethodVisitor visitMethod(final int access, final String name, final String desc,
255                        final String signature, final String[] exceptions) {
256                        final MethodVisitor mv = // NOPMD
257                                super.visitMethod(access, name, desc, signature, exceptions);
258                        return new MethodVisitor(Opcodes.ASM4, mv) {
259                            @Override
260                            public void visitMethodInsn(final int opcode, final String owner, final String name,
261                                final String desc) {
262                                String useDescriptor = desc;
263                                if (INIT.equals(name)) {
264                                    final ClassWrapper wrapper = entry.getValue().get(owner);
265                                    if (wrapper != null && wrapper.mustRewriteConstructor) {
266                                        // simply replace first argument type with OBJECT_TYPE:
267                                        final Type[] args = Type.getArgumentTypes(desc);
268                                        args[0] = OBJECT_TYPE;
269                                        useDescriptor = new Method(INIT, Type.VOID_TYPE, args).getDescriptor();
270                                    }
271                                }
272                                super.visitMethodInsn(opcode, owner, name, useDescriptor);
273                            }
274                        };
275                    }
276                };
277
278                reader.accept(cv, 0);
279            } finally {
280                IOUtils.closeQuietly(enclosingBytecode);
281            }
282            for (final String merged : entry.getValue().keySet()) {
283                if (env.deleteClassfile(merged)) {
284                    env.debug("Deleted class %s", merged);
285                } else {
286                    env.warn("Unable to delete class %s", merged);
287                }
288            }
289        }
290
291    }
292
293    /**
294     * <p>Find subclasses/implementors of {code supertype} that:
295     * <ul>
296     * <li>are anonymous</li>
297     * <li>declare a single constructor (probably redundant in the case of an anonymous class)</li>
298     * <li>do not implement any methods</li>
299     * </ul>
300     * </p><p>
301     * Considered "broadly" eligible because the instructions in the implemented constructor may remove the class from
302     * consideration later on.
303     * </p>
304     * @param supertype whose subtypes are sought
305     * @param scanner to use
306     * @return {@link Set} of {@link Class}
307     * @see #segregate(Iterable)
308     */
309    private Set<Class<?>> getBroadlyEligibleSubclasses(final Class<?> supertype, final Scanner scanner) {
310        final ScanResult scanResult = scanner.scan(new ScanRequest().addSupertypes(supertype));
311        final Set<Class<?>> result = new LinkedHashSet<Class<?>>();
312        for (final WeavableClass<?> cls : scanResult.getClasses()) {
313            final Class<?> subtype = cls.getTarget();
314            final IneligibilityReason reason;
315            if (!subtype.isAnonymousClass()) {
316                reason = IneligibilityReason.NOT_ANONYMOUS;
317            } else if (subtype.getDeclaredConstructors().length != 1) {
318                reason = IneligibilityReason.TOO_MANY_CONSTRUCTORS;
319            } else if (subtype.getDeclaredMethods().length > 0) {
320                reason = IneligibilityReason.IMPLEMENTS_METHODS;
321            } else {
322                result.add(subtype);
323                continue;
324            }
325            env.debug("Removed %s from consideration due to %s", subtype, reason);
326        }
327        return result;
328    }
329
330    /**
331     * <p>Segregate a number of classes (presumed subclasses/implementors of a
332     * common supertype/interface). The keys of the map consist of the important
333     * parts for identifying similar anonymous types: the "signature" and the
334     * invoked superclass constructor. For our purposes, the signature consists
335     * of the first applicable item of:
336     * <ol>
337     * <li>The generic signature of the class</li>
338     * <li>The sole implemented interface</li>
339     * <li>The superclass</li>
340     * </ol>
341     * </p><p>
342     * The class will be considered ineligible if its constructor is too "busy" as its side effects cannot be
343     * anticipated; the normalizer will err on the side of caution.
344     * </p><p>
345     * Further, we will here avail ourselves of the opportunity to discard any types we have already normalized.
346     * </p>
347     * @param subtypes
348     * @return Map of Pair<String, String> to Set of Classes
349     * @throws IOException
350     */
351    private Map<Pair<String, String>, Set<ClassWrapper>> segregate(final Iterable<Class<?>> subtypes)
352        throws IOException {
353        final Map<Pair<String, String>, Set<ClassWrapper>> classMap =
354            new LinkedHashMap<Pair<String, String>, Set<ClassWrapper>>();
355        for (final Class<?> subtype : subtypes) {
356            final MutablePair<String, String> key = new MutablePair<String, String>();
357            final MutableBoolean ignore = new MutableBoolean(false);
358            final MutableBoolean valid = new MutableBoolean(true);
359            final MutableBoolean mustRewriteConstructor = new MutableBoolean();
360            InputStream bytecode = null;
361
362            try {
363                bytecode = env.getClassfile(subtype).getInputStream();
364                new ClassReader(bytecode).accept(new ClassVisitor(Opcodes.ASM4) {
365                    String superName;
366
367                    @Override
368                    public void visit(final int version, final int access, final String name, final String signature,
369                        final String superName, final String[] interfaces) {
370                        super.visit(version, access, name, signature, superName, interfaces);
371                        this.superName = superName;
372                        final String left;
373                        if (signature != null) {
374                            left = signature;
375                        } else if (ArrayUtils.getLength(interfaces) == 1) {
376                            left = interfaces[0];
377                        } else {
378                            left = superName;
379                        }
380                        key.setLeft(left);
381                    }
382
383                    @Override
384                    public AnnotationVisitor visitAnnotation(final String desc, final boolean visible) {
385                        if (Type.getType(Marker.class).getDescriptor().equals(desc)) {
386                            ignore.setValue(true);
387                        }
388                        return null;
389                    }
390
391                    @Override
392                    public MethodVisitor visitMethod(final int access, final String name, final String desc,
393                        final String signature, final String[] exceptions) {
394                        if (INIT.equals(name)) {
395                            return new MethodVisitor(Opcodes.ASM4) {
396                                @Override
397                                public void visitMethodInsn(final int opcode, final String owner, final String name,
398                                    final String desc) {
399                                    if (INIT.equals(name) && owner.equals(superName)) {
400                                        key.setRight(desc);
401                                    } else {
402                                        valid.setValue(false);
403                                    }
404                                }
405
406                                @Override
407                                public void visitFieldInsn(final int opcode, final String owner, final String name,
408                                    final String desc) {
409                                    if ("this$0".equals(name) && opcode == Opcodes.PUTFIELD) {
410                                        mustRewriteConstructor.setValue(true);
411                                        return;
412                                    }
413                                    valid.setValue(false);
414                                }
415                            };
416                        }
417                        return null;
418                    }
419                }, 0);
420            } finally {
421                IOUtils.closeQuietly(bytecode);
422            }
423            if (ignore.booleanValue()) {
424                continue;
425            }
426            if (valid.booleanValue()) {
427                Set<ClassWrapper> set = classMap.get(key);
428                if (set == null) {
429                    set = new LinkedHashSet<ClassWrapper>();
430                    classMap.put(key, set);
431                }
432                set.add(new ClassWrapper(subtype, mustRewriteConstructor.booleanValue()));
433            } else {
434                env.debug("%s is ineligible for normalization due to %s", subtype,
435                    IneligibilityReason.TOO_BUSY_CONSTRUCTOR);
436            }
437        }
438        return classMap;
439    }
440
441    /**
442     * Create the normalized version of a given class in the configured target package. The {@link Normalizer} will
443     * gladly do so in a package from which the normalized class will not actually be able to reference any types upon
444     * which it relies; in such a situation you must specify the target package as the package of the supertype.
445     * @param key used to generate the normalized classname.
446     * @param classWrapper
447     * @return the generated classname.
448     * @throws IOException
449     * @throws ClassNotFoundException
450     */
451    private String copy(final Pair<String, String> key, final ClassWrapper classWrapper) throws IOException,
452        ClassNotFoundException {
453        final MessageDigest md5;
454        try {
455            md5 = MessageDigest.getInstance("MD5");
456        } catch (final NoSuchAlgorithmException e) {
457            throw new RuntimeException(e);
458        }
459        md5.update(key.getLeft().getBytes(UTF8));
460        md5.update(key.getRight().getBytes(UTF8));
461
462        final long digest = Conversion.byteArrayToLong(md5.digest(), 0, 0L, 0, Long.SIZE / Byte.SIZE);
463
464        final String result = MessageFormat.format("{0}/$normalized{1,number,0;_0}", targetPackage, digest);
465
466        env.debug("Copying class %s to %s", classWrapper.wrapped.getName(), result);
467
468        InputStream bytecode = null;
469
470        try {
471            bytecode = env.getClassfile(classWrapper.wrapped).getInputStream();
472            final ClassReader reader = new ClassReader(bytecode);
473
474            final ClassVisitor writeClass = new WriteClass();
475
476            // we're doing most of this by hand; we only read the original class to hijack signature, ctor exceptions,
477            // etc.:
478
479            reader.accept(new ClassVisitor(Opcodes.ASM4) {
480                Type supertype;
481
482                @Override
483                public void visit(final int version, final int access, final String name, final String signature,
484                    final String superName, final String[] interfaces) {
485                    supertype = Type.getObjectType(superName);
486                    writeClass.visit(version, Opcodes.ACC_PUBLIC, result, signature, superName, interfaces);
487
488                    visitAnnotation(Type.getType(Marker.class).getDescriptor(), false);
489                }
490
491                @Override
492                public MethodVisitor visitMethod(final int access, final String name, final String desc,
493                    final String signature, final String[] exceptions) {
494                    if (INIT.equals(name)) {
495
496                        final Method staticCtor = new Method(INIT, key.getRight());
497                        final Type[] argumentTypes = staticCtor.getArgumentTypes();
498                        final Type[] exceptionTypes = toObjectTypes(exceptions);
499
500                        {
501                            final GeneratorAdapter mgen =
502                                new GeneratorAdapter(Opcodes.ACC_PUBLIC, staticCtor, signature, exceptionTypes,
503                                    writeClass);
504                            mgen.visitCode();
505                            mgen.loadThis();
506                            for (int i = 0; i < argumentTypes.length; i++) {
507                                mgen.loadArg(i);
508                            }
509                            mgen.invokeConstructor(supertype, staticCtor);
510                            mgen.returnValue();
511                            mgen.endMethod();
512                        }
513                        /*
514                         * now declare a dummy constructor that will match, and discard,
515                         * any originally inner-class bound constructor i.e. that set up a this$0 field.
516                         * By doing this we can avoid playing with the stack that originally
517                         * invoked such a constructor and simply rewrite the method
518                         */
519                        {
520                            final Method instanceCtor =
521                                new Method(INIT, Type.VOID_TYPE, ArrayUtils.add(argumentTypes, 0, OBJECT_TYPE));
522                            final GeneratorAdapter mgen =
523                                new GeneratorAdapter(Opcodes.ACC_PUBLIC, instanceCtor, signature, exceptionTypes,
524                                    writeClass);
525                            mgen.visitCode();
526                            mgen.loadThis();
527                            for (int i = 0; i < argumentTypes.length; i++) {
528                                mgen.loadArg(i + 1);
529                            }
530                            mgen.invokeConstructor(supertype, staticCtor);
531                            mgen.returnValue();
532                            mgen.endMethod();
533                        }
534                        return null;
535                    }
536                    return null;
537                }
538
539                @Override
540                public void visitEnd() {
541                    writeClass.visitEnd();
542                }
543            }, 0);
544        } finally {
545            IOUtils.closeQuietly(bytecode);
546        }
547        return result;
548    }
549
550    /**
551     * Translate internal names to Java type names.
552     * @param types to translate
553     * @return {@link Type}[]
554     * @see Type#getObjectType(String)
555     */
556    private static Type[] toObjectTypes(final String[] types) {
557        if (types == null) {
558            return null;
559        }
560        final Type[] result = new Type[types.length];
561        for (int i = 0; i < types.length; i++) {
562            result[i] = Type.getObjectType(types[i]);
563        }
564        return result;
565    }
566}