Merge changes from topic "get-rid-of-joda-time"

* changes:
  Guard against null values for date during mail parsing
  Provide Joda time only for Elasticsearch
  Don't export Joda time for plugins anymore
  Remove joda-time from BUILD files for core Gerrit
  Switch to Java Date/Time API for TimeUtil
  Switch to Java Date/Time API for mail related code
  Switch to Java Date/Time API in ScheduleConfig
  Switch to Java Date/Time API in ResourceServletTest
  Switch to Java Date/Time API in ChangeBundleTest
  OutputStreamQueryTest: Remove test proving equivalence to Joda time
  Switch to Java Date/Time API in OutputStreamQuery
diff --git a/WORKSPACE b/WORKSPACE
index acb2a69..6458571 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -192,18 +192,6 @@
     sha1 = "de80fe047052445869b96f6def6baca7182c95af",
 )
 
-maven_jar(
-    name = "joda_time",
-    artifact = "joda-time:joda-time:2.9.9",
-    sha1 = "f7b520c458572890807d143670c9b24f4de90897",
-)
-
-maven_jar(
-    name = "joda_convert",
-    artifact = "org.joda:joda-convert:1.8.1",
-    sha1 = "675642ac208e0b741bc9118dcbcae44c271b992a",
-)
-
 load("//lib:guava.bzl", "GUAVA_VERSION", "GUAVA_BIN_SHA1")
 
 maven_jar(
@@ -942,6 +930,18 @@
 )
 
 maven_jar(
+    name = "joda_time",
+    artifact = "joda-time:joda-time:2.9.9",
+    sha1 = "f7b520c458572890807d143670c9b24f4de90897",
+)
+
+maven_jar(
+    name = "joda_convert",
+    artifact = "org.joda:joda-convert:1.8.1",
+    sha1 = "675642ac208e0b741bc9118dcbcae44c271b992a",
+)
+
+maven_jar(
     name = "compress_lzf",
     artifact = "com.ning:compress-lzf:1.0.2",
     sha1 = "62896e6fca184c79cc01a14d143f3ae2b4f4b4ae",
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD
index 990bad6..6bfecfa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD
@@ -4,7 +4,4 @@
     srcs = ["ChangeEditIT.java"],
     group = "edit",
     labels = ["edit"],
-    deps = [
-        "//lib/joda:joda-time",
-    ],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUILD
index 43ec5bc..897b99f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUILD
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUILD
@@ -16,7 +16,6 @@
     srcs = ["AbstractPushForReview.java"],
     deps = [
         "//gerrit-acceptance-tests:lib",
-        "//lib/joda:joda-time",
     ],
 )
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD
index b7ed2e8..49f00f9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD
@@ -15,7 +15,6 @@
     labels = ["rest"],
     deps = [
         ":submit_util",
-        "//lib/joda:joda-time",
     ],
 )
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
index 6f4bdab..32f1ce5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
@@ -23,8 +23,8 @@
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.server.mail.receive.MailMessage;
+import java.time.Instant;
 import java.util.HashMap;
-import org.joda.time.DateTime;
 import org.junit.Ignore;
 
 @Ignore
@@ -36,7 +36,7 @@
     b.from(user.emailAddress);
     b.addTo(user.emailAddress); // Not evaluated
     b.subject("");
-    b.dateReceived(new DateTime());
+    b.dateReceived(Instant.now());
     return b;
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUILD
index 71a6135..c3a4e20 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUILD
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUILD
@@ -2,7 +2,6 @@
 
 DEPS = [
     "//lib/greenmail",
-    "//lib/joda:joda-time",
     "//lib/mail",
 ]
 
diff --git a/gerrit-common/BUILD b/gerrit-common/BUILD
index 4389080..d9d4392 100644
--- a/gerrit-common/BUILD
+++ b/gerrit-common/BUILD
@@ -28,7 +28,6 @@
         "//lib:gwtorm_client",
         "//lib:servlet-api-3_1",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/joda:joda-time",
         "//lib/log:api",
     ],
     gwt_xml = SRC + "Common.gwt.xml",
