Fix CQL tojson timestamp output on negative timestamp values before Gregorian calendar reform in 1582

patch by Stefan Miklosovic; reviewed by Brandon Williams, Berenguer Blasi for CASSANDRA-19566
diff --git a/CHANGES.txt b/CHANGES.txt
index b3fe699..f9a61a4 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.0.13
+ * Fix CQL tojson timestamp output on negative timestamp values before Gregorian calendar reform in 1582 (CASSANDRA-19566)
  * Fix few types issues and implement types compatibility tests (CASSANDRA-19479)
  * Optionally avoid hint transfer during decommission (CASSANDRA-19525)
  * Change logging to TRACE when failing to get peer certificate (CASSANDRA-19508)
diff --git a/src/java/org/apache/cassandra/db/marshal/DateType.java b/src/java/org/apache/cassandra/db/marshal/DateType.java
index 473cedf..372dac4 100644
--- a/src/java/org/apache/cassandra/db/marshal/DateType.java
+++ b/src/java/org/apache/cassandra/db/marshal/DateType.java
@@ -80,7 +80,7 @@
     @Override
     public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
     {
-        return '"' + TimestampSerializer.getJsonDateFormatter().format(TimestampSerializer.instance.deserialize(buffer)) + '"';
+        return '"' + TimestampSerializer.getJsonDateFormatter().format(TimestampSerializer.instance.deserialize(buffer).toInstant()) + '"';
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/db/marshal/TimestampType.java b/src/java/org/apache/cassandra/db/marshal/TimestampType.java
index 0dac6b0..f876adb 100644
--- a/src/java/org/apache/cassandra/db/marshal/TimestampType.java
+++ b/src/java/org/apache/cassandra/db/marshal/TimestampType.java
@@ -102,7 +102,7 @@
 
     private String toString(Date date)
     {
-        return date != null ? TimestampSerializer.getJsonDateFormatter().format(date) : "";
+        return date != null ? TimestampSerializer.getJsonDateFormatter().format(date.toInstant()) : "";
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/serializers/TimestampSerializer.java b/src/java/org/apache/cassandra/serializers/TimestampSerializer.java
index 21cac66..e21af5a 100644
--- a/src/java/org/apache/cassandra/serializers/TimestampSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/TimestampSerializer.java
@@ -22,7 +22,7 @@
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 import java.nio.ByteBuffer;
-import java.text.SimpleDateFormat;
+import java.text.Format;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
@@ -32,7 +32,6 @@
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
-import java.util.TimeZone;
 import java.util.regex.Pattern;
 
 
@@ -103,23 +102,29 @@
 
     private static final Pattern timestampPattern = Pattern.compile("^-?\\d+$");
 
-    private static final FastThreadLocal<SimpleDateFormat> FORMATTER_UTC = new FastThreadLocal<SimpleDateFormat>()
+    private static final FastThreadLocal<Format> FORMATTER_UTC = new FastThreadLocal<Format>()
     {
-        protected SimpleDateFormat initialValue()
+        protected java.text.Format initialValue()
         {
-            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX");
-            sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
-            return sdf;
+            return new DateTimeFormatterBuilder()
+                   .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX")
+                   .parseDefaulting(ChronoField.NANO_OF_DAY, 0)
+                   .toFormatter()
+                   .withZone(ZoneId.of("UTC"))
+                   .toFormat();
         }
     };
 
-    private static final FastThreadLocal<SimpleDateFormat> FORMATTER_TO_JSON = new FastThreadLocal<SimpleDateFormat>()
+    private static final FastThreadLocal<Format> FORMATTER_TO_JSON = new FastThreadLocal<Format>()
     {
-        protected SimpleDateFormat initialValue()
+        protected java.text.Format initialValue()
         {
-            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSX");
-            sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
-            return sdf;
+            return new DateTimeFormatterBuilder()
+                   .appendPattern("yyyy-MM-dd HH:mm:ss.SSSX")
+                   .parseDefaulting(ChronoField.NANO_OF_DAY, 0)
+                   .toFormatter()
+                   .withZone(ZoneId.of("UTC"))
+                   .toFormat();
         }
     };
 
@@ -169,7 +174,7 @@
         throw new MarshalException(String.format("Unable to parse a date/time from '%s'", source));
     }
 
-    public static SimpleDateFormat getJsonDateFormatter()
+    public static Format getJsonDateFormatter()
     {
     	return FORMATTER_TO_JSON.get();
     }
@@ -187,7 +192,7 @@
 
     public String toStringUTC(Date value)
     {
-        return value == null ? "" : FORMATTER_UTC.get().format(value);
+        return value == null ? "" : FORMATTER_UTC.get().format(value.toInstant());
     }
 
     public Class<Date> getType()
@@ -204,6 +209,6 @@
     {
         return buffer == null || !buffer.hasRemaining()
                ? "null"
-               : FORMATTER_UTC.get().format(deserialize(buffer));
+               : FORMATTER_UTC.get().format(deserialize(buffer).toInstant());
     }
 }