diff options
author | 2017-04-27 07:14:14 +0200 | |
---|---|---|
committer | 2017-04-27 11:18:28 +0200 | |
commit | 15d403d3c06fb47838cc4d294898e6530deca3d3 (patch) | |
tree | 7061ac98639b8106f91afba180af972d4e8ad8a7 /src/tools/android/java | |
parent | 11cc89a27544311111473dc7a17522635c2b6e70 (diff) |
Desugar try-with-resources statements for Android. Any call to
Throwable.addSuppressed(Throwable), getSuppressed(), printStackTrace()
printStackTrace(PrintStream), printStackTrace(PrintWriter) is directed
to the ThrowableExtension class.
At runtime, ThrowableExtension will determine the best behavior for
try-with-resources. If the device has API level >= 19, the device's Thowable
will be used. Otherwise, this class will mimic the behavior.
RELNOTES: Desugar try-with-resources so that this language feature is available
to deveces with API level under 19.
PiperOrigin-RevId: 154386342
Diffstat (limited to 'src/tools/android/java')
5 files changed, 523 insertions, 28 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD index 73c28453de..2d5f91e96e 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD @@ -13,6 +13,7 @@ filegroup( java_library( name = "desugar", srcs = glob(["*.java"]), + runtime_deps = ["//src/tools/android/java/com/google/devtools/build/android/desugar/runtime:throwable_extension"], deps = [ "//src/main/java/com/google/devtools/common/options", "//src/main/protobuf:worker_protocol_java_proto", @@ -34,6 +35,8 @@ java_binary( filegroup( name = "srcs", - srcs = glob(["**"]), + srcs = glob(["**"]) + [ + "//src/tools/android/java/com/google/devtools/build/android/desugar/runtime:srcs", + ], visibility = ["//src/tools/android/java/com/google/devtools/build/android:__pkg__"], ) diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java index 8f4c68a6f8..c4528ae88e 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; +import com.google.common.io.ByteStreams; import com.google.common.io.Closer; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.PathConverter; @@ -32,6 +33,7 @@ import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.OptionsParser.OptionUsageRestrictions; import com.google.errorprone.annotations.MustBeClosed; +import java.io.IOError; import java.io.IOException; import java.io.InputStream; import java.nio.file.FileVisitResult; @@ -43,6 +45,7 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -152,13 +155,22 @@ class Desugar { name = "desugar_interface_method_bodies_if_needed", defaultValue = "true", category = "misc", - help = "Rewrites default and static methods in interfaces if --min_sdk_version < 24. This " - + "only works correctly if subclasses of rewritten interfaces as well as uses of static " - + "interface methods are run through this tool as well." + help = + "Rewrites default and static methods in interfaces if --min_sdk_version < 24. This " + + "only works correctly if subclasses of rewritten interfaces as well as uses of " + + "static interface methods are run through this tool as well." ) public boolean desugarInterfaceMethodBodiesIfNeeded; @Option( + name = "desugar_try_with_resources_if_needed", + defaultValue = "false", + category = "misc", + help = "Rewrites try-with-resources statements if --min_sdk_version < 19." + ) + public boolean desugarTryWithResourcesIfNeeded; + + @Option( name = "copy_bridges_from_classpath", defaultValue = "false", category = "misc", @@ -181,10 +193,12 @@ class Desugar { private final CoreLibraryRewriter rewriter; private final LambdaClassMaker lambdas; private final GeneratedClassStore store; + /** The counter to record the times of try-with-resources desugaring is invoked. */ + private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger(); + private final boolean outputJava7; private final boolean allowDefaultMethods; private final boolean allowCallsToObjectsNonNull; - /** An instance of Desugar is expected to be used ONLY ONCE */ private boolean used; @@ -213,23 +227,27 @@ class Desugar { ClassLoader bootclassloader = options.bootclasspath.isEmpty() ? new ThrowingClassLoader() - : new HeaderClassLoader( - indexedBootclasspath, - rewriter, - new ThrowingClassLoader()); + : new HeaderClassLoader(indexedBootclasspath, rewriter, new ThrowingClassLoader()); IndexedInputs indexedClasspath = new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath)); // Process each input separately for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) { - desugarOneInput(inputOutputPair, indexedClasspath, bootclassloader, + desugarOneInput( + inputOutputPair, + indexedClasspath, + bootclassloader, new ClassReaderFactory(indexedBootclasspath, rewriter)); } } } - private void desugarOneInput(InputOutputPair inputOutputPair, IndexedInputs indexedClasspath, - ClassLoader bootclassloader, ClassReaderFactory bootclasspathReader) throws Exception { + private void desugarOneInput( + InputOutputPair inputOutputPair, + IndexedInputs indexedClasspath, + ClassLoader bootclassloader, + ClassReaderFactory bootclasspathReader) + throws Exception { Path inputPath = inputOutputPair.getInput(); Path outputPath = inputOutputPair.getOutput(); checkArgument( @@ -261,13 +279,24 @@ class Desugar { ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder(); - desugarClassesInInput(inputFiles, outputFileProvider, loader, classpathReader, - bootclasspathReader, interfaceLambdaMethodCollector); - - desugarAndWriteDumpedLambdaClassesToOutput(outputFileProvider, loader, classpathReader, - bootclasspathReader, interfaceLambdaMethodCollector.build(), bridgeMethodReader); + desugarClassesInInput( + inputFiles, + outputFileProvider, + loader, + classpathReader, + bootclasspathReader, + interfaceLambdaMethodCollector); + + desugarAndWriteDumpedLambdaClassesToOutput( + outputFileProvider, + loader, + classpathReader, + bootclasspathReader, + interfaceLambdaMethodCollector.build(), + bridgeMethodReader); desugarAndWriteGeneratedClasses(outputFileProvider); + copyThrowableExtensionClass(outputFileProvider); } ImmutableMap<Path, LambdaInfo> lambdasLeftBehind = lambdas.drain(); @@ -276,6 +305,25 @@ class Desugar { checkState(generatedLeftBehind.isEmpty(), "Didn't process %s", generatedLeftBehind.keySet()); } + private void copyThrowableExtensionClass(OutputFileProvider outputFileProvider) { + if (!outputJava7 || !options.desugarTryWithResourcesIfNeeded) { + // try-with-resources statements are okay in the output jar. + return; + } + if (this.numOfTryWithResourcesInvoked.get() <= 0) { + // the try-with-resources desugaring pass does nothing, so no need to copy these class files. + return; + } + for (String className : + TryWithResourcesRewriter.THROWABLE_EXT_CLASS_INTERNAL_NAMES_WITH_CLASS_EXT) { + try (InputStream stream = Desugar.class.getClassLoader().getResourceAsStream(className)) { + outputFileProvider.write(className, ByteStreams.toByteArray(stream)); + } catch (IOException e) { + throw new IOError(e); + } + } + } + /** Desugar the classes that are in the inputs specified in the command line arguments. */ private void desugarClassesInInput( InputFileProvider inputFiles, @@ -360,8 +408,10 @@ class Desugar { throws IOException { // Write out any classes we generated along the way ImmutableMap<String, ClassNode> generatedClasses = store.drain(); - checkState(generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7), - "Didn't expect generated classes but got %s", generatedClasses.keySet()); + checkState( + generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7), + "Didn't expect generated classes but got %s", + generatedClasses.keySet()); for (Map.Entry<String, ClassNode> generated : generatedClasses.entrySet()) { UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS); // checkState above implies that we want Java 7 .class files, so send through that visitor. @@ -390,6 +440,9 @@ class Desugar { if (outputJava7) { // null ClassReaderFactory b/c we don't expect to need it for lambda classes visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null); + if (options.desugarTryWithResourcesIfNeeded) { + visitor = new TryWithResourcesRewriter(visitor, loader, numOfTryWithResourcesInvoked); + } if (options.desugarInterfaceMethodBodiesIfNeeded) { visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader); visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store); @@ -433,6 +486,9 @@ class Desugar { if (!options.onlyDesugarJavac9ForLint) { if (outputJava7) { visitor = new Java7Compatibility(visitor, classpathReader); + if (options.desugarTryWithResourcesIfNeeded) { + visitor = new TryWithResourcesRewriter(visitor, loader, numOfTryWithResourcesInvoked); + } if (options.desugarInterfaceMethodBodiesIfNeeded) { visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader); visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store); @@ -452,7 +508,6 @@ class Desugar { return visitor; } - public static void main(String[] args) throws Exception { // It is important that this method is called first. See its javadoc. Path dumpDirectory = createAndRegisterLambdaDumpDirectory(); @@ -510,7 +565,8 @@ class Desugar { final ImmutableList.Builder<InputOutputPair> ioPairListbuilder = ImmutableList.builder(); for (Iterator<Path> inputIt = options.inputJars.iterator(), outputIt = options.outputJars.iterator(); - inputIt.hasNext();) { + inputIt.hasNext(); + ) { ioPairListbuilder.add(InputOutputPair.create(inputIt.next(), outputIt.next())); } return ioPairListbuilder.build(); @@ -568,8 +624,7 @@ class Desugar { /** Transform a Path to an {@link OutputFileProvider} */ @MustBeClosed - private static OutputFileProvider toOutputFileProvider(Path path) - throws IOException { + private static OutputFileProvider toOutputFileProvider(Path path) throws IOException { if (Files.isDirectory(path)) { return new DirectoryOutputFileProvider(path); } else { @@ -579,8 +634,7 @@ class Desugar { /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */ @MustBeClosed - private static InputFileProvider toInputFileProvider(Path path) - throws IOException { + private static InputFileProvider toInputFileProvider(Path path) throws IOException { if (Files.isDirectory(path)) { return new DirectoryInputFileProvider(path); } else { @@ -602,9 +656,7 @@ class Desugar { return builder.build(); } - /** - * Pair input and output. - */ + /** Pair input and output. */ @AutoValue abstract static class InputOutputPair { diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/src/tools/android/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java new file mode 100644 index 0000000000..2429d2f7b8 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java @@ -0,0 +1,146 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import static org.objectweb.asm.Opcodes.ASM5; +import static org.objectweb.asm.Opcodes.INVOKESTATIC; +import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; + +import com.google.common.base.Function; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import java.util.concurrent.atomic.AtomicInteger; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; + +/** + * Desugar try-with-resources. This class visitor intercepts calls to the following methods, and + * redirect them to ThrowableExtension. + * <li>{@code Throwable.addSuppressed(Throwable)} + * <li>{@code Throwable.getSuppressed()} + * <li>{@code Throwable.printStackTrace()} + * <li>{@code Throwable.printStackTrace(PrintStream)} + * <li>{@code Throwable.printStackTrace(PringWriter)} + */ +public class TryWithResourcesRewriter extends ClassVisitor { + + private static final String RUNTIME_PACKAGE_INTERNAL_NAME = + "com/google/devtools/build/android/desugar/runtime"; + + static final String THROWABLE_EXTENSION_INTERNAL_NAME = + RUNTIME_PACKAGE_INTERNAL_NAME + '/' + "ThrowableExtension"; + + /** The extension classes for java.lang.Throwable. */ + static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES = + ImmutableSet.of( + THROWABLE_EXTENSION_INTERNAL_NAME, + THROWABLE_EXTENSION_INTERNAL_NAME + "$AbstractDesugaringStrategy", + THROWABLE_EXTENSION_INTERNAL_NAME + "$MimicDesugaringStrategy", + THROWABLE_EXTENSION_INTERNAL_NAME + "$NullDesugaringStrategy", + THROWABLE_EXTENSION_INTERNAL_NAME + "$ReuseDesugaringStrategy"); + + /** The extension classes for java.lang.Throwable. All the names end with ".class" */ + static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES_WITH_CLASS_EXT = + FluentIterable.from(THROWABLE_EXT_CLASS_INTERNAL_NAMES) + .transform( + new Function<String, String>() { + @Override + public String apply(String s) { + return s + ".class"; + } + }) + .toSet(); + + static final ImmutableMultimap<String, String> TARGET_METHODS = + ImmutableMultimap.<String, String>builder() + .put("addSuppressed", "(Ljava/lang/Throwable;)V") + .put("getSuppressed", "()[Ljava/lang/Throwable;") + .put("printStackTrace", "()V") + .put("printStackTrace", "(Ljava/io/PrintStream;)V") + .put("printStackTrace", "(Ljava/io/PrintWriter;)V") + .build(); + + static final ImmutableMap<String, String> METHOD_DESC_MAP = + ImmutableMap.<String, String>builder() + .put("(Ljava/lang/Throwable;)V", "(Ljava/lang/Throwable;Ljava/lang/Throwable;)V") + .put("()[Ljava/lang/Throwable;", "(Ljava/lang/Throwable;)[Ljava/lang/Throwable;") + .put("()V", "(Ljava/lang/Throwable;)V") + .put("(Ljava/io/PrintStream;)V", "(Ljava/lang/Throwable;Ljava/io/PrintStream;)V") + .put("(Ljava/io/PrintWriter;)V", "(Ljava/lang/Throwable;Ljava/io/PrintWriter;)V") + .build(); + + private final ClassLoader classLoader; + + private final AtomicInteger numOfTryWithResourcesInvoked; + + public TryWithResourcesRewriter( + ClassVisitor classVisitor, + ClassLoader classLoader, + AtomicInteger numOfTryWithResourcesInvoked) { + super(ASM5, classVisitor); + this.classLoader = classLoader; + this.numOfTryWithResourcesInvoked = numOfTryWithResourcesInvoked; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + MethodVisitor visitor = super.cv.visitMethod(access, name, desc, signature, exceptions); + return visitor == null || THROWABLE_EXT_CLASS_INTERNAL_NAMES.contains(name) + ? visitor + : new TryWithResourceVisitor(visitor, classLoader); + } + + private class TryWithResourceVisitor extends MethodVisitor { + + private final ClassLoader classLoader; + + public TryWithResourceVisitor(MethodVisitor methodVisitor, ClassLoader classLoader) { + super(ASM5, methodVisitor); + this.classLoader = classLoader; + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + if (!isMethodCallTargeted(opcode, owner, name, desc)) { + super.visitMethodInsn(opcode, owner, name, desc, itf); + return; + } + numOfTryWithResourcesInvoked.incrementAndGet(); + super.visitMethodInsn( + INVOKESTATIC, THROWABLE_EXTENSION_INTERNAL_NAME, name, METHOD_DESC_MAP.get(desc), false); + } + + private boolean isMethodCallTargeted(int opcode, String owner, String name, String desc) { + if (opcode != INVOKEVIRTUAL) { + return false; + } + if (!TARGET_METHODS.containsEntry(name, desc)) { + return false; + } + if (owner.equals("java/lang/Throwable")) { + return true; // early return, for performance. + } + try { + Class<?> throwableClass = classLoader.loadClass("java.lang.Throwable"); + Class<?> klass = classLoader.loadClass(owner.replace('/', '.')); + return throwableClass.isAssignableFrom(klass); + } catch (ClassNotFoundException e) { + throw new AssertionError(e); + } + } + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/runtime/BUILD b/src/tools/android/java/com/google/devtools/build/android/desugar/runtime/BUILD new file mode 100644 index 0000000000..9bed892709 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/runtime/BUILD @@ -0,0 +1,18 @@ +# Description: +# This is the extension package to support desugaring try-with-resources statements. + +java_library( + name = "throwable_extension", + srcs = ["ThrowableExtension.java"], + javacopts = [ + "-source 7", + "-target 7", + ], + visibility = ["//src/tools/android/java/com/google/devtools/build/android/desugar:__pkg__"], +) + +filegroup( + name = "srcs", + srcs = glob(["**"]), + visibility = ["//src/tools/android/java/com/google/devtools/build/android/desugar:__pkg__"], +) diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java b/src/tools/android/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java new file mode 100644 index 0000000000..3581fe813c --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java @@ -0,0 +1,276 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar.runtime; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.WeakHashMap; + +/** + * This is an extension class for java.lang.Throwable. It emulates the methods + * addSuppressed(Throwable) and getSuppressed(), so the language feature try-with-resources can be + * used on Android devices whose API level is below 19. + * + * <p>Note that the Desugar should avoid desugaring this class. + */ +public class ThrowableExtension { + + static final AbstractDesugaringStrategy STRATEGY; + /** + * This property allows users to change the desugared behavior of try-with-resources at runtime. + * If its value is {@code true}, then {@link MimicDesugaringStrategy} will NOT be used, and {@link + * NullDesugaringStrategy} is used instead. + * + * <p>Note: this property is ONLY used when the API level on the device is below 19. + */ + public static final String SYSTEM_PROPERTY_TWR_DISABLE_MIMIC = + "com.google.devtools.build.android.desugar.runtime.twr_disable_mimic"; + + static { + AbstractDesugaringStrategy strategy; + try { + Integer apiLevel = readApiLevelFromBuildVersion(); + if (apiLevel != null && apiLevel.intValue() >= 19) { + strategy = new ReuseDesugaringStrategy(); + } else if (useMimicStrategy()) { + strategy = new MimicDesugaringStrategy(); + } else { + strategy = new NullDesugaringStrategy(); + } + } catch (Throwable e) { + // This catchall block is intentionally created to avoid anything unexpected, so that + // the desugared app will continue running in case of exceptions. + System.err.println( + "An error has occured when initializing the try-with-resources desuguring strategy. " + + "The default strategy " + + NullDesugaringStrategy.class.getName() + + "will be used. The error is: "); + e.printStackTrace(System.err); + strategy = new NullDesugaringStrategy(); + } + STRATEGY = strategy; + } + + public static AbstractDesugaringStrategy getStrategy() { + return STRATEGY; + } + + public static void addSuppressed(Throwable receiver, Throwable suppressed) { + STRATEGY.addSuppressed(receiver, suppressed); + } + + public static Throwable[] getSuppressed(Throwable receiver) { + return STRATEGY.getSuppressed(receiver); + } + + public static void printStackTrace(Throwable receiver) { + STRATEGY.printStackTrace(receiver); + } + + public static void printStackTrace(Throwable receiver, PrintWriter writer) { + STRATEGY.printStackTrace(receiver, writer); + } + + public static void printStackTrace(Throwable receiver, PrintStream stream) { + STRATEGY.printStackTrace(receiver, stream); + } + + private static boolean useMimicStrategy() { + return !Boolean.getBoolean(SYSTEM_PROPERTY_TWR_DISABLE_MIMIC); + } + + private static final String ANDROID_OS_BUILD_VERSION = "android.os.Build$VERSION"; + + /** + * Get the API level from {@link android.os.Build.VERSION} via reflection. The reason to use + * relection is to avoid dependency on {@link android.os.Build.VERSION}. The advantage of doing + * this is that even when you desugar a jar twice, and Desugars sees this class, there is no need + * to put {@link android.os.Build.VERSION} on the classpath. + * + * <p>Another reason of doing this is that it does not introduce any additional dependency into + * the input jars. + * + * @return The API level of the current device. If it is {@code null}, then it means there was an + * exception. + */ + private static Integer readApiLevelFromBuildVersion() { + try { + Class<?> buildVersionClass = Class.forName(ANDROID_OS_BUILD_VERSION); + Field field = buildVersionClass.getField("SDK_INT"); + return (Integer) field.get(null); + } catch (Exception e) { + System.err.println( + "Failed to retrieve value from " + + ANDROID_OS_BUILD_VERSION + + ".SDK_INT due to the following exception."); + e.printStackTrace(System.err); + return null; + } + } + + /** + * The strategy to desugar try-with-resources statements. A strategy handles the behavior of an + * exception in terms of suppressed exceptions and stack trace printing. + */ + abstract static class AbstractDesugaringStrategy { + + protected static final Throwable[] EMPTY_THROWABLE_ARRAY = new Throwable[0]; + + public abstract void addSuppressed(Throwable receiver, Throwable suppressed); + + public abstract Throwable[] getSuppressed(Throwable receiver); + + public abstract void printStackTrace(Throwable receiver); + + public abstract void printStackTrace(Throwable receiver, PrintStream stream); + + public abstract void printStackTrace(Throwable receiver, PrintWriter writer); + } + + /** This strategy just delegates all the method calls to java.lang.Throwable. */ + static class ReuseDesugaringStrategy extends AbstractDesugaringStrategy { + + @Override + public void addSuppressed(Throwable receiver, Throwable suppressed) { + receiver.addSuppressed(suppressed); + } + + @Override + public Throwable[] getSuppressed(Throwable receiver) { + return receiver.getSuppressed(); + } + + @Override + public void printStackTrace(Throwable receiver) { + receiver.printStackTrace(); + } + + @Override + public void printStackTrace(Throwable receiver, PrintStream stream) { + receiver.printStackTrace(stream); + } + + @Override + public void printStackTrace(Throwable receiver, PrintWriter writer) { + receiver.printStackTrace(writer); + } + } + + /** This strategy mimics the behavior of suppressed exceptions with a map. */ + static class MimicDesugaringStrategy extends AbstractDesugaringStrategy { + + public static final String SUPPRESSED_PREFIX = "Suppressed: "; + private final WeakHashMap<Throwable, List<Throwable>> map = new WeakHashMap<>(); + + /** + * Suppress an exception. If the exception to be suppressed is {@receiver} or {@null}, an + * exception will be thrown. + * + * @param receiver + * @param suppressed + */ + @Override + public void addSuppressed(Throwable receiver, Throwable suppressed) { + if (suppressed == receiver) { + throw new IllegalArgumentException("Self suppression is not allowed.", suppressed); + } + if (suppressed == null) { + throw new NullPointerException("The suppressed exception cannot be null."); + } + synchronized (this) { + List<Throwable> list = map.get(receiver); + if (list == null) { + list = new ArrayList<>(1); + map.put(receiver, list); + } + list.add(suppressed); + } + } + + @Override + public synchronized Throwable[] getSuppressed(Throwable receiver) { + List<Throwable> list = map.get(receiver); + if (list == null || list.isEmpty()) { + return EMPTY_THROWABLE_ARRAY; + } + return list.toArray(new Throwable[0]); + } + + /** + * Print the stack trace for the parameter {@code receiver}. Note that it is deliberate to NOT + * reuse the implementation {@code MimicDesugaringStrategy.printStackTrace(Throwable, + * PrintStream)}, because we are not sure whether the developer prints the stack trace to a + * different stream other than System.err. Therefore, it is a caveat that the stack traces of + * {@code receiver} and its suppressed exceptions are printed in two different streams. + */ + @Override + public synchronized void printStackTrace(Throwable receiver) { + receiver.printStackTrace(); + for (Throwable suppressed : getSuppressed(receiver)) { + System.err.print(SUPPRESSED_PREFIX); + suppressed.printStackTrace(); + } + } + + @Override + public synchronized void printStackTrace(Throwable receiver, PrintStream stream) { + receiver.printStackTrace(stream); + for (Throwable suppressed : getSuppressed(receiver)) { + stream.print(SUPPRESSED_PREFIX); + suppressed.printStackTrace(stream); + } + } + + @Override + public synchronized void printStackTrace(Throwable receiver, PrintWriter writer) { + receiver.printStackTrace(writer); + for (Throwable suppressed : getSuppressed(receiver)) { + writer.print(SUPPRESSED_PREFIX); + suppressed.printStackTrace(writer); + } + } + } + + /** This strategy ignores all suppressed exceptions, which is how retrolambda does. */ + static class NullDesugaringStrategy extends AbstractDesugaringStrategy { + + @Override + public void addSuppressed(Throwable receiver, Throwable suppressed) { + // Do nothing. The suppressed exception is discarded. + } + + @Override + public Throwable[] getSuppressed(Throwable receiver) { + return EMPTY_THROWABLE_ARRAY; + } + + @Override + public void printStackTrace(Throwable receiver) { + receiver.printStackTrace(); + } + + @Override + public void printStackTrace(Throwable receiver, PrintStream stream) { + receiver.printStackTrace(stream); + } + + @Override + public void printStackTrace(Throwable receiver, PrintWriter writer) { + receiver.printStackTrace(writer); + } + } +} |