From fbb3ef28c91026bd56585cb825f40a563301466e Mon Sep 17 00:00:00 2001 From: Feng Xiao Date: Tue, 25 Aug 2015 14:32:09 -0700 Subject: Merge Java util package to github. --- .../com/google/protobuf/util/FieldMaskTree.java | 259 ++++ .../com/google/protobuf/util/FieldMaskUtil.java | 222 +++ .../java/com/google/protobuf/util/JsonFormat.java | 1571 ++++++++++++++++++++ .../java/com/google/protobuf/util/TimeUtil.java | 545 +++++++ 4 files changed, 2597 insertions(+) create mode 100644 java/util/src/main/java/com/google/protobuf/util/FieldMaskTree.java create mode 100644 java/util/src/main/java/com/google/protobuf/util/FieldMaskUtil.java create mode 100644 java/util/src/main/java/com/google/protobuf/util/JsonFormat.java create mode 100644 java/util/src/main/java/com/google/protobuf/util/TimeUtil.java (limited to 'java/util/src/main') 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..dc2f4b84 --- /dev/null +++ b/java/util/src/main/java/com/google/protobuf/util/FieldMaskTree.java @@ -0,0 +1,259 @@ +// 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.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. + * + *

For example, FieldMask "foo.bar,foo.baz,bar.baz" as a tree will be: + *

+ *   [root] -+- foo -+- bar
+ *           |       |
+ *           |       +- baz
+ *           |
+ *           +- bar --- baz
+ * 
+ * + *

