diff --git a/src/main/java/com/amazon/ion/Timestamp.java b/src/main/java/com/amazon/ion/Timestamp.java index 3ecb91baa3..13057cf689 100644 --- a/src/main/java/com/amazon/ion/Timestamp.java +++ b/src/main/java/com/amazon/ion/Timestamp.java @@ -19,6 +19,7 @@ import static com.amazon.ion.util.IonTextUtils.printCodePointAsString; import com.amazon.ion.impl._Private_Utils; +import com.amazon.ion.system.IonTextWriterBuilder; import com.amazon.ion.util.IonTextUtils; import java.io.IOException; import java.math.BigDecimal; @@ -133,6 +134,18 @@ public final class Timestamp private static final int FLAG_MINUTE = 0x08; private static final int FLAG_SECOND = 0x10; + /** + * The default maximum number of digits in the fractional component that will be written when a text representation + * of a timestamp is requested. If more precision is needed, timestamps can be serialized using a text IonWriter + * configured with {@link IonTextWriterBuilder#withMaximumTimestampPrecisionDigits(int)}. + * + * @see #toString() + * @see #toZString() + * @see #print(Appendable) + * @see #printZ(Appendable) + */ + public static final int DEFAULT_MAXIMUM_DIGITS_TEXT = 10000; + /** * The precision of the Timestamp. */ @@ -2035,16 +2048,34 @@ public Timestamp withLocalOffset(Integer offset) * Returns the string representation (in Ion format) of this Timestamp in * its local time. * + * @throws IonException if the text representation of the timestamp requires more than + * {@link Timestamp#DEFAULT_MAXIMUM_DIGITS_TEXT}. + * * @see #toZString() * @see #print(Appendable) */ @Override public String toString() + { + return toString(DEFAULT_MAXIMUM_DIGITS_TEXT); + } + + /** + * Returns the string representation (in Ion format) of this Timestamp in + * its local time. + * + * @throws IonException if the text representation of the timestamp requires more than + * the given maximum. + * + * @see #toZString() + * @see #print(Appendable) + */ + String toString(int maximumDigits) { StringBuilder buffer = new StringBuilder(32); try { - print(buffer); + print(buffer, maximumDigits); } catch (IOException e) { @@ -2059,6 +2090,9 @@ public String toString() * Returns the string representation (in Ion format) of this Timestamp * in UTC. * + * @throws IonException if the text representation of the timestamp requires more than + * {@link Timestamp#DEFAULT_MAXIMUM_DIGITS_TEXT}. + * * @see #toString() * @see #printZ(Appendable) */ @@ -2087,11 +2121,33 @@ public String toZString() * @param out not {@code null} * * @throws IOException propagated when the {@link Appendable} throws it + * @throws IonException if the text representation of the timestamp requires more than + * {@link Timestamp#DEFAULT_MAXIMUM_DIGITS_TEXT}. * * @see #printZ(Appendable) */ public void print(Appendable out) throws IOException + { + print(out, DEFAULT_MAXIMUM_DIGITS_TEXT); + } + + /** + * Prints to an {@code Appendable} the string representation (in Ion format) + * of this Timestamp in its local time. + *
+ * This method produces the same output as {@link #toString()}.
+ *
+ * @param out not {@code null}
+ *
+ * @throws IOException propagated when the {@link Appendable} throws it
+ * @throws IonException if the text representation of the timestamp requires more than
+ * the given maximum.
+ *
+ * @see #printZ(Appendable)
+ */
+ private void print(Appendable out, int maximumDigits)
+ throws IOException
{
// we have to make a copy to preserve the "immutable" contract
// on Timestamp and we don't want someone reading the calendar
@@ -2103,7 +2159,7 @@ public void print(Appendable out)
adjusted = make_localtime();
}
- print(out, adjusted);
+ print(out, adjusted, maximumDigits);
}
@@ -2116,6 +2172,8 @@ public void print(Appendable out)
* @param out not {@code null}
*
* @throws IOException propagated when the {@code Appendable} throws it.
+ * @throws IonException if the text representation of the timestamp requires more than
+ * {@link Timestamp#DEFAULT_MAXIMUM_DIGITS_TEXT}.
*
* @see #print(Appendable)
*/
@@ -2146,6 +2204,25 @@ public void printZ(Appendable out)
}
+ /**
+ * Throws if the text representation of the timestamp would require more than the given number of digits in its
+ * fractional component.
+ * @param value a Timestamp with a non-null fraction.
+ * @param maximumDigits the maximum number of digits allowed.
+ */
+ private static void requirePrecisionWithinLimit(Timestamp value, int maximumDigits) {
+ if (value._fraction.scale() > maximumDigits) {
+ throw new IonException(String.format(
+ "Timestamp with %d digits of precision cannot be serialized because it exceeds the " +
+ "configurable maximum timestamp precision of %d digits. Timestamps that require more digits " +
+ "may be written using a text writer configured with " +
+ "IonTextWriterBuilder.withMaximumTimestampPrecisionDigits.",
+ value._fraction.scale(),
+ maximumDigits
+ ));
+ }
+ }
+
/**
* helper for print(out) and printZ(out) so that printZ can create
* a zulu time and pass it directly and print can apply the local
@@ -2153,9 +2230,10 @@ public void printZ(Appendable out)
* contract to be immutable).
* @param out destination for the text image of the value
* @param adjusted the time value with the fields adjusted to match the desired text output
+ * @param maximumDigits the maximum number of digits allowed in the fractional seconds
* @throws IOException
*/
- private static void print(Appendable out, Timestamp adjusted)
+ private static void print(Appendable out, Timestamp adjusted, int maximumDigits)
throws IOException
{
// null is our first "guess" to get it out of the way
@@ -2164,11 +2242,14 @@ private static void print(Appendable out, Timestamp adjusted)
return;
}
+ if (adjusted._precision == Precision.SECOND && adjusted._fraction != null) {
+ requirePrecisionWithinLimit(adjusted, maximumDigits);
+ }
+
// so we have a real value - we'll start with the date portion
// which we always have
print_digits(out, adjusted._year, 4);
if (adjusted._precision == Precision.YEAR) {
- assert adjusted._offset == UNKNOWN_OFFSET;
out.append("T");
return;
}
@@ -2176,7 +2257,6 @@ private static void print(Appendable out, Timestamp adjusted)
out.append("-");
print_digits(out, adjusted._month, 2); // convert calendar months to a base 1 value
if (adjusted._precision == Precision.MONTH) {
- assert adjusted._offset == UNKNOWN_OFFSET;
out.append("T");
return;
}
@@ -2184,8 +2264,6 @@ private static void print(Appendable out, Timestamp adjusted)
out.append("-");
print_digits(out, adjusted._day, 2);
if (adjusted._precision == Precision.DAY) {
- assert adjusted._offset == UNKNOWN_OFFSET;
- // out.append("T");
return;
}
@@ -2202,7 +2280,7 @@ private static void print(Appendable out, Timestamp adjusted)
}
}
- if (adjusted._offset != UNKNOWN_OFFSET) {
+ if (adjusted._offset != null) {
int min, hour;
min = adjusted._offset;
if (min == 0) {
diff --git a/src/main/java/com/amazon/ion/_Private_Trampoline.kt b/src/main/java/com/amazon/ion/_Private_Trampoline.kt
new file mode 100644
index 0000000000..c4b9680c24
--- /dev/null
+++ b/src/main/java/com/amazon/ion/_Private_Trampoline.kt
@@ -0,0 +1,9 @@
+package com.amazon.ion
+
+/**
+ * **NOT FOR APPLICATION USE. This method may be removed at any time.**
+ * Trampoline to the non-public `Timestamp.toString(Int)` method.
+ */
+internal fun printTimestamp(timestamp: Timestamp, maximumDigits: Int): String {
+ return timestamp.toString(maximumDigits)
+}
diff --git a/src/main/java/com/amazon/ion/impl/IonWriterSystemText.java b/src/main/java/com/amazon/ion/impl/IonWriterSystemText.java
index c0f7de3fc5..bf8f545505 100644
--- a/src/main/java/com/amazon/ion/impl/IonWriterSystemText.java
+++ b/src/main/java/com/amazon/ion/impl/IonWriterSystemText.java
@@ -16,6 +16,7 @@
package com.amazon.ion.impl;
import static com.amazon.ion.SystemSymbols.SYMBOLS;
+import static com.amazon.ion._Private_TrampolineKt.printTimestamp;
import static com.amazon.ion.impl._Private_IonConstants.tidList;
import static com.amazon.ion.impl._Private_IonConstants.tidSexp;
import static com.amazon.ion.impl._Private_IonConstants.tidStruct;
@@ -641,13 +642,14 @@ public void writeTimestamp(Timestamp value) throws IOException
else if (_options._timestamp_as_string)
{
// Timestamp is ASCII-safe so this is easy
+ String valueText = printTimestamp(value, _options.getMaximumTimestampPrecisionDigits());
_output.appendAscii('"');
- _output.appendAscii(value.toString());
+ _output.appendAscii(valueText);
_output.appendAscii('"');
}
else
{
- _output.appendAscii(value.toString());
+ _output.appendAscii(printTimestamp(value, _options.getMaximumTimestampPrecisionDigits()));
}
closeValue();
diff --git a/src/main/java/com/amazon/ion/impl/_Private_IonTextWriterBuilder.java b/src/main/java/com/amazon/ion/impl/_Private_IonTextWriterBuilder.java
index ced626d845..9d8ae52c2a 100644
--- a/src/main/java/com/amazon/ion/impl/_Private_IonTextWriterBuilder.java
+++ b/src/main/java/com/amazon/ion/impl/_Private_IonTextWriterBuilder.java
@@ -199,6 +199,13 @@ private _Private_IonTextWriterBuilder fillDefaults()
b.setNewLineType(NewLineType.PLATFORM_DEPENDENT);
}
+ if (b.getMaximumTimestampPrecisionDigits() < 1) {
+ throw new IllegalArgumentException(String.format(
+ "Configured maximum timestamp precision must be positive, not %d.",
+ b.getMaximumTimestampPrecisionDigits()
+ ));
+ }
+
return (_Private_IonTextWriterBuilder) b.immutable();
}
diff --git a/src/main/java/com/amazon/ion/system/IonTextWriterBuilder.java b/src/main/java/com/amazon/ion/system/IonTextWriterBuilder.java
index b88fb4bca0..40785cd1f4 100644
--- a/src/main/java/com/amazon/ion/system/IonTextWriterBuilder.java
+++ b/src/main/java/com/amazon/ion/system/IonTextWriterBuilder.java
@@ -20,6 +20,7 @@
import com.amazon.ion.IonCatalog;
import com.amazon.ion.IonWriter;
import com.amazon.ion.SymbolTable;
+import com.amazon.ion.Timestamp;
import com.amazon.ion.impl._Private_IonTextWriterBuilder;
import com.amazon.ion.impl._Private_Utils;
import java.io.OutputStream;
@@ -221,6 +222,7 @@ public static IonTextWriterBuilder json()
private int myLongStringThreshold;
private NewLineType myNewLineType;
private boolean myTopLevelValuesOnNewLines;
+ private int myMaximumTimestampPrecisionDigits = Timestamp.DEFAULT_MAXIMUM_DIGITS_TEXT;
/** NOT FOR APPLICATION USE! */
@@ -240,6 +242,7 @@ protected IonTextWriterBuilder(IonTextWriterBuilder that)
this.myLongStringThreshold = that.myLongStringThreshold;
this.myNewLineType = that.myNewLineType;
this.myTopLevelValuesOnNewLines = that.myTopLevelValuesOnNewLines;
+ this.myMaximumTimestampPrecisionDigits = that.myMaximumTimestampPrecisionDigits;
}
@@ -764,6 +767,47 @@ public final IonTextWriterBuilder withWriteTopLevelValuesOnNewLines(boolean writ
//=========================================================================
+ /**
+ * Gets the maximum number of digits of fractional second precision allowed to be written for timestamp values.
+ *
+ * @return the currently configured maximum.
+ *
+ * @see #setMaximumTimestampPrecisionDigits(int)
+ * @see #withMaximumTimestampPrecisionDigits(int)
+ */
+ public final int getMaximumTimestampPrecisionDigits() {
+ return myMaximumTimestampPrecisionDigits;
+ }
+
+ /**
+ * Sets the maximum number of digits of fractional second precision allowed to be written for timestamp values.
+ * Default: {@link Timestamp#DEFAULT_MAXIMUM_DIGITS_TEXT}.
+ *
+ * @see #getMaximumTimestampPrecisionDigits()
+ * @see #withMaximumTimestampPrecisionDigits(int)
+ */
+ public void setMaximumTimestampPrecisionDigits(int maximumTimestampPrecisionDigits) {
+ mutationCheck();
+ myMaximumTimestampPrecisionDigits = maximumTimestampPrecisionDigits;
+ }
+
+ /**
+ * Sets the maximum number of digits of fractional second precision allowed to be written for timestamp values.
+ * Default: {@link Timestamp#DEFAULT_MAXIMUM_DIGITS_TEXT}.
+ *
+ * @return this instance, if mutable; otherwise a mutable copy of this instance.
+ *
+ * @see #getMaximumTimestampPrecisionDigits()
+ * @see #setMaximumTimestampPrecisionDigits(int)
+ */
+ public final IonTextWriterBuilder withMaximumTimestampPrecisionDigits(int maximumTimestampPrecisionDigits) {
+ IonTextWriterBuilder b = mutable();
+ b.setMaximumTimestampPrecisionDigits(maximumTimestampPrecisionDigits);
+ return b;
+ }
+
+ //=========================================================================
+
/**
* Creates a new writer that will write text to the given output
* stream.
diff --git a/src/test/java/com/amazon/ion/TimestampTest.java b/src/test/java/com/amazon/ion/TimestampTest.java
index 1709fe8da8..819f980a19 100644
--- a/src/test/java/com/amazon/ion/TimestampTest.java
+++ b/src/test/java/com/amazon/ion/TimestampTest.java
@@ -17,6 +17,7 @@
import static com.amazon.ion.Decimal.NEGATIVE_ZERO;
import static com.amazon.ion.Decimal.negativeZero;
+import static com.amazon.ion.Timestamp.DEFAULT_MAXIMUM_DIGITS_TEXT;
import static com.amazon.ion.Timestamp.MAXIMUM_ALLOWED_TIMESTAMP_IN_MILLIS_DECIMAL;
import static com.amazon.ion.Timestamp.MINIMUM_TIMESTAMP_IN_MILLIS;
import static com.amazon.ion.Timestamp.MINIMUM_TIMESTAMP_IN_MILLIS_DECIMAL;
@@ -32,6 +33,8 @@
import static com.amazon.ion.impl._Private_Utils.UTC;
import com.amazon.ion.Timestamp.Precision;
+
+import java.io.IOException;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.ParseException;
@@ -40,7 +43,9 @@
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
+import java.util.function.Function;
+import com.amazon.ion.system.IonTextWriterBuilder;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
@@ -3098,4 +3103,80 @@ public void testInstantVsTimestampMillis() {
assertEquals(millisFromTimestamp, ts2.getMillis());
*/
}
+
+ private void expectCleanToStringFailure(Timestamp timestamp, Function