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