From 295f1125ceff5e07dfb8bfd2d7bada6c28918c0c Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Fri, 21 Apr 2023 16:10:15 -0700 Subject: [PATCH] Adds `Timestamps.now()`. PiperOrigin-RevId: 526160914 --- .../com/google/protobuf/util/Timestamps.java | 51 ++++++++++++++++--- .../google/protobuf/util/TimestampsTest.java | 37 ++++++++++++-- 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/java/util/src/main/java/com/google/protobuf/util/Timestamps.java b/java/util/src/main/java/com/google/protobuf/util/Timestamps.java index 0b09fccd2a..3e43700f7e 100644 --- a/java/util/src/main/java/com/google/protobuf/util/Timestamps.java +++ b/java/util/src/main/java/com/google/protobuf/util/Timestamps.java @@ -42,6 +42,7 @@ import com.google.j2objc.annotations.J2ObjCIncompatible; import com.google.protobuf.Duration; import com.google.protobuf.Timestamp; import java.io.Serializable; +import java.lang.reflect.Method; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Comparator; @@ -49,6 +50,7 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import java.util.TimeZone; +import javax.annotation.Nullable; /** * Utilities to help create/manipulate {@code protobuf/timestamp.proto}. All operations throw an @@ -117,8 +119,8 @@ public final class Timestamps { /** * Returns a {@link Comparator} for {@link Timestamp Timestamps} which sorts in increasing - * chronological order. Nulls and invalid {@link Timestamp Timestamps} are not allowed (see - * {@link #isValid}). The returned comparator is serializable. + * chronological order. Nulls and invalid {@link Timestamp Timestamps} are not allowed (see {@link + * #isValid}). The returned comparator is serializable. */ public static Comparator comparator() { return TimestampComparator.INSTANCE; @@ -281,8 +283,9 @@ public final class Timestamps { try { return normalizedTimestamp(seconds, nanos); } catch (IllegalArgumentException e) { - ParseException ex = new ParseException( - "Failed to parse timestamp " + value + " Timestamp is out of range.", 0); + ParseException ex = + new ParseException( + "Failed to parse timestamp " + value + " Timestamp is out of range.", 0); ex.initCause(e); throw ex; } @@ -307,6 +310,39 @@ public final class Timestamps { } } + // the following 3 constants contain references to java.time.Instant methods (if that class is + // available at runtime); otherwise, they are null. + @Nullable private static final Method INSTANT_NOW = instantMethod("now"); + + @Nullable private static final Method INSTANT_GET_EPOCH_SECOND = instantMethod("getEpochSecond"); + + @Nullable private static final Method INSTANT_GET_NANO = instantMethod("getNano"); + + @Nullable + private static Method instantMethod(String methodName) { + try { + return Class.forName("java.time.Instant").getMethod(methodName); + } catch (Exception e) { + return null; + } + } + + /** Create a Timestamp using the best-available system clock. */ + public static Timestamp now() { + if (INSTANT_NOW != null) { + try { + Object now = INSTANT_NOW.invoke(null); + long epochSecond = (long) INSTANT_GET_EPOCH_SECOND.invoke(now); + int nanoAdjustment = (int) INSTANT_GET_NANO.invoke(now); + return normalizedTimestamp(epochSecond, nanoAdjustment); + } catch (Throwable fallThrough) { + throw new AssertionError(fallThrough); + } + } + // otherwise, fall back on millisecond precision + return fromMillis(System.currentTimeMillis()); + } + /** Create a Timestamp from the number of seconds elapsed from the epoch. */ @SuppressWarnings("GoodTime") // this is a legacy conversion API public static Timestamp fromSeconds(long seconds) { @@ -333,7 +369,7 @@ public final class Timestamps { } /** - * Create a Timestamp from a java.util.Date. If the java.util.Date is a java.sql.Timestamp, + * Create a Timestamp from a {@link Date}. If the {@link Date} is a {@link java.sql.Timestamp}, * full nanonsecond precision is retained. * * @throws IllegalArgumentException if the year is before 1 CE or after 9999 CE @@ -344,7 +380,10 @@ public final class Timestamps { if (date instanceof java.sql.Timestamp) { java.sql.Timestamp sqlTimestamp = (java.sql.Timestamp) date; long time = sqlTimestamp.getTime(); - long integralSeconds = (time < 0 && time % 1000 != 0) ? time / 1000L - 1 : time / 1000L ; // truncate the fractional seconds + long integralSeconds = + (time < 0 && time % 1000 != 0) + ? time / 1000L - 1 + : time / 1000L; // truncate the fractional seconds return Timestamp.newBuilder() .setSeconds(integralSeconds) .setNanos(sqlTimestamp.getNanos()) diff --git a/java/util/src/test/java/com/google/protobuf/util/TimestampsTest.java b/java/util/src/test/java/com/google/protobuf/util/TimestampsTest.java index 51f7a85b49..f339d4cb35 100644 --- a/java/util/src/test/java/com/google/protobuf/util/TimestampsTest.java +++ b/java/util/src/test/java/com/google/protobuf/util/TimestampsTest.java @@ -73,13 +73,41 @@ public class TimestampsTest { private static final Timestamp INVALID_MIN = Timestamp.newBuilder().setSeconds(Long.MIN_VALUE).setNanos(Integer.MIN_VALUE).build(); + @Test + public void testNow() { + Timestamp now = Timestamps.now(); + long epochSeconds = System.currentTimeMillis() / 1000; + assertThat(now.getSeconds()).isAtLeast(epochSeconds - 1); + assertThat(now.getSeconds()).isAtMost(epochSeconds + 1); + } + + @Test + public void testNowWithSubMillisecondPrecision() { + try { + // throws if we're not on Java9+ + Class.forName("java.lang.Runtime$Version"); + } catch (ClassNotFoundException e) { + // ignored; we're not on Java 9+ + return; + } + + // grab 100 timestamps, and ensure that at least 1 of them has sub-millisecond precision + for (int i = 0; i < 100; i++) { + Timestamp now = Timestamps.now(); + Timestamp nowWithMilliPrecision = Timestamps.fromMillis(Timestamps.toMillis(now)); + if (!now.equals(nowWithMilliPrecision)) { + return; + } + } + assertWithMessage("no timestamp had sub-millisecond precision").fail(); + } + @Test public void testMinMaxAreValid() { assertThat(Timestamps.isValid(Timestamps.MAX_VALUE)).isTrue(); assertThat(Timestamps.isValid(Timestamps.MIN_VALUE)).isTrue(); } - @Test public void testIsValid_false() { assertThat(Timestamps.isValid(0L, -1)).isFalse(); @@ -475,7 +503,7 @@ public class TimestampsTest { Timestamp timestamp = Timestamps.fromDate(date); assertThat(Timestamps.toString(timestamp)).isEqualTo("1970-01-01T00:00:01.111Z"); } - + @Test public void testFromSqlTimestamp_beforeEpoch() { Date date = new java.sql.Timestamp(-1111); @@ -615,6 +643,7 @@ public class TimestampsTest { } catch (IllegalArgumentException expected) { } } + @Test public void testInvalidMaxMicrosecondsOverflow() { try { @@ -686,14 +715,13 @@ public class TimestampsTest { @Test public void testIllegalArgumentExceptionForMaxMicroseconds() { - try { + try { Timestamps.fromMicros(Long.MAX_VALUE); assertWithMessage("Expected an IllegalArgumentException to be thrown").fail(); } catch (IllegalArgumentException expected) { } } - @Test public void testIllegalArgumentExceptionForMaxMilliseconds() { try { @@ -718,7 +746,6 @@ public class TimestampsTest { } } - @Test public void testIllegalArgumentExceptionForMinMilliseconds() { try {