summaryrefslogtreecommitdiff
path: root/java/util/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'java/util/src/main/java')
-rw-r--r--java/util/src/main/java/com/google/protobuf/util/Durations.java256
-rw-r--r--java/util/src/main/java/com/google/protobuf/util/FieldMaskTree.java282
-rw-r--r--java/util/src/main/java/com/google/protobuf/util/FieldMaskUtil.java342
-rw-r--r--java/util/src/main/java/com/google/protobuf/util/JsonFormat.java1696
-rw-r--r--java/util/src/main/java/com/google/protobuf/util/TimeUtil.java400
-rw-r--r--java/util/src/main/java/com/google/protobuf/util/Timestamps.java349
6 files changed, 3325 insertions, 0 deletions
diff --git a/java/util/src/main/java/com/google/protobuf/util/Durations.java b/java/util/src/main/java/com/google/protobuf/util/Durations.java
new file mode 100644
index 00000000..5fe6ebca
--- /dev/null
+++ b/java/util/src/main/java/com/google/protobuf/util/Durations.java
@@ -0,0 +1,256 @@
+// 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 static com.google.protobuf.util.Timestamps.MICROS_PER_SECOND;
+import static com.google.protobuf.util.Timestamps.MILLIS_PER_SECOND;
+import static com.google.protobuf.util.Timestamps.NANOS_PER_MICROSECOND;
+import static com.google.protobuf.util.Timestamps.NANOS_PER_MILLISECOND;
+import static com.google.protobuf.util.Timestamps.NANOS_PER_SECOND;
+
+import com.google.protobuf.Duration;
+
+import java.text.ParseException;
+
+/**
+ * Utilities to help create/manipulate {@code protobuf/duration.proto}.
+ */
+public final class Durations {
+ static final long DURATION_SECONDS_MIN = -315576000000L;
+ static final long DURATION_SECONDS_MAX = 315576000000L;
+
+ // TODO(kak): Do we want to expose Duration constants for MAX/MIN?
+
+ private Durations() {}
+
+ /**
+ * Returns true if the given {@link Duration} is valid. The {@code seconds} value must be in the
+ * range [-315,576,000,000, +315,576,000,000]. The {@code nanos} value must be in the range
+ * [-999,999,999, +999,999,999].
+ *
+ * <p>Note: Durations less than one second are represented with a 0 {@code seconds} field and a
+ * positive or negative {@code nanos} field. For durations of one second or more, a non-zero value
+ * for the {@code nanos} field must be of the same sign as the {@code seconds} field.
+ */
+ public static boolean isValid(Duration duration) {
+ return isValid(duration.getSeconds(), duration.getNanos());
+ }
+
+ /**
+ * Returns true if the given number of seconds and nanos is a valid {@link Duration}. The
+ * {@code seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The
+ * {@code nanos} value must be in the range [-999,999,999, +999,999,999].
+ *
+ * <p>Note: Durations less than one second are represented with a 0 {@code seconds} field and a
+ * positive or negative {@code nanos} field. For durations of one second or more, a non-zero value
+ * for the {@code nanos} field must be of the same sign as the {@code seconds} field.
+ */
+ public static boolean isValid(long seconds, long nanos) {
+ if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
+ return false;
+ }
+ if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
+ return false;
+ }
+ if (seconds < 0 || nanos < 0) {
+ if (seconds > 0 || nanos > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Throws an {@link IllegalArgumentException} if the given seconds/nanos are not
+ * a valid {@link Duration}.
+ */
+ private static void checkValid(long seconds, int nanos) {
+ if (!isValid(seconds, nanos)) {
+ throw new IllegalArgumentException(String.format(
+ "Duration is not valid. See proto definition for valid values. "
+ + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]."
+ + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
+ + "Nanos must have the same sign as seconds", seconds, nanos));
+ }
+ }
+
+ /**
+ * 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) {
+ long seconds = duration.getSeconds();
+ int nanos = duration.getNanos();
+ checkValid(seconds, nanos);
+
+ StringBuilder result = new StringBuilder();
+ if (seconds < 0 || nanos < 0) {
+ result.append("-");
+ seconds = -seconds;
+ nanos = -nanos;
+ }
+ result.append(seconds);
+ if (nanos != 0) {
+ result.append(".");
+ result.append(Timestamps.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 parse(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 : Timestamps.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 Duration from the number of milliseconds.
+ */
+ public static Duration fromMillis(long milliseconds) {
+ return normalizedDuration(
+ milliseconds / MILLIS_PER_SECOND,
+ (int) (milliseconds % MILLIS_PER_SECOND * 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 Duration from the number of microseconds.
+ */
+ public static Duration fromMicros(long microseconds) {
+ return normalizedDuration(
+ microseconds / MICROS_PER_SECOND,
+ (int) (microseconds % MICROS_PER_SECOND * 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 Duration from the number of nanoseconds.
+ */
+ public static Duration fromNanos(long nanoseconds) {
+ return normalizedDuration(
+ nanoseconds / NANOS_PER_SECOND, (int) (nanoseconds % NANOS_PER_SECOND));
+ }
+
+ /**
+ * Convert a Duration to the number of nanoseconds.
+ */
+ public static long toNanos(Duration duration) {
+ return duration.getSeconds() * NANOS_PER_SECOND + duration.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());
+ }
+
+ 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;
+ }
+ checkValid(seconds, nanos);
+ return Duration.newBuilder().setSeconds(seconds).setNanos(nanos).build();
+ }
+}
diff --git a/java/util/src/main/java/com/google/protobuf/util/FieldMaskTree.java b/java/util/src/main/java/com/google/protobuf/util/FieldMaskTree.java
new file mode 100644
index 00000000..b577495d
--- /dev/null
+++ b/java/util/src/main/java/com/google/protobuf/util/FieldMaskTree.java
@@ -0,0 +1,282 @@
+// 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.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.FieldMask;
+import com.google.protobuf.Message;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.logging.Logger;
+
+/**
+ * A tree representation of a FieldMask. Each leaf node in this tree represent
+ * a field path in the FieldMask.
+ *
+ * <p>For example, FieldMask "foo.bar,foo.baz,bar.baz" as a tree will be:
+ * <pre>
+ * [root] -+- foo -+- bar
+ * | |
+ * | +- baz
+ * |
+ * +- bar --- baz
+ * </pre>
+ *
+ * <p>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.
+ */
+final class FieldMaskTree {
+ private static final Logger logger = Logger.getLogger(FieldMaskTree.class.getName());
+
+ private static final String FIELD_PATH_SEPARATOR_REGEX = "\\.";
+
+ private static final class Node {
+ final SortedMap<String, Node> children = new TreeMap<String, Node>();
+ }
+
+ private final Node root = new Node();
+
+ /**
+ * Creates an empty FieldMaskTree.
+ */
+ FieldMaskTree() {}
+
+ /**
+ * Creates a FieldMaskTree for a given FieldMask.
+ */
+ FieldMaskTree(FieldMask mask) {
+ mergeFromFieldMask(mask);
+ }
+
+ @Override
+ public String toString() {
+ return FieldMaskUtil.toString(toFieldMask());
+ }
+
+ /**
+ * Adds a field path to the tree. In a FieldMask, every field path matches the
+ * specified field as well as all its sub-fields. For example, a field path
+ * "foo.bar" matches field "foo.bar" and also "foo.bar.baz", etc. When adding
+ * a field path to the tree, redundant sub-paths will be removed. That is,
+ * after adding "foo.bar" to the tree, "foo.bar.baz" will be removed if it
+ * exists, which will turn the tree node for "foo.bar" to a leaf node.
+ * Likewise, if the field path to add is a sub-path of an existing leaf node,
+ * nothing will be changed in the tree.
+ */
+ FieldMaskTree addFieldPath(String path) {
+ String[] parts = path.split(FIELD_PATH_SEPARATOR_REGEX);
+ if (parts.length == 0) {
+ return this;
+ }
+ Node node = root;
+ boolean createNewBranch = false;
+ // Find the matching node in the tree.
+ for (String part : parts) {
+ // Check whether the path matches an existing leaf node.
+ if (!createNewBranch && node != root && node.children.isEmpty()) {
+ // The path to add is a sub-path of an existing leaf node.
+ return this;
+ }
+ if (node.children.containsKey(part)) {
+ node = node.children.get(part);
+ } else {
+ createNewBranch = true;
+ Node tmp = new Node();
+ node.children.put(part, tmp);
+ node = tmp;
+ }
+ }
+ // Turn the matching node into a leaf node (i.e., remove sub-paths).
+ node.children.clear();
+ return this;
+ }
+
+ /**
+ * Merges all field paths in a FieldMask into this tree.
+ */
+ FieldMaskTree mergeFromFieldMask(FieldMask mask) {
+ for (String path : mask.getPathsList()) {
+ addFieldPath(path);
+ }
+ return this;
+ }
+
+ /**
+ * Converts this tree to a FieldMask.
+ */
+ FieldMask toFieldMask() {
+ if (root.children.isEmpty()) {
+ return FieldMask.getDefaultInstance();
+ }
+ List<String> paths = new ArrayList<String>();
+ getFieldPaths(root, "", paths);
+ return FieldMask.newBuilder().addAllPaths(paths).build();
+ }
+
+ /**
+ * Gathers all field paths in a sub-tree.
+ */
+ private void getFieldPaths(Node node, String path, List<String> paths) {
+ if (node.children.isEmpty()) {
+ paths.add(path);
+ return;
+ }
+ for (Entry<String, Node> entry : node.children.entrySet()) {
+ String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
+ getFieldPaths(entry.getValue(), childPath, paths);
+ }
+ }
+
+ /**
+ * Adds the intersection of this tree with the given {@code path} to {@code output}.
+ */
+ void intersectFieldPath(String path, FieldMaskTree output) {
+ if (root.children.isEmpty()) {
+ return;
+ }
+ String[] parts = path.split(FIELD_PATH_SEPARATOR_REGEX);
+ if (parts.length == 0) {
+ return;
+ }
+ Node node = root;
+ for (String part : parts) {
+ if (node != root && node.children.isEmpty()) {
+ // The given path is a sub-path of an existing leaf node in the tree.
+ output.addFieldPath(path);
+ return;
+ }
+ if (node.children.containsKey(part)) {
+ node = node.children.get(part);
+ } else {
+ return;
+ }
+ }
+ // We found a matching node for the path. All leaf children of this matching
+ // node is in the intersection.
+ List<String> paths = new ArrayList<String>();
+ getFieldPaths(node, path, paths);
+ for (String value : paths) {
+ output.addFieldPath(value);
+ }
+ }
+
+ /**
+ * Merges all fields specified by this FieldMaskTree from {@code source} to {@code destination}.
+ */
+ void merge(Message source, Message.Builder destination, FieldMaskUtil.MergeOptions options) {
+ if (source.getDescriptorForType() != destination.getDescriptorForType()) {
+ throw new IllegalArgumentException("Cannot merge messages of different types.");
+ }
+ if (root.children.isEmpty()) {
+ return;
+ }
+ merge(root, "", source, destination, options);
+ }
+
+ /**
+ * Merges all fields specified by a sub-tree from {@code source} to {@code destination}.
+ */
+ private void merge(
+ Node node,
+ String path,
+ Message source,
+ Message.Builder destination,
+ FieldMaskUtil.MergeOptions options) {
+ assert source.getDescriptorForType() == destination.getDescriptorForType();
+
+ Descriptor descriptor = source.getDescriptorForType();
+ for (Entry<String, Node> entry : node.children.entrySet()) {
+ FieldDescriptor field = descriptor.findFieldByName(entry.getKey());
+ if (field == null) {
+ logger.warning(
+ "Cannot find field \""
+ + entry.getKey()
+ + "\" in message type "
+ + descriptor.getFullName());
+ continue;
+ }
+ if (!entry.getValue().children.isEmpty()) {
+ if (field.isRepeated() || field.getJavaType() != FieldDescriptor.JavaType.MESSAGE) {
+ logger.warning(
+ "Field \""
+ + field.getFullName()
+ + "\" is not a "
+ + "singluar message field and cannot have sub-fields.");
+ continue;
+ }
+ String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
+ merge(
+ entry.getValue(),
+ childPath,
+ (Message) source.getField(field),
+ destination.getFieldBuilder(field),
+ options);
+ continue;
+ }
+ if (field.isRepeated()) {
+ if (options.replaceRepeatedFields()) {
+ destination.setField(field, source.getField(field));
+ } else {
+ for (Object element : (List) source.getField(field)) {
+ destination.addRepeatedField(field, element);
+ }
+ }
+ } else {
+ if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
+ if (options.replaceMessageFields()) {
+ if (!source.hasField(field)) {
+ destination.clearField(field);
+ } else {
+ destination.setField(field, source.getField(field));
+ }
+ } else {
+ if (source.hasField(field)) {
+ destination.getFieldBuilder(field).mergeFrom((Message) source.getField(field));
+ }
+ }
+ } else {
+ if (source.hasField(field) || !options.replacePrimitiveFields()) {
+ destination.setField(field, source.getField(field));
+ } else {
+ destination.clearField(field);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/java/util/src/main/java/com/google/protobuf/util/FieldMaskUtil.java b/java/util/src/main/java/com/google/protobuf/util/FieldMaskUtil.java
new file mode 100644
index 00000000..21d11b2c
--- /dev/null
+++ b/java/util/src/main/java/com/google/protobuf/util/FieldMaskUtil.java
@@ -0,0 +1,342 @@
+// 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 static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.primitives.Ints;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.FieldMask;
+import com.google.protobuf.Internal;
+import com.google.protobuf.Message;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility helper functions to work with {@link com.google.protobuf.FieldMask}.
+ */
+public class FieldMaskUtil {
+ private static final String FIELD_PATH_SEPARATOR = ",";
+ private static final String FIELD_PATH_SEPARATOR_REGEX = ",";
+ private static final String FIELD_SEPARATOR_REGEX = "\\.";
+
+ private FieldMaskUtil() {}
+
+ /**
+ * Converts a FieldMask to a string.
+ */
+ public static String toString(FieldMask fieldMask) {
+ // TODO(xiaofeng): Consider using com.google.common.base.Joiner here instead.
+ StringBuilder result = new StringBuilder();
+ boolean first = true;
+ for (String value : fieldMask.getPathsList()) {
+ if (value.isEmpty()) {
+ // Ignore empty paths.
+ continue;
+ }
+ if (first) {
+ first = false;
+ } else {
+ result.append(FIELD_PATH_SEPARATOR);
+ }
+ result.append(value);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Parses from a string to a FieldMask.
+ */
+ public static FieldMask fromString(String value) {
+ // TODO(xiaofeng): Consider using com.google.common.base.Splitter here instead.
+ return fromStringList(null, Arrays.asList(value.split(FIELD_PATH_SEPARATOR_REGEX)));
+ }
+
+ /**
+ * Parses from a string to a FieldMask and validates all field paths.
+ *
+ * @throws IllegalArgumentException if any of the field path is invalid.
+ */
+ public static FieldMask fromString(Class<? extends Message> type, String value) {
+ // TODO(xiaofeng): Consider using com.google.common.base.Splitter here instead.
+ return fromStringList(type, Arrays.asList(value.split(FIELD_PATH_SEPARATOR_REGEX)));
+ }
+
+ /**
+ * Constructs a FieldMask for a list of field paths in a certain type.
+ *
+ * @throws IllegalArgumentException if any of the field path is not valid.
+ */
+ // TODO(xiaofeng): Consider renaming fromStrings()
+ public static FieldMask fromStringList(Class<? extends Message> type, Iterable<String> paths) {
+ FieldMask.Builder builder = FieldMask.newBuilder();
+ for (String path : paths) {
+ if (path.isEmpty()) {
+ // Ignore empty field paths.
+ continue;
+ }
+ if (type != null && !isValid(type, path)) {
+ throw new IllegalArgumentException(path + " is not a valid path for " + type);
+ }
+ builder.addPaths(path);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Constructs a FieldMask from the passed field numbers.
+ *
+ * @throws IllegalArgumentException if any of the fields are invalid for the message.
+ */
+ public static FieldMask fromFieldNumbers(Class<? extends Message> type, int... fieldNumbers) {
+ return fromFieldNumbers(type, Ints.asList(fieldNumbers));
+ }
+
+ /**
+ * Constructs a FieldMask from the passed field numbers.
+ *
+ * @throws IllegalArgumentException if any of the fields are invalid for the message.
+ */
+ public static FieldMask fromFieldNumbers(
+ Class<? extends Message> type, Iterable<Integer> fieldNumbers) {
+ Descriptor descriptor = Internal.getDefaultInstance(type).getDescriptorForType();
+
+ FieldMask.Builder builder = FieldMask.newBuilder();
+ for (Integer fieldNumber : fieldNumbers) {
+ FieldDescriptor field = descriptor.findFieldByNumber(fieldNumber);
+ checkArgument(
+ field != null,
+ String.format("%s is not a valid field number for %s.", fieldNumber, type));
+ builder.addPaths(field.getName());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Converts a field mask to a Proto3 JSON string, that is converting from snake case to camel
+ * case and joining all paths into one string with commas.
+ */
+ public static String toJsonString(FieldMask fieldMask) {
+ List<String> paths = new ArrayList<String>(fieldMask.getPathsCount());
+ for (String path : fieldMask.getPathsList()) {
+ if (path.isEmpty()) {
+ continue;
+ }
+ paths.add(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, path));
+ }
+ return Joiner.on(FIELD_PATH_SEPARATOR).join(paths);
+ }
+
+ /**
+ * Converts a field mask from a Proto3 JSON string, that is splitting the paths along commas and
+ * converting from camel case to snake case.
+ */
+ public static FieldMask fromJsonString(String value) {
+ Iterable<String> paths = Splitter.on(FIELD_PATH_SEPARATOR).split(value);
+ FieldMask.Builder builder = FieldMask.newBuilder();
+ for (String path : paths) {
+ if (path.isEmpty()) {
+ continue;
+ }
+ builder.addPaths(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, path));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Checks whether paths in a given fields mask are valid.
+ */
+ public static boolean isValid(Class<? extends Message> type, FieldMask fieldMask) {
+ Descriptor descriptor = Internal.getDefaultInstance(type).getDescriptorForType();
+
+ return isValid(descriptor, fieldMask);
+ }
+
+ /**
+ * Checks whether paths in a given fields mask are valid.
+ */
+ public static boolean isValid(Descriptor descriptor, FieldMask fieldMask) {
+ for (String path : fieldMask.getPathsList()) {
+ if (!isValid(descriptor, path)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether a given field path is valid.
+ */
+ public static boolean isValid(Class<? extends Message> type, String path) {
+ Descriptor descriptor = Internal.getDefaultInstance(type).getDescriptorForType();
+
+ return isValid(descriptor, path);
+ }
+
+ /**
+ * Checks whether paths in a given fields mask are valid.
+ */
+ public static boolean isValid(Descriptor descriptor, String path) {
+ String[] parts = path.split(FIELD_SEPARATOR_REGEX);
+ if (parts.length == 0) {
+ return false;
+ }
+ for (String name : parts) {
+ if (descriptor == null) {
+ return false;
+ }
+ FieldDescriptor field = descriptor.findFieldByName(name);
+ if (field == null) {
+ return false;
+ }
+ if (!field.isRepeated() && field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
+ descriptor = field.getMessageType();
+ } else {
+ descriptor = null;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Converts a FieldMask to its canonical form. In the canonical form of a
+ * FieldMask, all field paths are sorted alphabetically and redundant field
+ * paths are moved.
+ */
+ public static FieldMask normalize(FieldMask mask) {
+ return new FieldMaskTree(mask).toFieldMask();
+ }
+
+ /**
+ * Creates a union of two or more FieldMasks.
+ */
+ public static FieldMask union(
+ FieldMask firstMask, FieldMask secondMask, FieldMask... otherMasks) {
+ FieldMaskTree maskTree = new FieldMaskTree(firstMask).mergeFromFieldMask(secondMask);
+ for (FieldMask mask : otherMasks) {
+ maskTree.mergeFromFieldMask(mask);
+ }
+ return maskTree.toFieldMask();
+ }
+
+ /**
+ * Calculates the intersection of two FieldMasks.
+ */
+ public static FieldMask intersection(FieldMask mask1, FieldMask mask2) {
+ FieldMaskTree tree = new FieldMaskTree(mask1);
+ FieldMaskTree result = new FieldMaskTree();
+ for (String path : mask2.getPathsList()) {
+ tree.intersectFieldPath(path, result);
+ }
+ return result.toFieldMask();
+ }
+
+ /**
+ * Options to customize merging behavior.
+ */
+ public static final class MergeOptions {
+ private boolean replaceMessageFields = false;
+ private boolean replaceRepeatedFields = false;
+ // TODO(b/28277137): change the default behavior to always replace primitive fields after
+ // fixing all failing TAP tests.
+ private boolean replacePrimitiveFields = false;
+
+ /**
+ * Whether to replace message fields (i.e., discard existing content in
+ * destination message fields) when merging.
+ * Default behavior is to merge the source message field into the
+ * destination message field.
+ */
+ public boolean replaceMessageFields() {
+ return replaceMessageFields;
+ }
+
+ /**
+ * Whether to replace repeated fields (i.e., discard existing content in
+ * destination repeated fields) when merging.
+ * Default behavior is to append elements from source repeated field to the
+ * destination repeated field.
+ */
+ public boolean replaceRepeatedFields() {
+ return replaceRepeatedFields;
+ }
+
+ /**
+ * Whether to replace primitive (non-repeated and non-message) fields in
+ * destination message fields with the source primitive fields (i.e., if the
+ * field is set in the source, the value is copied to the
+ * destination; if the field is unset in the source, the field is cleared
+ * from the destination) when merging.
+ *
+ * <p>Default behavior is to always set the value of the source primitive
+ * field to the destination primitive field, and if the source field is
+ * unset, the default value of the source field is copied to the
+ * destination.
+ */
+ public boolean replacePrimitiveFields() {
+ return replacePrimitiveFields;
+ }
+
+ public void setReplaceMessageFields(boolean value) {
+ replaceMessageFields = value;
+ }
+
+ public void setReplaceRepeatedFields(boolean value) {
+ replaceRepeatedFields = value;
+ }
+
+ public void setReplacePrimitiveFields(boolean value) {
+ replacePrimitiveFields = value;
+ }
+ }
+
+ /**
+ * Merges fields specified by a FieldMask from one message to another with the
+ * specified merge options.
+ */
+ public static void merge(
+ FieldMask mask, Message source, Message.Builder destination, MergeOptions options) {
+ new FieldMaskTree(mask).merge(source, destination, options);
+ }
+
+ /**
+ * Merges fields specified by a FieldMask from one message to another.
+ */
+ public static void merge(FieldMask mask, Message source, Message.Builder destination) {
+ merge(mask, source, destination, new MergeOptions());
+ }
+}
diff --git a/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java b/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java
new file mode 100644
index 00000000..297545e5
--- /dev/null
+++ b/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java
@@ -0,0 +1,1696 @@
+// 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.common.io.BaseEncoding;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.stream.JsonReader;
+import com.google.protobuf.Any;
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.BytesValue;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.EnumDescriptor;
+import com.google.protobuf.Descriptors.EnumValueDescriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Descriptors.FileDescriptor;
+import com.google.protobuf.DoubleValue;
+import com.google.protobuf.Duration;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.FieldMask;
+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.MessageOrBuilder;
+import com.google.protobuf.NullValue;
+import com.google.protobuf.StringValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Timestamp;
+import com.google.protobuf.UInt32Value;
+import com.google.protobuf.UInt64Value;
+import com.google.protobuf.Value;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.text.ParseException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.logging.Logger;
+
+/**
+ * Utility classes to convert protobuf messages to/from JSON format. The JSON
+ * format follows Proto3 JSON specification and only proto3 features are
+ * supported. Proto2 only features (e.g., extensions and unknown fields) will
+ * be discarded in the conversion. That is, when converting proto2 messages
+ * to JSON format, extensions and unknown fields will be treated as if they
+ * do not exist. This applies to proto2 messages embedded in proto3 messages
+ * as well.
+ */
+public class JsonFormat {
+ private static final Logger logger = Logger.getLogger(JsonFormat.class.getName());
+
+ private JsonFormat() {}
+
+ /**
+ * Creates a {@link Printer} with default configurations.
+ */
+ public static Printer printer() {
+ return new Printer(TypeRegistry.getEmptyTypeRegistry(), false, false, false);
+ }
+
+ /**
+ * A Printer converts protobuf message to JSON format.
+ */
+ public static class Printer {
+ private final TypeRegistry registry;
+ private final boolean includingDefaultValueFields;
+ private final boolean preservingProtoFieldNames;
+ private final boolean omittingInsignificantWhitespace;
+
+ private Printer(
+ TypeRegistry registry,
+ boolean includingDefaultValueFields,
+ boolean preservingProtoFieldNames, boolean omittingInsignificantWhitespace) {
+ this.registry = registry;
+ this.includingDefaultValueFields = includingDefaultValueFields;
+ this.preservingProtoFieldNames = preservingProtoFieldNames;
+ this.omittingInsignificantWhitespace = omittingInsignificantWhitespace;
+ }
+
+ /**
+ * Creates a new {@link Printer} using the given registry. The new Printer
+ * clones all other configurations from the current {@link Printer}.
+ *
+ * @throws IllegalArgumentException if a registry is already set.
+ */
+ public Printer usingTypeRegistry(TypeRegistry registry) {
+ if (this.registry != TypeRegistry.getEmptyTypeRegistry()) {
+ throw new IllegalArgumentException("Only one registry is allowed.");
+ }
+ return new Printer(registry, includingDefaultValueFields, preservingProtoFieldNames, omittingInsignificantWhitespace);
+ }
+
+ /**
+ * Creates a new {@link Printer} that will also print fields set to their
+ * defaults. Empty repeated fields and map fields will be printed as well.
+ * The new Printer clones all other configurations from the current
+ * {@link Printer}.
+ */
+ public Printer includingDefaultValueFields() {
+ return new Printer(registry, true, preservingProtoFieldNames, omittingInsignificantWhitespace);
+ }
+
+ /**
+ * Creates a new {@link Printer} that is configured to use the original proto
+ * field names as defined in the .proto file rather than converting them to
+ * lowerCamelCase. The new Printer clones all other configurations from the
+ * current {@link Printer}.
+ */
+ public Printer preservingProtoFieldNames() {
+ return new Printer(registry, includingDefaultValueFields, true, omittingInsignificantWhitespace);
+ }
+
+
+ /**
+ * Create a new {@link Printer} that will omit all insignificant whitespace
+ * in the JSON output. This new Printer clones all other configurations from the
+ * current Printer. Insignificant whitespace is defined by the JSON spec as whitespace
+ * that appear between JSON structural elements:
+ * <pre>
+ * ws = *(
+ * %x20 / ; Space
+ * %x09 / ; Horizontal tab
+ * %x0A / ; Line feed or New line
+ * %x0D ) ; Carriage return
+ * </pre>
+ * See <a href="https://tools.ietf.org/html/rfc7159">https://tools.ietf.org/html/rfc7159</a>
+ * current {@link Printer}.
+ */
+ public Printer omittingInsignificantWhitespace(){
+ return new Printer(registry, includingDefaultValueFields, preservingProtoFieldNames, true);
+ }
+
+ /**
+ * Converts a protobuf message to JSON format.
+ *
+ * @throws InvalidProtocolBufferException if the message contains Any types
+ * that can't be resolved.
+ * @throws IOException if writing to the output fails.
+ */
+ public void appendTo(MessageOrBuilder message, Appendable output) throws IOException {
+ // TODO(xiaofeng): Investigate the allocation overhead and optimize for
+ // mobile.
+ new PrinterImpl(registry, includingDefaultValueFields, preservingProtoFieldNames, output, omittingInsignificantWhitespace)
+ .print(message);
+ }
+
+ /**
+ * Converts a protobuf message to JSON format. Throws exceptions if there
+ * are unknown Any types in the message.
+ */
+ public String print(MessageOrBuilder message) throws InvalidProtocolBufferException {
+ try {
+ StringBuilder builder = new StringBuilder();
+ appendTo(message, builder);
+ return builder.toString();
+ } catch (InvalidProtocolBufferException e) {
+ throw e;
+ } catch (IOException e) {
+ // Unexpected IOException.
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ /**
+ * Creates a {@link Parser} with default configuration.
+ */
+ public static Parser parser() {
+ return new Parser(TypeRegistry.getEmptyTypeRegistry());
+ }
+
+ /**
+ * A Parser parses JSON to protobuf message.
+ */
+ public static class Parser {
+ private final TypeRegistry registry;
+
+ private Parser(TypeRegistry registry) {
+ this.registry = registry;
+ }
+
+ /**
+ * Creates a new {@link Parser} using the given registry. The new Parser
+ * clones all other configurations from this Parser.
+ *
+ * @throws IllegalArgumentException if a registry is already set.
+ */
+ public Parser usingTypeRegistry(TypeRegistry registry) {
+ if (this.registry != TypeRegistry.getEmptyTypeRegistry()) {
+ throw new IllegalArgumentException("Only one registry is allowed.");
+ }
+ return new Parser(registry);
+ }
+
+ /**
+ * Parses from JSON into a protobuf message.
+ *
+ * @throws InvalidProtocolBufferException if the input is not valid JSON
+ * format or there are unknown fields in the input.
+ */
+ public void merge(String json, Message.Builder builder) throws InvalidProtocolBufferException {
+ // TODO(xiaofeng): Investigate the allocation overhead and optimize for
+ // mobile.
+ new ParserImpl(registry).merge(json, builder);
+ }
+
+ /**
+ * Parses from JSON into a protobuf message.
+ *
+ * @throws InvalidProtocolBufferException if the input is not valid JSON
+ * format or there are unknown fields in the input.
+ * @throws IOException if reading from the input throws.
+ */
+ public void merge(Reader json, Message.Builder builder) throws IOException {
+ // TODO(xiaofeng): Investigate the allocation overhead and optimize for
+ // mobile.
+ new ParserImpl(registry).merge(json, builder);
+ }
+ }
+
+ /**
+ * A TypeRegistry is used to resolve Any messages in the JSON conversion.
+ * You must provide a TypeRegistry containing all message types used in
+ * Any message fields, or the JSON conversion will fail because data
+ * in Any message fields is unrecognizable. You don't need to supply a
+ * TypeRegistry if you don't use Any message fields.
+ */
+ public static class TypeRegistry {
+ private static class EmptyTypeRegistryHolder {
+ private static final TypeRegistry EMPTY =
+ new TypeRegistry(Collections.<String, Descriptor>emptyMap());
+ }
+
+ public static TypeRegistry getEmptyTypeRegistry() {
+ return EmptyTypeRegistryHolder.EMPTY;
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /**
+ * Find a type by its full name. Returns null if it cannot be found in
+ * this {@link TypeRegistry}.
+ */
+ public Descriptor find(String name) {
+ return types.get(name);
+ }
+
+ private final Map<String, Descriptor> types;
+
+ private TypeRegistry(Map<String, Descriptor> types) {
+ this.types = types;
+ }
+
+ /**
+ * A Builder is used to build {@link TypeRegistry}.
+ */
+ public static class Builder {
+ private Builder() {}
+
+ /**
+ * Adds a message type and all types defined in the same .proto file as
+ * well as all transitively imported .proto files to this {@link Builder}.
+ */
+ public Builder add(Descriptor messageType) {
+ if (types == null) {
+ throw new IllegalStateException("A TypeRegistry.Builer can only be used once.");
+ }
+ addFile(messageType.getFile());
+ return this;
+ }
+
+ /**
+ * Adds message types and all types defined in the same .proto file as
+ * well as all transitively imported .proto files to this {@link Builder}.
+ */
+ public Builder add(Iterable<Descriptor> messageTypes) {
+ if (types == null) {
+ throw new IllegalStateException("A TypeRegistry.Builer can only be used once.");
+ }
+ for (Descriptor type : messageTypes) {
+ addFile(type.getFile());
+ }
+ return this;
+ }
+
+ /**
+ * Builds a {@link TypeRegistry}. This method can only be called once for
+ * one Builder.
+ */
+ public TypeRegistry build() {
+ TypeRegistry result = new TypeRegistry(types);
+ // Make sure the built {@link TypeRegistry} is immutable.
+ types = null;
+ return result;
+ }
+
+ private void addFile(FileDescriptor file) {
+ // Skip the file if it's already added.
+ if (!files.add(file.getFullName())) {
+ return;
+ }
+ for (FileDescriptor dependency : file.getDependencies()) {
+ addFile(dependency);
+ }
+ for (Descriptor message : file.getMessageTypes()) {
+ addMessage(message);
+ }
+ }
+
+ private void addMessage(Descriptor message) {
+ for (Descriptor nestedType : message.getNestedTypes()) {
+ addMessage(nestedType);
+ }
+
+ if (types.containsKey(message.getFullName())) {
+ logger.warning("Type " + message.getFullName() + " is added multiple times.");
+ return;
+ }
+
+ types.put(message.getFullName(), message);
+ }
+
+ private final Set<String> files = new HashSet<String>();
+ private Map<String, Descriptor> types = new HashMap<String, Descriptor>();
+ }
+ }
+
+ /**
+ * An interface for json formatting that can be used in
+ * combination with the omittingInsignificantWhitespace() method
+ */
+ interface TextGenerator {
+ void indent();
+ void outdent();
+ void print(final CharSequence text) throws IOException;
+ }
+
+
+ /**
+ * Format the json without indentation
+ */
+ private static final class CompactTextGenerator implements TextGenerator{
+ private final Appendable output;
+
+
+ private CompactTextGenerator(final Appendable output) {
+ this.output = output;
+ }
+
+ /**
+ * ignored by compact printer
+ */
+ public void indent() {}
+
+ /**
+ * ignored by compact printer
+ */
+ public void outdent() {}
+
+ /**
+ * Print text to the output stream.
+ */
+ public void print(final CharSequence text) throws IOException {
+ output.append(text);
+ }
+
+ }
+ /**
+ * A TextGenerator adds indentation when writing formatted text.
+ */
+ private static final class PrettyTextGenerator implements TextGenerator{
+ private final Appendable output;
+ private final StringBuilder indent = new StringBuilder();
+ private boolean atStartOfLine = true;
+
+ private PrettyTextGenerator(final Appendable output) {
+ this.output = output;
+ }
+
+ /**
+ * Indent text by two spaces. After calling Indent(), two spaces will be
+ * inserted at the beginning of each line of text. Indent() may be called
+ * multiple times to produce deeper indents.
+ */
+ public void indent() {
+ indent.append(" ");
+ }
+
+ /**
+ * Reduces the current indent level by two spaces, or crashes if the indent
+ * level is zero.
+ */
+ public void outdent() {
+ final int length = indent.length();
+ if (length < 2) {
+ throw new IllegalArgumentException(" Outdent() without matching Indent().");
+ }
+ indent.delete(length - 2, length);
+ }
+
+ /**
+ * Print text to the output stream.
+ */
+ public void print(final CharSequence text) throws IOException {
+ final int size = text.length();
+ int pos = 0;
+
+ for (int i = 0; i < size; i++) {
+ if (text.charAt(i) == '\n') {
+ write(text.subSequence(pos, i + 1));
+ pos = i + 1;
+ atStartOfLine = true;
+ }
+ }
+ write(text.subSequence(pos, size));
+ }
+
+ private void write(final CharSequence data) throws IOException {
+ if (data.length() == 0) {
+ return;
+ }
+ if (atStartOfLine) {
+ atStartOfLine = false;
+ output.append(indent);
+ }
+ output.append(data);
+ }
+ }
+
+ /**
+ * A Printer converts protobuf messages to JSON format.
+ */
+ private static final class PrinterImpl {
+ private final TypeRegistry registry;
+ private final boolean includingDefaultValueFields;
+ private final boolean preservingProtoFieldNames;
+ private final TextGenerator generator;
+ // We use Gson to help handle string escapes.
+ private final Gson gson;
+ private final CharSequence blankOrSpace;
+ private final CharSequence blankOrNewLine;
+
+ private static class GsonHolder {
+ private static final Gson DEFAULT_GSON = new GsonBuilder().disableHtmlEscaping().create();
+ }
+
+ PrinterImpl(
+ TypeRegistry registry,
+ boolean includingDefaultValueFields,
+ boolean preservingProtoFieldNames,
+ Appendable jsonOutput, boolean omittingInsignificantWhitespace) {
+ this.registry = registry;
+ this.includingDefaultValueFields = includingDefaultValueFields;
+ this.preservingProtoFieldNames = preservingProtoFieldNames;
+ this.gson = GsonHolder.DEFAULT_GSON;
+ // json format related properties, determined by printerType
+ if (omittingInsignificantWhitespace) {
+ this.generator = new CompactTextGenerator(jsonOutput);
+ this.blankOrSpace = "";
+ this.blankOrNewLine = "";
+ } else {
+ this.generator = new PrettyTextGenerator(jsonOutput);
+ this.blankOrSpace = " ";
+ this.blankOrNewLine = "\n";
+ }
+ }
+
+ void print(MessageOrBuilder message) throws IOException {
+ WellKnownTypePrinter specialPrinter =
+ wellKnownTypePrinters.get(message.getDescriptorForType().getFullName());
+ if (specialPrinter != null) {
+ specialPrinter.print(this, message);
+ return;
+ }
+ print(message, null);
+ }
+
+ private interface WellKnownTypePrinter {
+ void print(PrinterImpl printer, MessageOrBuilder message) throws IOException;
+ }
+
+ private static final Map<String, WellKnownTypePrinter> wellKnownTypePrinters =
+ buildWellKnownTypePrinters();
+
+ private static Map<String, WellKnownTypePrinter> buildWellKnownTypePrinters() {
+ Map<String, WellKnownTypePrinter> printers = new HashMap<String, WellKnownTypePrinter>();
+ // Special-case Any.
+ printers.put(
+ Any.getDescriptor().getFullName(),
+ new WellKnownTypePrinter() {
+ @Override
+ public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
+ printer.printAny(message);
+ }
+ });
+ // Special-case wrapper types.
+ WellKnownTypePrinter wrappersPrinter =
+ new WellKnownTypePrinter() {
+ @Override
+ public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
+ printer.printWrapper(message);
+ }
+ };
+ printers.put(BoolValue.getDescriptor().getFullName(), wrappersPrinter);
+ printers.put(Int32Value.getDescriptor().getFullName(), wrappersPrinter);
+ printers.put(UInt32Value.getDescriptor().getFullName(), wrappersPrinter);
+ printers.put(Int64Value.getDescriptor().getFullName(), wrappersPrinter);
+ printers.put(UInt64Value.getDescriptor().getFullName(), wrappersPrinter);
+ printers.put(StringValue.getDescriptor().getFullName(), wrappersPrinter);
+ printers.put(BytesValue.getDescriptor().getFullName(), wrappersPrinter);
+ printers.put(FloatValue.getDescriptor().getFullName(), wrappersPrinter);
+ printers.put(DoubleValue.getDescriptor().getFullName(), wrappersPrinter);
+ // Special-case Timestamp.
+ printers.put(
+ Timestamp.getDescriptor().getFullName(),
+ new WellKnownTypePrinter() {
+ @Override
+ public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
+ printer.printTimestamp(message);
+ }
+ });
+ // Special-case Duration.
+ printers.put(
+ Duration.getDescriptor().getFullName(),
+ new WellKnownTypePrinter() {
+ @Override
+ public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
+ printer.printDuration(message);
+ }
+ });
+ // Special-case FieldMask.
+ printers.put(
+ FieldMask.getDescriptor().getFullName(),
+ new WellKnownTypePrinter() {
+ @Override
+ public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
+ printer.printFieldMask(message);
+ }
+ });
+ // Special-case Struct.
+ printers.put(
+ Struct.getDescriptor().getFullName(),
+ new WellKnownTypePrinter() {
+ @Override
+ public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
+ printer.printStruct(message);
+ }
+ });
+ // Special-case Value.
+ printers.put(
+ Value.getDescriptor().getFullName(),
+ new WellKnownTypePrinter() {
+ @Override
+ public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
+ printer.printValue(message);
+ }
+ });
+ // Special-case ListValue.
+ printers.put(
+ ListValue.getDescriptor().getFullName(),
+ new WellKnownTypePrinter() {
+ @Override
+ public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
+ printer.printListValue(message);
+ }
+ });
+ return printers;
+ }
+
+ /** Prints google.protobuf.Any */
+ private void printAny(MessageOrBuilder message) throws IOException {
+ Descriptor descriptor = message.getDescriptorForType();
+ FieldDescriptor typeUrlField = descriptor.findFieldByName("type_url");
+ FieldDescriptor valueField = descriptor.findFieldByName("value");
+ // Validates type of the message. Note that we can't just cast the message
+ // to com.google.protobuf.Any because it might be a DynamicMessage.
+ if (typeUrlField == null
+ || valueField == null
+ || typeUrlField.getType() != FieldDescriptor.Type.STRING
+ || valueField.getType() != FieldDescriptor.Type.BYTES) {
+ throw new InvalidProtocolBufferException("Invalid Any type.");
+ }
+ String typeUrl = (String) message.getField(typeUrlField);
+ String typeName = getTypeName(typeUrl);
+ Descriptor type = registry.find(typeName);
+ if (type == null) {
+ throw new InvalidProtocolBufferException("Cannot find type for url: " + typeUrl);
+ }
+ ByteString content = (ByteString) message.getField(valueField);
+ Message contentMessage =
+ DynamicMessage.getDefaultInstance(type).getParserForType().parseFrom(content);
+ WellKnownTypePrinter printer = wellKnownTypePrinters.get(typeName);
+ if (printer != null) {
+ // If the type is one of the well-known types, we use a special
+ // formatting.
+ generator.print("{" + blankOrNewLine);
+ generator.indent();
+ generator.print("\"@type\":" + blankOrSpace + gson.toJson(typeUrl) + "," + blankOrNewLine);
+ generator.print("\"value\":" + blankOrSpace);
+ printer.print(this, contentMessage);
+ generator.print(blankOrNewLine);
+ generator.outdent();
+ generator.print("}");
+ } else {
+ // Print the content message instead (with a "@type" field added).
+ print(contentMessage, typeUrl);
+ }
+ }
+
+ /** Prints wrapper types (e.g., google.protobuf.Int32Value) */
+ private void printWrapper(MessageOrBuilder message) throws IOException {
+ Descriptor descriptor = message.getDescriptorForType();
+ FieldDescriptor valueField = descriptor.findFieldByName("value");
+ if (valueField == null) {
+ throw new InvalidProtocolBufferException("Invalid Wrapper type.");
+ }
+ // When formatting wrapper types, we just print its value field instead of
+ // the whole message.
+ printSingleFieldValue(valueField, message.getField(valueField));
+ }
+
+ private ByteString toByteString(MessageOrBuilder message) {
+ if (message instanceof Message) {
+ return ((Message) message).toByteString();
+ } else {
+ return ((Message.Builder) message).build().toByteString();
+ }
+ }
+
+ /** Prints google.protobuf.Timestamp */
+ private void printTimestamp(MessageOrBuilder message) throws IOException {
+ Timestamp value = Timestamp.parseFrom(toByteString(message));
+ generator.print("\"" + Timestamps.toString(value) + "\"");
+ }
+
+ /** Prints google.protobuf.Duration */
+ private void printDuration(MessageOrBuilder message) throws IOException {
+ Duration value = Duration.parseFrom(toByteString(message));
+ generator.print("\"" + Durations.toString(value) + "\"");
+ }
+
+ /** Prints google.protobuf.FieldMask */
+ private void printFieldMask(MessageOrBuilder message) throws IOException {
+ FieldMask value = FieldMask.parseFrom(toByteString(message));
+ generator.print("\"" + FieldMaskUtil.toJsonString(value) + "\"");
+ }
+
+ /** Prints google.protobuf.Struct */
+ private void printStruct(MessageOrBuilder message) throws IOException {
+ Descriptor descriptor = message.getDescriptorForType();
+ FieldDescriptor field = descriptor.findFieldByName("fields");
+ if (field == null) {
+ throw new InvalidProtocolBufferException("Invalid Struct type.");
+ }
+ // Struct is formatted as a map object.
+ printMapFieldValue(field, message.getField(field));
+ }
+
+ /** Prints google.protobuf.Value */
+ private void printValue(MessageOrBuilder message) throws IOException {
+ // For a Value message, only the value of the field is formatted.
+ Map<FieldDescriptor, Object> fields = message.getAllFields();
+ if (fields.isEmpty()) {
+ // No value set.
+ generator.print("null");
+ return;
+ }
+ // A Value message can only have at most one field set (it only contains
+ // an oneof).
+ if (fields.size() != 1) {
+ throw new InvalidProtocolBufferException("Invalid Value type.");
+ }
+ for (Map.Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
+ printSingleFieldValue(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /** Prints google.protobuf.ListValue */
+ private void printListValue(MessageOrBuilder message) throws IOException {
+ Descriptor descriptor = message.getDescriptorForType();
+ FieldDescriptor field = descriptor.findFieldByName("values");
+ if (field == null) {
+ throw new InvalidProtocolBufferException("Invalid ListValue type.");
+ }
+ printRepeatedFieldValue(field, message.getField(field));
+ }
+
+ /** Prints a regular message with an optional type URL. */
+
+ private void print(MessageOrBuilder message, String typeUrl)
+ throws IOException {
+ generator.print("{" + blankOrNewLine);
+ generator.indent();
+
+ boolean printedField = false;
+ if (typeUrl != null) {
+ generator.print("\"@type\":" + blankOrSpace + gson.toJson(typeUrl));
+ printedField = true;
+ }
+ Map<FieldDescriptor, Object> fieldsToPrint = null;
+ if (includingDefaultValueFields) {
+ fieldsToPrint = new TreeMap<FieldDescriptor, Object>();
+ for (FieldDescriptor field : message.getDescriptorForType().getFields()) {
+ if (field.isOptional()
+ && field.getJavaType() == FieldDescriptor.JavaType.MESSAGE
+ && !message.hasField(field)) {
+ // Always skip empty optional message fields. If not we will recurse indefinitely if
+ // a message has itself as a sub-field.
+ continue;
+ }
+ fieldsToPrint.put(field, message.getField(field));
+ }
+ } else {
+ fieldsToPrint = message.getAllFields();
+ }
+ for (Map.Entry<FieldDescriptor, Object> field : fieldsToPrint.entrySet()) {
+ if (printedField) {
+ // Add line-endings for the previous field.
+ generator.print("," + blankOrNewLine);
+ } else {
+ printedField = true;
+ }
+ printField(field.getKey(), field.getValue());
+ }
+
+ // Add line-endings for the last field.
+ if (printedField) {
+ generator.print(blankOrNewLine);
+ }
+ generator.outdent();
+ generator.print("}");
+ }
+
+ private void printField(FieldDescriptor field, Object value) throws IOException {
+ if (preservingProtoFieldNames) {
+ generator.print("\"" + field.getName() + "\":" + blankOrSpace);
+ } else {
+ generator.print("\"" + field.getJsonName() + "\":" + blankOrSpace);
+ }
+ if (field.isMapField()) {
+ printMapFieldValue(field, value);
+ } else if (field.isRepeated()) {
+ printRepeatedFieldValue(field, value);
+ } else {
+ printSingleFieldValue(field, value);
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void printRepeatedFieldValue(FieldDescriptor field, Object value) throws IOException {
+ generator.print("[");
+ boolean printedElement = false;
+ for (Object element : (List) value) {
+ if (printedElement) {
+ generator.print("," + blankOrSpace);
+ } else {
+ printedElement = true;
+ }
+ printSingleFieldValue(field, element);
+ }
+ generator.print("]");
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void printMapFieldValue(FieldDescriptor field, Object value) throws IOException {
+ Descriptor type = field.getMessageType();
+ FieldDescriptor keyField = type.findFieldByName("key");
+ FieldDescriptor valueField = type.findFieldByName("value");
+ if (keyField == null || valueField == null) {
+ throw new InvalidProtocolBufferException("Invalid map field.");
+ }
+ generator.print("{" + blankOrNewLine);
+ generator.indent();
+ boolean printedElement = false;
+ for (Object element : (List) value) {
+ Message entry = (Message) element;
+ Object entryKey = entry.getField(keyField);
+ Object entryValue = entry.getField(valueField);
+ if (printedElement) {
+ generator.print("," + blankOrNewLine);
+ } else {
+ printedElement = true;
+ }
+ // Key fields are always double-quoted.
+ printSingleFieldValue(keyField, entryKey, true);
+ generator.print(":" + blankOrSpace);
+ printSingleFieldValue(valueField, entryValue);
+ }
+ if (printedElement) {
+ generator.print(blankOrNewLine);
+ }
+ generator.outdent();
+ generator.print("}");
+ }
+
+ private void printSingleFieldValue(FieldDescriptor field, Object value) throws IOException {
+ printSingleFieldValue(field, value, false);
+ }
+
+ /**
+ * Prints a field's value in JSON format.
+ *
+ * @param alwaysWithQuotes whether to always add double-quotes to primitive
+ * types.
+ */
+ private void printSingleFieldValue(
+ final FieldDescriptor field, final Object value, boolean alwaysWithQuotes)
+ throws IOException {
+ switch (field.getType()) {
+ case INT32:
+ case SINT32:
+ case SFIXED32:
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ generator.print(((Integer) value).toString());
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ break;
+
+ case INT64:
+ case SINT64:
+ case SFIXED64:
+ generator.print("\"" + ((Long) value).toString() + "\"");
+ break;
+
+ case BOOL:
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ if (((Boolean) value).booleanValue()) {
+ generator.print("true");
+ } else {
+ generator.print("false");
+ }
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ break;
+
+ case FLOAT:
+ Float floatValue = (Float) value;
+ if (floatValue.isNaN()) {
+ generator.print("\"NaN\"");
+ } else if (floatValue.isInfinite()) {
+ if (floatValue < 0) {
+ generator.print("\"-Infinity\"");
+ } else {
+ generator.print("\"Infinity\"");
+ }
+ } else {
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ generator.print(floatValue.toString());
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ }
+ break;
+
+ case DOUBLE:
+ Double doubleValue = (Double) value;
+ if (doubleValue.isNaN()) {
+ generator.print("\"NaN\"");
+ } else if (doubleValue.isInfinite()) {
+ if (doubleValue < 0) {
+ generator.print("\"-Infinity\"");
+ } else {
+ generator.print("\"Infinity\"");
+ }
+ } else {
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ generator.print(doubleValue.toString());
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ }
+ break;
+
+ case UINT32:
+ case FIXED32:
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ generator.print(unsignedToString((Integer) value));
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ break;
+
+ case UINT64:
+ case FIXED64:
+ generator.print("\"" + unsignedToString((Long) value) + "\"");
+ break;
+
+ case STRING:
+ generator.print(gson.toJson(value));
+ break;
+
+ case BYTES:
+ generator.print("\"");
+ generator.print(BaseEncoding.base64().encode(((ByteString) value).toByteArray()));
+ generator.print("\"");
+ break;
+
+ case ENUM:
+ // Special-case google.protobuf.NullValue (it's an Enum).
+ if (field.getEnumType().getFullName().equals("google.protobuf.NullValue")) {
+ // No matter what value it contains, we always print it as "null".
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ generator.print("null");
+ if (alwaysWithQuotes) {
+ generator.print("\"");
+ }
+ } else {
+ if (((EnumValueDescriptor) value).getIndex() == -1) {
+ generator.print(String.valueOf(((EnumValueDescriptor) value).getNumber()));
+ } else {
+ generator.print("\"" + ((EnumValueDescriptor) value).getName() + "\"");
+ }
+ }
+ break;
+
+ case MESSAGE:
+ case GROUP:
+ print((Message) value);
+ break;
+ }
+ }
+ }
+
+ /** Convert an unsigned 32-bit integer to a string. */
+ private static String unsignedToString(final int value) {
+ if (value >= 0) {
+ return Integer.toString(value);
+ } else {
+ return Long.toString(value & 0x00000000FFFFFFFFL);
+ }
+ }
+
+ /** Convert an unsigned 64-bit integer to a string. */
+ private static String unsignedToString(final long value) {
+ if (value >= 0) {
+ return Long.toString(value);
+ } else {
+ // Pull off the most-significant bit so that BigInteger doesn't think
+ // the number is negative, then set it again using setBit().
+ return BigInteger.valueOf(value & Long.MAX_VALUE).setBit(Long.SIZE - 1).toString();
+ }
+ }
+
+ private static String getTypeName(String typeUrl) throws InvalidProtocolBufferException {
+ String[] parts = typeUrl.split("/");
+ if (parts.length == 1) {
+ throw new InvalidProtocolBufferException("Invalid type url found: " + typeUrl);
+ }
+ return parts[parts.length - 1];
+ }
+
+ private static class ParserImpl {
+ private final TypeRegistry registry;
+ private final JsonParser jsonParser;
+
+ ParserImpl(TypeRegistry registry) {
+ this.registry = registry;
+ this.jsonParser = new JsonParser();
+ }
+
+ void merge(Reader json, Message.Builder builder) throws IOException {
+ JsonReader reader = new JsonReader(json);
+ reader.setLenient(false);
+ merge(jsonParser.parse(reader), builder);
+ }
+
+ void merge(String json, Message.Builder builder) throws InvalidProtocolBufferException {
+ try {
+ JsonReader reader = new JsonReader(new StringReader(json));
+ reader.setLenient(false);
+ merge(jsonParser.parse(reader), builder);
+ } catch (InvalidProtocolBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ // We convert all exceptions from JSON parsing to our own exceptions.
+ throw new InvalidProtocolBufferException(e.getMessage());
+ }
+ }
+
+ private interface WellKnownTypeParser {
+ void merge(ParserImpl parser, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException;
+ }
+
+ private static final Map<String, WellKnownTypeParser> wellKnownTypeParsers =
+ buildWellKnownTypeParsers();
+
+ private static Map<String, WellKnownTypeParser> buildWellKnownTypeParsers() {
+ Map<String, WellKnownTypeParser> parsers = new HashMap<String, WellKnownTypeParser>();
+ // Special-case Any.
+ parsers.put(
+ Any.getDescriptor().getFullName(),
+ new WellKnownTypeParser() {
+ @Override
+ public void merge(ParserImpl parser, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ parser.mergeAny(json, builder);
+ }
+ });
+ // Special-case wrapper types.
+ WellKnownTypeParser wrappersPrinter =
+ new WellKnownTypeParser() {
+ @Override
+ public void merge(ParserImpl parser, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ parser.mergeWrapper(json, builder);
+ }
+ };
+ parsers.put(BoolValue.getDescriptor().getFullName(), wrappersPrinter);
+ parsers.put(Int32Value.getDescriptor().getFullName(), wrappersPrinter);
+ parsers.put(UInt32Value.getDescriptor().getFullName(), wrappersPrinter);
+ parsers.put(Int64Value.getDescriptor().getFullName(), wrappersPrinter);
+ parsers.put(UInt64Value.getDescriptor().getFullName(), wrappersPrinter);
+ parsers.put(StringValue.getDescriptor().getFullName(), wrappersPrinter);
+ parsers.put(BytesValue.getDescriptor().getFullName(), wrappersPrinter);
+ parsers.put(FloatValue.getDescriptor().getFullName(), wrappersPrinter);
+ parsers.put(DoubleValue.getDescriptor().getFullName(), wrappersPrinter);
+ // Special-case Timestamp.
+ parsers.put(
+ Timestamp.getDescriptor().getFullName(),
+ new WellKnownTypeParser() {
+ @Override
+ public void merge(ParserImpl parser, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ parser.mergeTimestamp(json, builder);
+ }
+ });
+ // Special-case Duration.
+ parsers.put(
+ Duration.getDescriptor().getFullName(),
+ new WellKnownTypeParser() {
+ @Override
+ public void merge(ParserImpl parser, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ parser.mergeDuration(json, builder);
+ }
+ });
+ // Special-case FieldMask.
+ parsers.put(
+ FieldMask.getDescriptor().getFullName(),
+ new WellKnownTypeParser() {
+ @Override
+ public void merge(ParserImpl parser, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ parser.mergeFieldMask(json, builder);
+ }
+ });
+ // Special-case Struct.
+ parsers.put(
+ Struct.getDescriptor().getFullName(),
+ new WellKnownTypeParser() {
+ @Override
+ public void merge(ParserImpl parser, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ parser.mergeStruct(json, builder);
+ }
+ });
+ // Special-case ListValue.
+ parsers.put(
+ ListValue.getDescriptor().getFullName(),
+ new WellKnownTypeParser() {
+ @Override
+ public void merge(ParserImpl parser, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ parser.mergeListValue(json, builder);
+ }
+ });
+ // Special-case Value.
+ parsers.put(
+ Value.getDescriptor().getFullName(),
+ new WellKnownTypeParser() {
+ @Override
+ public void merge(ParserImpl parser, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ parser.mergeValue(json, builder);
+ }
+ });
+ return parsers;
+ }
+
+ private void merge(JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ WellKnownTypeParser specialParser =
+ wellKnownTypeParsers.get(builder.getDescriptorForType().getFullName());
+ if (specialParser != null) {
+ specialParser.merge(this, json, builder);
+ return;
+ }
+ mergeMessage(json, builder, false);
+ }
+
+ // Maps from camel-case field names to FieldDescriptor.
+ private final Map<Descriptor, Map<String, FieldDescriptor>> fieldNameMaps =
+ new HashMap<Descriptor, Map<String, FieldDescriptor>>();
+
+ private Map<String, FieldDescriptor> getFieldNameMap(Descriptor descriptor) {
+ if (!fieldNameMaps.containsKey(descriptor)) {
+ Map<String, FieldDescriptor> fieldNameMap = new HashMap<String, FieldDescriptor>();
+ for (FieldDescriptor field : descriptor.getFields()) {
+ fieldNameMap.put(field.getName(), field);
+ fieldNameMap.put(field.getJsonName(), field);
+ }
+ fieldNameMaps.put(descriptor, fieldNameMap);
+ return fieldNameMap;
+ }
+ return fieldNameMaps.get(descriptor);
+ }
+
+ private void mergeMessage(JsonElement json, Message.Builder builder, boolean skipTypeUrl)
+ throws InvalidProtocolBufferException {
+ if (!(json instanceof JsonObject)) {
+ throw new InvalidProtocolBufferException("Expect message object but got: " + json);
+ }
+ JsonObject object = (JsonObject) json;
+ Map<String, FieldDescriptor> fieldNameMap = getFieldNameMap(builder.getDescriptorForType());
+ for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
+ if (skipTypeUrl && entry.getKey().equals("@type")) {
+ continue;
+ }
+ FieldDescriptor field = fieldNameMap.get(entry.getKey());
+ if (field == null) {
+ throw new InvalidProtocolBufferException(
+ "Cannot find field: "
+ + entry.getKey()
+ + " in message "
+ + builder.getDescriptorForType().getFullName());
+ }
+ mergeField(field, entry.getValue(), builder);
+ }
+ }
+
+ private void mergeAny(JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ Descriptor descriptor = builder.getDescriptorForType();
+ FieldDescriptor typeUrlField = descriptor.findFieldByName("type_url");
+ FieldDescriptor valueField = descriptor.findFieldByName("value");
+ // Validates type of the message. Note that we can't just cast the message
+ // to com.google.protobuf.Any because it might be a DynamicMessage.
+ if (typeUrlField == null
+ || valueField == null
+ || typeUrlField.getType() != FieldDescriptor.Type.STRING
+ || valueField.getType() != FieldDescriptor.Type.BYTES) {
+ throw new InvalidProtocolBufferException("Invalid Any type.");
+ }
+
+ if (!(json instanceof JsonObject)) {
+ throw new InvalidProtocolBufferException("Expect message object but got: " + json);
+ }
+ JsonObject object = (JsonObject) json;
+ JsonElement typeUrlElement = object.get("@type");
+ if (typeUrlElement == null) {
+ throw new InvalidProtocolBufferException("Missing type url when parsing: " + json);
+ }
+ String typeUrl = typeUrlElement.getAsString();
+ Descriptor contentType = registry.find(getTypeName(typeUrl));
+ if (contentType == null) {
+ throw new InvalidProtocolBufferException("Cannot resolve type: " + typeUrl);
+ }
+ builder.setField(typeUrlField, typeUrl);
+ Message.Builder contentBuilder =
+ DynamicMessage.getDefaultInstance(contentType).newBuilderForType();
+ WellKnownTypeParser specialParser = wellKnownTypeParsers.get(contentType.getFullName());
+ if (specialParser != null) {
+ JsonElement value = object.get("value");
+ if (value != null) {
+ specialParser.merge(this, value, contentBuilder);
+ }
+ } else {
+ mergeMessage(json, contentBuilder, true);
+ }
+ builder.setField(valueField, contentBuilder.build().toByteString());
+ }
+
+ private void mergeFieldMask(JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ FieldMask value = FieldMaskUtil.fromJsonString(json.getAsString());
+ builder.mergeFrom(value.toByteString());
+ }
+
+ private void mergeTimestamp(JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ try {
+ Timestamp value = Timestamps.parse(json.getAsString());
+ builder.mergeFrom(value.toByteString());
+ } catch (ParseException e) {
+ throw new InvalidProtocolBufferException("Failed to parse timestamp: " + json);
+ }
+ }
+
+ private void mergeDuration(JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ try {
+ Duration value = Durations.parse(json.getAsString());
+ builder.mergeFrom(value.toByteString());
+ } catch (ParseException e) {
+ throw new InvalidProtocolBufferException("Failed to parse duration: " + json);
+ }
+ }
+
+ private void mergeStruct(JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ Descriptor descriptor = builder.getDescriptorForType();
+ FieldDescriptor field = descriptor.findFieldByName("fields");
+ if (field == null) {
+ throw new InvalidProtocolBufferException("Invalid Struct type.");
+ }
+ mergeMapField(field, json, builder);
+ }
+
+ private void mergeListValue(JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ Descriptor descriptor = builder.getDescriptorForType();
+ FieldDescriptor field = descriptor.findFieldByName("values");
+ if (field == null) {
+ throw new InvalidProtocolBufferException("Invalid ListValue type.");
+ }
+ mergeRepeatedField(field, json, builder);
+ }
+
+ private void mergeValue(JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ Descriptor type = builder.getDescriptorForType();
+ if (json instanceof JsonPrimitive) {
+ JsonPrimitive primitive = (JsonPrimitive) json;
+ if (primitive.isBoolean()) {
+ builder.setField(type.findFieldByName("bool_value"), primitive.getAsBoolean());
+ } else if (primitive.isNumber()) {
+ builder.setField(type.findFieldByName("number_value"), primitive.getAsDouble());
+ } else {
+ builder.setField(type.findFieldByName("string_value"), primitive.getAsString());
+ }
+ } else if (json instanceof JsonObject) {
+ FieldDescriptor field = type.findFieldByName("struct_value");
+ Message.Builder structBuilder = builder.newBuilderForField(field);
+ merge(json, structBuilder);
+ builder.setField(field, structBuilder.build());
+ } else if (json instanceof JsonArray) {
+ FieldDescriptor field = type.findFieldByName("list_value");
+ Message.Builder listBuilder = builder.newBuilderForField(field);
+ merge(json, listBuilder);
+ builder.setField(field, listBuilder.build());
+ } else {
+ throw new IllegalStateException("Unexpected json data: " + json);
+ }
+ }
+
+ private void mergeWrapper(JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ Descriptor type = builder.getDescriptorForType();
+ FieldDescriptor field = type.findFieldByName("value");
+ if (field == null) {
+ throw new InvalidProtocolBufferException("Invalid wrapper type: " + type.getFullName());
+ }
+ builder.setField(field, parseFieldValue(field, json, builder));
+ }
+
+ private void mergeField(FieldDescriptor field, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ if (field.isRepeated()) {
+ if (builder.getRepeatedFieldCount(field) > 0) {
+ throw new InvalidProtocolBufferException(
+ "Field " + field.getFullName() + " has already been set.");
+ }
+ } else {
+ if (builder.hasField(field)) {
+ throw new InvalidProtocolBufferException(
+ "Field " + field.getFullName() + " has already been set.");
+ }
+ if (field.getContainingOneof() != null
+ && builder.getOneofFieldDescriptor(field.getContainingOneof()) != null) {
+ FieldDescriptor other = builder.getOneofFieldDescriptor(field.getContainingOneof());
+ throw new InvalidProtocolBufferException(
+ "Cannot set field "
+ + field.getFullName()
+ + " because another field "
+ + other.getFullName()
+ + " belonging to the same oneof has already been set ");
+ }
+ }
+ if (field.isRepeated() && json instanceof JsonNull) {
+ // We allow "null" as value for all field types and treat it as if the
+ // field is not present.
+ return;
+ }
+ if (field.isMapField()) {
+ mergeMapField(field, json, builder);
+ } else if (field.isRepeated()) {
+ mergeRepeatedField(field, json, builder);
+ } else {
+ Object value = parseFieldValue(field, json, builder);
+ if (value != null) {
+ builder.setField(field, value);
+ }
+ }
+ }
+
+ private void mergeMapField(FieldDescriptor field, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ if (!(json instanceof JsonObject)) {
+ throw new InvalidProtocolBufferException("Expect a map object but found: " + json);
+ }
+ Descriptor type = field.getMessageType();
+ FieldDescriptor keyField = type.findFieldByName("key");
+ FieldDescriptor valueField = type.findFieldByName("value");
+ if (keyField == null || valueField == null) {
+ throw new InvalidProtocolBufferException("Invalid map field: " + field.getFullName());
+ }
+ JsonObject object = (JsonObject) json;
+ for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
+ Message.Builder entryBuilder = builder.newBuilderForField(field);
+ Object key = parseFieldValue(keyField, new JsonPrimitive(entry.getKey()), entryBuilder);
+ Object value = parseFieldValue(valueField, entry.getValue(), entryBuilder);
+ if (value == null) {
+ throw new InvalidProtocolBufferException("Map value cannot be null.");
+ }
+ entryBuilder.setField(keyField, key);
+ entryBuilder.setField(valueField, value);
+ builder.addRepeatedField(field, entryBuilder.build());
+ }
+ }
+
+ /**
+ * Gets the default value for a field type. Note that we use proto3
+ * language defaults and ignore any default values set through the
+ * proto "default" option.
+ */
+ private Object getDefaultValue(FieldDescriptor field, Message.Builder builder) {
+ switch (field.getType()) {
+ case INT32:
+ case SINT32:
+ case SFIXED32:
+ case UINT32:
+ case FIXED32:
+ return 0;
+ case INT64:
+ case SINT64:
+ case SFIXED64:
+ case UINT64:
+ case FIXED64:
+ return 0L;
+ case FLOAT:
+ return 0.0f;
+ case DOUBLE:
+ return 0.0;
+ case BOOL:
+ return false;
+ case STRING:
+ return "";
+ case BYTES:
+ return ByteString.EMPTY;
+ case ENUM:
+ return field.getEnumType().getValues().get(0);
+ case MESSAGE:
+ case GROUP:
+ return builder.newBuilderForField(field).getDefaultInstanceForType();
+ default:
+ throw new IllegalStateException("Invalid field type: " + field.getType());
+ }
+ }
+
+ private void mergeRepeatedField(
+ FieldDescriptor field, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ if (!(json instanceof JsonArray)) {
+ throw new InvalidProtocolBufferException("Expect an array but found: " + json);
+ }
+ JsonArray array = (JsonArray) json;
+ for (int i = 0; i < array.size(); ++i) {
+ Object value = parseFieldValue(field, array.get(i), builder);
+ if (value == null) {
+ throw new InvalidProtocolBufferException("Repeated field elements cannot be null");
+ }
+ builder.addRepeatedField(field, value);
+ }
+ }
+
+ private int parseInt32(JsonElement json) throws InvalidProtocolBufferException {
+ try {
+ return Integer.parseInt(json.getAsString());
+ } catch (Exception e) {
+ // Fall through.
+ }
+ // JSON doesn't distinguish between integer values and floating point values so "1" and
+ // "1.000" are treated as equal in JSON. For this reason we accept floating point values for
+ // integer fields as well as long as it actually is an integer (i.e., round(value) == value).
+ try {
+ BigDecimal value = new BigDecimal(json.getAsString());
+ return value.intValueExact();
+ } catch (Exception e) {
+ throw new InvalidProtocolBufferException("Not an int32 value: " + json);
+ }
+ }
+
+ private long parseInt64(JsonElement json) throws InvalidProtocolBufferException {
+ try {
+ return Long.parseLong(json.getAsString());
+ } catch (Exception e) {
+ // Fall through.
+ }
+ // JSON doesn't distinguish between integer values and floating point values so "1" and
+ // "1.000" are treated as equal in JSON. For this reason we accept floating point values for
+ // integer fields as well as long as it actually is an integer (i.e., round(value) == value).
+ try {
+ BigDecimal value = new BigDecimal(json.getAsString());
+ return value.longValueExact();
+ } catch (Exception e) {
+ throw new InvalidProtocolBufferException("Not an int32 value: " + json);
+ }
+ }
+
+ private int parseUint32(JsonElement json) throws InvalidProtocolBufferException {
+ try {
+ long result = Long.parseLong(json.getAsString());
+ if (result < 0 || result > 0xFFFFFFFFL) {
+ throw new InvalidProtocolBufferException("Out of range uint32 value: " + json);
+ }
+ return (int) result;
+ } catch (InvalidProtocolBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ // Fall through.
+ }
+ // JSON doesn't distinguish between integer values and floating point values so "1" and
+ // "1.000" are treated as equal in JSON. For this reason we accept floating point values for
+ // integer fields as well as long as it actually is an integer (i.e., round(value) == value).
+ try {
+ BigDecimal decimalValue = new BigDecimal(json.getAsString());
+ BigInteger value = decimalValue.toBigIntegerExact();
+ if (value.signum() < 0 || value.compareTo(new BigInteger("FFFFFFFF", 16)) > 0) {
+ throw new InvalidProtocolBufferException("Out of range uint32 value: " + json);
+ }
+ return value.intValue();
+ } catch (InvalidProtocolBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new InvalidProtocolBufferException("Not an uint32 value: " + json);
+ }
+ }
+
+ private static final BigInteger MAX_UINT64 = new BigInteger("FFFFFFFFFFFFFFFF", 16);
+
+ private long parseUint64(JsonElement json) throws InvalidProtocolBufferException {
+ try {
+ BigDecimal decimalValue = new BigDecimal(json.getAsString());
+ BigInteger value = decimalValue.toBigIntegerExact();
+ if (value.compareTo(BigInteger.ZERO) < 0 || value.compareTo(MAX_UINT64) > 0) {
+ throw new InvalidProtocolBufferException("Out of range uint64 value: " + json);
+ }
+ return value.longValue();
+ } catch (InvalidProtocolBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new InvalidProtocolBufferException("Not an uint64 value: " + json);
+ }
+ }
+
+ private boolean parseBool(JsonElement json) throws InvalidProtocolBufferException {
+ if (json.getAsString().equals("true")) {
+ return true;
+ }
+ if (json.getAsString().equals("false")) {
+ return false;
+ }
+ throw new InvalidProtocolBufferException("Invalid bool value: " + json);
+ }
+
+ private static final double EPSILON = 1e-6;
+
+ private float parseFloat(JsonElement json) throws InvalidProtocolBufferException {
+ if (json.getAsString().equals("NaN")) {
+ return Float.NaN;
+ } else if (json.getAsString().equals("Infinity")) {
+ return Float.POSITIVE_INFINITY;
+ } else if (json.getAsString().equals("-Infinity")) {
+ return Float.NEGATIVE_INFINITY;
+ }
+ try {
+ // We don't use Float.parseFloat() here because that function simply
+ // accepts all double values. Here we parse the value into a Double
+ // and do explicit range check on it.
+ double value = Double.parseDouble(json.getAsString());
+ // When a float value is printed, the printed value might be a little
+ // larger or smaller due to precision loss. Here we need to add a bit
+ // of tolerance when checking whether the float value is in range.
+ if (value > Float.MAX_VALUE * (1.0 + EPSILON)
+ || value < -Float.MAX_VALUE * (1.0 + EPSILON)) {
+ throw new InvalidProtocolBufferException("Out of range float value: " + json);
+ }
+ return (float) value;
+ } catch (InvalidProtocolBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new InvalidProtocolBufferException("Not a float value: " + json);
+ }
+ }
+
+ private static final BigDecimal MORE_THAN_ONE = new BigDecimal(String.valueOf(1.0 + EPSILON));
+ // When a float value is printed, the printed value might be a little
+ // larger or smaller due to precision loss. Here we need to add a bit
+ // of tolerance when checking whether the float value is in range.
+ private static final BigDecimal MAX_DOUBLE =
+ new BigDecimal(String.valueOf(Double.MAX_VALUE)).multiply(MORE_THAN_ONE);
+ private static final BigDecimal MIN_DOUBLE =
+ new BigDecimal(String.valueOf(-Double.MAX_VALUE)).multiply(MORE_THAN_ONE);
+
+ private double parseDouble(JsonElement json) throws InvalidProtocolBufferException {
+ if (json.getAsString().equals("NaN")) {
+ return Double.NaN;
+ } else if (json.getAsString().equals("Infinity")) {
+ return Double.POSITIVE_INFINITY;
+ } else if (json.getAsString().equals("-Infinity")) {
+ return Double.NEGATIVE_INFINITY;
+ }
+ try {
+ // We don't use Double.parseDouble() here because that function simply
+ // accepts all values. Here we parse the value into a BigDecimal and do
+ // explicit range check on it.
+ BigDecimal value = new BigDecimal(json.getAsString());
+ if (value.compareTo(MAX_DOUBLE) > 0 || value.compareTo(MIN_DOUBLE) < 0) {
+ throw new InvalidProtocolBufferException("Out of range double value: " + json);
+ }
+ return value.doubleValue();
+ } catch (InvalidProtocolBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new InvalidProtocolBufferException("Not an double value: " + json);
+ }
+ }
+
+ private String parseString(JsonElement json) {
+ return json.getAsString();
+ }
+
+ private ByteString parseBytes(JsonElement json) throws InvalidProtocolBufferException {
+ String encoded = json.getAsString();
+ if (encoded.length() % 4 != 0) {
+ throw new InvalidProtocolBufferException(
+ "Bytes field is not encoded in standard BASE64 with paddings: " + encoded);
+ }
+ return ByteString.copyFrom(BaseEncoding.base64().decode(json.getAsString()));
+ }
+
+ private EnumValueDescriptor parseEnum(EnumDescriptor enumDescriptor, JsonElement json)
+ throws InvalidProtocolBufferException {
+ String value = json.getAsString();
+ EnumValueDescriptor result = enumDescriptor.findValueByName(value);
+ if (result == null) {
+ // Try to interpret the value as a number.
+ try {
+ int numericValue = parseInt32(json);
+ if (enumDescriptor.getFile().getSyntax() == FileDescriptor.Syntax.PROTO3) {
+ result = enumDescriptor.findValueByNumberCreatingIfUnknown(numericValue);
+ } else {
+ result = enumDescriptor.findValueByNumber(numericValue);
+ }
+ } catch (InvalidProtocolBufferException e) {
+ // Fall through. This exception is about invalid int32 value we get from parseInt32() but
+ // that's not the exception we want the user to see. Since result == null, we will throw
+ // an exception later.
+ }
+
+ if (result == null) {
+ throw new InvalidProtocolBufferException(
+ "Invalid enum value: " + value + " for enum type: " + enumDescriptor.getFullName());
+ }
+ }
+ return result;
+ }
+
+ private Object parseFieldValue(FieldDescriptor field, JsonElement json, Message.Builder builder)
+ throws InvalidProtocolBufferException {
+ if (json instanceof JsonNull) {
+ if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE
+ && field.getMessageType().getFullName().equals(Value.getDescriptor().getFullName())) {
+ // For every other type, "null" means absence, but for the special
+ // Value message, it means the "null_value" field has been set.
+ Value value = Value.newBuilder().setNullValueValue(0).build();
+ return builder.newBuilderForField(field).mergeFrom(value.toByteString()).build();
+ } else if (field.getJavaType() == FieldDescriptor.JavaType.ENUM
+ && field.getEnumType().getFullName().equals(NullValue.getDescriptor().getFullName())) {
+ // If the type of the field is a NullValue, then the value should be explicitly set.
+ return field.getEnumType().findValueByNumber(0);
+ }
+ return null;
+ }
+ switch (field.getType()) {
+ case INT32:
+ case SINT32:
+ case SFIXED32:
+ return parseInt32(json);
+
+ case INT64:
+ case SINT64:
+ case SFIXED64:
+ return parseInt64(json);
+
+ case BOOL:
+ return parseBool(json);
+
+ case FLOAT:
+ return parseFloat(json);
+
+ case DOUBLE:
+ return parseDouble(json);
+
+ case UINT32:
+ case FIXED32:
+ return parseUint32(json);
+
+ case UINT64:
+ case FIXED64:
+ return parseUint64(json);
+
+ case STRING:
+ return parseString(json);
+
+ case BYTES:
+ return parseBytes(json);
+
+ case ENUM:
+ return parseEnum(field.getEnumType(), json);
+
+ case MESSAGE:
+ case GROUP:
+ Message.Builder subBuilder = builder.newBuilderForField(field);
+ merge(json, subBuilder);
+ return subBuilder.build();
+
+ default:
+ throw new InvalidProtocolBufferException("Invalid field type: " + field.getType());
+ }
+ }
+ }
+}
diff --git a/java/util/src/main/java/com/google/protobuf/util/TimeUtil.java b/java/util/src/main/java/com/google/protobuf/util/TimeUtil.java
new file mode 100644
index 00000000..04758473
--- /dev/null
+++ b/java/util/src/main/java/com/google/protobuf/util/TimeUtil.java
@@ -0,0 +1,400 @@
+// 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 java.math.BigInteger;
+import java.text.ParseException;
+
+/**
+ * Utilities to help create/manipulate Timestamp/Duration
+ *
+ * @deprecated Use {@link Durations} and {@link Timestamps} instead.
+ */
+@Deprecated
+public final class TimeUtil {
+ // Timestamp for "0001-01-01T00:00:00Z"
+ public static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
+
+ // Timestamp for "9999-12-31T23:59:59Z"
+ public static final long TIMESTAMP_SECONDS_MAX = 253402300799L;
+ public static final long DURATION_SECONDS_MIN = -315576000000L;
+ public static final long DURATION_SECONDS_MAX = 315576000000L;
+
+ private static final long NANOS_PER_SECOND = 1000000000;
+
+ private TimeUtil() {}
+
+ /**
+ * Convert Timestamp to RFC 3339 date string format. The output will always
+ * be Z-normalized and uses 3, 6 or 9 fractional digits as required to
+ * represent the exact value. Note that Timestamp can only represent time
+ * from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. See
+ * https://www.ietf.org/rfc/rfc3339.txt
+ *
+ * <p>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.
+ * @deprecated Use {@link Timestamps#toString} instead.
+ */
+ @Deprecated
+ public static String toString(Timestamp timestamp) {
+ return Timestamps.toString(timestamp);
+ }
+
+ /**
+ * 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.
+ *
+ * <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00"
+ *
+ * @return A Timestamp parsed from the string.
+ * @throws ParseException if parsing fails.
+ * @deprecated Use {@link Timestamps#parse} instead.
+ */
+ @Deprecated
+ public static Timestamp parseTimestamp(String value) throws ParseException {
+ return Timestamps.parse(value);
+ }
+
+ /**
+ * 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.
+ * @deprecated Use {@link Durations#toString} instead.
+ */
+ @Deprecated
+ public static String toString(Duration duration) {
+ return Durations.toString(duration);
+ }
+
+ /**
+ * Parse from a string to produce a duration.
+ *
+ * @return A Duration parsed from the string.
+ * @throws ParseException if parsing fails.
+ * @deprecated Use {@link Durations#parse} instead.
+ */
+ @Deprecated
+ public static Duration parseDuration(String value) throws ParseException {
+ return Durations.parse(value);
+ }
+
+ /**
+ * Create a Timestamp from the number of milliseconds elapsed from the epoch.
+ *
+ * @deprecated Use {@link Timestamps#fromMillis} instead.
+ */
+ @Deprecated
+ public static Timestamp createTimestampFromMillis(long milliseconds) {
+ return Timestamps.fromMillis(milliseconds);
+ }
+
+ /**
+ * Create a Duration from the number of milliseconds.
+ *
+ * @deprecated Use {@link Durations#fromMillis} instead.
+ */
+ @Deprecated
+ public static Duration createDurationFromMillis(long milliseconds) {
+ return Durations.fromMillis(milliseconds);
+ }
+
+ /**
+ * Convert a Timestamp to the number of milliseconds elapsed from the epoch.
+ *
+ * <p>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.
+ *
+ * @deprecated Use {@link Timestamps#toMillis} instead.
+ */
+ @Deprecated
+ public static long toMillis(Timestamp timestamp) {
+ return Timestamps.toMillis(timestamp);
+ }
+
+ /**
+ * 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.
+ *
+ * @deprecated Use {@link Durations#toMillis} instead.
+ */
+ @Deprecated
+ public static long toMillis(Duration duration) {
+ return Durations.toMillis(duration);
+ }
+
+ /**
+ * Create a Timestamp from the number of microseconds elapsed from the epoch.
+ *
+ * @deprecated Use {@link Timestamps#fromMicros} instead.
+ */
+ @Deprecated
+ public static Timestamp createTimestampFromMicros(long microseconds) {
+ return Timestamps.fromMicros(microseconds);
+ }
+
+ /**
+ * Create a Duration from the number of microseconds.
+ *
+ * @deprecated Use {@link Durations#fromMicros} instead.
+ */
+ @Deprecated
+ public static Duration createDurationFromMicros(long microseconds) {
+ return Durations.fromMicros(microseconds);
+ }
+
+ /**
+ * Convert a Timestamp to the number of microseconds elapsed from the epoch.
+ *
+ * <p>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.
+ *
+ * @deprecated Use {@link Timestamps#toMicros} instead.
+ */
+ @Deprecated
+ public static long toMicros(Timestamp timestamp) {
+ return Timestamps.toMicros(timestamp);
+ }
+
+ /**
+ * 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.
+ *
+ * @deprecated Use {@link Durations#toMicros} instead.
+ */
+ @Deprecated
+ public static long toMicros(Duration duration) {
+ return Durations.toMicros(duration);
+ }
+
+ /**
+ * Create a Timestamp from the number of nanoseconds elapsed from the epoch.
+ *
+ * @deprecated Use {@link Timestamps#fromNanos} instead.
+ */
+ @Deprecated
+ public static Timestamp createTimestampFromNanos(long nanoseconds) {
+ return Timestamps.fromNanos(nanoseconds);
+ }
+
+ /**
+ * Create a Duration from the number of nanoseconds.
+ *
+ * @deprecated Use {@link Durations#fromNanos} instead.
+ */
+ @Deprecated
+ public static Duration createDurationFromNanos(long nanoseconds) {
+ return Durations.fromNanos(nanoseconds);
+ }
+
+ /**
+ * Convert a Timestamp to the number of nanoseconds elapsed from the epoch.
+ *
+ * @deprecated Use {@link Timestamps#toNanos} instead.
+ */
+ @Deprecated
+ public static long toNanos(Timestamp timestamp) {
+ return Timestamps.toNanos(timestamp);
+ }
+
+ /**
+ * Convert a Duration to the number of nanoseconds.
+ *
+ * @deprecated Use {@link Durations#toNanos} instead.
+ */
+ @Deprecated
+ public static long toNanos(Duration duration) {
+ return Durations.toNanos(duration);
+ }
+
+ /**
+ * Get the current time.
+ *
+ * @deprecated Use {@code Timestamps.fromMillis(System.currentTimeMillis())} instead.
+ */
+ @Deprecated
+ public static Timestamp getCurrentTime() {
+ return Timestamps.fromMillis(System.currentTimeMillis());
+ }
+
+ /**
+ * Get the epoch.
+ *
+ * @deprecated Use {@code Timestamps.fromMillis(0)} instead.
+ */
+ @Deprecated
+ public static Timestamp getEpoch() {
+ return Timestamp.getDefaultInstance();
+ }
+
+ /**
+ * Calculate the difference between two timestamps.
+ *
+ * @deprecated Use {@link Timestamps#between} instead.
+ */
+ @Deprecated
+ public static Duration distance(Timestamp from, Timestamp to) {
+ return Timestamps.between(from, to);
+ }
+
+ /**
+ * Add a duration to a timestamp.
+ *
+ * @deprecated Use {@link Timestamps#add} instead.
+ */
+ @Deprecated
+ public static Timestamp add(Timestamp start, Duration length) {
+ return Timestamps.add(start, length);
+ }
+
+ /**
+ * Subtract a duration from a timestamp.
+ *
+ * @deprecated Use {@link Timestamps#subtract} instead.
+ */
+ @Deprecated
+ public static Timestamp subtract(Timestamp start, Duration length) {
+ return Timestamps.subtract(start, length);
+ }
+
+ /**
+ * Add two durations.
+ *
+ * @deprecated Use {@link Durations#add} instead.
+ */
+ @Deprecated
+ public static Duration add(Duration d1, Duration d2) {
+ return Durations.add(d1, d2);
+ }
+
+ /**
+ * Subtract a duration from another.
+ *
+ * @deprecated Use {@link Durations#subtract} instead.
+ */
+ @Deprecated
+ public static Duration subtract(Duration d1, Duration d2) {
+ return Durations.subtract(d1, d2);
+ }
+
+ // Multiplications and divisions.
+
+ // TODO(kak): Delete this.
+ 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);
+ }
+
+ // TODO(kak): Delete this.
+ public static Duration divide(Duration duration, double value) {
+ return multiply(duration, 1.0 / value);
+ }
+
+ // TODO(kak): Delete this.
+ public static Duration multiply(Duration duration, long times) {
+ return createDurationFromBigInteger(toBigInteger(duration).multiply(toBigInteger(times)));
+ }
+
+ // TODO(kak): Delete this.
+ public static Duration divide(Duration duration, long times) {
+ return createDurationFromBigInteger(toBigInteger(duration).divide(toBigInteger(times)));
+ }
+
+ // TODO(kak): Delete this.
+ public static long divide(Duration d1, Duration d2) {
+ return toBigInteger(d1).divide(toBigInteger(d2)).longValue();
+ }
+
+ // TODO(kak): Delete this.
+ 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();
+ }
+}
diff --git a/java/util/src/main/java/com/google/protobuf/util/Timestamps.java b/java/util/src/main/java/com/google/protobuf/util/Timestamps.java
new file mode 100644
index 00000000..f1f202da
--- /dev/null
+++ b/java/util/src/main/java/com/google/protobuf/util/Timestamps.java
@@ -0,0 +1,349 @@
+// 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 java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/**
+ * Utilities to help create/manipulate {@code protobuf/timestamp.proto}.
+ */
+public final class Timestamps {
+ // Timestamp for "0001-01-01T00:00:00Z"
+ static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
+
+ // Timestamp for "9999-12-31T23:59:59Z"
+ static final long TIMESTAMP_SECONDS_MAX = 253402300799L;
+
+ static final long NANOS_PER_SECOND = 1000000000;
+ static final long NANOS_PER_MILLISECOND = 1000000;
+ static final long NANOS_PER_MICROSECOND = 1000;
+ static final long MILLIS_PER_SECOND = 1000;
+ static final long MICROS_PER_SECOND = 1000000;
+
+ // TODO(kak): Do we want to expose Timestamp constants for MAX/MIN?
+
+ private static final ThreadLocal<SimpleDateFormat> timestampFormat =
+ new ThreadLocal<SimpleDateFormat>() {
+ protected SimpleDateFormat initialValue() {
+ return createTimestampFormat();
+ }
+ };
+
+ private static SimpleDateFormat createTimestampFormat() {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+ GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ // We use Proleptic Gregorian Calendar (i.e., Gregorian calendar extends
+ // backwards to year one) for timestamp formating.
+ calendar.setGregorianChange(new Date(Long.MIN_VALUE));
+ sdf.setCalendar(calendar);
+ return sdf;
+ }
+
+ private Timestamps() {}
+
+ /**
+ * Returns true if the given {@link Timestamp} is valid. The {@code seconds} value must be in the
+ * range [-62,135,596,800, +253,402,300,799] (i.e., between 0001-01-01T00:00:00Z and
+ * 9999-12-31T23:59:59Z). The {@code nanos} value must be in the range [0, +999,999,999].
+ *
+ * <p>Note: Negative second values with fractions must still have non-negative nanos value that
+ * counts forward in time.
+ */
+ public static boolean isValid(Timestamp timestamp) {
+ return isValid(timestamp.getSeconds(), timestamp.getNanos());
+ }
+
+ /**
+ * Returns true if the given number of seconds and nanos is a valid {@link Timestamp}. The
+ * {@code seconds} value must be in the range [-62,135,596,800, +253,402,300,799] (i.e., between
+ * 0001-01-01T00:00:00Z and 9999-12-31T23:59:59Z). The {@code nanos} value must be in the range
+ * [0, +999,999,999].
+ *
+ * <p>Note: Negative second values with fractions must still have non-negative nanos value that
+ * counts forward in time.
+ */
+ public static boolean isValid(long seconds, long nanos) {
+ if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
+ return false;
+ }
+ if (nanos < 0 || nanos >= NANOS_PER_SECOND) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Throws an {@link IllegalArgumentException} if the given seconds/nanos are not
+ * a valid {@link Timestamp}.
+ */
+ private static void checkValid(long seconds, int nanos) {
+ if (!isValid(seconds, nanos)) {
+ throw new IllegalArgumentException(String.format(
+ "Timestamp is not valid. See proto definition for valid values. "
+ + "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]."
+ + "Nanos (%s) must be in range [0, +999,999,999].",
+ seconds, nanos));
+ }
+ }
+
+ /**
+ * Convert Timestamp to RFC 3339 date string format. The output will always
+ * be Z-normalized and uses 3, 6 or 9 fractional digits as required to
+ * represent the exact value. Note that Timestamp can only represent time
+ * from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. See
+ * https://www.ietf.org/rfc/rfc3339.txt
+ *
+ * <p>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) {
+ long seconds = timestamp.getSeconds();
+ int nanos = timestamp.getNanos();
+ checkValid(seconds, nanos);
+ StringBuilder result = new StringBuilder();
+ // Format the seconds part.
+ Date date = new Date(seconds * MILLIS_PER_SECOND);
+ result.append(timestampFormat.get().format(date));
+ // Format the nanos part.
+ if (nanos != 0) {
+ result.append(".");
+ result.append(formatNanos(nanos));
+ }
+ 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.
+ *
+ * <p>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 parse(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.get().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);
+ }
+ }
+
+ /**
+ * Create a Timestamp from the number of milliseconds elapsed from the epoch.
+ */
+ public static Timestamp fromMillis(long milliseconds) {
+ return normalizedTimestamp(
+ milliseconds / MILLIS_PER_SECOND,
+ (int) (milliseconds % MILLIS_PER_SECOND * NANOS_PER_MILLISECOND));
+ }
+
+ /**
+ * Convert a Timestamp to the number of milliseconds elapsed from the epoch.
+ *
+ * <p>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;
+ }
+
+ /**
+ * Create a Timestamp from the number of microseconds elapsed from the epoch.
+ */
+ public static Timestamp fromMicros(long microseconds) {
+ return normalizedTimestamp(
+ microseconds / MICROS_PER_SECOND,
+ (int) (microseconds % MICROS_PER_SECOND * NANOS_PER_MICROSECOND));
+ }
+
+ /**
+ * Convert a Timestamp to the number of microseconds elapsed from the epoch.
+ *
+ * <p>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;
+ }
+
+ /**
+ * Create a Timestamp from the number of nanoseconds elapsed from the epoch.
+ */
+ public static Timestamp fromNanos(long nanoseconds) {
+ return normalizedTimestamp(
+ 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();
+ }
+
+ /**
+ * Calculate the difference between two timestamps.
+ */
+ public static Duration between(Timestamp from, Timestamp to) {
+ return Durations.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());
+ }
+
+ 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;
+ }
+ checkValid(seconds, nanos);
+ return Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build();
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ /**
+ * Format the nano part of a timestamp or a duration.
+ */
+ 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);
+ }
+ }
+}