By representing FieldMasks with this tree structure we can easily convert + * a FieldMask to a canonical form, merge two FieldMasks, calculate the + * intersection to two FieldMasks and traverse all fields specified by the + * FieldMask in a message tree. + */ +class FieldMaskTree { + private static final Logger logger = + Logger.getLogger(FieldMaskTree.class.getName()); + + private static final String FIELD_PATH_SEPARATOR_REGEX = "\\."; + + private static class Node { + public TreeMap children = new TreeMap(); + } + + private final Node root = new Node(); + + /** Creates an empty FieldMaskTree. */ + public FieldMaskTree() {} + + /** Creates a FieldMaskTree for a given FieldMask. */ + public 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. + */ + public 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. + */ + public FieldMaskTree mergeFromFieldMask(FieldMask mask) { + for (String path : mask.getPathsList()) { + addFieldPath(path); + } + return this; + } + + /** Converts this tree to a FieldMask. */ + public FieldMask toFieldMask() { + if (root.children.isEmpty()) { + return FieldMask.getDefaultInstance(); + } + List paths = new ArrayList(); + 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 paths) { + if (node.children.isEmpty()) { + paths.add(path); + return; + } + for (Entry 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}. + */ + public 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 paths = new ArrayList(); + getFieldPaths(node, path, paths); + for (String value : paths) { + output.addFieldPath(value); + } + } + + /** + * Merges all fields specified by this FieldMaskTree from {@code source} to + * {@code destination}. + */ + public 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 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()) { + destination.setField(field, source.getField(field)); + } else { + destination.getFieldBuilder(field).mergeFrom( + (Message) source.getField(field)); + } + } else { + destination.setField(field, source.getField(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..7bf87858 --- /dev/null +++ b/java/util/src/main/java/com/google/protobuf/util/FieldMaskUtil.java @@ -0,0 +1,222 @@ +// 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.Internal; +import com.google.protobuf.Message; + +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) { + 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) { + 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 type, String value) + throws IllegalArgumentException { + 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. + */ + public static FieldMask fromStringList( + Class type, List paths) + throws IllegalArgumentException { + 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(); + } + + /** + * Checks whether a given field path is valid. + */ + public static boolean isValid(Class type, String path) { + String[] parts = path.split(FIELD_SEPARATOR_REGEX); + if (parts.length == 0) { + return false; + } + Descriptor descriptor = + Internal.getDefaultInstance(type).getDescriptorForType(); + 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 an union of two FieldMasks. + */ + public static FieldMask union(FieldMask mask1, FieldMask mask2) { + return new FieldMaskTree(mask1).mergeFromFieldMask(mask2).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 class MergeOptions { + private boolean replaceMessageFields = false; + private boolean replaceRepeatedFields = 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; + } + + public void setReplaceMessageFields(boolean value) { + replaceMessageFields = value; + } + + public void setReplaceRepeatedFields(boolean value) { + replaceRepeatedFields = value; + } + } + + /** + * Merges fields specified by a FieldMask from one message to another. + */ + 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..c9a39153 --- /dev/null +++ b/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java @@ -0,0 +1,1571 @@ +// 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.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.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.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()); + } + + /** + * A Printer converts protobuf message to JSON format. + */ + public static class Printer { + private final TypeRegistry registry; + + private Printer(TypeRegistry registry) { + this.registry = registry; + } + + /** + * 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); + } + + /** + * 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, output).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.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 types; + + private TypeRegistry(Map 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 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.contains(file.getName())) { + 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 files = new HashSet(); + private Map types = + new HashMap(); + } + } + + /** + * A TextGenerator adds indentation when writing formatted text. + */ + private static final class TextGenerator { + private final Appendable output; + private final StringBuilder indent = new StringBuilder(); + private boolean atStartOfLine = true; + + private TextGenerator(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 TextGenerator generator; + // We use Gson to help handle string escapes. + private final Gson gson; + + private static class GsonHolder { + private static final Gson DEFAULT_GSON = new Gson(); + } + + PrinterImpl(TypeRegistry registry, Appendable jsonOutput) { + this.registry = registry; + this.generator = new TextGenerator(jsonOutput); + this.gson = GsonHolder.DEFAULT_GSON; + } + + 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 + wellKnownTypePrinters = buildWellKnownTypePrinters(); + + private static Map + buildWellKnownTypePrinters() { + Map printers = + new HashMap(); + // 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("{\n"); + generator.indent(); + generator.print("\"@type\": " + gson.toJson(typeUrl) + ",\n"); + generator.print("\"value\": "); + printer.print(this, contentMessage); + generator.print("\n"); + 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("\"" + TimeUtil.toString(value) + "\""); + } + + /** Prints google.protobuf.Duration */ + private void printDuration(MessageOrBuilder message) throws IOException { + Duration value = Duration.parseFrom(toByteString(message)); + generator.print("\"" + TimeUtil.toString(value) + "\""); + + } + + /** Prints google.protobuf.FieldMask */ + private void printFieldMask(MessageOrBuilder message) throws IOException { + FieldMask value = FieldMask.parseFrom(toByteString(message)); + generator.print("\"" + FieldMaskUtil.toString(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 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 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("{\n"); + generator.indent(); + + boolean printedField = false; + if (typeUrl != null) { + generator.print("\"@type\": " + gson.toJson(typeUrl)); + printedField = true; + } + for (Map.Entry field + : message.getAllFields().entrySet()) { + // Skip unknown enum fields. + if (field.getValue() instanceof EnumValueDescriptor + && ((EnumValueDescriptor) field.getValue()).getIndex() == -1) { + continue; + } + if (printedField) { + // Add line-endings for the previous field. + generator.print(",\n"); + } else { + printedField = true; + } + printField(field.getKey(), field.getValue()); + } + + // Add line-endings for the last field. + if (printedField) { + generator.print("\n"); + } + generator.outdent(); + generator.print("}"); + } + + private void printField(FieldDescriptor field, Object value) + throws IOException { + generator.print("\"" + fieldNameToCamelName(field.getName()) + "\": "); + 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) { + // Skip unknown enum entries. + if (element instanceof EnumValueDescriptor + && ((EnumValueDescriptor) element).getIndex() == -1) { + continue; + } + if (printedElement) { + generator.print(", "); + } 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("{\n"); + generator.indent(); + boolean printedElement = false; + for (Object element : (List) value) { + Message entry = (Message) element; + Object entryKey = entry.getField(keyField); + Object entryValue = entry.getField(valueField); + // Skip unknown enum entries. + if (entryValue instanceof EnumValueDescriptor + && ((EnumValueDescriptor) entryValue).getIndex() == -1) { + continue; + } + if (printedElement) { + generator.print(",\n"); + } else { + printedElement = true; + } + // Key fields are always double-quoted. + printSingleFieldValue(keyField, entryKey, true); + generator.print(": "); + printSingleFieldValue(valueField, entryValue); + } + if (printedElement) { + generator.print("\n"); + } + 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 { + 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 final String TYPE_URL_PREFIX = "type.googleapis.com"; + + private static String getTypeName(String typeUrl) + throws InvalidProtocolBufferException { + String[] parts = typeUrl.split("/"); + if (parts.length != 2 || !parts[0].equals(TYPE_URL_PREFIX)) { + throw new InvalidProtocolBufferException( + "Invalid type url found: " + typeUrl); + } + return parts[1]; + } + + private static String fieldNameToCamelName(String name) { + StringBuilder result = new StringBuilder(name.length()); + boolean isNextUpperCase = false; + for (int i = 0; i < name.length(); i++) { + Character ch = name.charAt(i); + if (Character.isLowerCase(ch)) { + if (isNextUpperCase) { + result.append(Character.toUpperCase(ch)); + } else { + result.append(ch); + } + isNextUpperCase = false; + } else if (Character.isUpperCase(ch)) { + if (i == 0 && !isNextUpperCase) { + // Force first letter to lower-case unless explicitly told to + // capitalize it. + result.append(Character.toLowerCase(ch)); + } else { + // Capital letters after the first are left as-is. + result.append(ch); + } + isNextUpperCase = false; + } else if (Character.isDigit(ch)) { + result.append(ch); + isNextUpperCase = true; + } else { + isNextUpperCase = true; + } + } + return result.toString(); + } + + 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 wellKnownTypeParsers = + buildWellKnownTypeParsers(); + + private static Map + buildWellKnownTypeParsers() { + Map parsers = + new HashMap(); + // 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 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> fieldNameMaps = + new HashMap>(); + + private Map getFieldNameMap( + Descriptor descriptor) { + if (!fieldNameMaps.containsKey(descriptor)) { + Map fieldNameMap = + new HashMap(); + for (FieldDescriptor field : descriptor.getFields()) { + fieldNameMap.put(fieldNameToCamelName(field.getName()), 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 fieldNameMap = + getFieldNameMap(builder.getDescriptorForType()); + for (Map.Entry 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.fromString(json.getAsString()); + builder.mergeFrom(value.toByteString()); + } + + private void mergeTimestamp(JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { + try { + Timestamp value = TimeUtil.parseTimestamp(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 = TimeUtil.parseDuration(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 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); + FieldDescriptor listField = + listBuilder.getDescriptorForType().findFieldByName("values"); + mergeRepeatedField(listField, 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 (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 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) { + value = getDefaultValue(valueField, entryBuilder); + } + 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) { + value = getDefaultValue(field, builder); + } + builder.addRepeatedField(field, value); + } + } + + private int parseInt32(JsonElement json) + throws InvalidProtocolBufferException { + try { + return Integer.parseInt(json.getAsString()); + } 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) { + throw new InvalidProtocolBufferException("Not an int64 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) { + 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 { + BigInteger value = new BigInteger(json.getAsString()); + 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) { + 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) { + 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(); + } + 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..6e4b7c03 --- /dev/null +++ b/java/util/src/main/java/com/google/protobuf/util/TimeUtil.java @@ -0,0 +1,545 @@ +// 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; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * Utilities to help create/manipulate Timestamp/Duration + */ +public 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 static final long NANOS_PER_MILLISECOND = 1000000; + private static final long NANOS_PER_MICROSECOND = 1000; + private static final long MILLIS_PER_SECOND = 1000; + private static final long MICROS_PER_SECOND = 1000000; + + private static final SimpleDateFormat timestampFormat = + 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 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 + * + *

Example of generated format: "1972-01-01T10:00:20.021Z" + * + * @return The string representation of the given timestamp. + * @throws IllegalArgumentException if the given timestamp is not in the + * valid range. + */ + public static String toString(Timestamp timestamp) + throws IllegalArgumentException { + StringBuilder result = new StringBuilder(); + // Format the seconds part. + if (timestamp.getSeconds() < TIMESTAMP_SECONDS_MIN + || timestamp.getSeconds() > TIMESTAMP_SECONDS_MAX) { + throw new IllegalArgumentException("Timestamp is out of range."); + } + Date date = new Date(timestamp.getSeconds() * MILLIS_PER_SECOND); + result.append(timestampFormat.format(date)); + // Format the nanos part. + if (timestamp.getNanos() < 0 || timestamp.getNanos() >= NANOS_PER_SECOND) { + throw new IllegalArgumentException("Timestamp has invalid nanos value."); + } + if (timestamp.getNanos() != 0) { + result.append("."); + result.append(formatNanos(timestamp.getNanos())); + } + result.append("Z"); + return result.toString(); + } + + /** + * Parse from RFC 3339 date string to Timestamp. This method accepts all + * outputs of {@link #toString(Timestamp)} and it also accepts any fractional + * digits (or none) and any offset as long as they fit into nano-seconds + * precision. + * + *

Example of accepted format: "1972-01-01T10:00:20.021-05:00" + * + * @return A Timestamp parsed from the string. + * @throws ParseException if parsing fails. + */ + + public static Timestamp parseTimestamp(String value) throws ParseException { + int dayOffset = value.indexOf('T'); + if (dayOffset == -1) { + throw new ParseException( + "Failed to parse timestamp: invalid timestamp \"" + value + "\"", 0); + } + int timezoneOffsetPosition = value.indexOf('Z', dayOffset); + if (timezoneOffsetPosition == -1) { + timezoneOffsetPosition = value.indexOf('+', dayOffset); + } + if (timezoneOffsetPosition == -1) { + timezoneOffsetPosition = value.indexOf('-', dayOffset); + } + if (timezoneOffsetPosition == -1) { + throw new ParseException( + "Failed to parse timestamp: missing valid timezone offset.", 0); + } + // Parse seconds and nanos. + String timeValue = value.substring(0, timezoneOffsetPosition); + String secondValue = timeValue; + String nanoValue = ""; + int pointPosition = timeValue.indexOf('.'); + if (pointPosition != -1) { + secondValue = timeValue.substring(0, pointPosition); + nanoValue = timeValue.substring(pointPosition + 1); + } + Date date = timestampFormat.parse(secondValue); + long seconds = date.getTime() / MILLIS_PER_SECOND; + int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue); + // Parse timezone offsets. + if (value.charAt(timezoneOffsetPosition) == 'Z') { + if (value.length() != timezoneOffsetPosition + 1) { + throw new ParseException( + "Failed to parse timestamp: invalid trailing data \"" + + value.substring(timezoneOffsetPosition) + "\"", 0); + } + } else { + String offsetValue = value.substring(timezoneOffsetPosition + 1); + long offset = parseTimezoneOffset(offsetValue); + if (value.charAt(timezoneOffsetPosition) == '+') { + seconds -= offset; + } else { + seconds += offset; + } + } + try { + return normalizedTimestamp(seconds, nanos); + } catch (IllegalArgumentException e) { + throw new ParseException( + "Failed to parse timestmap: timestamp is out of range.", 0); + } + } + + /** + * Convert Duration to string format. The string format will contains 3, 6, + * or 9 fractional digits depending on the precision required to represent + * the exact Duration value. For example: "1s", "1.010s", "1.000000100s", + * "-3.100s" The range that can be represented by Duration is from + * -315,576,000,000 to +315,576,000,000 inclusive (in seconds). + * + * @return The string representation of the given duration. + * @throws IllegalArgumentException if the given duration is not in the valid + * range. + */ + public static String toString(Duration duration) + throws IllegalArgumentException { + if (duration.getSeconds() < DURATION_SECONDS_MIN + || duration.getSeconds() > DURATION_SECONDS_MAX) { + throw new IllegalArgumentException("Duration is out of valid range."); + } + StringBuilder result = new StringBuilder(); + long seconds = duration.getSeconds(); + int nanos = duration.getNanos(); + if (seconds < 0 || nanos < 0) { + if (seconds > 0 || nanos > 0) { + throw new IllegalArgumentException( + "Invalid duration: seconds value and nanos value must have the same" + + "sign."); + } + result.append("-"); + seconds = -seconds; + nanos = -nanos; + } + result.append(seconds); + if (nanos != 0) { + result.append("."); + result.append(formatNanos(nanos)); + } + result.append("s"); + return result.toString(); + } + + /** + * Parse from a string to produce a duration. + * + * @return A Duration parsed from the string. + * @throws ParseException if parsing fails. + */ + public static Duration parseDuration(String value) throws ParseException { + // Must ended with "s". + if (value.isEmpty() || value.charAt(value.length() - 1) != 's') { + throw new ParseException("Invalid duration string: " + value, 0); + } + boolean negative = false; + if (value.charAt(0) == '-') { + negative = true; + value = value.substring(1); + } + String secondValue = value.substring(0, value.length() - 1); + String nanoValue = ""; + int pointPosition = secondValue.indexOf('.'); + if (pointPosition != -1) { + nanoValue = secondValue.substring(pointPosition + 1); + secondValue = secondValue.substring(0, pointPosition); + } + long seconds = Long.parseLong(secondValue); + int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue); + if (seconds < 0) { + throw new ParseException("Invalid duration string: " + value, 0); + } + if (negative) { + seconds = -seconds; + nanos = -nanos; + } + try { + return normalizedDuration(seconds, nanos); + } catch (IllegalArgumentException e) { + throw new ParseException("Duration value is out of range.", 0); + } + } + + /** + * Create a Timestamp from the number of milliseconds elapsed from the epoch. + */ + public static Timestamp createTimestampFromMillis(long milliseconds) { + return normalizedTimestamp(milliseconds / MILLIS_PER_SECOND, + (int) (milliseconds % MILLIS_PER_SECOND * NANOS_PER_MILLISECOND)); + } + + /** + * Create a Duration from the number of milliseconds. + */ + public static Duration createDurationFromMillis(long milliseconds) { + return normalizedDuration(milliseconds / MILLIS_PER_SECOND, + (int) (milliseconds % MILLIS_PER_SECOND * NANOS_PER_MILLISECOND)); + } + + /** + * Convert a Timestamp to the number of milliseconds elapsed from the epoch. + * + *

The result will be rounded down to the nearest millisecond. E.g., if the + * timestamp represents "1969-12-31T23:59:59.999999999Z", it will be rounded + * to -1 millisecond. + */ + public static long toMillis(Timestamp timestamp) { + return timestamp.getSeconds() * MILLIS_PER_SECOND + timestamp.getNanos() + / NANOS_PER_MILLISECOND; + } + + /** + * Convert a Duration to the number of milliseconds.The result will be + * rounded towards 0 to the nearest millisecond. E.g., if the duration + * represents -1 nanosecond, it will be rounded to 0. + */ + public static long toMillis(Duration duration) { + return duration.getSeconds() * MILLIS_PER_SECOND + duration.getNanos() + / NANOS_PER_MILLISECOND; + } + + /** + * Create a Timestamp from the number of microseconds elapsed from the epoch. + */ + public static Timestamp createTimestampFromMicros(long microseconds) { + return normalizedTimestamp(microseconds / MICROS_PER_SECOND, + (int) (microseconds % MICROS_PER_SECOND * NANOS_PER_MICROSECOND)); + } + + /** + * Create a Duration from the number of microseconds. + */ + public static Duration createDurationFromMicros(long microseconds) { + return normalizedDuration(microseconds / MICROS_PER_SECOND, + (int) (microseconds % MICROS_PER_SECOND * NANOS_PER_MICROSECOND)); + } + + /** + * Convert a Timestamp to the number of microseconds elapsed from the epoch. + * + *

The result will be rounded down to the nearest microsecond. E.g., if the + * timestamp represents "1969-12-31T23:59:59.999999999Z", it will be rounded + * to -1 millisecond. + */ + public static long toMicros(Timestamp timestamp) { + return timestamp.getSeconds() * MICROS_PER_SECOND + timestamp.getNanos() + / NANOS_PER_MICROSECOND; + } + + /** + * Convert a Duration to the number of microseconds.The result will be + * rounded towards 0 to the nearest microseconds. E.g., if the duration + * represents -1 nanosecond, it will be rounded to 0. + */ + public static long toMicros(Duration duration) { + return duration.getSeconds() * MICROS_PER_SECOND + duration.getNanos() + / NANOS_PER_MICROSECOND; + } + + /** + * Create a Timestamp from the number of nanoseconds elapsed from the epoch. + */ + public static Timestamp createTimestampFromNanos(long nanoseconds) { + return normalizedTimestamp(nanoseconds / NANOS_PER_SECOND, + (int) (nanoseconds % NANOS_PER_SECOND)); + } + + /** + * Create a Duration from the number of nanoseconds. + */ + public static Duration createDurationFromNanos(long nanoseconds) { + return normalizedDuration(nanoseconds / NANOS_PER_SECOND, + (int) (nanoseconds % NANOS_PER_SECOND)); + } + + /** + * Convert a Timestamp to the number of nanoseconds elapsed from the epoch. + */ + public static long toNanos(Timestamp timestamp) { + return timestamp.getSeconds() * NANOS_PER_SECOND + timestamp.getNanos(); + } + + /** + * Convert a Duration to the number of nanoseconds. + */ + public static long toNanos(Duration duration) { + return duration.getSeconds() * NANOS_PER_SECOND + duration.getNanos(); + } + + /** + * Get the current time. + */ + public static Timestamp getCurrentTime() { + return createTimestampFromMillis(System.currentTimeMillis()); + } + + /** + * Get the epoch. + */ + public static Timestamp getEpoch() { + return Timestamp.getDefaultInstance(); + } + + /** + * Calculate the difference between two timestamps. + */ + public static Duration distance(Timestamp from, Timestamp to) { + return normalizedDuration(to.getSeconds() - from.getSeconds(), + to.getNanos() - from.getNanos()); + } + + /** + * Add a duration to a timestamp. + */ + public static Timestamp add(Timestamp start, Duration length) { + return normalizedTimestamp(start.getSeconds() + length.getSeconds(), + start.getNanos() + length.getNanos()); + } + + /** + * Subtract a duration from a timestamp. + */ + public static Timestamp subtract(Timestamp start, Duration length) { + return normalizedTimestamp(start.getSeconds() - length.getSeconds(), + start.getNanos() - length.getNanos()); + } + + /** + * Add two durations. + */ + public static Duration add(Duration d1, Duration d2) { + return normalizedDuration(d1.getSeconds() + d2.getSeconds(), + d1.getNanos() + d2.getNanos()); + } + + /** + * Subtract a duration from another. + */ + public static Duration subtract(Duration d1, Duration d2) { + return normalizedDuration(d1.getSeconds() - d2.getSeconds(), + d1.getNanos() - d2.getNanos()); + } + + // Multiplications and divisions. + + public static Duration multiply(Duration duration, double times) { + double result = duration.getSeconds() * times + duration.getNanos() * times + / 1000000000.0; + if (result < Long.MIN_VALUE || result > Long.MAX_VALUE) { + throw new IllegalArgumentException("Result is out of valid range."); + } + long seconds = (long) result; + int nanos = (int) ((result - seconds) * 1000000000); + return normalizedDuration(seconds, nanos); + } + + public static Duration divide(Duration duration, double value) { + return multiply(duration, 1.0 / value); + } + + public static Duration multiply(Duration duration, long times) { + return createDurationFromBigInteger( + toBigInteger(duration).multiply(toBigInteger(times))); + } + + public static Duration divide(Duration duration, long times) { + return createDurationFromBigInteger( + toBigInteger(duration).divide(toBigInteger(times))); + } + + public static long divide(Duration d1, Duration d2) { + return toBigInteger(d1).divide(toBigInteger(d2)).longValue(); + } + + public static Duration remainder(Duration d1, Duration d2) { + return createDurationFromBigInteger( + toBigInteger(d1).remainder(toBigInteger(d2))); + } + + private static final BigInteger NANOS_PER_SECOND_BIG_INTEGER = + new BigInteger(String.valueOf(NANOS_PER_SECOND)); + + private static BigInteger toBigInteger(Duration duration) { + return toBigInteger(duration.getSeconds()) + .multiply(NANOS_PER_SECOND_BIG_INTEGER) + .add(toBigInteger(duration.getNanos())); + } + + private static BigInteger toBigInteger(long value) { + return new BigInteger(String.valueOf(value)); + } + + private static Duration createDurationFromBigInteger(BigInteger value) { + long seconds = value.divide( + new BigInteger(String.valueOf(NANOS_PER_SECOND))).longValue(); + int nanos = value.remainder( + new BigInteger(String.valueOf(NANOS_PER_SECOND))).intValue(); + return normalizedDuration(seconds, nanos); + + } + + private static Duration normalizedDuration(long seconds, int nanos) { + if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) { + seconds += nanos / NANOS_PER_SECOND; + nanos %= NANOS_PER_SECOND; + } + if (seconds > 0 && nanos < 0) { + nanos += NANOS_PER_SECOND; + seconds -= 1; + } + if (seconds < 0 && nanos > 0) { + nanos -= NANOS_PER_SECOND; + seconds += 1; + } + if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) { + throw new IllegalArgumentException("Duration is out of valid range."); + } + return Duration.newBuilder().setSeconds(seconds).setNanos(nanos).build(); + } + + private static Timestamp normalizedTimestamp(long seconds, int nanos) { + if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) { + seconds += nanos / NANOS_PER_SECOND; + nanos %= NANOS_PER_SECOND; + } + if (nanos < 0) { + nanos += NANOS_PER_SECOND; + seconds -= 1; + } + if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) { + throw new IllegalArgumentException("Timestamp is out of valid range."); + } + return Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build(); + } + + /** + * Format the nano part of a timestamp or a duration. + */ + private static String formatNanos(int nanos) { + assert nanos >= 1 && nanos <= 999999999; + // Determine whether to use 3, 6, or 9 digits for the nano part. + if (nanos % NANOS_PER_MILLISECOND == 0) { + return String.format("%1$03d", nanos / NANOS_PER_MILLISECOND); + } else if (nanos % NANOS_PER_MICROSECOND == 0) { + return String.format("%1$06d", nanos / NANOS_PER_MICROSECOND); + } else { + return String.format("%1$09d", nanos); + } + } + + private static int parseNanos(String value) throws ParseException { + int result = 0; + for (int i = 0; i < 9; ++i) { + result = result * 10; + if (i < value.length()) { + if (value.charAt(i) < '0' || value.charAt(i) > '9') { + throw new ParseException("Invalid nanosecnds.", 0); + } + result += value.charAt(i) - '0'; + } + } + return result; + } + + private static long parseTimezoneOffset(String value) throws ParseException { + int pos = value.indexOf(':'); + if (pos == -1) { + throw new ParseException("Invalid offset value: " + value, 0); + } + String hours = value.substring(0, pos); + String minutes = value.substring(pos + 1); + return (Long.parseLong(hours) * 60 + Long.parseLong(minutes)) * 60; + } +} -- cgit v1.2.3