diff --git a/android/guava-tests/test/com/google/common/base/SuppliersTest.java b/android/guava-tests/test/com/google/common/base/SuppliersTest.java index fef3a2684b98..6f3d7bdbe03e 100644 --- a/android/guava-tests/test/com/google/common/base/SuppliersTest.java +++ b/android/guava-tests/test/com/google/common/base/SuppliersTest.java @@ -27,6 +27,7 @@ import com.google.common.testing.ClassSanityTester; import com.google.common.testing.EqualsTester; import java.io.Serializable; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -216,7 +217,7 @@ public List apply(List list) { @J2ktIncompatible @GwtIncompatible // Thread.sleep - public void testMemoizeWithExpiration() throws InterruptedException { + public void testMemoizeWithExpiration_longTimeUnit() throws InterruptedException { CountingSupplier countingSupplier = new CountingSupplier(); Supplier memoizedSupplier = @@ -225,6 +226,50 @@ public void testMemoizeWithExpiration() throws InterruptedException { checkExpiration(countingSupplier, memoizedSupplier); } + @J2ktIncompatible + @GwtIncompatible // Thread.sleep + @SuppressWarnings("Java7ApiChecker") // test of Java 8+ API + public void testMemoizeWithExpiration_duration() throws InterruptedException { + CountingSupplier countingSupplier = new CountingSupplier(); + + Supplier memoizedSupplier = + Suppliers.memoizeWithExpiration(countingSupplier, Duration.ofMillis(75)); + + checkExpiration(countingSupplier, memoizedSupplier); + } + + public void testMemoizeWithExpiration_longTimeUnitNegative() throws InterruptedException { + try { + Supplier unused = Suppliers.memoizeWithExpiration(() -> "", 0, TimeUnit.MILLISECONDS); + fail(); + } catch (IllegalArgumentException expected) { + } + + try { + Supplier unused = + Suppliers.memoizeWithExpiration(() -> "", -1, TimeUnit.MILLISECONDS); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @SuppressWarnings("Java7ApiChecker") // test of Java 8+ API + @J2ktIncompatible // Duration + @GwtIncompatible // Duration + public void testMemoizeWithExpiration_durationNegative() throws InterruptedException { + try { + Supplier unused = Suppliers.memoizeWithExpiration(() -> "", Duration.ZERO); + fail(); + } catch (IllegalArgumentException expected) { + } + + try { + Supplier unused = Suppliers.memoizeWithExpiration(() -> "", Duration.ofMillis(-1)); + fail(); + } catch (IllegalArgumentException expected) { + } + } + @J2ktIncompatible @GwtIncompatible // Thread.sleep, SerializationTester public void testMemoizeWithExpirationSerialized() throws InterruptedException { @@ -451,15 +496,23 @@ public void testSerialization() { @J2ktIncompatible @GwtIncompatible // reflection + @SuppressWarnings("Java7ApiChecker") // includes test of Java 8+ API public void testSuppliersNullChecks() throws Exception { - new ClassSanityTester().forAllPublicStaticMethods(Suppliers.class).testNulls(); + new ClassSanityTester() + .setDefault(Duration.class, Duration.ofSeconds(1)) + .forAllPublicStaticMethods(Suppliers.class) + .testNulls(); } @J2ktIncompatible @GwtIncompatible // reflection @AndroidIncompatible // TODO(cpovirk): ClassNotFoundException: com.google.common.base.Function + @SuppressWarnings("Java7ApiChecker") // includes test of Java 8+ API public void testSuppliersSerializable() throws Exception { - new ClassSanityTester().forAllPublicStaticMethods(Suppliers.class).testSerializable(); + new ClassSanityTester() + .setDefault(Duration.class, Duration.ofSeconds(1)) + .forAllPublicStaticMethods(Suppliers.class) + .testSerializable(); } public void testOfInstance_equals() { diff --git a/android/guava/src/com/google/common/base/IgnoreJRERequirement.java b/android/guava/src/com/google/common/base/IgnoreJRERequirement.java new file mode 100644 index 000000000000..c34a9cdd974b --- /dev/null +++ b/android/guava/src/com/google/common/base/IgnoreJRERequirement.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 The Guava Authors + * + * 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.common.base; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Target; + +/** + * Disables Animal Sniffer's checking of compatibility with older versions of Java/Android. + * + *

Each package's copy of this annotation needs to be listed in our {@code pom.xml}. + */ +@Target({METHOD, CONSTRUCTOR, TYPE}) +@ElementTypesAreNonnullByDefault +@interface IgnoreJRERequirement {} diff --git a/android/guava/src/com/google/common/base/Internal.java b/android/guava/src/com/google/common/base/Internal.java new file mode 100644 index 000000000000..0e1ee2400f24 --- /dev/null +++ b/android/guava/src/com/google/common/base/Internal.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 The Guava Authors + * + * 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.common.base; + +import com.google.common.annotations.GwtIncompatible; +import com.google.common.annotations.J2ktIncompatible; +import java.time.Duration; + +/** This class is for {@code com.google.common.base} use only! */ +@J2ktIncompatible +@GwtIncompatible // java.time.Duration +@ElementTypesAreNonnullByDefault +final class Internal { + + /** + * Returns the number of nanoseconds of the given duration without throwing or overflowing. + * + *

Instead of throwing {@link ArithmeticException}, this method silently saturates to either + * {@link Long#MAX_VALUE} or {@link Long#MIN_VALUE}. This behavior can be useful when decomposing + * a duration in order to call a legacy API which requires a {@code long, TimeUnit} pair. + */ + @SuppressWarnings({ + // We use this method only for cases in which we need to decompose to primitives. + "GoodTime-ApiWithNumericTimeUnit", + "GoodTime-DecomposeToPrimitive", + // We use this method only from within APIs that require a Duration. + "Java7ApiChecker", + }) + @IgnoreJRERequirement + static long toNanosSaturated(Duration duration) { + // Using a try/catch seems lazy, but the catch block will rarely get invoked (except for + // durations longer than approximately +/- 292 years). + try { + return duration.toNanos(); + } catch (ArithmeticException tooBig) { + return duration.isNegative() ? Long.MIN_VALUE : Long.MAX_VALUE; + } + } + + private Internal() {} +} diff --git a/android/guava/src/com/google/common/base/Suppliers.java b/android/guava/src/com/google/common/base/Suppliers.java index 60bcf6921fee..07b27b607e0f 100644 --- a/android/guava/src/com/google/common/base/Suppliers.java +++ b/android/guava/src/com/google/common/base/Suppliers.java @@ -14,13 +14,18 @@ package com.google.common.base; +import static com.google.common.base.Internal.toNanosSaturated; import static com.google.common.base.NullnessCasts.uncheckedCastNullableTToT; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.annotations.Beta; import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.GwtIncompatible; +import com.google.common.annotations.J2ktIncompatible; import com.google.common.annotations.VisibleForTesting; import java.io.Serializable; +import java.time.Duration; import java.util.concurrent.TimeUnit; import javax.annotation.CheckForNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -34,7 +39,7 @@ * @author Harry Heymann * @since 2.0 */ -@GwtCompatible +@GwtCompatible(emulated = true) @ElementTypesAreNonnullByDefault public final class Suppliers { private Suppliers() {} @@ -221,10 +226,44 @@ public String toString() { * @throws IllegalArgumentException if {@code duration} is not positive * @since 2.0 */ - @SuppressWarnings("GoodTime") // should accept a java.time.Duration + @SuppressWarnings("GoodTime") // Prefer the Duration overload public static Supplier memoizeWithExpiration( Supplier delegate, long duration, TimeUnit unit) { - return new ExpiringMemoizingSupplier<>(delegate, duration, unit); + checkNotNull(delegate); + checkArgument(duration > 0, "duration (%s %s) must be > 0", duration, unit); + return new ExpiringMemoizingSupplier<>(delegate, unit.toNanos(duration)); + } + + /** + * Returns a supplier that caches the instance supplied by the delegate and removes the cached + * value after the specified time has passed. Subsequent calls to {@code get()} return the cached + * value if the expiration time has not passed. After the expiration time, a new value is + * retrieved, cached, and returned. See: memoization + * + *

The returned supplier is thread-safe. The supplier's serialized form does not contain the + * cached value, which will be recalculated when {@code get()} is called on the reserialized + * instance. The actual memoization does not happen when the underlying delegate throws an + * exception. + * + *

When the underlying delegate throws an exception then this memoizing supplier will keep + * delegating calls until it returns valid data. + * + * @param duration the length of time after a value is created that it should stop being returned + * by subsequent {@code get()} calls + * @throws IllegalArgumentException if {@code duration} is not positive + * @since NEXT + */ + @Beta // only until we're confident that Java 8 APIs are safe for our Android users + @J2ktIncompatible + @GwtIncompatible // java.time.Duration + @SuppressWarnings("Java7ApiChecker") // no more dangerous that wherever the user got the Duration + @IgnoreJRERequirement + public static Supplier memoizeWithExpiration( + Supplier delegate, Duration duration) { + checkNotNull(delegate); + checkArgument(duration.compareTo(Duration.ZERO) > 0, "duration (%s) must be > 0", duration); + return new ExpiringMemoizingSupplier(delegate, toNanosSaturated(duration)); } @VisibleForTesting @@ -237,15 +276,13 @@ static class ExpiringMemoizingSupplier // The special value 0 means "not yet initialized". transient volatile long expirationNanos; - ExpiringMemoizingSupplier(Supplier delegate, long duration, TimeUnit unit) { - this.delegate = checkNotNull(delegate); - this.durationNanos = unit.toNanos(duration); - checkArgument(duration > 0, "duration (%s %s) must be > 0", duration, unit); + ExpiringMemoizingSupplier(Supplier delegate, long durationNanos) { + this.delegate = delegate; + this.durationNanos = durationNanos; } @Override @ParametricNullness - @SuppressWarnings("GoodTime") // reading system time without TimeSource public T get() { // Another variant of Double Checked Locking. // diff --git a/android/pom.xml b/android/pom.xml index 61bbc787d370..59811c7d3de0 100644 --- a/android/pom.xml +++ b/android/pom.xml @@ -177,7 +177,7 @@ animal-sniffer-maven-plugin 1.23 - com.google.common.collect.IgnoreJRERequirement,com.google.common.hash.IgnoreJRERequirement,com.google.common.io.IgnoreJRERequirement,com.google.common.reflect.IgnoreJRERequirement,com.google.common.testing.IgnoreJRERequirement + com.google.common.base.IgnoreJRERequirement,com.google.common.collect.IgnoreJRERequirement,com.google.common.hash.IgnoreJRERequirement,com.google.common.io.IgnoreJRERequirement,com.google.common.reflect.IgnoreJRERequirement,com.google.common.testing.IgnoreJRERequirement true com.toasttab.android diff --git a/guava-tests/test/com/google/common/base/SuppliersTest.java b/guava-tests/test/com/google/common/base/SuppliersTest.java index fef3a2684b98..6f3d7bdbe03e 100644 --- a/guava-tests/test/com/google/common/base/SuppliersTest.java +++ b/guava-tests/test/com/google/common/base/SuppliersTest.java @@ -27,6 +27,7 @@ import com.google.common.testing.ClassSanityTester; import com.google.common.testing.EqualsTester; import java.io.Serializable; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -216,7 +217,7 @@ public List apply(List list) { @J2ktIncompatible @GwtIncompatible // Thread.sleep - public void testMemoizeWithExpiration() throws InterruptedException { + public void testMemoizeWithExpiration_longTimeUnit() throws InterruptedException { CountingSupplier countingSupplier = new CountingSupplier(); Supplier memoizedSupplier = @@ -225,6 +226,50 @@ public void testMemoizeWithExpiration() throws InterruptedException { checkExpiration(countingSupplier, memoizedSupplier); } + @J2ktIncompatible + @GwtIncompatible // Thread.sleep + @SuppressWarnings("Java7ApiChecker") // test of Java 8+ API + public void testMemoizeWithExpiration_duration() throws InterruptedException { + CountingSupplier countingSupplier = new CountingSupplier(); + + Supplier memoizedSupplier = + Suppliers.memoizeWithExpiration(countingSupplier, Duration.ofMillis(75)); + + checkExpiration(countingSupplier, memoizedSupplier); + } + + public void testMemoizeWithExpiration_longTimeUnitNegative() throws InterruptedException { + try { + Supplier unused = Suppliers.memoizeWithExpiration(() -> "", 0, TimeUnit.MILLISECONDS); + fail(); + } catch (IllegalArgumentException expected) { + } + + try { + Supplier unused = + Suppliers.memoizeWithExpiration(() -> "", -1, TimeUnit.MILLISECONDS); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @SuppressWarnings("Java7ApiChecker") // test of Java 8+ API + @J2ktIncompatible // Duration + @GwtIncompatible // Duration + public void testMemoizeWithExpiration_durationNegative() throws InterruptedException { + try { + Supplier unused = Suppliers.memoizeWithExpiration(() -> "", Duration.ZERO); + fail(); + } catch (IllegalArgumentException expected) { + } + + try { + Supplier unused = Suppliers.memoizeWithExpiration(() -> "", Duration.ofMillis(-1)); + fail(); + } catch (IllegalArgumentException expected) { + } + } + @J2ktIncompatible @GwtIncompatible // Thread.sleep, SerializationTester public void testMemoizeWithExpirationSerialized() throws InterruptedException { @@ -451,15 +496,23 @@ public void testSerialization() { @J2ktIncompatible @GwtIncompatible // reflection + @SuppressWarnings("Java7ApiChecker") // includes test of Java 8+ API public void testSuppliersNullChecks() throws Exception { - new ClassSanityTester().forAllPublicStaticMethods(Suppliers.class).testNulls(); + new ClassSanityTester() + .setDefault(Duration.class, Duration.ofSeconds(1)) + .forAllPublicStaticMethods(Suppliers.class) + .testNulls(); } @J2ktIncompatible @GwtIncompatible // reflection @AndroidIncompatible // TODO(cpovirk): ClassNotFoundException: com.google.common.base.Function + @SuppressWarnings("Java7ApiChecker") // includes test of Java 8+ API public void testSuppliersSerializable() throws Exception { - new ClassSanityTester().forAllPublicStaticMethods(Suppliers.class).testSerializable(); + new ClassSanityTester() + .setDefault(Duration.class, Duration.ofSeconds(1)) + .forAllPublicStaticMethods(Suppliers.class) + .testSerializable(); } public void testOfInstance_equals() { diff --git a/guava/src/com/google/common/base/IgnoreJRERequirement.java b/guava/src/com/google/common/base/IgnoreJRERequirement.java new file mode 100644 index 000000000000..c34a9cdd974b --- /dev/null +++ b/guava/src/com/google/common/base/IgnoreJRERequirement.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 The Guava Authors + * + * 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.common.base; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Target; + +/** + * Disables Animal Sniffer's checking of compatibility with older versions of Java/Android. + * + *

Each package's copy of this annotation needs to be listed in our {@code pom.xml}. + */ +@Target({METHOD, CONSTRUCTOR, TYPE}) +@ElementTypesAreNonnullByDefault +@interface IgnoreJRERequirement {} diff --git a/guava/src/com/google/common/base/Internal.java b/guava/src/com/google/common/base/Internal.java new file mode 100644 index 000000000000..0e1ee2400f24 --- /dev/null +++ b/guava/src/com/google/common/base/Internal.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 The Guava Authors + * + * 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.common.base; + +import com.google.common.annotations.GwtIncompatible; +import com.google.common.annotations.J2ktIncompatible; +import java.time.Duration; + +/** This class is for {@code com.google.common.base} use only! */ +@J2ktIncompatible +@GwtIncompatible // java.time.Duration +@ElementTypesAreNonnullByDefault +final class Internal { + + /** + * Returns the number of nanoseconds of the given duration without throwing or overflowing. + * + *

Instead of throwing {@link ArithmeticException}, this method silently saturates to either + * {@link Long#MAX_VALUE} or {@link Long#MIN_VALUE}. This behavior can be useful when decomposing + * a duration in order to call a legacy API which requires a {@code long, TimeUnit} pair. + */ + @SuppressWarnings({ + // We use this method only for cases in which we need to decompose to primitives. + "GoodTime-ApiWithNumericTimeUnit", + "GoodTime-DecomposeToPrimitive", + // We use this method only from within APIs that require a Duration. + "Java7ApiChecker", + }) + @IgnoreJRERequirement + static long toNanosSaturated(Duration duration) { + // Using a try/catch seems lazy, but the catch block will rarely get invoked (except for + // durations longer than approximately +/- 292 years). + try { + return duration.toNanos(); + } catch (ArithmeticException tooBig) { + return duration.isNegative() ? Long.MIN_VALUE : Long.MAX_VALUE; + } + } + + private Internal() {} +} diff --git a/guava/src/com/google/common/base/Suppliers.java b/guava/src/com/google/common/base/Suppliers.java index 60bcf6921fee..07b27b607e0f 100644 --- a/guava/src/com/google/common/base/Suppliers.java +++ b/guava/src/com/google/common/base/Suppliers.java @@ -14,13 +14,18 @@ package com.google.common.base; +import static com.google.common.base.Internal.toNanosSaturated; import static com.google.common.base.NullnessCasts.uncheckedCastNullableTToT; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.annotations.Beta; import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.GwtIncompatible; +import com.google.common.annotations.J2ktIncompatible; import com.google.common.annotations.VisibleForTesting; import java.io.Serializable; +import java.time.Duration; import java.util.concurrent.TimeUnit; import javax.annotation.CheckForNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -34,7 +39,7 @@ * @author Harry Heymann * @since 2.0 */ -@GwtCompatible +@GwtCompatible(emulated = true) @ElementTypesAreNonnullByDefault public final class Suppliers { private Suppliers() {} @@ -221,10 +226,44 @@ public String toString() { * @throws IllegalArgumentException if {@code duration} is not positive * @since 2.0 */ - @SuppressWarnings("GoodTime") // should accept a java.time.Duration + @SuppressWarnings("GoodTime") // Prefer the Duration overload public static Supplier memoizeWithExpiration( Supplier delegate, long duration, TimeUnit unit) { - return new ExpiringMemoizingSupplier<>(delegate, duration, unit); + checkNotNull(delegate); + checkArgument(duration > 0, "duration (%s %s) must be > 0", duration, unit); + return new ExpiringMemoizingSupplier<>(delegate, unit.toNanos(duration)); + } + + /** + * Returns a supplier that caches the instance supplied by the delegate and removes the cached + * value after the specified time has passed. Subsequent calls to {@code get()} return the cached + * value if the expiration time has not passed. After the expiration time, a new value is + * retrieved, cached, and returned. See: memoization + * + *

The returned supplier is thread-safe. The supplier's serialized form does not contain the + * cached value, which will be recalculated when {@code get()} is called on the reserialized + * instance. The actual memoization does not happen when the underlying delegate throws an + * exception. + * + *

When the underlying delegate throws an exception then this memoizing supplier will keep + * delegating calls until it returns valid data. + * + * @param duration the length of time after a value is created that it should stop being returned + * by subsequent {@code get()} calls + * @throws IllegalArgumentException if {@code duration} is not positive + * @since NEXT + */ + @Beta // only until we're confident that Java 8 APIs are safe for our Android users + @J2ktIncompatible + @GwtIncompatible // java.time.Duration + @SuppressWarnings("Java7ApiChecker") // no more dangerous that wherever the user got the Duration + @IgnoreJRERequirement + public static Supplier memoizeWithExpiration( + Supplier delegate, Duration duration) { + checkNotNull(delegate); + checkArgument(duration.compareTo(Duration.ZERO) > 0, "duration (%s) must be > 0", duration); + return new ExpiringMemoizingSupplier(delegate, toNanosSaturated(duration)); } @VisibleForTesting @@ -237,15 +276,13 @@ static class ExpiringMemoizingSupplier // The special value 0 means "not yet initialized". transient volatile long expirationNanos; - ExpiringMemoizingSupplier(Supplier delegate, long duration, TimeUnit unit) { - this.delegate = checkNotNull(delegate); - this.durationNanos = unit.toNanos(duration); - checkArgument(duration > 0, "duration (%s %s) must be > 0", duration, unit); + ExpiringMemoizingSupplier(Supplier delegate, long durationNanos) { + this.delegate = delegate; + this.durationNanos = durationNanos; } @Override @ParametricNullness - @SuppressWarnings("GoodTime") // reading system time without TimeSource public T get() { // Another variant of Double Checked Locking. // diff --git a/pom.xml b/pom.xml index d4fa95458c2b..a8a12ecc44a7 100644 --- a/pom.xml +++ b/pom.xml @@ -178,7 +178,7 @@ animal-sniffer-maven-plugin 1.23 - com.google.common.collect.IgnoreJRERequirement,com.google.common.hash.IgnoreJRERequirement,com.google.common.io.IgnoreJRERequirement,com.google.common.reflect.IgnoreJRERequirement,com.google.common.testing.IgnoreJRERequirement + com.google.common.base.IgnoreJRERequirement,com.google.common.collect.IgnoreJRERequirement,com.google.common.hash.IgnoreJRERequirement,com.google.common.io.IgnoreJRERequirement,com.google.common.reflect.IgnoreJRERequirement,com.google.common.testing.IgnoreJRERequirement true org.codehaus.mojo.signature