@@ -53,7 +52,6 @@
         "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/joda:joda-time",
         "//lib/log:api",
     ],
 )
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
index a8e40c6..b1697dc 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
@@ -15,14 +15,21 @@
 package com.google.gerrit.common;
 
 import com.google.common.annotations.GwtIncompatible;
+import com.google.common.annotations.VisibleForTesting;
 import java.sql.Timestamp;
-import org.joda.time.DateTimeUtils;
+import java.util.function.LongSupplier;
 
 /** Static utility methods for dealing with dates and times. */
-@GwtIncompatible("Unemulated org.joda.time.DateTimeUtils")
+@GwtIncompatible("Unemulated Java 8 functionalities")
 public class TimeUtil {
+  private static final LongSupplier SYSTEM_CURRENT_MILLIS_SUPPLIER = System::currentTimeMillis;
+
+  private static volatile LongSupplier currentMillisSupplier = SYSTEM_CURRENT_MILLIS_SUPPLIER;
+
   public static long nowMs() {
-    return DateTimeUtils.currentTimeMillis();
+    // We should rather use Instant.now(Clock).toEpochMilli() instead but this would require some
+    // changes in our testing code as we wouldn't have clock steps anymore.
+    return currentMillisSupplier.getAsLong();
   }
 
   public static Timestamp nowTs() {
@@ -33,5 +40,15 @@
     return new Timestamp((t.getTime() / 1000) * 1000);
   }
 
+  @VisibleForTesting
+  public static void setCurrentMillisSupplier(LongSupplier customCurrentMillisSupplier) {
+    currentMillisSupplier = customCurrentMillisSupplier;
+  }
+
+  @VisibleForTesting
+  public static void resetCurrentMillisSupplier() {
+    currentMillisSupplier = SYSTEM_CURRENT_MILLIS_SUPPLIER;
+  }
+
   private TimeUtil() {}
 }
diff --git a/gerrit-elasticsearch/BUILD b/gerrit-elasticsearch/BUILD
index fb86aaf..d278bcf 100644
--- a/gerrit-elasticsearch/BUILD
+++ b/gerrit-elasticsearch/BUILD
@@ -17,10 +17,10 @@
         "//lib/elasticsearch",
         "//lib/elasticsearch:jest",
         "//lib/elasticsearch:jest-common",
+        "//lib/elasticsearch:joda-time",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/joda:joda-time",
         "//lib/log:api",
         "//lib/lucene:lucene-analyzers-common",
         "//lib/lucene:lucene-core",
diff --git a/gerrit-httpd/BUILD b/gerrit-httpd/BUILD
index dbca10c..cc2160f 100644
--- a/gerrit-httpd/BUILD
+++ b/gerrit-httpd/BUILD
@@ -77,6 +77,5 @@
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
-        "//lib/joda:joda-time",
     ],
 )
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java
index 18256c6..6dd15bc 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java
@@ -36,9 +36,11 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.attribute.FileTime;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.zip.GZIPInputStream;
-import org.joda.time.format.ISODateTimeFormat;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -91,7 +93,12 @@
   @Before
   public void setUp() {
     fs = Jimfs.newFileSystem(Configuration.unix());
-    ts = new AtomicLong(ISODateTimeFormat.dateTime().parseMillis("2010-01-30T12:00:00.000-08:00"));
+    ts =
+        new AtomicLong(
+            LocalDateTime.of(2010, Month.JANUARY, 30, 12, 0, 0)
+                .atOffset(ZoneOffset.ofHours(-8))
+                .toInstant()
+                .toEpochMilli());
   }
 
   @Test
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD
index 1fd3165..60663d7 100644
--- a/gerrit-pgm/BUILD
+++ b/gerrit-pgm/BUILD
@@ -22,7 +22,6 @@
     "//lib/guice:guice-assistedinject",
     "//lib/guice:guice-servlet",
     "//lib/jgit/org.eclipse.jgit:jgit",
