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}