diff --git a/java/util/pom.xml b/java/util/pom.xml
new file mode 100644
index 0000000000..9416f380d3
--- /dev/null
+++ b/java/util/pom.xml
@@ -0,0 +1,202 @@
+
+
For example, FieldMask "foo.bar,foo.baz,bar.baz" as a tree will be: + *
+ * [root] -+- foo -+- bar + * | | + * | +- baz + * | + * +- bar --- baz + *+ * + *
By representing FieldMasks with this tree structure we can easily convert
+ * a FieldMask to a canonical form, merge two FieldMasks, calculate the
+ * intersection to two FieldMasks and traverse all fields specified by the
+ * FieldMask in a message tree.
+ */
+class FieldMaskTree {
+ private static final Logger logger =
+ Logger.getLogger(FieldMaskTree.class.getName());
+
+ private static final String FIELD_PATH_SEPARATOR_REGEX = "\\.";
+
+ private static class Node {
+ public TreeMap Example of generated format: "1972-01-01T10:00:20.021Z"
+ *
+ * @return The string representation of the given timestamp.
+ * @throws IllegalArgumentException if the given timestamp is not in the
+ * valid range.
+ */
+ public static String toString(Timestamp timestamp)
+ throws IllegalArgumentException {
+ StringBuilder result = new StringBuilder();
+ // Format the seconds part.
+ if (timestamp.getSeconds() < TIMESTAMP_SECONDS_MIN
+ || timestamp.getSeconds() > TIMESTAMP_SECONDS_MAX) {
+ throw new IllegalArgumentException("Timestamp is out of range.");
+ }
+ Date date = new Date(timestamp.getSeconds() * MILLIS_PER_SECOND);
+ result.append(timestampFormat.format(date));
+ // Format the nanos part.
+ if (timestamp.getNanos() < 0 || timestamp.getNanos() >= NANOS_PER_SECOND) {
+ throw new IllegalArgumentException("Timestamp has invalid nanos value.");
+ }
+ if (timestamp.getNanos() != 0) {
+ result.append(".");
+ result.append(formatNanos(timestamp.getNanos()));
+ }
+ result.append("Z");
+ return result.toString();
+ }
+
+ /**
+ * Parse from RFC 3339 date string to Timestamp. This method accepts all
+ * outputs of {@link #toString(Timestamp)} and it also accepts any fractional
+ * digits (or none) and any offset as long as they fit into nano-seconds
+ * precision.
+ *
+ * Example of accepted format: "1972-01-01T10:00:20.021-05:00"
+ *
+ * @return A Timestamp parsed from the string.
+ * @throws ParseException if parsing fails.
+ */
+
+ public static Timestamp parseTimestamp(String value) throws ParseException {
+ int dayOffset = value.indexOf('T');
+ if (dayOffset == -1) {
+ throw new ParseException(
+ "Failed to parse timestamp: invalid timestamp \"" + value + "\"", 0);
+ }
+ int timezoneOffsetPosition = value.indexOf('Z', dayOffset);
+ if (timezoneOffsetPosition == -1) {
+ timezoneOffsetPosition = value.indexOf('+', dayOffset);
+ }
+ if (timezoneOffsetPosition == -1) {
+ timezoneOffsetPosition = value.indexOf('-', dayOffset);
+ }
+ if (timezoneOffsetPosition == -1) {
+ throw new ParseException(
+ "Failed to parse timestamp: missing valid timezone offset.", 0);
+ }
+ // Parse seconds and nanos.
+ String timeValue = value.substring(0, timezoneOffsetPosition);
+ String secondValue = timeValue;
+ String nanoValue = "";
+ int pointPosition = timeValue.indexOf('.');
+ if (pointPosition != -1) {
+ secondValue = timeValue.substring(0, pointPosition);
+ nanoValue = timeValue.substring(pointPosition + 1);
+ }
+ Date date = timestampFormat.parse(secondValue);
+ long seconds = date.getTime() / MILLIS_PER_SECOND;
+ int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue);
+ // Parse timezone offsets.
+ if (value.charAt(timezoneOffsetPosition) == 'Z') {
+ if (value.length() != timezoneOffsetPosition + 1) {
+ throw new ParseException(
+ "Failed to parse timestamp: invalid trailing data \""
+ + value.substring(timezoneOffsetPosition) + "\"", 0);
+ }
+ } else {
+ String offsetValue = value.substring(timezoneOffsetPosition + 1);
+ long offset = parseTimezoneOffset(offsetValue);
+ if (value.charAt(timezoneOffsetPosition) == '+') {
+ seconds -= offset;
+ } else {
+ seconds += offset;
+ }
+ }
+ try {
+ return normalizedTimestamp(seconds, nanos);
+ } catch (IllegalArgumentException e) {
+ throw new ParseException(
+ "Failed to parse timestmap: timestamp is out of range.", 0);
+ }
+ }
+
+ /**
+ * Convert Duration to string format. The string format will contains 3, 6,
+ * or 9 fractional digits depending on the precision required to represent
+ * the exact Duration value. For example: "1s", "1.010s", "1.000000100s",
+ * "-3.100s" The range that can be represented by Duration is from
+ * -315,576,000,000 to +315,576,000,000 inclusive (in seconds).
+ *
+ * @return The string representation of the given duration.
+ * @throws IllegalArgumentException if the given duration is not in the valid
+ * range.
+ */
+ public static String toString(Duration duration)
+ throws IllegalArgumentException {
+ if (duration.getSeconds() < DURATION_SECONDS_MIN
+ || duration.getSeconds() > DURATION_SECONDS_MAX) {
+ throw new IllegalArgumentException("Duration is out of valid range.");
+ }
+ StringBuilder result = new StringBuilder();
+ long seconds = duration.getSeconds();
+ int nanos = duration.getNanos();
+ if (seconds < 0 || nanos < 0) {
+ if (seconds > 0 || nanos > 0) {
+ throw new IllegalArgumentException(
+ "Invalid duration: seconds value and nanos value must have the same"
+ + "sign.");
+ }
+ result.append("-");
+ seconds = -seconds;
+ nanos = -nanos;
+ }
+ result.append(seconds);
+ if (nanos != 0) {
+ result.append(".");
+ result.append(formatNanos(nanos));
+ }
+ result.append("s");
+ return result.toString();
+ }
+
+ /**
+ * Parse from a string to produce a duration.
+ *
+ * @return A Duration parsed from the string.
+ * @throws ParseException if parsing fails.
+ */
+ public static Duration parseDuration(String value) throws ParseException {
+ // Must ended with "s".
+ if (value.isEmpty() || value.charAt(value.length() - 1) != 's') {
+ throw new ParseException("Invalid duration string: " + value, 0);
+ }
+ boolean negative = false;
+ if (value.charAt(0) == '-') {
+ negative = true;
+ value = value.substring(1);
+ }
+ String secondValue = value.substring(0, value.length() - 1);
+ String nanoValue = "";
+ int pointPosition = secondValue.indexOf('.');
+ if (pointPosition != -1) {
+ nanoValue = secondValue.substring(pointPosition + 1);
+ secondValue = secondValue.substring(0, pointPosition);
+ }
+ long seconds = Long.parseLong(secondValue);
+ int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue);
+ if (seconds < 0) {
+ throw new ParseException("Invalid duration string: " + value, 0);
+ }
+ if (negative) {
+ seconds = -seconds;
+ nanos = -nanos;
+ }
+ try {
+ return normalizedDuration(seconds, nanos);
+ } catch (IllegalArgumentException e) {
+ throw new ParseException("Duration value is out of range.", 0);
+ }
+ }
+
+ /**
+ * Create a Timestamp from the number of milliseconds elapsed from the epoch.
+ */
+ public static Timestamp createTimestampFromMillis(long milliseconds) {
+ return normalizedTimestamp(milliseconds / MILLIS_PER_SECOND,
+ (int) (milliseconds % MILLIS_PER_SECOND * NANOS_PER_MILLISECOND));
+ }
+
+ /**
+ * Create a Duration from the number of milliseconds.
+ */
+ public static Duration createDurationFromMillis(long milliseconds) {
+ return normalizedDuration(milliseconds / MILLIS_PER_SECOND,
+ (int) (milliseconds % MILLIS_PER_SECOND * NANOS_PER_MILLISECOND));
+ }
+
+ /**
+ * Convert a Timestamp to the number of milliseconds elapsed from the epoch.
+ *
+ * The result will be rounded down to the nearest millisecond. E.g., if the
+ * timestamp represents "1969-12-31T23:59:59.999999999Z", it will be rounded
+ * to -1 millisecond.
+ */
+ public static long toMillis(Timestamp timestamp) {
+ return timestamp.getSeconds() * MILLIS_PER_SECOND + timestamp.getNanos()
+ / NANOS_PER_MILLISECOND;
+ }
+
+ /**
+ * Convert a Duration to the number of milliseconds.The result will be
+ * rounded towards 0 to the nearest millisecond. E.g., if the duration
+ * represents -1 nanosecond, it will be rounded to 0.
+ */
+ public static long toMillis(Duration duration) {
+ return duration.getSeconds() * MILLIS_PER_SECOND + duration.getNanos()
+ / NANOS_PER_MILLISECOND;
+ }
+
+ /**
+ * Create a Timestamp from the number of microseconds elapsed from the epoch.
+ */
+ public static Timestamp createTimestampFromMicros(long microseconds) {
+ return normalizedTimestamp(microseconds / MICROS_PER_SECOND,
+ (int) (microseconds % MICROS_PER_SECOND * NANOS_PER_MICROSECOND));
+ }
+
+ /**
+ * Create a Duration from the number of microseconds.
+ */
+ public static Duration createDurationFromMicros(long microseconds) {
+ return normalizedDuration(microseconds / MICROS_PER_SECOND,
+ (int) (microseconds % MICROS_PER_SECOND * NANOS_PER_MICROSECOND));
+ }
+
+ /**
+ * Convert a Timestamp to the number of microseconds elapsed from the epoch.
+ *
+ * The result will be rounded down to the nearest microsecond. E.g., if the
+ * timestamp represents "1969-12-31T23:59:59.999999999Z", it will be rounded
+ * to -1 millisecond.
+ */
+ public static long toMicros(Timestamp timestamp) {
+ return timestamp.getSeconds() * MICROS_PER_SECOND + timestamp.getNanos()
+ / NANOS_PER_MICROSECOND;
+ }
+
+ /**
+ * Convert a Duration to the number of microseconds.The result will be
+ * rounded towards 0 to the nearest microseconds. E.g., if the duration
+ * represents -1 nanosecond, it will be rounded to 0.
+ */
+ public static long toMicros(Duration duration) {
+ return duration.getSeconds() * MICROS_PER_SECOND + duration.getNanos()
+ / NANOS_PER_MICROSECOND;
+ }
+
+ /**
+ * Create a Timestamp from the number of nanoseconds elapsed from the epoch.
+ */
+ public static Timestamp createTimestampFromNanos(long nanoseconds) {
+ return normalizedTimestamp(nanoseconds / NANOS_PER_SECOND,
+ (int) (nanoseconds % NANOS_PER_SECOND));
+ }
+
+ /**
+ * Create a Duration from the number of nanoseconds.
+ */
+ public static Duration createDurationFromNanos(long nanoseconds) {
+ return normalizedDuration(nanoseconds / NANOS_PER_SECOND,
+ (int) (nanoseconds % NANOS_PER_SECOND));
+ }
+
+ /**
+ * Convert a Timestamp to the number of nanoseconds elapsed from the epoch.
+ */
+ public static long toNanos(Timestamp timestamp) {
+ return timestamp.getSeconds() * NANOS_PER_SECOND + timestamp.getNanos();
+ }
+
+ /**
+ * Convert a Duration to the number of nanoseconds.
+ */
+ public static long toNanos(Duration duration) {
+ return duration.getSeconds() * NANOS_PER_SECOND + duration.getNanos();
+ }
+
+ /**
+ * Get the current time.
+ */
+ public static Timestamp getCurrentTime() {
+ return createTimestampFromMillis(System.currentTimeMillis());
+ }
+
+ /**
+ * Get the epoch.
+ */
+ public static Timestamp getEpoch() {
+ return Timestamp.getDefaultInstance();
+ }
+
+ /**
+ * Calculate the difference between two timestamps.
+ */
+ public static Duration distance(Timestamp from, Timestamp to) {
+ return normalizedDuration(to.getSeconds() - from.getSeconds(),
+ to.getNanos() - from.getNanos());
+ }
+
+ /**
+ * Add a duration to a timestamp.
+ */
+ public static Timestamp add(Timestamp start, Duration length) {
+ return normalizedTimestamp(start.getSeconds() + length.getSeconds(),
+ start.getNanos() + length.getNanos());
+ }
+
+ /**
+ * Subtract a duration from a timestamp.
+ */
+ public static Timestamp subtract(Timestamp start, Duration length) {
+ return normalizedTimestamp(start.getSeconds() - length.getSeconds(),
+ start.getNanos() - length.getNanos());
+ }
+
+ /**
+ * Add two durations.
+ */
+ public static Duration add(Duration d1, Duration d2) {
+ return normalizedDuration(d1.getSeconds() + d2.getSeconds(),
+ d1.getNanos() + d2.getNanos());
+ }
+
+ /**
+ * Subtract a duration from another.
+ */
+ public static Duration subtract(Duration d1, Duration d2) {
+ return normalizedDuration(d1.getSeconds() - d2.getSeconds(),
+ d1.getNanos() - d2.getNanos());
+ }
+
+ // Multiplications and divisions.
+
+ public static Duration multiply(Duration duration, double times) {
+ double result = duration.getSeconds() * times + duration.getNanos() * times
+ / 1000000000.0;
+ if (result < Long.MIN_VALUE || result > Long.MAX_VALUE) {
+ throw new IllegalArgumentException("Result is out of valid range.");
+ }
+ long seconds = (long) result;
+ int nanos = (int) ((result - seconds) * 1000000000);
+ return normalizedDuration(seconds, nanos);
+ }
+
+ public static Duration divide(Duration duration, double value) {
+ return multiply(duration, 1.0 / value);
+ }
+
+ public static Duration multiply(Duration duration, long times) {
+ return createDurationFromBigInteger(
+ toBigInteger(duration).multiply(toBigInteger(times)));
+ }
+
+ public static Duration divide(Duration duration, long times) {
+ return createDurationFromBigInteger(
+ toBigInteger(duration).divide(toBigInteger(times)));
+ }
+
+ public static long divide(Duration d1, Duration d2) {
+ return toBigInteger(d1).divide(toBigInteger(d2)).longValue();
+ }
+
+ public static Duration remainder(Duration d1, Duration d2) {
+ return createDurationFromBigInteger(
+ toBigInteger(d1).remainder(toBigInteger(d2)));
+ }
+
+ private static final BigInteger NANOS_PER_SECOND_BIG_INTEGER =
+ new BigInteger(String.valueOf(NANOS_PER_SECOND));
+
+ private static BigInteger toBigInteger(Duration duration) {
+ return toBigInteger(duration.getSeconds())
+ .multiply(NANOS_PER_SECOND_BIG_INTEGER)
+ .add(toBigInteger(duration.getNanos()));
+ }
+
+ private static BigInteger toBigInteger(long value) {
+ return new BigInteger(String.valueOf(value));
+ }
+
+ private static Duration createDurationFromBigInteger(BigInteger value) {
+ long seconds = value.divide(
+ new BigInteger(String.valueOf(NANOS_PER_SECOND))).longValue();
+ int nanos = value.remainder(
+ new BigInteger(String.valueOf(NANOS_PER_SECOND))).intValue();
+ return normalizedDuration(seconds, nanos);
+
+ }
+
+ private static Duration normalizedDuration(long seconds, int nanos) {
+ if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) {
+ seconds += nanos / NANOS_PER_SECOND;
+ nanos %= NANOS_PER_SECOND;
+ }
+ if (seconds > 0 && nanos < 0) {
+ nanos += NANOS_PER_SECOND;
+ seconds -= 1;
+ }
+ if (seconds < 0 && nanos > 0) {
+ nanos -= NANOS_PER_SECOND;
+ seconds += 1;
+ }
+ if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
+ throw new IllegalArgumentException("Duration is out of valid range.");
+ }
+ return Duration.newBuilder().setSeconds(seconds).setNanos(nanos).build();
+ }
+
+ private static Timestamp normalizedTimestamp(long seconds, int nanos) {
+ if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) {
+ seconds += nanos / NANOS_PER_SECOND;
+ nanos %= NANOS_PER_SECOND;
+ }
+ if (nanos < 0) {
+ nanos += NANOS_PER_SECOND;
+ seconds -= 1;
+ }
+ if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
+ throw new IllegalArgumentException("Timestamp is out of valid range.");
+ }
+ return Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build();
+ }
+
+ /**
+ * Format the nano part of a timestamp or a duration.
+ */
+ private static String formatNanos(int nanos) {
+ assert nanos >= 1 && nanos <= 999999999;
+ // Determine whether to use 3, 6, or 9 digits for the nano part.
+ if (nanos % NANOS_PER_MILLISECOND == 0) {
+ return String.format("%1$03d", nanos / NANOS_PER_MILLISECOND);
+ } else if (nanos % NANOS_PER_MICROSECOND == 0) {
+ return String.format("%1$06d", nanos / NANOS_PER_MICROSECOND);
+ } else {
+ return String.format("%1$09d", nanos);
+ }
+ }
+
+ private static int parseNanos(String value) throws ParseException {
+ int result = 0;
+ for (int i = 0; i < 9; ++i) {
+ result = result * 10;
+ if (i < value.length()) {
+ if (value.charAt(i) < '0' || value.charAt(i) > '9') {
+ throw new ParseException("Invalid nanosecnds.", 0);
+ }
+ result += value.charAt(i) - '0';
+ }
+ }
+ return result;
+ }
+
+ private static long parseTimezoneOffset(String value) throws ParseException {
+ int pos = value.indexOf(':');
+ if (pos == -1) {
+ throw new ParseException("Invalid offset value: " + value, 0);
+ }
+ String hours = value.substring(0, pos);
+ String minutes = value.substring(pos + 1);
+ return (Long.parseLong(hours) * 60 + Long.parseLong(minutes)) * 60;
+ }
+}
diff --git a/java/util/src/test/java/com/google/protobuf/util/FieldMaskTreeTest.java b/java/util/src/test/java/com/google/protobuf/util/FieldMaskTreeTest.java
new file mode 100644
index 0000000000..3391f239fe
--- /dev/null
+++ b/java/util/src/test/java/com/google/protobuf/util/FieldMaskTreeTest.java
@@ -0,0 +1,229 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package com.google.protobuf.util;
+
+import protobuf_unittest.UnittestProto.NestedTestAllTypes;
+import protobuf_unittest.UnittestProto.TestAllTypes;
+import protobuf_unittest.UnittestProto.TestAllTypes.NestedMessage;
+
+import junit.framework.TestCase;
+
+public class FieldMaskTreeTest extends TestCase {
+ public void testAddFieldPath() throws Exception {
+ FieldMaskTree tree = new FieldMaskTree();
+ assertEquals("", tree.toString());
+ tree.addFieldPath("");
+ assertEquals("", tree.toString());
+ // New branch.
+ tree.addFieldPath("foo");
+ assertEquals("foo", tree.toString());
+ // Redundant path.
+ tree.addFieldPath("foo");
+ assertEquals("foo", tree.toString());
+ // New branch.
+ tree.addFieldPath("bar.baz");
+ assertEquals("bar.baz,foo", tree.toString());
+ // Redundant sub-path.
+ tree.addFieldPath("foo.bar");
+ assertEquals("bar.baz,foo", tree.toString());
+ // New branch from a non-root node.
+ tree.addFieldPath("bar.quz");
+ assertEquals("bar.baz,bar.quz,foo", tree.toString());
+ // A path that matches several existing sub-paths.
+ tree.addFieldPath("bar");
+ assertEquals("bar,foo", tree.toString());
+ }
+
+ public void testMergeFromFieldMask() throws Exception {
+ FieldMaskTree tree = new FieldMaskTree(
+ FieldMaskUtil.fromString("foo,bar.baz,bar.quz"));
+ assertEquals("bar.baz,bar.quz,foo", tree.toString());
+ tree.mergeFromFieldMask(
+ FieldMaskUtil.fromString("foo.bar,bar"));
+ assertEquals("bar,foo", tree.toString());
+ }
+
+ public void testIntersectFieldPath() throws Exception {
+ FieldMaskTree tree = new FieldMaskTree(
+ FieldMaskUtil.fromString("foo,bar.baz,bar.quz"));
+ FieldMaskTree result = new FieldMaskTree();
+ // Empty path.
+ tree.intersectFieldPath("", result);
+ assertEquals("", result.toString());
+ // Non-exist path.
+ tree.intersectFieldPath("quz", result);
+ assertEquals("", result.toString());
+ // Sub-path of an existing leaf.
+ tree.intersectFieldPath("foo.bar", result);
+ assertEquals("foo.bar", result.toString());
+ // Match an existing leaf node.
+ tree.intersectFieldPath("foo", result);
+ assertEquals("foo", result.toString());
+ // Non-exist path.
+ tree.intersectFieldPath("bar.foo", result);
+ assertEquals("foo", result.toString());
+ // Match a non-leaf node.
+ tree.intersectFieldPath("bar", result);
+ assertEquals("bar.baz,bar.quz,foo", result.toString());
+ }
+
+ public void testMerge() throws Exception {
+ TestAllTypes value = TestAllTypes.newBuilder()
+ .setOptionalInt32(1234)
+ .setOptionalNestedMessage(NestedMessage.newBuilder().setBb(5678))
+ .addRepeatedInt32(4321)
+ .addRepeatedNestedMessage(NestedMessage.newBuilder().setBb(8765))
+ .build();
+ NestedTestAllTypes source = NestedTestAllTypes.newBuilder()
+ .setPayload(value)
+ .setChild(NestedTestAllTypes.newBuilder().setPayload(value))
+ .build();
+ // Now we have a message source with the following structure:
+ // [root] -+- payload -+- optional_int32
+ // | +- optional_nested_message
+ // | +- repeated_int32
+ // | +- repeated_nested_message
+ // |
+ // +- child --- payload -+- optional_int32
+ // +- optional_nested_message
+ // +- repeated_int32
+ // +- repeated_nested_message
+
+ FieldMaskUtil.MergeOptions options = new FieldMaskUtil.MergeOptions();
+
+ // Test merging each individual field.
+ NestedTestAllTypes.Builder builder = NestedTestAllTypes.newBuilder();
+ new FieldMaskTree().addFieldPath("payload.optional_int32")
+ .merge(source, builder, options);
+ NestedTestAllTypes.Builder expected = NestedTestAllTypes.newBuilder();
+ expected.getPayloadBuilder().setOptionalInt32(1234);
+ assertEquals(expected.build(), builder.build());
+
+ builder = NestedTestAllTypes.newBuilder();
+ new FieldMaskTree().addFieldPath("payload.optional_nested_message")
+ .merge(source, builder, options);
+ expected = NestedTestAllTypes.newBuilder();
+ expected.getPayloadBuilder().setOptionalNestedMessage(
+ NestedMessage.newBuilder().setBb(5678));
+ assertEquals(expected.build(), builder.build());
+
+
+ builder = NestedTestAllTypes.newBuilder();
+ new FieldMaskTree().addFieldPath("payload.repeated_int32")
+ .merge(source, builder, options);
+ expected = NestedTestAllTypes.newBuilder();
+ expected.getPayloadBuilder().addRepeatedInt32(4321);
+ assertEquals(expected.build(), builder.build());
+
+ builder = NestedTestAllTypes.newBuilder();
+ new FieldMaskTree().addFieldPath("payload.repeated_nested_message")
+ .merge(source, builder, options);
+ expected = NestedTestAllTypes.newBuilder();
+ expected.getPayloadBuilder().addRepeatedNestedMessage(
+ NestedMessage.newBuilder().setBb(8765));
+ assertEquals(expected.build(), builder.build());
+
+ builder = NestedTestAllTypes.newBuilder();
+ new FieldMaskTree().addFieldPath("child.payload.optional_int32")
+ .merge(source, builder, options);
+ expected = NestedTestAllTypes.newBuilder();
+ expected.getChildBuilder().getPayloadBuilder().setOptionalInt32(1234);
+ assertEquals(expected.build(), builder.build());
+
+ builder = NestedTestAllTypes.newBuilder();
+ new FieldMaskTree().addFieldPath("child.payload.optional_nested_message")
+ .merge(source, builder, options);
+ expected = NestedTestAllTypes.newBuilder();
+ expected.getChildBuilder().getPayloadBuilder().setOptionalNestedMessage(
+ NestedMessage.newBuilder().setBb(5678));
+ assertEquals(expected.build(), builder.build());
+
+
+ builder = NestedTestAllTypes.newBuilder();
+ new FieldMaskTree().addFieldPath("child.payload.repeated_int32")
+ .merge(source, builder, options);
+ expected = NestedTestAllTypes.newBuilder();
+ expected.getChildBuilder().getPayloadBuilder().addRepeatedInt32(4321);
+ assertEquals(expected.build(), builder.build());
+
+
+ builder = NestedTestAllTypes.newBuilder();
+ new FieldMaskTree().addFieldPath("child.payload.repeated_nested_message")
+ .merge(source, builder, options);
+ expected = NestedTestAllTypes.newBuilder();
+ expected.getChildBuilder().getPayloadBuilder().addRepeatedNestedMessage(
+ NestedMessage.newBuilder().setBb(8765));
+ assertEquals(expected.build(), builder.build());
+
+ // Test merging all fields.
+ builder = NestedTestAllTypes.newBuilder();
+ new FieldMaskTree().addFieldPath("child").addFieldPath("payload")
+ .merge(source, builder, options);
+ assertEquals(source, builder.build());
+
+ // Test repeated options.
+ builder = NestedTestAllTypes.newBuilder();
+ builder.getPayloadBuilder().addRepeatedInt32(1000);
+ new FieldMaskTree().addFieldPath("payload.repeated_int32")
+ .merge(source, builder, options);
+ // Default behavior is to append repeated fields.
+ assertEquals(2, builder.getPayload().getRepeatedInt32Count());
+ assertEquals(1000, builder.getPayload().getRepeatedInt32(0));
+ assertEquals(4321, builder.getPayload().getRepeatedInt32(1));
+ // Change to replace repeated fields.
+ options.setReplaceRepeatedFields(true);
+ new FieldMaskTree().addFieldPath("payload.repeated_int32")
+ .merge(source, builder, options);
+ assertEquals(1, builder.getPayload().getRepeatedInt32Count());
+ assertEquals(4321, builder.getPayload().getRepeatedInt32(0));
+
+ // Test message options.
+ builder = NestedTestAllTypes.newBuilder();
+ builder.getPayloadBuilder().setOptionalInt32(1000);
+ builder.getPayloadBuilder().setOptionalUint32(2000);
+ new FieldMaskTree().addFieldPath("payload")
+ .merge(source, builder, options);
+ // Default behavior is to merge message fields.
+ assertEquals(1234, builder.getPayload().getOptionalInt32());
+ assertEquals(2000, builder.getPayload().getOptionalUint32());
+
+ // Change to replace message fields.
+ options.setReplaceMessageFields(true);
+ builder = NestedTestAllTypes.newBuilder();
+ builder.getPayloadBuilder().setOptionalInt32(1000);
+ builder.getPayloadBuilder().setOptionalUint32(2000);
+ new FieldMaskTree().addFieldPath("payload")
+ .merge(source, builder, options);
+ assertEquals(1234, builder.getPayload().getOptionalInt32());
+ assertEquals(0, builder.getPayload().getOptionalUint32());
+ }
+}
+
diff --git a/java/util/src/test/java/com/google/protobuf/util/FieldMaskUtilTest.java b/java/util/src/test/java/com/google/protobuf/util/FieldMaskUtilTest.java
new file mode 100644
index 0000000000..67fbe0b1ac
--- /dev/null
+++ b/java/util/src/test/java/com/google/protobuf/util/FieldMaskUtilTest.java
@@ -0,0 +1,135 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package com.google.protobuf.util;
+
+import com.google.protobuf.FieldMask;
+import protobuf_unittest.UnittestProto.NestedTestAllTypes;
+import protobuf_unittest.UnittestProto.TestAllTypes;
+
+import junit.framework.TestCase;
+
+/** Unit tests for {@link FieldMaskUtil}. */
+public class FieldMaskUtilTest extends TestCase {
+ public void testIsValid() throws Exception {
+ assertTrue(FieldMaskUtil.isValid(NestedTestAllTypes.class, "payload"));
+ assertFalse(FieldMaskUtil.isValid(NestedTestAllTypes.class, "nonexist"));
+ assertTrue(FieldMaskUtil.isValid(
+ NestedTestAllTypes.class, "payload.optional_int32"));
+ assertTrue(FieldMaskUtil.isValid(
+ NestedTestAllTypes.class, "payload.repeated_int32"));
+ assertTrue(FieldMaskUtil.isValid(
+ NestedTestAllTypes.class, "payload.optional_nested_message"));
+ assertTrue(FieldMaskUtil.isValid(
+ NestedTestAllTypes.class, "payload.repeated_nested_message"));
+ assertFalse(FieldMaskUtil.isValid(
+ NestedTestAllTypes.class, "payload.nonexist"));
+
+ assertTrue(FieldMaskUtil.isValid(
+ NestedTestAllTypes.class, "payload.optional_nested_message.bb"));
+ // Repeated fields cannot have sub-paths.
+ assertFalse(FieldMaskUtil.isValid(
+ NestedTestAllTypes.class, "payload.repeated_nested_message.bb"));
+ // Non-message fields cannot have sub-paths.
+ assertFalse(FieldMaskUtil.isValid(
+ NestedTestAllTypes.class, "payload.optional_int32.bb"));
+ }
+
+ public void testToString() throws Exception {
+ assertEquals("", FieldMaskUtil.toString(FieldMask.getDefaultInstance()));
+ FieldMask mask = FieldMask.newBuilder().addPaths("foo").build();
+ assertEquals("foo", FieldMaskUtil.toString(mask));
+ mask = FieldMask.newBuilder().addPaths("foo").addPaths("bar").build();
+ assertEquals("foo,bar", FieldMaskUtil.toString(mask));
+
+ // Empty field paths are ignored.
+ mask = FieldMask.newBuilder().addPaths("").addPaths("foo").addPaths("").
+ addPaths("bar").addPaths("").build();
+ assertEquals("foo,bar", FieldMaskUtil.toString(mask));
+ }
+
+ public void testFromString() throws Exception {
+ FieldMask mask = FieldMaskUtil.fromString("");
+ assertEquals(0, mask.getPathsCount());
+ mask = FieldMaskUtil.fromString("foo");
+ assertEquals(1, mask.getPathsCount());
+ assertEquals("foo", mask.getPaths(0));
+ mask = FieldMaskUtil.fromString("foo,bar.baz");
+ assertEquals(2, mask.getPathsCount());
+ assertEquals("foo", mask.getPaths(0));
+ assertEquals("bar.baz", mask.getPaths(1));
+
+ // Empty field paths are ignore.
+ mask = FieldMaskUtil.fromString(",foo,,bar,");
+ assertEquals(2, mask.getPathsCount());
+ assertEquals("foo", mask.getPaths(0));
+ assertEquals("bar", mask.getPaths(1));
+
+ // Check whether the field paths are valid if a class parameter is provided.
+ mask = FieldMaskUtil.fromString(NestedTestAllTypes.class, ",payload");
+
+ try {
+ mask = FieldMaskUtil.fromString(
+ NestedTestAllTypes.class, "payload,nonexist");
+ fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testUnion() throws Exception {
+ // Only test a simple case here and expect
+ // {@link FieldMaskTreeTest#testAddFieldPath} to cover all scenarios.
+ FieldMask mask1 = FieldMaskUtil.fromString("foo,bar.baz,bar.quz");
+ FieldMask mask2 = FieldMaskUtil.fromString("foo.bar,bar");
+ FieldMask result = FieldMaskUtil.union(mask1, mask2);
+ assertEquals("bar,foo", FieldMaskUtil.toString(result));
+ }
+
+ public void testIntersection() throws Exception {
+ // Only test a simple case here and expect
+ // {@link FieldMaskTreeTest#testIntersectFieldPath} to cover all scenarios.
+ FieldMask mask1 = FieldMaskUtil.fromString("foo,bar.baz,bar.quz");
+ FieldMask mask2 = FieldMaskUtil.fromString("foo.bar,bar");
+ FieldMask result = FieldMaskUtil.intersection(mask1, mask2);
+ assertEquals("bar.baz,bar.quz,foo.bar", FieldMaskUtil.toString(result));
+ }
+
+ public void testMerge() throws Exception {
+ // Only test a simple case here and expect
+ // {@link FieldMaskTreeTest#testMerge} to cover all scenarios.
+ NestedTestAllTypes source = NestedTestAllTypes.newBuilder()
+ .setPayload(TestAllTypes.newBuilder().setOptionalInt32(1234))
+ .build();
+ NestedTestAllTypes.Builder builder = NestedTestAllTypes.newBuilder();
+ FieldMaskUtil.merge(FieldMaskUtil.fromString("payload"), source, builder);
+ assertEquals(1234, builder.getPayload().getOptionalInt32());
+ }
+}
diff --git a/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java b/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java
new file mode 100644
index 0000000000..ddf5ad2a4f
--- /dev/null
+++ b/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java
@@ -0,0 +1,976 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package com.google.protobuf.util;
+
+import com.google.protobuf.Any;
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.BytesValue;
+import com.google.protobuf.DoubleValue;
+import com.google.protobuf.FloatValue;
+import com.google.protobuf.Int32Value;
+import com.google.protobuf.Int64Value;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Message;
+import com.google.protobuf.StringValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.UInt32Value;
+import com.google.protobuf.UInt64Value;
+import com.google.protobuf.Value;
+import com.google.protobuf.util.JsonFormat.TypeRegistry;
+import com.google.protobuf.util.JsonTestProto.TestAllTypes;
+import com.google.protobuf.util.JsonTestProto.TestAllTypes.NestedEnum;
+import com.google.protobuf.util.JsonTestProto.TestAllTypes.NestedMessage;
+import com.google.protobuf.util.JsonTestProto.TestAny;
+import com.google.protobuf.util.JsonTestProto.TestDuration;
+import com.google.protobuf.util.JsonTestProto.TestFieldMask;
+import com.google.protobuf.util.JsonTestProto.TestMap;
+import com.google.protobuf.util.JsonTestProto.TestStruct;
+import com.google.protobuf.util.JsonTestProto.TestTimestamp;
+import com.google.protobuf.util.JsonTestProto.TestWrappers;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public class JsonFormatTest extends TestCase {
+ private void setAllFields(TestAllTypes.Builder builder) {
+ builder.setOptionalInt32(1234);
+ builder.setOptionalInt64(1234567890123456789L);
+ builder.setOptionalUint32(5678);
+ builder.setOptionalUint64(2345678901234567890L);
+ builder.setOptionalSint32(9012);
+ builder.setOptionalSint64(3456789012345678901L);
+ builder.setOptionalFixed32(3456);
+ builder.setOptionalFixed64(4567890123456789012L);
+ builder.setOptionalSfixed32(7890);
+ builder.setOptionalSfixed64(5678901234567890123L);
+ builder.setOptionalFloat(1.5f);
+ builder.setOptionalDouble(1.25);
+ builder.setOptionalBool(true);
+ builder.setOptionalString("Hello world!");
+ builder.setOptionalBytes(ByteString.copyFrom(new byte[]{0, 1, 2}));
+ builder.setOptionalNestedEnum(NestedEnum.BAR);
+ builder.getOptionalNestedMessageBuilder().setValue(100);
+
+ builder.addRepeatedInt32(1234);
+ builder.addRepeatedInt64(1234567890123456789L);
+ builder.addRepeatedUint32(5678);
+ builder.addRepeatedUint64(2345678901234567890L);
+ builder.addRepeatedSint32(9012);
+ builder.addRepeatedSint64(3456789012345678901L);
+ builder.addRepeatedFixed32(3456);
+ builder.addRepeatedFixed64(4567890123456789012L);
+ builder.addRepeatedSfixed32(7890);
+ builder.addRepeatedSfixed64(5678901234567890123L);
+ builder.addRepeatedFloat(1.5f);
+ builder.addRepeatedDouble(1.25);
+ builder.addRepeatedBool(true);
+ builder.addRepeatedString("Hello world!");
+ builder.addRepeatedBytes(ByteString.copyFrom(new byte[]{0, 1, 2}));
+ builder.addRepeatedNestedEnum(NestedEnum.BAR);
+ builder.addRepeatedNestedMessageBuilder().setValue(100);
+
+ builder.addRepeatedInt32(234);
+ builder.addRepeatedInt64(234567890123456789L);
+ builder.addRepeatedUint32(678);
+ builder.addRepeatedUint64(345678901234567890L);
+ builder.addRepeatedSint32(012);
+ builder.addRepeatedSint64(456789012345678901L);
+ builder.addRepeatedFixed32(456);
+ builder.addRepeatedFixed64(567890123456789012L);
+ builder.addRepeatedSfixed32(890);
+ builder.addRepeatedSfixed64(678901234567890123L);
+ builder.addRepeatedFloat(11.5f);
+ builder.addRepeatedDouble(11.25);
+ builder.addRepeatedBool(true);
+ builder.addRepeatedString("ello world!");
+ builder.addRepeatedBytes(ByteString.copyFrom(new byte[]{1, 2}));
+ builder.addRepeatedNestedEnum(NestedEnum.BAZ);
+ builder.addRepeatedNestedMessageBuilder().setValue(200);
+ }
+
+ private void assertRoundTripEquals(Message message) throws Exception {
+ assertRoundTripEquals(message, TypeRegistry.getEmptyTypeRegistry());
+ }
+
+ private void assertRoundTripEquals(Message message, TypeRegistry registry) throws Exception {
+ JsonFormat.Printer printer = JsonFormat.printer().usingTypeRegistry(registry);
+ JsonFormat.Parser parser = JsonFormat.parser().usingTypeRegistry(registry);
+ Message.Builder builder = message.newBuilderForType();
+ parser.merge(printer.print(message), builder);
+ Message parsedMessage = builder.build();
+ assertEquals(message.toString(), parsedMessage.toString());
+ }
+
+ private String toJsonString(Message message) throws IOException {
+ return JsonFormat.printer().print(message);
+ }
+
+ private void mergeFromJson(String json, Message.Builder builder) throws IOException {
+ JsonFormat.parser().merge(json, builder);
+ }
+
+ public void testAllFields() throws Exception {
+ TestAllTypes.Builder builder = TestAllTypes.newBuilder();
+ setAllFields(builder);
+ TestAllTypes message = builder.build();
+
+ assertEquals(
+ "{\n"
+ + " \"optionalInt32\": 1234,\n"
+ + " \"optionalInt64\": \"1234567890123456789\",\n"
+ + " \"optionalUint32\": 5678,\n"
+ + " \"optionalUint64\": \"2345678901234567890\",\n"
+ + " \"optionalSint32\": 9012,\n"
+ + " \"optionalSint64\": \"3456789012345678901\",\n"
+ + " \"optionalFixed32\": 3456,\n"
+ + " \"optionalFixed64\": \"4567890123456789012\",\n"
+ + " \"optionalSfixed32\": 7890,\n"
+ + " \"optionalSfixed64\": \"5678901234567890123\",\n"
+ + " \"optionalFloat\": 1.5,\n"
+ + " \"optionalDouble\": 1.25,\n"
+ + " \"optionalBool\": true,\n"
+ + " \"optionalString\": \"Hello world!\",\n"
+ + " \"optionalBytes\": \"AAEC\",\n"
+ + " \"optionalNestedMessage\": {\n"
+ + " \"value\": 100\n"
+ + " },\n"
+ + " \"optionalNestedEnum\": \"BAR\",\n"
+ + " \"repeatedInt32\": [1234, 234],\n"
+ + " \"repeatedInt64\": [\"1234567890123456789\", \"234567890123456789\"],\n"
+ + " \"repeatedUint32\": [5678, 678],\n"
+ + " \"repeatedUint64\": [\"2345678901234567890\", \"345678901234567890\"],\n"
+ + " \"repeatedSint32\": [9012, 10],\n"
+ + " \"repeatedSint64\": [\"3456789012345678901\", \"456789012345678901\"],\n"
+ + " \"repeatedFixed32\": [3456, 456],\n"
+ + " \"repeatedFixed64\": [\"4567890123456789012\", \"567890123456789012\"],\n"
+ + " \"repeatedSfixed32\": [7890, 890],\n"
+ + " \"repeatedSfixed64\": [\"5678901234567890123\", \"678901234567890123\"],\n"
+ + " \"repeatedFloat\": [1.5, 11.5],\n"
+ + " \"repeatedDouble\": [1.25, 11.25],\n"
+ + " \"repeatedBool\": [true, true],\n"
+ + " \"repeatedString\": [\"Hello world!\", \"ello world!\"],\n"
+ + " \"repeatedBytes\": [\"AAEC\", \"AQI=\"],\n"
+ + " \"repeatedNestedMessage\": [{\n"
+ + " \"value\": 100\n"
+ + " }, {\n"
+ + " \"value\": 200\n"
+ + " }],\n"
+ + " \"repeatedNestedEnum\": [\"BAR\", \"BAZ\"]\n"
+ + "}",
+ toJsonString(message));
+
+ assertRoundTripEquals(message);
+ }
+
+ public void testUnknownEnumValues() throws Exception {
+ // Unknown enum values will be dropped.
+ // TODO(xiaofeng): We may want to revisit this (whether we should omit
+ // unknown enum values).
+ TestAllTypes message = TestAllTypes.newBuilder()
+ .setOptionalNestedEnumValue(12345)
+ .addRepeatedNestedEnumValue(12345)
+ .addRepeatedNestedEnumValue(0)
+ .build();
+ assertEquals(
+ "{\n"
+ + " \"repeatedNestedEnum\": [\"FOO\"]\n"
+ + "}", toJsonString(message));
+
+ TestMap.Builder mapBuilder = TestMap.newBuilder();
+ mapBuilder.getMutableInt32ToEnumMapValue().put(1, 0);
+ mapBuilder.getMutableInt32ToEnumMapValue().put(2, 12345);
+ TestMap mapMessage = mapBuilder.build();
+ assertEquals(
+ "{\n"
+ + " \"int32ToEnumMap\": {\n"
+ + " \"1\": \"FOO\"\n"
+ + " }\n"
+ + "}", toJsonString(mapMessage));
+ }
+
+ public void testSpecialFloatValues() throws Exception {
+ TestAllTypes message = TestAllTypes.newBuilder()
+ .addRepeatedFloat(Float.NaN)
+ .addRepeatedFloat(Float.POSITIVE_INFINITY)
+ .addRepeatedFloat(Float.NEGATIVE_INFINITY)
+ .addRepeatedDouble(Double.NaN)
+ .addRepeatedDouble(Double.POSITIVE_INFINITY)
+ .addRepeatedDouble(Double.NEGATIVE_INFINITY)
+ .build();
+ assertEquals(
+ "{\n"
+ + " \"repeatedFloat\": [\"NaN\", \"Infinity\", \"-Infinity\"],\n"
+ + " \"repeatedDouble\": [\"NaN\", \"Infinity\", \"-Infinity\"]\n"
+ + "}", toJsonString(message));
+
+ assertRoundTripEquals(message);
+ }
+
+ public void testParserAcceptStringForNumbericField() throws Exception {
+ TestAllTypes.Builder builder = TestAllTypes.newBuilder();
+ mergeFromJson(
+ "{\n"
+ + " \"optionalInt32\": \"1234\",\n"
+ + " \"optionalUint32\": \"5678\",\n"
+ + " \"optionalSint32\": \"9012\",\n"
+ + " \"optionalFixed32\": \"3456\",\n"
+ + " \"optionalSfixed32\": \"7890\",\n"
+ + " \"optionalFloat\": \"1.5\",\n"
+ + " \"optionalDouble\": \"1.25\",\n"
+ + " \"optionalBool\": \"true\"\n"
+ + "}", builder);
+ TestAllTypes message = builder.build();
+ assertEquals(1234, message.getOptionalInt32());
+ assertEquals(5678, message.getOptionalUint32());
+ assertEquals(9012, message.getOptionalSint32());
+ assertEquals(3456, message.getOptionalFixed32());
+ assertEquals(7890, message.getOptionalSfixed32());
+ assertEquals(1.5f, message.getOptionalFloat());
+ assertEquals(1.25, message.getOptionalDouble());
+ assertEquals(true, message.getOptionalBool());
+ }
+
+ private void assertRejects(String name, String value) {
+ TestAllTypes.Builder builder = TestAllTypes.newBuilder();
+ try {
+ // Numeric form is rejected.
+ mergeFromJson("{\"" + name + "\":" + value + "}", builder);
+ fail("Exception is expected.");
+ } catch (IOException e) {
+ // Expected.
+ }
+ try {
+ // String form is also rejected.
+ mergeFromJson("{\"" + name + "\":\"" + value + "\"}", builder);
+ fail("Exception is expected.");
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
+
+ private void assertAccepts(String name, String value) throws IOException {
+ TestAllTypes.Builder builder = TestAllTypes.newBuilder();
+ // Both numeric form and string form are accepted.
+ mergeFromJson("{\"" + name + "\":" + value + "}", builder);
+ mergeFromJson("{\"" + name + "\":\"" + value + "\"}", builder);
+ }
+
+ public void testParserRejectOutOfRangeNumericValues() throws Exception {
+ assertAccepts("optionalInt32", String.valueOf(Integer.MAX_VALUE));
+ assertAccepts("optionalInt32", String.valueOf(Integer.MIN_VALUE));
+ assertRejects("optionalInt32", String.valueOf(Integer.MAX_VALUE + 1L));
+ assertRejects("optionalInt32", String.valueOf(Integer.MIN_VALUE - 1L));
+
+ assertAccepts("optionalUint32", String.valueOf(Integer.MAX_VALUE + 1L));
+ assertRejects("optionalUint32", "123456789012345");
+ assertRejects("optionalUint32", "-1");
+
+ BigInteger one = new BigInteger("1");
+ BigInteger maxLong = new BigInteger(String.valueOf(Long.MAX_VALUE));
+ BigInteger minLong = new BigInteger(String.valueOf(Long.MIN_VALUE));
+ assertAccepts("optionalInt64", maxLong.toString());
+ assertAccepts("optionalInt64", minLong.toString());
+ assertRejects("optionalInt64", maxLong.add(one).toString());
+ assertRejects("optionalInt64", minLong.subtract(one).toString());
+
+ assertAccepts("optionalUint64", maxLong.add(one).toString());
+ assertRejects("optionalUint64", "1234567890123456789012345");
+ assertRejects("optionalUint64", "-1");
+
+ assertAccepts("optionalBool", "true");
+ assertRejects("optionalBool", "1");
+ assertRejects("optionalBool", "0");
+
+ assertAccepts("optionalFloat", String.valueOf(Float.MAX_VALUE));
+ assertAccepts("optionalFloat", String.valueOf(-Float.MAX_VALUE));
+ assertRejects("optionalFloat", String.valueOf(Double.MAX_VALUE));
+ assertRejects("optionalFloat", String.valueOf(-Double.MAX_VALUE));
+
+ BigDecimal moreThanOne = new BigDecimal("1.000001");
+ BigDecimal maxDouble = new BigDecimal(Double.MAX_VALUE);
+ BigDecimal minDouble = new BigDecimal(-Double.MAX_VALUE);
+ assertAccepts("optionalDouble", maxDouble.toString());
+ assertAccepts("optionalDouble", minDouble.toString());
+ assertRejects("optionalDouble", maxDouble.multiply(moreThanOne).toString());
+ assertRejects("optionalDouble", minDouble.multiply(moreThanOne).toString());
+ }
+
+ public void testParserAcceptNull() throws Exception {
+ TestAllTypes.Builder builder = TestAllTypes.newBuilder();
+ mergeFromJson(
+ "{\n"
+ + " \"optionalInt32\": null,\n"
+ + " \"optionalInt64\": null,\n"
+ + " \"optionalUint32\": null,\n"
+ + " \"optionalUint64\": null,\n"
+ + " \"optionalSint32\": null,\n"
+ + " \"optionalSint64\": null,\n"
+ + " \"optionalFixed32\": null,\n"
+ + " \"optionalFixed64\": null,\n"
+ + " \"optionalSfixed32\": null,\n"
+ + " \"optionalSfixed64\": null,\n"
+ + " \"optionalFloat\": null,\n"
+ + " \"optionalDouble\": null,\n"
+ + " \"optionalBool\": null,\n"
+ + " \"optionalString\": null,\n"
+ + " \"optionalBytes\": null,\n"
+ + " \"optionalNestedMessage\": null,\n"
+ + " \"optionalNestedEnum\": null,\n"
+ + " \"repeatedInt32\": null,\n"
+ + " \"repeatedInt64\": null,\n"
+ + " \"repeatedUint32\": null,\n"
+ + " \"repeatedUint64\": null,\n"
+ + " \"repeatedSint32\": null,\n"
+ + " \"repeatedSint64\": null,\n"
+ + " \"repeatedFixed32\": null,\n"
+ + " \"repeatedFixed64\": null,\n"
+ + " \"repeatedSfixed32\": null,\n"
+ + " \"repeatedSfixed64\": null,\n"
+ + " \"repeatedFloat\": null,\n"
+ + " \"repeatedDouble\": null,\n"
+ + " \"repeatedBool\": null,\n"
+ + " \"repeatedString\": null,\n"
+ + " \"repeatedBytes\": null,\n"
+ + " \"repeatedNestedMessage\": null,\n"
+ + " \"repeatedNestedEnum\": null\n"
+ + "}", builder);
+ TestAllTypes message = builder.build();
+ assertEquals(TestAllTypes.getDefaultInstance(), message);
+
+ // Repeated field elements can also be null.
+ builder = TestAllTypes.newBuilder();
+ mergeFromJson(
+ "{\n"
+ + " \"repeatedInt32\": [null, null],\n"
+ + " \"repeatedInt64\": [null, null],\n"
+ + " \"repeatedUint32\": [null, null],\n"
+ + " \"repeatedUint64\": [null, null],\n"
+ + " \"repeatedSint32\": [null, null],\n"
+ + " \"repeatedSint64\": [null, null],\n"
+ + " \"repeatedFixed32\": [null, null],\n"
+ + " \"repeatedFixed64\": [null, null],\n"
+ + " \"repeatedSfixed32\": [null, null],\n"
+ + " \"repeatedSfixed64\": [null, null],\n"
+ + " \"repeatedFloat\": [null, null],\n"
+ + " \"repeatedDouble\": [null, null],\n"
+ + " \"repeatedBool\": [null, null],\n"
+ + " \"repeatedString\": [null, null],\n"
+ + " \"repeatedBytes\": [null, null],\n"
+ + " \"repeatedNestedMessage\": [null, null],\n"
+ + " \"repeatedNestedEnum\": [null, null]\n"
+ + "}", builder);
+ message = builder.build();
+ // "null" elements will be parsed to default values.
+ assertEquals(2, message.getRepeatedInt32Count());
+ assertEquals(0, message.getRepeatedInt32(0));
+ assertEquals(0, message.getRepeatedInt32(1));
+ assertEquals(2, message.getRepeatedInt32Count());
+ assertEquals(0, message.getRepeatedInt32(0));
+ assertEquals(0, message.getRepeatedInt32(1));
+ assertEquals(2, message.getRepeatedInt64Count());
+ assertEquals(0, message.getRepeatedInt64(0));
+ assertEquals(0, message.getRepeatedInt64(1));
+ assertEquals(2, message.getRepeatedUint32Count());
+ assertEquals(0, message.getRepeatedUint32(0));
+ assertEquals(0, message.getRepeatedUint32(1));
+ assertEquals(2, message.getRepeatedUint64Count());
+ assertEquals(0, message.getRepeatedUint64(0));
+ assertEquals(0, message.getRepeatedUint64(1));
+ assertEquals(2, message.getRepeatedSint32Count());
+ assertEquals(0, message.getRepeatedSint32(0));
+ assertEquals(0, message.getRepeatedSint32(1));
+ assertEquals(2, message.getRepeatedSint64Count());
+ assertEquals(0, message.getRepeatedSint64(0));
+ assertEquals(0, message.getRepeatedSint64(1));
+ assertEquals(2, message.getRepeatedFixed32Count());
+ assertEquals(0, message.getRepeatedFixed32(0));
+ assertEquals(0, message.getRepeatedFixed32(1));
+ assertEquals(2, message.getRepeatedFixed64Count());
+ assertEquals(0, message.getRepeatedFixed64(0));
+ assertEquals(0, message.getRepeatedFixed64(1));
+ assertEquals(2, message.getRepeatedSfixed32Count());
+ assertEquals(0, message.getRepeatedSfixed32(0));
+ assertEquals(0, message.getRepeatedSfixed32(1));
+ assertEquals(2, message.getRepeatedSfixed64Count());
+ assertEquals(0, message.getRepeatedSfixed64(0));
+ assertEquals(0, message.getRepeatedSfixed64(1));
+ assertEquals(2, message.getRepeatedFloatCount());
+ assertEquals(0f, message.getRepeatedFloat(0));
+ assertEquals(0f, message.getRepeatedFloat(1));
+ assertEquals(2, message.getRepeatedDoubleCount());
+ assertEquals(0.0, message.getRepeatedDouble(0));
+ assertEquals(0.0, message.getRepeatedDouble(1));
+ assertEquals(2, message.getRepeatedBoolCount());
+ assertFalse(message.getRepeatedBool(0));
+ assertFalse(message.getRepeatedBool(1));
+ assertEquals(2, message.getRepeatedStringCount());
+ assertTrue(message.getRepeatedString(0).isEmpty());
+ assertTrue(message.getRepeatedString(1).isEmpty());
+ assertEquals(2, message.getRepeatedBytesCount());
+ assertTrue(message.getRepeatedBytes(0).isEmpty());
+ assertTrue(message.getRepeatedBytes(1).isEmpty());
+ assertEquals(2, message.getRepeatedNestedMessageCount());
+ assertEquals(NestedMessage.getDefaultInstance(), message.getRepeatedNestedMessage(0));
+ assertEquals(NestedMessage.getDefaultInstance(), message.getRepeatedNestedMessage(1));
+ assertEquals(2, message.getRepeatedNestedEnumCount());
+ assertEquals(0, message.getRepeatedNestedEnumValue(0));
+ assertEquals(0, message.getRepeatedNestedEnumValue(1));
+ }
+
+ public void testMapFields() throws Exception {
+ TestMap.Builder builder = TestMap.newBuilder();
+ builder.getMutableInt32ToInt32Map().put(1, 10);
+ builder.getMutableInt64ToInt32Map().put(1234567890123456789L, 10);
+ builder.getMutableUint32ToInt32Map().put(2, 20);
+ builder.getMutableUint64ToInt32Map().put(2234567890123456789L, 20);
+ builder.getMutableSint32ToInt32Map().put(3, 30);
+ builder.getMutableSint64ToInt32Map().put(3234567890123456789L, 30);
+ builder.getMutableFixed32ToInt32Map().put(4, 40);
+ builder.getMutableFixed64ToInt32Map().put(4234567890123456789L, 40);
+ builder.getMutableSfixed32ToInt32Map().put(5, 50);
+ builder.getMutableSfixed64ToInt32Map().put(5234567890123456789L, 50);
+ builder.getMutableBoolToInt32Map().put(false, 6);
+ builder.getMutableStringToInt32Map().put("Hello", 10);
+
+ builder.getMutableInt32ToInt64Map().put(1, 1234567890123456789L);
+ builder.getMutableInt32ToUint32Map().put(2, 20);
+ builder.getMutableInt32ToUint64Map().put(2, 2234567890123456789L);
+ builder.getMutableInt32ToSint32Map().put(3, 30);
+ builder.getMutableInt32ToSint64Map().put(3, 3234567890123456789L);
+ builder.getMutableInt32ToFixed32Map().put(4, 40);
+ builder.getMutableInt32ToFixed64Map().put(4, 4234567890123456789L);
+ builder.getMutableInt32ToSfixed32Map().put(5, 50);
+ builder.getMutableInt32ToSfixed64Map().put(5, 5234567890123456789L);
+ builder.getMutableInt32ToFloatMap().put(6, 1.5f);
+ builder.getMutableInt32ToDoubleMap().put(6, 1.25);
+ builder.getMutableInt32ToBoolMap().put(7, false);
+ builder.getMutableInt32ToStringMap().put(7, "World");
+ builder.getMutableInt32ToBytesMap().put(
+ 8, ByteString.copyFrom(new byte[]{1, 2, 3}));
+ builder.getMutableInt32ToMessageMap().put(
+ 8, NestedMessage.newBuilder().setValue(1234).build());
+ builder.getMutableInt32ToEnumMap().put(9, NestedEnum.BAR);
+ TestMap message = builder.build();
+
+ assertEquals(
+ "{\n"
+ + " \"int32ToInt32Map\": {\n"
+ + " \"1\": 10\n"
+ + " },\n"
+ + " \"int64ToInt32Map\": {\n"
+ + " \"1234567890123456789\": 10\n"
+ + " },\n"
+ + " \"uint32ToInt32Map\": {\n"
+ + " \"2\": 20\n"
+ + " },\n"
+ + " \"uint64ToInt32Map\": {\n"
+ + " \"2234567890123456789\": 20\n"
+ + " },\n"
+ + " \"sint32ToInt32Map\": {\n"
+ + " \"3\": 30\n"
+ + " },\n"
+ + " \"sint64ToInt32Map\": {\n"
+ + " \"3234567890123456789\": 30\n"
+ + " },\n"
+ + " \"fixed32ToInt32Map\": {\n"
+ + " \"4\": 40\n"
+ + " },\n"
+ + " \"fixed64ToInt32Map\": {\n"
+ + " \"4234567890123456789\": 40\n"
+ + " },\n"
+ + " \"sfixed32ToInt32Map\": {\n"
+ + " \"5\": 50\n"
+ + " },\n"
+ + " \"sfixed64ToInt32Map\": {\n"
+ + " \"5234567890123456789\": 50\n"
+ + " },\n"
+ + " \"boolToInt32Map\": {\n"
+ + " \"false\": 6\n"
+ + " },\n"
+ + " \"stringToInt32Map\": {\n"
+ + " \"Hello\": 10\n"
+ + " },\n"
+ + " \"int32ToInt64Map\": {\n"
+ + " \"1\": \"1234567890123456789\"\n"
+ + " },\n"
+ + " \"int32ToUint32Map\": {\n"
+ + " \"2\": 20\n"
+ + " },\n"
+ + " \"int32ToUint64Map\": {\n"
+ + " \"2\": \"2234567890123456789\"\n"
+ + " },\n"
+ + " \"int32ToSint32Map\": {\n"
+ + " \"3\": 30\n"
+ + " },\n"
+ + " \"int32ToSint64Map\": {\n"
+ + " \"3\": \"3234567890123456789\"\n"
+ + " },\n"
+ + " \"int32ToFixed32Map\": {\n"
+ + " \"4\": 40\n"
+ + " },\n"
+ + " \"int32ToFixed64Map\": {\n"
+ + " \"4\": \"4234567890123456789\"\n"
+ + " },\n"
+ + " \"int32ToSfixed32Map\": {\n"
+ + " \"5\": 50\n"
+ + " },\n"
+ + " \"int32ToSfixed64Map\": {\n"
+ + " \"5\": \"5234567890123456789\"\n"
+ + " },\n"
+ + " \"int32ToFloatMap\": {\n"
+ + " \"6\": 1.5\n"
+ + " },\n"
+ + " \"int32ToDoubleMap\": {\n"
+ + " \"6\": 1.25\n"
+ + " },\n"
+ + " \"int32ToBoolMap\": {\n"
+ + " \"7\": false\n"
+ + " },\n"
+ + " \"int32ToStringMap\": {\n"
+ + " \"7\": \"World\"\n"
+ + " },\n"
+ + " \"int32ToBytesMap\": {\n"
+ + " \"8\": \"AQID\"\n"
+ + " },\n"
+ + " \"int32ToMessageMap\": {\n"
+ + " \"8\": {\n"
+ + " \"value\": 1234\n"
+ + " }\n"
+ + " },\n"
+ + " \"int32ToEnumMap\": {\n"
+ + " \"9\": \"BAR\"\n"
+ + " }\n"
+ + "}", toJsonString(message));
+ assertRoundTripEquals(message);
+
+ // Test multiple entries.
+ builder = TestMap.newBuilder();
+ builder.getMutableInt32ToInt32Map().put(1, 2);
+ builder.getMutableInt32ToInt32Map().put(3, 4);
+ message = builder.build();
+
+ assertEquals(
+ "{\n"
+ + " \"int32ToInt32Map\": {\n"
+ + " \"1\": 2,\n"
+ + " \"3\": 4\n"
+ + " }\n"
+ + "}", toJsonString(message));
+ assertRoundTripEquals(message);
+ }
+
+ public void testMapNullValueIsDefault() throws Exception {
+ TestMap.Builder builder = TestMap.newBuilder();
+ mergeFromJson(
+ "{\n"
+ + " \"int32ToInt32Map\": {\"1\": null},\n"
+ + " \"int32ToMessageMap\": {\"2\": null}\n"
+ + "}", builder);
+ TestMap message = builder.build();
+ assertTrue(message.getInt32ToInt32Map().containsKey(1));
+ assertEquals(0, message.getInt32ToInt32Map().get(1).intValue());
+ assertTrue(message.getInt32ToMessageMap().containsKey(2));
+ assertEquals(0, message.getInt32ToMessageMap().get(2).getValue());
+ }
+
+ public void testParserAcceptNonQuotedObjectKey() throws Exception {
+ TestMap.Builder builder = TestMap.newBuilder();
+ mergeFromJson(
+ "{\n"
+ + " int32ToInt32Map: {1: 2},\n"
+ + " stringToInt32Map: {hello: 3}\n"
+ + "}", builder);
+ TestMap message = builder.build();
+ assertEquals(2, message.getInt32ToInt32Map().get(1).intValue());
+ assertEquals(3, message.getStringToInt32Map().get("hello").intValue());
+ }
+
+ public void testWrappers() throws Exception {
+ TestWrappers.Builder builder = TestWrappers.newBuilder();
+ builder.getBoolValueBuilder().setValue(false);
+ builder.getInt32ValueBuilder().setValue(0);
+ builder.getInt64ValueBuilder().setValue(0);
+ builder.getUint32ValueBuilder().setValue(0);
+ builder.getUint64ValueBuilder().setValue(0);
+ builder.getFloatValueBuilder().setValue(0.0f);
+ builder.getDoubleValueBuilder().setValue(0.0);
+ builder.getStringValueBuilder().setValue("");
+ builder.getBytesValueBuilder().setValue(ByteString.EMPTY);
+ TestWrappers message = builder.build();
+
+ assertEquals(
+ "{\n"
+ + " \"int32Value\": 0,\n"
+ + " \"uint32Value\": 0,\n"
+ + " \"int64Value\": \"0\",\n"
+ + " \"uint64Value\": \"0\",\n"
+ + " \"floatValue\": 0.0,\n"
+ + " \"doubleValue\": 0.0,\n"
+ + " \"boolValue\": false,\n"
+ + " \"stringValue\": \"\",\n"
+ + " \"bytesValue\": \"\"\n"
+ + "}", toJsonString(message));
+ assertRoundTripEquals(message);
+
+ builder = TestWrappers.newBuilder();
+ builder.getBoolValueBuilder().setValue(true);
+ builder.getInt32ValueBuilder().setValue(1);
+ builder.getInt64ValueBuilder().setValue(2);
+ builder.getUint32ValueBuilder().setValue(3);
+ builder.getUint64ValueBuilder().setValue(4);
+ builder.getFloatValueBuilder().setValue(5.0f);
+ builder.getDoubleValueBuilder().setValue(6.0);
+ builder.getStringValueBuilder().setValue("7");
+ builder.getBytesValueBuilder().setValue(ByteString.copyFrom(new byte[]{8}));
+ message = builder.build();
+
+ assertEquals(
+ "{\n"
+ + " \"int32Value\": 1,\n"
+ + " \"uint32Value\": 3,\n"
+ + " \"int64Value\": \"2\",\n"
+ + " \"uint64Value\": \"4\",\n"
+ + " \"floatValue\": 5.0,\n"
+ + " \"doubleValue\": 6.0,\n"
+ + " \"boolValue\": true,\n"
+ + " \"stringValue\": \"7\",\n"
+ + " \"bytesValue\": \"CA==\"\n"
+ + "}", toJsonString(message));
+ assertRoundTripEquals(message);
+ }
+
+ public void testTimestamp() throws Exception {
+ TestTimestamp message = TestTimestamp.newBuilder()
+ .setTimestampValue(TimeUtil.parseTimestamp("1970-01-01T00:00:00Z"))
+ .build();
+
+ assertEquals(
+ "{\n"
+ + " \"timestampValue\": \"1970-01-01T00:00:00Z\"\n"
+ + "}", toJsonString(message));
+ assertRoundTripEquals(message);
+ }
+
+ public void testDuration() throws Exception {
+ TestDuration message = TestDuration.newBuilder()
+ .setDurationValue(TimeUtil.parseDuration("12345s"))
+ .build();
+
+ assertEquals(
+ "{\n"
+ + " \"durationValue\": \"12345s\"\n"
+ + "}", toJsonString(message));
+ assertRoundTripEquals(message);
+ }
+
+ public void testFieldMask() throws Exception {
+ TestFieldMask message = TestFieldMask.newBuilder()
+ .setFieldMaskValue(FieldMaskUtil.fromString("foo.bar,baz"))
+ .build();
+
+ assertEquals(
+ "{\n"
+ + " \"fieldMaskValue\": \"foo.bar,baz\"\n"
+ + "}", toJsonString(message));
+ assertRoundTripEquals(message);
+ }
+
+ public void testStruct() throws Exception {
+ // Build a struct with all possible values.
+ TestStruct.Builder builder = TestStruct.newBuilder();
+ Struct.Builder structBuilder = builder.getStructValueBuilder();
+ structBuilder.getMutableFields().put(
+ "null_value", Value.newBuilder().setNullValueValue(0).build());
+ structBuilder.getMutableFields().put(
+ "number_value", Value.newBuilder().setNumberValue(1.25).build());
+ structBuilder.getMutableFields().put(
+ "string_value", Value.newBuilder().setStringValue("hello").build());
+ Struct.Builder subStructBuilder = Struct.newBuilder();
+ subStructBuilder.getMutableFields().put(
+ "number_value", Value.newBuilder().setNumberValue(1234).build());
+ structBuilder.getMutableFields().put(
+ "struct_value", Value.newBuilder().setStructValue(subStructBuilder.build()).build());
+ ListValue.Builder listBuilder = ListValue.newBuilder();
+ listBuilder.addValues(Value.newBuilder().setNumberValue(1.125).build());
+ listBuilder.addValues(Value.newBuilder().setNullValueValue(0).build());
+ structBuilder.getMutableFields().put(
+ "list_value", Value.newBuilder().setListValue(listBuilder.build()).build());
+ TestStruct message = builder.build();
+
+ assertEquals(
+ "{\n"
+ + " \"structValue\": {\n"
+ + " \"null_value\": null,\n"
+ + " \"number_value\": 1.25,\n"
+ + " \"string_value\": \"hello\",\n"
+ + " \"struct_value\": {\n"
+ + " \"number_value\": 1234.0\n"
+ + " },\n"
+ + " \"list_value\": [1.125, null]\n"
+ + " }\n"
+ + "}", toJsonString(message));
+ assertRoundTripEquals(message);
+ }
+
+ public void testAnyFields() throws Exception {
+ TestAllTypes content = TestAllTypes.newBuilder().setOptionalInt32(1234).build();
+ TestAny message = TestAny.newBuilder().setAnyValue(Any.pack(content)).build();
+
+ // A TypeRegistry must be provided in order to convert Any types.
+ try {
+ toJsonString(message);
+ fail("Exception is expected.");
+ } catch (IOException e) {
+ // Expected.
+ }
+
+ JsonFormat.TypeRegistry registry = JsonFormat.TypeRegistry.newBuilder()
+ .add(TestAllTypes.getDescriptor()).build();
+ JsonFormat.Printer printer = JsonFormat.printer().usingTypeRegistry(registry);
+
+ assertEquals(
+ "{\n"
+ + " \"anyValue\": {\n"
+ + " \"@type\": \"type.googleapis.com/json_test.TestAllTypes\",\n"
+ + " \"optionalInt32\": 1234\n"
+ + " }\n"
+ + "}" , printer.print(message));
+ assertRoundTripEquals(message, registry);
+
+
+ // Well-known types have a special formatting when embedded in Any.
+ //
+ // 1. Any in Any.
+ Any anyMessage = Any.pack(Any.pack(content));
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.Any\",\n"
+ + " \"value\": {\n"
+ + " \"@type\": \"type.googleapis.com/json_test.TestAllTypes\",\n"
+ + " \"optionalInt32\": 1234\n"
+ + " }\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+
+ // 2. Wrappers in Any.
+ anyMessage = Any.pack(Int32Value.newBuilder().setValue(12345).build());
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.Int32Value\",\n"
+ + " \"value\": 12345\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+ anyMessage = Any.pack(UInt32Value.newBuilder().setValue(12345).build());
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.UInt32Value\",\n"
+ + " \"value\": 12345\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+ anyMessage = Any.pack(Int64Value.newBuilder().setValue(12345).build());
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.Int64Value\",\n"
+ + " \"value\": \"12345\"\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+ anyMessage = Any.pack(UInt64Value.newBuilder().setValue(12345).build());
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.UInt64Value\",\n"
+ + " \"value\": \"12345\"\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+ anyMessage = Any.pack(FloatValue.newBuilder().setValue(12345).build());
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.FloatValue\",\n"
+ + " \"value\": 12345.0\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+ anyMessage = Any.pack(DoubleValue.newBuilder().setValue(12345).build());
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.DoubleValue\",\n"
+ + " \"value\": 12345.0\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+ anyMessage = Any.pack(BoolValue.newBuilder().setValue(true).build());
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.BoolValue\",\n"
+ + " \"value\": true\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+ anyMessage = Any.pack(StringValue.newBuilder().setValue("Hello").build());
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.StringValue\",\n"
+ + " \"value\": \"Hello\"\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+ anyMessage = Any.pack(BytesValue.newBuilder().setValue(
+ ByteString.copyFrom(new byte[]{1, 2})).build());
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.BytesValue\",\n"
+ + " \"value\": \"AQI=\"\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+
+ // 3. Timestamp in Any.
+ anyMessage = Any.pack(TimeUtil.parseTimestamp("1969-12-31T23:59:59Z"));
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.Timestamp\",\n"
+ + " \"value\": \"1969-12-31T23:59:59Z\"\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+
+ // 4. Duration in Any
+ anyMessage = Any.pack(TimeUtil.parseDuration("12345.10s"));
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n"
+ + " \"value\": \"12345.100s\"\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+
+ // 5. FieldMask in Any
+ anyMessage = Any.pack(FieldMaskUtil.fromString("foo.bar,baz"));
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.FieldMask\",\n"
+ + " \"value\": \"foo.bar,baz\"\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+
+ // 6. Struct in Any
+ Struct.Builder structBuilder = Struct.newBuilder();
+ structBuilder.getMutableFields().put(
+ "number", Value.newBuilder().setNumberValue(1.125).build());
+ anyMessage = Any.pack(structBuilder.build());
+ assertEquals(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/google.protobuf.Struct\",\n"
+ + " \"value\": {\n"
+ + " \"number\": 1.125\n"
+ + " }\n"
+ + "}", printer.print(anyMessage));
+ assertRoundTripEquals(anyMessage, registry);
+ }
+
+ public void testParserMissingTypeUrl() throws Exception {
+ try {
+ Any.Builder builder = Any.newBuilder();
+ mergeFromJson(
+ "{\n"
+ + " \"optionalInt32\": 1234\n"
+ + "}", builder);
+ fail("Exception is expected.");
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
+
+ public void testParserUnexpectedTypeUrl() throws Exception {
+ try {
+ TestAllTypes.Builder builder = TestAllTypes.newBuilder();
+ mergeFromJson(
+ "{\n"
+ + " \"@type\": \"type.googleapis.com/json_test.TestAllTypes\",\n"
+ + " \"optionalInt32\": 12345\n"
+ + "}", builder);
+ fail("Exception is expected.");
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
+
+ public void testParserRejectTrailingComma() throws Exception {
+ try {
+ TestAllTypes.Builder builder = TestAllTypes.newBuilder();
+ mergeFromJson(
+ "{\n"
+ + " \"optionalInt32\": 12345,\n"
+ + "}", builder);
+ fail("Exception is expected.");
+ } catch (IOException e) {
+ // Expected.
+ }
+
+ // TODO(xiaofeng): GSON allows trailing comma in arrays even after I set
+ // the JsonReader to non-lenient mode. If we want to enforce strict JSON
+ // compliance, we might want to switch to a different JSON parser or
+ // implement one by ourselves.
+ // try {
+ // TestAllTypes.Builder builder = TestAllTypes.newBuilder();
+ // JsonFormat.merge(
+ // "{\n"
+ // + " \"repeatedInt32\": [12345,]\n"
+ // + "}", builder);
+ // fail("Exception is expected.");
+ // } catch (IOException e) {
+ // // Expected.
+ // }
+ }
+
+ public void testParserRejectInvalidBase64() throws Exception {
+ try {
+ TestAllTypes.Builder builder = TestAllTypes.newBuilder();
+ mergeFromJson(
+ "{\n"
+ + " \"optionalBytes\": \"!@#$\"\n"
+ + "}", builder);
+ fail("Exception is expected.");
+ } catch (InvalidProtocolBufferException e) {
+ // Expected.
+ }
+ }
+
+ public void testParserRejectInvalidEnumValue() throws Exception {
+ try {
+ TestAllTypes.Builder builder = TestAllTypes.newBuilder();
+ mergeFromJson(
+ "{\n"
+ + " \"optionalNestedEnum\": \"XXX\"\n"
+ + "}", builder);
+ fail("Exception is expected.");
+ } catch (InvalidProtocolBufferException e) {
+ // Expected.
+ }
+ }
+}
diff --git a/java/util/src/test/java/com/google/protobuf/util/TimeUtilTest.java b/java/util/src/test/java/com/google/protobuf/util/TimeUtilTest.java
new file mode 100644
index 0000000000..fe5617e115
--- /dev/null
+++ b/java/util/src/test/java/com/google/protobuf/util/TimeUtilTest.java
@@ -0,0 +1,439 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package com.google.protobuf.util;
+
+import com.google.protobuf.Duration;
+import com.google.protobuf.Timestamp;
+
+import junit.framework.TestCase;
+
+import org.junit.Assert;
+
+import java.text.ParseException;
+
+/** Unit tests for {@link TimeUtil}. */
+public class TimeUtilTest extends TestCase {
+ public void testTimestampStringFormat() throws Exception {
+ Timestamp start = TimeUtil.parseTimestamp("0001-01-01T00:00:00Z");
+ Timestamp end = TimeUtil.parseTimestamp("9999-12-31T23:59:59.999999999Z");
+ assertEquals(TimeUtil.TIMESTAMP_SECONDS_MIN, start.getSeconds());
+ assertEquals(0, start.getNanos());
+ assertEquals(TimeUtil.TIMESTAMP_SECONDS_MAX, end.getSeconds());
+ assertEquals(999999999, end.getNanos());
+ assertEquals("0001-01-01T00:00:00Z", TimeUtil.toString(start));
+ assertEquals("9999-12-31T23:59:59.999999999Z", TimeUtil.toString(end));
+
+ Timestamp value = TimeUtil.parseTimestamp("1970-01-01T00:00:00Z");
+ assertEquals(0, value.getSeconds());
+ assertEquals(0, value.getNanos());
+
+ // Test negative timestamps.
+ value = TimeUtil.parseTimestamp("1969-12-31T23:59:59.999Z");
+ assertEquals(-1, value.getSeconds());
+ // Nano part is in the range of [0, 999999999] for Timestamp.
+ assertEquals(999000000, value.getNanos());
+
+ // Test that 3, 6, or 9 digits are used for the fractional part.
+ value = Timestamp.newBuilder().setNanos(10).build();
+ assertEquals("1970-01-01T00:00:00.000000010Z", TimeUtil.toString(value));
+ value = Timestamp.newBuilder().setNanos(10000).build();
+ assertEquals("1970-01-01T00:00:00.000010Z", TimeUtil.toString(value));
+ value = Timestamp.newBuilder().setNanos(10000000).build();
+ assertEquals("1970-01-01T00:00:00.010Z", TimeUtil.toString(value));
+
+ // Test that parsing accepts timezone offsets.
+ value = TimeUtil.parseTimestamp("1970-01-01T00:00:00.010+08:00");
+ assertEquals("1969-12-31T16:00:00.010Z", TimeUtil.toString(value));
+ value = TimeUtil.parseTimestamp("1970-01-01T00:00:00.010-08:00");
+ assertEquals("1970-01-01T08:00:00.010Z", TimeUtil.toString(value));
+ }
+
+ public void testTimetampInvalidFormat() throws Exception {
+ try {
+ // Value too small.
+ Timestamp value = Timestamp.newBuilder()
+ .setSeconds(TimeUtil.TIMESTAMP_SECONDS_MIN - 1).build();
+ TimeUtil.toString(value);
+ Assert.fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ try {
+ // Value too large.
+ Timestamp value = Timestamp.newBuilder()
+ .setSeconds(TimeUtil.TIMESTAMP_SECONDS_MAX + 1).build();
+ TimeUtil.toString(value);
+ Assert.fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ try {
+ // Invalid nanos value.
+ Timestamp value = Timestamp.newBuilder().setNanos(-1).build();
+ TimeUtil.toString(value);
+ Assert.fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ try {
+ // Invalid nanos value.
+ Timestamp value = Timestamp.newBuilder().setNanos(1000000000).build();
+ TimeUtil.toString(value);
+ Assert.fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ try {
+ // Value to small.
+ TimeUtil.parseTimestamp("0000-01-01T00:00:00Z");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Value to large.
+ TimeUtil.parseTimestamp("10000-01-01T00:00:00Z");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Missing 'T'.
+ TimeUtil.parseTimestamp("1970-01-01 00:00:00Z");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Missing 'Z'.
+ TimeUtil.parseTimestamp("1970-01-01T00:00:00");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Invalid offset.
+ TimeUtil.parseTimestamp("1970-01-01T00:00:00+0000");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Trailing text.
+ TimeUtil.parseTimestamp("1970-01-01T00:00:00Z0");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Invalid nanosecond value.
+ TimeUtil.parseTimestamp("1970-01-01T00:00:00.ABCZ");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+ }
+
+ public void testDurationStringFormat() throws Exception {
+ Timestamp start = TimeUtil.parseTimestamp("0001-01-01T00:00:00Z");
+ Timestamp end = TimeUtil.parseTimestamp("9999-12-31T23:59:59.999999999Z");
+ Duration duration = TimeUtil.distance(start, end);
+ assertEquals("315537897599.999999999s", TimeUtil.toString(duration));
+ duration = TimeUtil.distance(end, start);
+ assertEquals("-315537897599.999999999s", TimeUtil.toString(duration));
+
+ // Generated output should contain 3, 6, or 9 fractional digits.
+ duration = Duration.newBuilder().setSeconds(1).build();
+ assertEquals("1s", TimeUtil.toString(duration));
+ duration = Duration.newBuilder().setNanos(10000000).build();
+ assertEquals("0.010s", TimeUtil.toString(duration));
+ duration = Duration.newBuilder().setNanos(10000).build();
+ assertEquals("0.000010s", TimeUtil.toString(duration));
+ duration = Duration.newBuilder().setNanos(10).build();
+ assertEquals("0.000000010s", TimeUtil.toString(duration));
+
+ // Parsing accepts an fractional digits as long as they fit into nano
+ // precision.
+ duration = TimeUtil.parseDuration("0.1s");
+ assertEquals(100000000, duration.getNanos());
+ duration = TimeUtil.parseDuration("0.0001s");
+ assertEquals(100000, duration.getNanos());
+ duration = TimeUtil.parseDuration("0.0000001s");
+ assertEquals(100, duration.getNanos());
+
+ // Duration must support range from -315,576,000,000s to +315576000000s
+ // which includes negative values.
+ duration = TimeUtil.parseDuration("315576000000.999999999s");
+ assertEquals(315576000000L, duration.getSeconds());
+ assertEquals(999999999, duration.getNanos());
+ duration = TimeUtil.parseDuration("-315576000000.999999999s");
+ assertEquals(-315576000000L, duration.getSeconds());
+ assertEquals(-999999999, duration.getNanos());
+ }
+
+ public void testDurationInvalidFormat() throws Exception {
+ try {
+ // Value too small.
+ Duration value = Duration.newBuilder()
+ .setSeconds(TimeUtil.DURATION_SECONDS_MIN - 1).build();
+ TimeUtil.toString(value);
+ Assert.fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ try {
+ // Value too large.
+ Duration value = Duration.newBuilder()
+ .setSeconds(TimeUtil.DURATION_SECONDS_MAX + 1).build();
+ TimeUtil.toString(value);
+ Assert.fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ try {
+ // Invalid nanos value.
+ Duration value = Duration.newBuilder().setSeconds(1).setNanos(-1)
+ .build();
+ TimeUtil.toString(value);
+ Assert.fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ try {
+ // Invalid nanos value.
+ Duration value = Duration.newBuilder().setSeconds(-1).setNanos(1)
+ .build();
+ TimeUtil.toString(value);
+ Assert.fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ try {
+ // Value too small.
+ TimeUtil.parseDuration("-315576000001s");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Value too large.
+ TimeUtil.parseDuration("315576000001s");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Empty.
+ TimeUtil.parseDuration("");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Missing "s".
+ TimeUtil.parseDuration("0");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Invalid trailing data.
+ TimeUtil.parseDuration("0s0");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+
+ try {
+ // Invalid prefix.
+ TimeUtil.parseDuration("--1s");
+ Assert.fail("Exception is expected.");
+ } catch (ParseException e) {
+ // Expected.
+ }
+ }
+
+ public void testTimestampConversion() throws Exception {
+ Timestamp timestamp =
+ TimeUtil.parseTimestamp("1970-01-01T00:00:01.111111111Z");
+ assertEquals(1111111111, TimeUtil.toNanos(timestamp));
+ assertEquals(1111111, TimeUtil.toMicros(timestamp));
+ assertEquals(1111, TimeUtil.toMillis(timestamp));
+ timestamp = TimeUtil.createTimestampFromNanos(1111111111);
+ assertEquals("1970-01-01T00:00:01.111111111Z", TimeUtil.toString(timestamp));
+ timestamp = TimeUtil.createTimestampFromMicros(1111111);
+ assertEquals("1970-01-01T00:00:01.111111Z", TimeUtil.toString(timestamp));
+ timestamp = TimeUtil.createTimestampFromMillis(1111);
+ assertEquals("1970-01-01T00:00:01.111Z", TimeUtil.toString(timestamp));
+
+ timestamp = TimeUtil.parseTimestamp("1969-12-31T23:59:59.111111111Z");
+ assertEquals(-888888889, TimeUtil.toNanos(timestamp));
+ assertEquals(-888889, TimeUtil.toMicros(timestamp));
+ assertEquals(-889, TimeUtil.toMillis(timestamp));
+ timestamp = TimeUtil.createTimestampFromNanos(-888888889);
+ assertEquals("1969-12-31T23:59:59.111111111Z", TimeUtil.toString(timestamp));
+ timestamp = TimeUtil.createTimestampFromMicros(-888889);
+ assertEquals("1969-12-31T23:59:59.111111Z", TimeUtil.toString(timestamp));
+ timestamp = TimeUtil.createTimestampFromMillis(-889);
+ assertEquals("1969-12-31T23:59:59.111Z", TimeUtil.toString(timestamp));
+ }
+
+ public void testDurationConversion() throws Exception {
+ Duration duration = TimeUtil.parseDuration("1.111111111s");
+ assertEquals(1111111111, TimeUtil.toNanos(duration));
+ assertEquals(1111111, TimeUtil.toMicros(duration));
+ assertEquals(1111, TimeUtil.toMillis(duration));
+ duration = TimeUtil.createDurationFromNanos(1111111111);
+ assertEquals("1.111111111s", TimeUtil.toString(duration));
+ duration = TimeUtil.createDurationFromMicros(1111111);
+ assertEquals("1.111111s", TimeUtil.toString(duration));
+ duration = TimeUtil.createDurationFromMillis(1111);
+ assertEquals("1.111s", TimeUtil.toString(duration));
+
+ duration = TimeUtil.parseDuration("-1.111111111s");
+ assertEquals(-1111111111, TimeUtil.toNanos(duration));
+ assertEquals(-1111111, TimeUtil.toMicros(duration));
+ assertEquals(-1111, TimeUtil.toMillis(duration));
+ duration = TimeUtil.createDurationFromNanos(-1111111111);
+ assertEquals("-1.111111111s", TimeUtil.toString(duration));
+ duration = TimeUtil.createDurationFromMicros(-1111111);
+ assertEquals("-1.111111s", TimeUtil.toString(duration));
+ duration = TimeUtil.createDurationFromMillis(-1111);
+ assertEquals("-1.111s", TimeUtil.toString(duration));
+ }
+
+ public void testTimeOperations() throws Exception {
+ Timestamp start = TimeUtil.parseTimestamp("0001-01-01T00:00:00Z");
+ Timestamp end = TimeUtil.parseTimestamp("9999-12-31T23:59:59.999999999Z");
+
+ Duration duration = TimeUtil.distance(start, end);
+ assertEquals("315537897599.999999999s", TimeUtil.toString(duration));
+ Timestamp value = TimeUtil.add(start, duration);
+ assertEquals(end, value);
+ value = TimeUtil.subtract(end, duration);
+ assertEquals(start, value);
+
+ duration = TimeUtil.distance(end, start);
+ assertEquals("-315537897599.999999999s", TimeUtil.toString(duration));
+ value = TimeUtil.add(end, duration);
+ assertEquals(start, value);
+ value = TimeUtil.subtract(start, duration);
+ assertEquals(end, value);
+
+ // Result is larger than Long.MAX_VALUE.
+ try {
+ duration = TimeUtil.parseDuration("315537897599.999999999s");
+ duration = TimeUtil.multiply(duration, 315537897599.999999999);
+ Assert.fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ // Result is lesser than Long.MIN_VALUE.
+ try {
+ duration = TimeUtil.parseDuration("315537897599.999999999s");
+ duration = TimeUtil.multiply(duration, -315537897599.999999999);
+ Assert.fail("Exception is expected.");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ duration = TimeUtil.parseDuration("-1.125s");
+ duration = TimeUtil.divide(duration, 2.0);
+ assertEquals("-0.562500s", TimeUtil.toString(duration));
+ duration = TimeUtil.multiply(duration, 2.0);
+ assertEquals("-1.125s", TimeUtil.toString(duration));
+
+ duration = TimeUtil.add(duration, duration);
+ assertEquals("-2.250s", TimeUtil.toString(duration));
+
+ duration = TimeUtil.subtract(duration, TimeUtil.parseDuration("-1s"));
+ assertEquals("-1.250s", TimeUtil.toString(duration));
+
+ // Multiplications (with results larger than Long.MAX_VALUE in nanoseconds).
+ duration = TimeUtil.parseDuration("0.999999999s");
+ assertEquals("315575999684.424s",
+ TimeUtil.toString(TimeUtil.multiply(duration, 315576000000L)));
+ duration = TimeUtil.parseDuration("-0.999999999s");
+ assertEquals("-315575999684.424s",
+ TimeUtil.toString(TimeUtil.multiply(duration, 315576000000L)));
+ assertEquals("315575999684.424s",
+ TimeUtil.toString(TimeUtil.multiply(duration, -315576000000L)));
+
+ // Divisions (with values larger than Long.MAX_VALUE in nanoseconds).
+ Duration d1 = TimeUtil.parseDuration("315576000000s");
+ Duration d2 = TimeUtil.subtract(d1, TimeUtil.createDurationFromNanos(1));
+ assertEquals(1, TimeUtil.divide(d1, d2));
+ assertEquals(0, TimeUtil.divide(d2, d1));
+ assertEquals("0.000000001s", TimeUtil.toString(TimeUtil.remainder(d1, d2)));
+ assertEquals("315575999999.999999999s",
+ TimeUtil.toString(TimeUtil.remainder(d2, d1)));
+
+ // Divisions involving negative values.
+ //
+ // (-5) / 2 = -2, remainder = -1
+ d1 = TimeUtil.parseDuration("-5s");
+ d2 = TimeUtil.parseDuration("2s");
+ assertEquals(-2, TimeUtil.divide(d1, d2));
+ assertEquals(-2, TimeUtil.divide(d1, 2).getSeconds());
+ assertEquals(-1, TimeUtil.remainder(d1, d2).getSeconds());
+ // (-5) / (-2) = 2, remainder = -1
+ d1 = TimeUtil.parseDuration("-5s");
+ d2 = TimeUtil.parseDuration("-2s");
+ assertEquals(2, TimeUtil.divide(d1, d2));
+ assertEquals(2, TimeUtil.divide(d1, -2).getSeconds());
+ assertEquals(-1, TimeUtil.remainder(d1, d2).getSeconds());
+ // 5 / (-2) = -2, remainder = 1
+ d1 = TimeUtil.parseDuration("5s");
+ d2 = TimeUtil.parseDuration("-2s");
+ assertEquals(-2, TimeUtil.divide(d1, d2));
+ assertEquals(-2, TimeUtil.divide(d1, -2).getSeconds());
+ assertEquals(1, TimeUtil.remainder(d1, d2).getSeconds());
+ }
+}
diff --git a/java/util/src/test/java/com/google/protobuf/util/json_test.proto b/java/util/src/test/java/com/google/protobuf/util/json_test.proto
new file mode 100644
index 0000000000..b2753af6bf
--- /dev/null
+++ b/java/util/src/test/java/com/google/protobuf/util/json_test.proto
@@ -0,0 +1,158 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+syntax = "proto3";
+
+package json_test;
+
+option java_package = "com.google.protobuf.util";
+option java_outer_classname = "JsonTestProto";
+
+import "google/protobuf/any.proto";
+import "google/protobuf/wrappers.proto";
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/duration.proto";
+import "google/protobuf/field_mask.proto";
+import "google/protobuf/struct.proto";
+
+message TestAllTypes {
+ enum NestedEnum {
+ FOO = 0;
+ BAR = 1;
+ BAZ = 2;
+ }
+ message NestedMessage {
+ int32 value = 1;
+ }
+
+ int32 optional_int32 = 1;
+ int64 optional_int64 = 2;
+ uint32 optional_uint32 = 3;
+ uint64 optional_uint64 = 4;
+ sint32 optional_sint32 = 5;
+ sint64 optional_sint64 = 6;
+ fixed32 optional_fixed32 = 7;
+ fixed64 optional_fixed64 = 8;
+ sfixed32 optional_sfixed32 = 9;
+ sfixed64 optional_sfixed64 = 10;
+ float optional_float = 11;
+ double optional_double = 12;
+ bool optional_bool = 13;
+ string optional_string = 14;
+ bytes optional_bytes = 15;
+ NestedMessage optional_nested_message = 18;
+ NestedEnum optional_nested_enum = 21;
+
+ // Repeated
+ repeated int32 repeated_int32 = 31;
+ repeated int64 repeated_int64 = 32;
+ repeated uint32 repeated_uint32 = 33;
+ repeated uint64 repeated_uint64 = 34;
+ repeated sint32 repeated_sint32 = 35;
+ repeated sint64 repeated_sint64 = 36;
+ repeated fixed32 repeated_fixed32 = 37;
+ repeated fixed64 repeated_fixed64 = 38;
+ repeated sfixed32 repeated_sfixed32 = 39;
+ repeated sfixed64 repeated_sfixed64 = 40;
+ repeated float repeated_float = 41;
+ repeated double repeated_double = 42;
+ repeated bool repeated_bool = 43;
+ repeated string repeated_string = 44;
+ repeated bytes repeated_bytes = 45;
+ repeated NestedMessage repeated_nested_message = 48;
+ repeated NestedEnum repeated_nested_enum = 51;
+}
+
+message TestMap {
+ // Instead of testing all combinations (too many), we only make sure all
+ // valid types have been used at least in one field as key and in one
+ // field as value.
+ map