-    "//lib/joda:joda-time",
     "//lib/log:api",
     "//lib/log:log4j",
 ]
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD
index fe9ce19..5ed4b8c 100644
--- a/gerrit-plugin-api/BUILD
+++ b/gerrit-plugin-api/BUILD
@@ -29,7 +29,6 @@
     "//lib/httpcomponents:httpcore",
     "//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet",
     "//lib/jgit/org.eclipse.jgit:jgit",
-    "//lib/joda:joda-time",
     "//lib/log:api",
     "//lib/log:log4j",
     "//lib/mina:sshd",
diff --git a/gerrit-server/BUILD b/gerrit-server/BUILD
index e124e89..e285ee5 100644
--- a/gerrit-server/BUILD
+++ b/gerrit-server/BUILD
@@ -92,7 +92,6 @@
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/joda:joda-time",
         "//lib/jsoup",
         "//lib/log:api",
         "//lib/log:jsonevent-layout",
@@ -181,7 +180,6 @@
     "//lib/guice:guice-servlet",
     "//lib/jgit/org.eclipse.jgit:jgit",
     "//lib/jgit/org.eclipse.jgit.junit:junit",
-    "//lib/joda:joda-time",
     "//lib/log:api",
     "//lib/log:impl_log4j",
     "//lib/log:log4j",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
index 4a87474..c5d60a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
@@ -16,16 +16,16 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import java.text.MessageFormat;
+import java.time.DayOfWeek;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
-import org.joda.time.DateTime;
-import org.joda.time.LocalDateTime;
-import org.joda.time.LocalTime;
-import org.joda.time.MutableDateTime;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
-import org.joda.time.format.ISODateTimeFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -49,16 +49,16 @@
   }
 
   public ScheduleConfig(Config rc, String section, String subsection) {
-    this(rc, section, subsection, DateTime.now());
+    this(rc, section, subsection, ZonedDateTime.now());
   }
 
   public ScheduleConfig(
       Config rc, String section, String subsection, String keyInterval, String keyStartTime) {
-    this(rc, section, subsection, keyInterval, keyStartTime, DateTime.now());
+    this(rc, section, subsection, keyInterval, keyStartTime, ZonedDateTime.now());
   }
 
   @VisibleForTesting
