aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/android/java
diff options
context:
space:
mode:
authorGravatar cnsun <cnsun@google.com>2017-04-27 07:14:14 +0200
committerGravatar Vladimir Moskva <vladmos@google.com>2017-04-27 11:18:28 +0200
commit15d403d3c06fb47838cc4d294898e6530deca3d3 (patch)
tree7061ac98639b8106f91afba180af972d4e8ad8a7 /src/tools/android/java
parent11cc89a27544311111473dc7a17522635c2b6e70 (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')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/BUILD5
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java106
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java146
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/runtime/BUILD18
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java276
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);
+ }
+ }
+}