-  ScheduleConfig(Config rc, String section, String subsection, DateTime now) {
+  ScheduleConfig(Config rc, String section, String subsection, ZonedDateTime now) {
     this(rc, section, subsection, KEY_INTERVAL, KEY_STARTTIME, now);
   }
 
@@ -69,7 +69,7 @@
       String subsection,
       String keyInterval,
       String keyStartTime,
-      DateTime now) {
+      ZonedDateTime now) {
     this.rc = rc;
     this.section = section;
     this.subsection = subsection;
@@ -122,31 +122,24 @@
       String section,
       String subsection,
       String keyStartTime,
-      DateTime now,
+      ZonedDateTime now,
       long interval) {
     long delay = MISSING_CONFIG;
     String start = rc.getString(section, subsection, keyStartTime);
     try {
       if (start != null) {
-        DateTimeFormatter formatter;
-        MutableDateTime startTime = now.toMutableDateTime();
+        DateTimeFormatter formatter =
+            DateTimeFormatter.ofPattern("[E ]HH:mm").withLocale(Locale.US);
+        LocalTime firstStartTime = LocalTime.parse(start, formatter);
+        ZonedDateTime startTime = now.with(firstStartTime);
         try {
-          formatter = ISODateTimeFormat.hourMinute();
-          LocalTime firstStartTime = formatter.parseLocalTime(start);
-          startTime.hourOfDay().set(firstStartTime.getHourOfDay());
-          startTime.minuteOfHour().set(firstStartTime.getMinuteOfHour());
-        } catch (IllegalArgumentException e1) {
-          formatter = DateTimeFormat.forPattern("E HH:mm").withLocale(Locale.US);
-          LocalDateTime firstStartDateTime = formatter.parseLocalDateTime(start);
-          startTime.dayOfWeek().set(firstStartDateTime.getDayOfWeek());
-          startTime.hourOfDay().set(firstStartDateTime.getHourOfDay());
-          startTime.minuteOfHour().set(firstStartDateTime.getMinuteOfHour());
+          DayOfWeek dayOfWeek = formatter.parse(start, DayOfWeek::from);
+          startTime = startTime.with(dayOfWeek);
+        } catch (DateTimeParseException ignored) {
+          // Day of week is an optional parameter.
         }
-        startTime.secondOfMinute().set(0);
-        startTime.millisOfSecond().set(0);
-        long s = startTime.getMillis();
-        long n = now.getMillis();
-        delay = (s - n) % interval;
+        startTime = startTime.truncatedTo(ChronoUnit.MINUTES);
+        delay = Duration.between(now, startTime).toMillis() % interval;
         if (delay <= 0) {
           delay += interval;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java
index 68b3c23..0d20464 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.mail.Address;
-import org.joda.time.DateTime;
+import java.time.Instant;
 
 /**
  * A simplified representation of an RFC 2045-2047 mime email message used for representing received
@@ -40,7 +40,7 @@
 
   public abstract ImmutableList<Address> cc();
   // Metadata
-  public abstract DateTime dateReceived();
+  public abstract Instant dateReceived();
 
   public abstract ImmutableList<String> additionalHeaders();
   // Content
@@ -84,7 +84,7 @@
       return this;
     }
 
-    public abstract Builder dateReceived(DateTime val);
+    public abstract Builder dateReceived(Instant instant);
 
     public abstract ImmutableList.Builder<String> additionalHeadersBuilder();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java
index d2f91ed..57fe21f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java
@@ -33,7 +33,6 @@
 import org.apache.james.mime4j.dom.TextBody;
 import org.apache.james.mime4j.dom.address.Mailbox;
 import org.apache.james.mime4j.message.DefaultMessageBuilder;
-import org.joda.time.DateTime;
 
 /** Parses raw email content received through POP3 or IMAP into an internal {@link MailMessage}. */
 public class RawMailParser {
@@ -66,7 +65,9 @@
     if (mimeMessage.getSubject() != null) {
       messageBuilder.subject(mimeMessage.getSubject());
     }
-    messageBuilder.dateReceived(new DateTime(mimeMessage.getDate()));
+    if (mimeMessage.getDate() != null) {
+      messageBuilder.dateReceived(mimeMessage.getDate().toInstant());
+    }
 
     // Add From, To and Cc
     if (mimeMessage.getFrom() != null && mimeMessage.getFrom().size() > 0) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index ee9c570..dde577d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -42,17 +42,19 @@
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.lang.reflect.Field;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -65,7 +67,10 @@
 public class OutputStreamQuery {
   private static final Logger log = LoggerFactory.getLogger(OutputStreamQuery.class);
 
-  private static final DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss zzz");
+  private static final DateTimeFormatter dtf =
+      DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzz")
+          .withLocale(Locale.US)
+          .withZone(ZoneId.systemDefault());
 
   public enum OutputFormat {
     TEXT,
@@ -402,7 +407,7 @@
       out.print('\n');
     } else if (value instanceof Long && isDateField(field)) {
       out.print(' ');
-      out.print(dtf.print(((Long) value) * 1000L));
+      out.print(dtf.format(Instant.ofEpochSecond((Long) value)));
       out.print('\n');
     } else if (isPrimitive(value)) {
       out.print(' ');
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java
index e6f36b9..0423a53 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java
@@ -20,15 +20,19 @@
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static org.junit.Assert.assertEquals;
 
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
-import org.joda.time.DateTime;
 import org.junit.Test;
 
 public class ScheduleConfigTest {
 
   // Friday June 13, 2014 10:00 UTC
-  private static final DateTime NOW = DateTime.parse("2014-06-13T10:00:00-00:00");
+  private static final ZonedDateTime NOW =
+      LocalDateTime.of(2014, Month.JUNE, 13, 10, 0, 0).atOffset(ZoneOffset.UTC).toZonedDateTime();
 
   @Test
   public void initialDelay() throws Exception {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/AbstractParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/AbstractParserTest.java
index 19ad8bb..7309437 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/AbstractParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/AbstractParserTest.java
@@ -20,9 +20,9 @@
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.server.mail.Address;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
-import org.joda.time.DateTime;
 import org.junit.Ignore;
 
 @Ignore
@@ -85,7 +85,7 @@
     MailMessage.Builder b = MailMessage.builder();
     b.id("id");
     b.from(new Address("Foo Bar", "foo@bar.com"));
-    b.dateReceived(new DateTime());
+    b.dateReceived(Instant.now());
     b.subject("");
     return b;
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
index 84bae96..dc25939 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
@@ -20,8 +20,10 @@
 
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.MetadataName;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
 import org.junit.Test;
 
 public class MetadataParserTest {
@@ -31,7 +33,7 @@
     // email headers of the message.
     MailMessage.Builder b = MailMessage.builder();
     b.id("");
-    b.dateReceived(new DateTime());
+    b.dateReceived(Instant.now());
     b.subject("");
 
     b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.CHANGE_NUMBER) + "123");
@@ -48,8 +50,11 @@
     assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
     assertThat(meta.messageType).isEqualTo("comment");
-    assertThat(meta.timestamp.getTime())
-        .isEqualTo(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC).getMillis());
+    assertThat(meta.timestamp.toInstant())
+        .isEqualTo(
+            LocalDateTime.of(2016, Month.OCTOBER, 25, 9, 11, 35)
+                .atOffset(ZoneOffset.UTC)
+                .toInstant());
   }
 
   @Test
@@ -58,7 +63,7 @@
     // the text body of the message.
     MailMessage.Builder b = MailMessage.builder();
     b.id("");
-    b.dateReceived(new DateTime());
+    b.dateReceived(Instant.now());
     b.subject("");
 
     StringBuilder stringBuilder = new StringBuilder();
@@ -77,8 +82,11 @@
     assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
     assertThat(meta.messageType).isEqualTo("comment");
-    assertThat(meta.timestamp.getTime())
-        .isEqualTo(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC).getMillis());
+    assertThat(meta.timestamp.toInstant())
+        .isEqualTo(
+            LocalDateTime.of(2016, Month.OCTOBER, 25, 9, 11, 35)
+                .atOffset(ZoneOffset.UTC)
+                .toInstant());
   }
 
   @Test
@@ -87,7 +95,7 @@
     // the HTML body of the message.
     MailMessage.Builder b = MailMessage.builder();
     b.id("");
-    b.dateReceived(new DateTime());
+    b.dateReceived(Instant.now());
     b.subject("");
 
     StringBuilder stringBuilder = new StringBuilder();
@@ -111,7 +119,10 @@
     assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
     assertThat(meta.messageType).isEqualTo("comment");
-    assertThat(meta.timestamp.getTime())
-        .isEqualTo(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC).getMillis());
+    assertThat(meta.timestamp.toInstant())
+        .isEqualTo(
+            LocalDateTime.of(2016, Month.OCTOBER, 25, 9, 11, 35)
+                .atOffset(ZoneOffset.UTC)
+                .toInstant());
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/RawMailParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/RawMailParserTest.java
index 4efa817..001d12d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/RawMailParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/RawMailParserTest.java
@@ -65,7 +65,7 @@
     assertThat(have.to()).isEqualTo(want.to());
     assertThat(have.from()).isEqualTo(want.from());
     assertThat(have.cc()).isEqualTo(want.cc());
-    assertThat(have.dateReceived().getMillis()).isEqualTo(want.dateReceived().getMillis());
+    assertThat(have.dateReceived()).isEqualTo(want.dateReceived());
     assertThat(have.additionalHeaders()).isEqualTo(want.additionalHeaders());
     assertThat(have.subject()).isEqualTo(want.subject());
     assertThat(have.textContent()).isEqualTo(want.textContent());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java
index be8d882..eb4d180 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java
@@ -16,8 +16,9 @@
 
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.receive.MailMessage;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
 import org.junit.Ignore;
 
 /**
@@ -82,7 +83,10 @@
         .htmlContent("<div dir=\"ltr\">Contains unwanted attachment</div>")
         .subject("Test Subject")
         .addAdditionalHeader("MIME-Version: 1.0")
-        .dateReceived(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
+        .dateReceived(
+            LocalDateTime.of(2016, Month.OCTOBER, 25, 9, 11, 35)
+                .atOffset(ZoneOffset.UTC)
+                .toInstant());
     return expect.build();
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java
index affa3bd..91dc6f1 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java
@@ -16,8 +16,9 @@
 
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.receive.MailMessage;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
 import org.junit.Ignore;
 
 /** Tests parsing a Base64 encoded subject. */
@@ -58,7 +59,10 @@
         .addTo(new Address("ekempin", "ekempin@google.com"))
         .textContent(textContent)
         .subject("\uD83D\uDE1B test")
-        .dateReceived(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
+        .dateReceived(
+            LocalDateTime.of(2016, Month.OCTOBER, 25, 9, 11, 35)
+                .atOffset(ZoneOffset.UTC)
+                .toInstant());
     return expect.build();
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java
index 487e9dd..756581f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java
@@ -16,8 +16,9 @@
 
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.receive.MailMessage;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
 import org.junit.Ignore;
 
 /** Tests a message containing mime/alternative (text + html) content. */
@@ -98,7 +99,10 @@
         .htmlContent(unencodedHtmlContent)
         .subject("Change in gerrit[master]: Implement receiver class structure and bindings")
         .addAdditionalHeader("MIME-Version: 1.0")
-        .dateReceived(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
+        .dateReceived(
+            LocalDateTime.of(2016, Month.OCTOBER, 25, 9, 11, 35)
+                .atOffset(ZoneOffset.UTC)
+                .toInstant());
     return expect.build();
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java
index 9f2af0d..3fafd4b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java
@@ -15,8 +15,9 @@
 
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.receive.MailMessage;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
 import org.junit.Ignore;
 
 /** Tests that non-UTF8 encodings are handled correctly. */
@@ -62,7 +63,10 @@
         .addTo(new Address("ekempin", "ekempin@google.com"))
         .textContent(textContent)
         .subject("\uD83D\uDE1B test")
-        .dateReceived(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
+        .dateReceived(
+            LocalDateTime.of(2016, Month.OCTOBER, 25, 9, 11, 35)
+                .atOffset(ZoneOffset.UTC)
+                .toInstant());
     return expect.build();
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java
index 2c17859..2dc48b5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java
@@ -16,8 +16,9 @@
 
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.receive.MailMessage;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
 import org.junit.Ignore;
 
 /** Tests parsing a quoted printable encoded subject */
@@ -59,7 +60,10 @@
         .addTo(new Address("ekempin", "ekempin@google.com"))
         .textContent(textContent)
         .subject("âme vulgaire")
-        .dateReceived(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
+        .dateReceived(
+            LocalDateTime.of(2016, Month.OCTOBER, 25, 9, 11, 35)
+                .atOffset(ZoneOffset.UTC)
+                .toInstant());
     return expect.build();
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
index ce833d5..aa5b78a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
@@ -16,8 +16,9 @@
 
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.receive.MailMessage;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
 import org.junit.Ignore;
 
 /** Tests parsing a simple text message with different headers. */
@@ -124,7 +125,10 @@
         .addCc(new Address("Patrick Hiesel", "hiesel@google.com"))
         .textContent(textContent)
         .subject("Change in gerrit[master]: (Re)enable voting buttons for merged changes")
-        .dateReceived(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC))
+        .dateReceived(
+            LocalDateTime.of(2016, Month.OCTOBER, 25, 9, 11, 35)
+                .atOffset(ZoneOffset.UTC)
+                .toInstant())
         .addAdditionalHeader(
             "Authentication-Results: mx.google.com; dkim=pass header.i=@google.com;")
         .addAdditionalHeader(
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
index 90e6800..33e1005 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
@@ -46,13 +46,14 @@
 import com.google.gwtorm.protobuf.CodecFactory;
 import com.google.gwtorm.protobuf.ProtobufCodec;
 import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.Month;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.TimeZone;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -67,6 +68,7 @@
       CodecFactory.encoder(PatchSetApproval.class);
   private static final ProtobufCodec<PatchLineComment> PATCH_LINE_COMMENT_CODEC =
       CodecFactory.encoder(PatchLineComment.class);
+  private static final String TIMEZONE_ID = "US/Eastern";
 
   private String systemTimeZoneProperty;
   private TimeZone systemTimeZone;
@@ -76,10 +78,9 @@
 
   @Before
   public void setUp() {
-    String tz = "US/Eastern";
-    systemTimeZoneProperty = System.setProperty("user.timezone", tz);
+    systemTimeZoneProperty = System.setProperty("user.timezone", TIMEZONE_ID);
     systemTimeZone = TimeZone.getDefault();
-    TimeZone.setDefault(TimeZone.getTimeZone(tz));
+    TimeZone.setDefault(TimeZone.getTimeZone(TIMEZONE_ID));
     long maxMs = ChangeRebuilderImpl.MAX_WINDOW_MS;
     assertThat(maxMs).isGreaterThan(1000L);
     TestTimeUtil.resetWithClockStep(maxMs * 2, MILLISECONDS);
@@ -1517,8 +1518,11 @@
     PatchSetApproval a2 = clone(a1);
     a2.setGranted(
         new Timestamp(
-            new DateTime(1900, 1, 1, 0, 0, 0, DateTimeZone.forTimeZone(TimeZone.getDefault()))
-                .getMillis()));
+            LocalDate.of(1900, Month.JANUARY, 1)
+                .atStartOfDay()
+                .atZone(ZoneId.of(TIMEZONE_ID))
+                .toInstant()
+                .toEpochMilli()));
 
     // Both are ReviewDb, exact match is required.
     ChangeBundle b1 =
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 1e722fc..7234acc 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1232,7 +1232,7 @@
     long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
     resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
     TestRepository<Repo> repo = createProject("repo");
-    long startMs = TestTimeUtil.START.getMillis();
+    long startMs = TestTimeUtil.START.toEpochMilli();
     Change change1 = insert(repo, newChange(repo), null, new Timestamp(startMs));
     Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
 
@@ -1259,7 +1259,7 @@
     long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
     resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
     TestRepository<Repo> repo = createProject("repo");
-    long startMs = TestTimeUtil.START.getMillis();
+    long startMs = TestTimeUtil.START.toEpochMilli();
     Change change1 = insert(repo, newChange(repo), null, new Timestamp(startMs));
     Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
     TestTimeUtil.setClockStep(0, MILLISECONDS);
@@ -1281,7 +1281,7 @@
     long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
     resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
     TestRepository<Repo> repo = createProject("repo");
-    long startMs = TestTimeUtil.START.getMillis();
+    long startMs = TestTimeUtil.START.toEpochMilli();
     Change change1 = insert(repo, newChange(repo), null, new Timestamp(startMs));
     Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
     TestTimeUtil.setClockStep(0, MILLISECONDS);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java
index dd44cb9ae..5bbe3b6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java
@@ -17,18 +17,21 @@
 import static com.google.common.base.Preconditions.checkState;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
+import com.google.gerrit.common.TimeUtil;
 import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
-import org.joda.time.DateTimeZone;
 
 /** Static utility methods for dealing with dates and times in tests. */
 public class TestTimeUtil {
-  public static final DateTime START =
-      new DateTime(2009, 9, 30, 17, 0, 0, DateTimeZone.forOffsetHours(-4));
+  public static final Instant START =
+      LocalDateTime.of(2009, Month.SEPTEMBER, 30, 17, 0, 0)
+          .atOffset(ZoneOffset.ofHours(-4))
+          .toInstant();
 
   private static Long clockStepMs;
   private static AtomicLong clockMs;
@@ -43,7 +46,7 @@
    */
   public static synchronized void resetWithClockStep(long clockStep, TimeUnit clockStepUnit) {
     // Set an arbitrary start point so tests are more repeatable.
-    clockMs = new AtomicLong(START.getMillis());
+    clockMs = new AtomicLong(START.toEpochMilli());
     setClockStep(clockStep, clockStepUnit);
   }
 
@@ -56,13 +59,7 @@
   public static synchronized void setClockStep(long clockStep, TimeUnit clockStepUnit) {
     checkState(clockMs != null, "call resetWithClockStep first");
     clockStepMs = MILLISECONDS.convert(clockStep, clockStepUnit);
-    DateTimeUtils.setCurrentMillisProvider(
-        new MillisProvider() {
-          @Override
-          public long getMillis() {
-            return clockMs.getAndAdd(clockStepMs);
-          }
-        });
+    TimeUtil.setCurrentMillisSupplier(() -> clockMs.getAndAdd(clockStepMs));
   }
 
   /**
@@ -89,7 +86,7 @@
   /** Reset the clock to use the actual system clock. */
   public static synchronized void useSystemTime() {
     clockMs = null;
-    DateTimeUtils.setCurrentMillisSystem();
+    TimeUtil.resetCurrentMillisSupplier();
   }
 
   private TestTimeUtil() {}
diff --git a/lib/elasticsearch/BUILD b/lib/elasticsearch/BUILD
index c40925e..18c62af 100644
--- a/lib/elasticsearch/BUILD
+++ b/lib/elasticsearch/BUILD
@@ -8,13 +8,13 @@
         ":compress-lzf",
         ":hppc",
         ":jna",
+        ":joda-time",
         ":jsr166e",
         ":netty",
         ":t-digest",
         "//lib/jackson:jackson-core",
         "//lib/jackson:jackson-dataformat-cbor",
         "//lib/jackson:jackson-dataformat-smile",
-        "//lib/joda:joda-time",
         "//lib/lucene:lucene-codecs",
         "//lib/lucene:lucene-highlighter",
         "//lib/lucene:lucene-join",
@@ -48,6 +48,19 @@
 )
 
 java_library(
+    name = "joda-time",
+    data = ["//lib:LICENSE-Apache2.0"],
+    exports = ["@joda_time//jar"],
+    runtime_deps = ["joda-convert"],
+)
+
+java_library(
+    name = "joda-convert",
+    data = ["//lib:LICENSE-Apache2.0"],
+    exports = ["@joda_convert//jar"],
+)
+
+java_library(
     name = "compress-lzf",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//lib/elasticsearch:__pkg__"],
diff --git a/lib/joda/BUILD b/lib/joda/BUILD
deleted file mode 100644
index e1a1924..0000000
--- a/lib/joda/BUILD
+++ /dev/null
@@ -1,13 +0,0 @@
-java_library(
-    name = "joda-time",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@joda_time//jar"],
-    runtime_deps = ["joda-convert"],
-)
-
-java_library(
-    name = "joda-convert",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@joda_convert//jar"],
-)