diff --git a/java/util/pom.xml b/java/util/pom.xml new file mode 100644 index 0000000000..9416f380d3 --- /dev/null +++ b/java/util/pom.xml @@ -0,0 +1,202 @@ + + + 4.0.0 + + com.google + google + 1 + + com.google.protobuf + protobuf-java-util + 3.0.0-alpha-4-pre + bundle + Protocol Buffer Java API + + Protocol Buffers are a way of encoding structured data in an efficient yet + extensible format. + + 2008 + https://developers.google.com/protocol-buffers/ + + + New BSD license + http://www.opensource.org/licenses/bsd-license.php + repo + + + + https://github.com/google/protobuf + + scm:git:https://github.com/google/protobuf.git + + + + + com.google.protobuf + protobuf-java + 3.0.0-alpha-4-pre + compile + + + com.google.guava + guava + 18.0 + compile + + + com.google.code.gson + gson + 2.3 + compile + + + junit + junit + 4.4 + test + + + org.easymock + easymock + 2.2 + test + + + org.easymock + easymockclassextension + 2.2.1 + test + + + + + + maven-compiler-plugin + + 1.5 + 1.5 + + + + maven-surefire-plugin + + + **/*Test.java + ../src/main/java/com/google/protobuf/TestUtil.java + + + + + maven-antrun-plugin + + + generate-test-sources + generate-test-sources + + + + + + + + + + + + + + target/generated-test-sources + + + run + + + + + + org.apache.felix + maven-bundle-plugin + true + + + https://developers.google.com/protocol-buffers/ + com.google.protobuf.util + com.google.protobuf.util;version=3.0.0-alpha-3 + + + + + + + + release + + + sonatype-nexus-staging + https://oss.sonatype.org/content/repositories/snapshots + + + sonatype-nexus-staging + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.3 + true + + sonatype-nexus-staging + https://oss.sonatype.org/ + false + + + + + + + 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 0000000000..dc2f4b841e --- /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 0000000000..7bf8785856 --- /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 0000000000..c9a3915368 --- /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 0000000000..6e4b7c03aa --- /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; + } +} diff --git a/java/util/src/test/java/com/google/protobuf/util/FieldMaskTreeTest.java b/java/util/src/test/java/com/google/protobuf/util/FieldMaskTreeTest.java new file mode 100644 index 0000000000..3391f239fe --- /dev/null +++ b/java/util/src/test/java/com/google/protobuf/util/FieldMaskTreeTest.java @@ -0,0 +1,229 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.google.protobuf.util; + +import protobuf_unittest.UnittestProto.NestedTestAllTypes; +import protobuf_unittest.UnittestProto.TestAllTypes; +import protobuf_unittest.UnittestProto.TestAllTypes.NestedMessage; + +import junit.framework.TestCase; + +public class FieldMaskTreeTest extends TestCase { + public void testAddFieldPath() throws Exception { + FieldMaskTree tree = new FieldMaskTree(); + assertEquals("", tree.toString()); + tree.addFieldPath(""); + assertEquals("", tree.toString()); + // New branch. + tree.addFieldPath("foo"); + assertEquals("foo", tree.toString()); + // Redundant path. + tree.addFieldPath("foo"); + assertEquals("foo", tree.toString()); + // New branch. + tree.addFieldPath("bar.baz"); + assertEquals("bar.baz,foo", tree.toString()); + // Redundant sub-path. + tree.addFieldPath("foo.bar"); + assertEquals("bar.baz,foo", tree.toString()); + // New branch from a non-root node. + tree.addFieldPath("bar.quz"); + assertEquals("bar.baz,bar.quz,foo", tree.toString()); + // A path that matches several existing sub-paths. + tree.addFieldPath("bar"); + assertEquals("bar,foo", tree.toString()); + } + + public void testMergeFromFieldMask() throws Exception { + FieldMaskTree tree = new FieldMaskTree( + FieldMaskUtil.fromString("foo,bar.baz,bar.quz")); + assertEquals("bar.baz,bar.quz,foo", tree.toString()); + tree.mergeFromFieldMask( + FieldMaskUtil.fromString("foo.bar,bar")); + assertEquals("bar,foo", tree.toString()); + } + + public void testIntersectFieldPath() throws Exception { + FieldMaskTree tree = new FieldMaskTree( + FieldMaskUtil.fromString("foo,bar.baz,bar.quz")); + FieldMaskTree result = new FieldMaskTree(); + // Empty path. + tree.intersectFieldPath("", result); + assertEquals("", result.toString()); + // Non-exist path. + tree.intersectFieldPath("quz", result); + assertEquals("", result.toString()); + // Sub-path of an existing leaf. + tree.intersectFieldPath("foo.bar", result); + assertEquals("foo.bar", result.toString()); + // Match an existing leaf node. + tree.intersectFieldPath("foo", result); + assertEquals("foo", result.toString()); + // Non-exist path. + tree.intersectFieldPath("bar.foo", result); + assertEquals("foo", result.toString()); + // Match a non-leaf node. + tree.intersectFieldPath("bar", result); + assertEquals("bar.baz,bar.quz,foo", result.toString()); + } + + public void testMerge() throws Exception { + TestAllTypes value = TestAllTypes.newBuilder() + .setOptionalInt32(1234) + .setOptionalNestedMessage(NestedMessage.newBuilder().setBb(5678)) + .addRepeatedInt32(4321) + .addRepeatedNestedMessage(NestedMessage.newBuilder().setBb(8765)) + .build(); + NestedTestAllTypes source = NestedTestAllTypes.newBuilder() + .setPayload(value) + .setChild(NestedTestAllTypes.newBuilder().setPayload(value)) + .build(); + // Now we have a message source with the following structure: + // [root] -+- payload -+- optional_int32 + // | +- optional_nested_message + // | +- repeated_int32 + // | +- repeated_nested_message + // | + // +- child --- payload -+- optional_int32 + // +- optional_nested_message + // +- repeated_int32 + // +- repeated_nested_message + + FieldMaskUtil.MergeOptions options = new FieldMaskUtil.MergeOptions(); + + // Test merging each individual field. + NestedTestAllTypes.Builder builder = NestedTestAllTypes.newBuilder(); + new FieldMaskTree().addFieldPath("payload.optional_int32") + .merge(source, builder, options); + NestedTestAllTypes.Builder expected = NestedTestAllTypes.newBuilder(); + expected.getPayloadBuilder().setOptionalInt32(1234); + assertEquals(expected.build(), builder.build()); + + builder = NestedTestAllTypes.newBuilder(); + new FieldMaskTree().addFieldPath("payload.optional_nested_message") + .merge(source, builder, options); + expected = NestedTestAllTypes.newBuilder(); + expected.getPayloadBuilder().setOptionalNestedMessage( + NestedMessage.newBuilder().setBb(5678)); + assertEquals(expected.build(), builder.build()); + + + builder = NestedTestAllTypes.newBuilder(); + new FieldMaskTree().addFieldPath("payload.repeated_int32") + .merge(source, builder, options); + expected = NestedTestAllTypes.newBuilder(); + expected.getPayloadBuilder().addRepeatedInt32(4321); + assertEquals(expected.build(), builder.build()); + + builder = NestedTestAllTypes.newBuilder(); + new FieldMaskTree().addFieldPath("payload.repeated_nested_message") + .merge(source, builder, options); + expected = NestedTestAllTypes.newBuilder(); + expected.getPayloadBuilder().addRepeatedNestedMessage( + NestedMessage.newBuilder().setBb(8765)); + assertEquals(expected.build(), builder.build()); + + builder = NestedTestAllTypes.newBuilder(); + new FieldMaskTree().addFieldPath("child.payload.optional_int32") + .merge(source, builder, options); + expected = NestedTestAllTypes.newBuilder(); + expected.getChildBuilder().getPayloadBuilder().setOptionalInt32(1234); + assertEquals(expected.build(), builder.build()); + + builder = NestedTestAllTypes.newBuilder(); + new FieldMaskTree().addFieldPath("child.payload.optional_nested_message") + .merge(source, builder, options); + expected = NestedTestAllTypes.newBuilder(); + expected.getChildBuilder().getPayloadBuilder().setOptionalNestedMessage( + NestedMessage.newBuilder().setBb(5678)); + assertEquals(expected.build(), builder.build()); + + + builder = NestedTestAllTypes.newBuilder(); + new FieldMaskTree().addFieldPath("child.payload.repeated_int32") + .merge(source, builder, options); + expected = NestedTestAllTypes.newBuilder(); + expected.getChildBuilder().getPayloadBuilder().addRepeatedInt32(4321); + assertEquals(expected.build(), builder.build()); + + + builder = NestedTestAllTypes.newBuilder(); + new FieldMaskTree().addFieldPath("child.payload.repeated_nested_message") + .merge(source, builder, options); + expected = NestedTestAllTypes.newBuilder(); + expected.getChildBuilder().getPayloadBuilder().addRepeatedNestedMessage( + NestedMessage.newBuilder().setBb(8765)); + assertEquals(expected.build(), builder.build()); + + // Test merging all fields. + builder = NestedTestAllTypes.newBuilder(); + new FieldMaskTree().addFieldPath("child").addFieldPath("payload") + .merge(source, builder, options); + assertEquals(source, builder.build()); + + // Test repeated options. + builder = NestedTestAllTypes.newBuilder(); + builder.getPayloadBuilder().addRepeatedInt32(1000); + new FieldMaskTree().addFieldPath("payload.repeated_int32") + .merge(source, builder, options); + // Default behavior is to append repeated fields. + assertEquals(2, builder.getPayload().getRepeatedInt32Count()); + assertEquals(1000, builder.getPayload().getRepeatedInt32(0)); + assertEquals(4321, builder.getPayload().getRepeatedInt32(1)); + // Change to replace repeated fields. + options.setReplaceRepeatedFields(true); + new FieldMaskTree().addFieldPath("payload.repeated_int32") + .merge(source, builder, options); + assertEquals(1, builder.getPayload().getRepeatedInt32Count()); + assertEquals(4321, builder.getPayload().getRepeatedInt32(0)); + + // Test message options. + builder = NestedTestAllTypes.newBuilder(); + builder.getPayloadBuilder().setOptionalInt32(1000); + builder.getPayloadBuilder().setOptionalUint32(2000); + new FieldMaskTree().addFieldPath("payload") + .merge(source, builder, options); + // Default behavior is to merge message fields. + assertEquals(1234, builder.getPayload().getOptionalInt32()); + assertEquals(2000, builder.getPayload().getOptionalUint32()); + + // Change to replace message fields. + options.setReplaceMessageFields(true); + builder = NestedTestAllTypes.newBuilder(); + builder.getPayloadBuilder().setOptionalInt32(1000); + builder.getPayloadBuilder().setOptionalUint32(2000); + new FieldMaskTree().addFieldPath("payload") + .merge(source, builder, options); + assertEquals(1234, builder.getPayload().getOptionalInt32()); + assertEquals(0, builder.getPayload().getOptionalUint32()); + } +} + diff --git a/java/util/src/test/java/com/google/protobuf/util/FieldMaskUtilTest.java b/java/util/src/test/java/com/google/protobuf/util/FieldMaskUtilTest.java new file mode 100644 index 0000000000..67fbe0b1ac --- /dev/null +++ b/java/util/src/test/java/com/google/protobuf/util/FieldMaskUtilTest.java @@ -0,0 +1,135 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.google.protobuf.util; + +import com.google.protobuf.FieldMask; +import protobuf_unittest.UnittestProto.NestedTestAllTypes; +import protobuf_unittest.UnittestProto.TestAllTypes; + +import junit.framework.TestCase; + +/** Unit tests for {@link FieldMaskUtil}. */ +public class FieldMaskUtilTest extends TestCase { + public void testIsValid() throws Exception { + assertTrue(FieldMaskUtil.isValid(NestedTestAllTypes.class, "payload")); + assertFalse(FieldMaskUtil.isValid(NestedTestAllTypes.class, "nonexist")); + assertTrue(FieldMaskUtil.isValid( + NestedTestAllTypes.class, "payload.optional_int32")); + assertTrue(FieldMaskUtil.isValid( + NestedTestAllTypes.class, "payload.repeated_int32")); + assertTrue(FieldMaskUtil.isValid( + NestedTestAllTypes.class, "payload.optional_nested_message")); + assertTrue(FieldMaskUtil.isValid( + NestedTestAllTypes.class, "payload.repeated_nested_message")); + assertFalse(FieldMaskUtil.isValid( + NestedTestAllTypes.class, "payload.nonexist")); + + assertTrue(FieldMaskUtil.isValid( + NestedTestAllTypes.class, "payload.optional_nested_message.bb")); + // Repeated fields cannot have sub-paths. + assertFalse(FieldMaskUtil.isValid( + NestedTestAllTypes.class, "payload.repeated_nested_message.bb")); + // Non-message fields cannot have sub-paths. + assertFalse(FieldMaskUtil.isValid( + NestedTestAllTypes.class, "payload.optional_int32.bb")); + } + + public void testToString() throws Exception { + assertEquals("", FieldMaskUtil.toString(FieldMask.getDefaultInstance())); + FieldMask mask = FieldMask.newBuilder().addPaths("foo").build(); + assertEquals("foo", FieldMaskUtil.toString(mask)); + mask = FieldMask.newBuilder().addPaths("foo").addPaths("bar").build(); + assertEquals("foo,bar", FieldMaskUtil.toString(mask)); + + // Empty field paths are ignored. + mask = FieldMask.newBuilder().addPaths("").addPaths("foo").addPaths(""). + addPaths("bar").addPaths("").build(); + assertEquals("foo,bar", FieldMaskUtil.toString(mask)); + } + + public void testFromString() throws Exception { + FieldMask mask = FieldMaskUtil.fromString(""); + assertEquals(0, mask.getPathsCount()); + mask = FieldMaskUtil.fromString("foo"); + assertEquals(1, mask.getPathsCount()); + assertEquals("foo", mask.getPaths(0)); + mask = FieldMaskUtil.fromString("foo,bar.baz"); + assertEquals(2, mask.getPathsCount()); + assertEquals("foo", mask.getPaths(0)); + assertEquals("bar.baz", mask.getPaths(1)); + + // Empty field paths are ignore. + mask = FieldMaskUtil.fromString(",foo,,bar,"); + assertEquals(2, mask.getPathsCount()); + assertEquals("foo", mask.getPaths(0)); + assertEquals("bar", mask.getPaths(1)); + + // Check whether the field paths are valid if a class parameter is provided. + mask = FieldMaskUtil.fromString(NestedTestAllTypes.class, ",payload"); + + try { + mask = FieldMaskUtil.fromString( + NestedTestAllTypes.class, "payload,nonexist"); + fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + public void testUnion() throws Exception { + // Only test a simple case here and expect + // {@link FieldMaskTreeTest#testAddFieldPath} to cover all scenarios. + FieldMask mask1 = FieldMaskUtil.fromString("foo,bar.baz,bar.quz"); + FieldMask mask2 = FieldMaskUtil.fromString("foo.bar,bar"); + FieldMask result = FieldMaskUtil.union(mask1, mask2); + assertEquals("bar,foo", FieldMaskUtil.toString(result)); + } + + public void testIntersection() throws Exception { + // Only test a simple case here and expect + // {@link FieldMaskTreeTest#testIntersectFieldPath} to cover all scenarios. + FieldMask mask1 = FieldMaskUtil.fromString("foo,bar.baz,bar.quz"); + FieldMask mask2 = FieldMaskUtil.fromString("foo.bar,bar"); + FieldMask result = FieldMaskUtil.intersection(mask1, mask2); + assertEquals("bar.baz,bar.quz,foo.bar", FieldMaskUtil.toString(result)); + } + + public void testMerge() throws Exception { + // Only test a simple case here and expect + // {@link FieldMaskTreeTest#testMerge} to cover all scenarios. + NestedTestAllTypes source = NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setOptionalInt32(1234)) + .build(); + NestedTestAllTypes.Builder builder = NestedTestAllTypes.newBuilder(); + FieldMaskUtil.merge(FieldMaskUtil.fromString("payload"), source, builder); + assertEquals(1234, builder.getPayload().getOptionalInt32()); + } +} diff --git a/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java b/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java new file mode 100644 index 0000000000..ddf5ad2a4f --- /dev/null +++ b/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java @@ -0,0 +1,976 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.google.protobuf.util; + +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.ListValue; +import com.google.protobuf.Message; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import com.google.protobuf.util.JsonFormat.TypeRegistry; +import com.google.protobuf.util.JsonTestProto.TestAllTypes; +import com.google.protobuf.util.JsonTestProto.TestAllTypes.NestedEnum; +import com.google.protobuf.util.JsonTestProto.TestAllTypes.NestedMessage; +import com.google.protobuf.util.JsonTestProto.TestAny; +import com.google.protobuf.util.JsonTestProto.TestDuration; +import com.google.protobuf.util.JsonTestProto.TestFieldMask; +import com.google.protobuf.util.JsonTestProto.TestMap; +import com.google.protobuf.util.JsonTestProto.TestStruct; +import com.google.protobuf.util.JsonTestProto.TestTimestamp; +import com.google.protobuf.util.JsonTestProto.TestWrappers; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; + +public class JsonFormatTest extends TestCase { + private void setAllFields(TestAllTypes.Builder builder) { + builder.setOptionalInt32(1234); + builder.setOptionalInt64(1234567890123456789L); + builder.setOptionalUint32(5678); + builder.setOptionalUint64(2345678901234567890L); + builder.setOptionalSint32(9012); + builder.setOptionalSint64(3456789012345678901L); + builder.setOptionalFixed32(3456); + builder.setOptionalFixed64(4567890123456789012L); + builder.setOptionalSfixed32(7890); + builder.setOptionalSfixed64(5678901234567890123L); + builder.setOptionalFloat(1.5f); + builder.setOptionalDouble(1.25); + builder.setOptionalBool(true); + builder.setOptionalString("Hello world!"); + builder.setOptionalBytes(ByteString.copyFrom(new byte[]{0, 1, 2})); + builder.setOptionalNestedEnum(NestedEnum.BAR); + builder.getOptionalNestedMessageBuilder().setValue(100); + + builder.addRepeatedInt32(1234); + builder.addRepeatedInt64(1234567890123456789L); + builder.addRepeatedUint32(5678); + builder.addRepeatedUint64(2345678901234567890L); + builder.addRepeatedSint32(9012); + builder.addRepeatedSint64(3456789012345678901L); + builder.addRepeatedFixed32(3456); + builder.addRepeatedFixed64(4567890123456789012L); + builder.addRepeatedSfixed32(7890); + builder.addRepeatedSfixed64(5678901234567890123L); + builder.addRepeatedFloat(1.5f); + builder.addRepeatedDouble(1.25); + builder.addRepeatedBool(true); + builder.addRepeatedString("Hello world!"); + builder.addRepeatedBytes(ByteString.copyFrom(new byte[]{0, 1, 2})); + builder.addRepeatedNestedEnum(NestedEnum.BAR); + builder.addRepeatedNestedMessageBuilder().setValue(100); + + builder.addRepeatedInt32(234); + builder.addRepeatedInt64(234567890123456789L); + builder.addRepeatedUint32(678); + builder.addRepeatedUint64(345678901234567890L); + builder.addRepeatedSint32(012); + builder.addRepeatedSint64(456789012345678901L); + builder.addRepeatedFixed32(456); + builder.addRepeatedFixed64(567890123456789012L); + builder.addRepeatedSfixed32(890); + builder.addRepeatedSfixed64(678901234567890123L); + builder.addRepeatedFloat(11.5f); + builder.addRepeatedDouble(11.25); + builder.addRepeatedBool(true); + builder.addRepeatedString("ello world!"); + builder.addRepeatedBytes(ByteString.copyFrom(new byte[]{1, 2})); + builder.addRepeatedNestedEnum(NestedEnum.BAZ); + builder.addRepeatedNestedMessageBuilder().setValue(200); + } + + private void assertRoundTripEquals(Message message) throws Exception { + assertRoundTripEquals(message, TypeRegistry.getEmptyTypeRegistry()); + } + + private void assertRoundTripEquals(Message message, TypeRegistry registry) throws Exception { + JsonFormat.Printer printer = JsonFormat.printer().usingTypeRegistry(registry); + JsonFormat.Parser parser = JsonFormat.parser().usingTypeRegistry(registry); + Message.Builder builder = message.newBuilderForType(); + parser.merge(printer.print(message), builder); + Message parsedMessage = builder.build(); + assertEquals(message.toString(), parsedMessage.toString()); + } + + private String toJsonString(Message message) throws IOException { + return JsonFormat.printer().print(message); + } + + private void mergeFromJson(String json, Message.Builder builder) throws IOException { + JsonFormat.parser().merge(json, builder); + } + + public void testAllFields() throws Exception { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + setAllFields(builder); + TestAllTypes message = builder.build(); + + assertEquals( + "{\n" + + " \"optionalInt32\": 1234,\n" + + " \"optionalInt64\": \"1234567890123456789\",\n" + + " \"optionalUint32\": 5678,\n" + + " \"optionalUint64\": \"2345678901234567890\",\n" + + " \"optionalSint32\": 9012,\n" + + " \"optionalSint64\": \"3456789012345678901\",\n" + + " \"optionalFixed32\": 3456,\n" + + " \"optionalFixed64\": \"4567890123456789012\",\n" + + " \"optionalSfixed32\": 7890,\n" + + " \"optionalSfixed64\": \"5678901234567890123\",\n" + + " \"optionalFloat\": 1.5,\n" + + " \"optionalDouble\": 1.25,\n" + + " \"optionalBool\": true,\n" + + " \"optionalString\": \"Hello world!\",\n" + + " \"optionalBytes\": \"AAEC\",\n" + + " \"optionalNestedMessage\": {\n" + + " \"value\": 100\n" + + " },\n" + + " \"optionalNestedEnum\": \"BAR\",\n" + + " \"repeatedInt32\": [1234, 234],\n" + + " \"repeatedInt64\": [\"1234567890123456789\", \"234567890123456789\"],\n" + + " \"repeatedUint32\": [5678, 678],\n" + + " \"repeatedUint64\": [\"2345678901234567890\", \"345678901234567890\"],\n" + + " \"repeatedSint32\": [9012, 10],\n" + + " \"repeatedSint64\": [\"3456789012345678901\", \"456789012345678901\"],\n" + + " \"repeatedFixed32\": [3456, 456],\n" + + " \"repeatedFixed64\": [\"4567890123456789012\", \"567890123456789012\"],\n" + + " \"repeatedSfixed32\": [7890, 890],\n" + + " \"repeatedSfixed64\": [\"5678901234567890123\", \"678901234567890123\"],\n" + + " \"repeatedFloat\": [1.5, 11.5],\n" + + " \"repeatedDouble\": [1.25, 11.25],\n" + + " \"repeatedBool\": [true, true],\n" + + " \"repeatedString\": [\"Hello world!\", \"ello world!\"],\n" + + " \"repeatedBytes\": [\"AAEC\", \"AQI=\"],\n" + + " \"repeatedNestedMessage\": [{\n" + + " \"value\": 100\n" + + " }, {\n" + + " \"value\": 200\n" + + " }],\n" + + " \"repeatedNestedEnum\": [\"BAR\", \"BAZ\"]\n" + + "}", + toJsonString(message)); + + assertRoundTripEquals(message); + } + + public void testUnknownEnumValues() throws Exception { + // Unknown enum values will be dropped. + // TODO(xiaofeng): We may want to revisit this (whether we should omit + // unknown enum values). + TestAllTypes message = TestAllTypes.newBuilder() + .setOptionalNestedEnumValue(12345) + .addRepeatedNestedEnumValue(12345) + .addRepeatedNestedEnumValue(0) + .build(); + assertEquals( + "{\n" + + " \"repeatedNestedEnum\": [\"FOO\"]\n" + + "}", toJsonString(message)); + + TestMap.Builder mapBuilder = TestMap.newBuilder(); + mapBuilder.getMutableInt32ToEnumMapValue().put(1, 0); + mapBuilder.getMutableInt32ToEnumMapValue().put(2, 12345); + TestMap mapMessage = mapBuilder.build(); + assertEquals( + "{\n" + + " \"int32ToEnumMap\": {\n" + + " \"1\": \"FOO\"\n" + + " }\n" + + "}", toJsonString(mapMessage)); + } + + public void testSpecialFloatValues() throws Exception { + TestAllTypes message = TestAllTypes.newBuilder() + .addRepeatedFloat(Float.NaN) + .addRepeatedFloat(Float.POSITIVE_INFINITY) + .addRepeatedFloat(Float.NEGATIVE_INFINITY) + .addRepeatedDouble(Double.NaN) + .addRepeatedDouble(Double.POSITIVE_INFINITY) + .addRepeatedDouble(Double.NEGATIVE_INFINITY) + .build(); + assertEquals( + "{\n" + + " \"repeatedFloat\": [\"NaN\", \"Infinity\", \"-Infinity\"],\n" + + " \"repeatedDouble\": [\"NaN\", \"Infinity\", \"-Infinity\"]\n" + + "}", toJsonString(message)); + + assertRoundTripEquals(message); + } + + public void testParserAcceptStringForNumbericField() throws Exception { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"optionalInt32\": \"1234\",\n" + + " \"optionalUint32\": \"5678\",\n" + + " \"optionalSint32\": \"9012\",\n" + + " \"optionalFixed32\": \"3456\",\n" + + " \"optionalSfixed32\": \"7890\",\n" + + " \"optionalFloat\": \"1.5\",\n" + + " \"optionalDouble\": \"1.25\",\n" + + " \"optionalBool\": \"true\"\n" + + "}", builder); + TestAllTypes message = builder.build(); + assertEquals(1234, message.getOptionalInt32()); + assertEquals(5678, message.getOptionalUint32()); + assertEquals(9012, message.getOptionalSint32()); + assertEquals(3456, message.getOptionalFixed32()); + assertEquals(7890, message.getOptionalSfixed32()); + assertEquals(1.5f, message.getOptionalFloat()); + assertEquals(1.25, message.getOptionalDouble()); + assertEquals(true, message.getOptionalBool()); + } + + private void assertRejects(String name, String value) { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + try { + // Numeric form is rejected. + mergeFromJson("{\"" + name + "\":" + value + "}", builder); + fail("Exception is expected."); + } catch (IOException e) { + // Expected. + } + try { + // String form is also rejected. + mergeFromJson("{\"" + name + "\":\"" + value + "\"}", builder); + fail("Exception is expected."); + } catch (IOException e) { + // Expected. + } + } + + private void assertAccepts(String name, String value) throws IOException { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + // Both numeric form and string form are accepted. + mergeFromJson("{\"" + name + "\":" + value + "}", builder); + mergeFromJson("{\"" + name + "\":\"" + value + "\"}", builder); + } + + public void testParserRejectOutOfRangeNumericValues() throws Exception { + assertAccepts("optionalInt32", String.valueOf(Integer.MAX_VALUE)); + assertAccepts("optionalInt32", String.valueOf(Integer.MIN_VALUE)); + assertRejects("optionalInt32", String.valueOf(Integer.MAX_VALUE + 1L)); + assertRejects("optionalInt32", String.valueOf(Integer.MIN_VALUE - 1L)); + + assertAccepts("optionalUint32", String.valueOf(Integer.MAX_VALUE + 1L)); + assertRejects("optionalUint32", "123456789012345"); + assertRejects("optionalUint32", "-1"); + + BigInteger one = new BigInteger("1"); + BigInteger maxLong = new BigInteger(String.valueOf(Long.MAX_VALUE)); + BigInteger minLong = new BigInteger(String.valueOf(Long.MIN_VALUE)); + assertAccepts("optionalInt64", maxLong.toString()); + assertAccepts("optionalInt64", minLong.toString()); + assertRejects("optionalInt64", maxLong.add(one).toString()); + assertRejects("optionalInt64", minLong.subtract(one).toString()); + + assertAccepts("optionalUint64", maxLong.add(one).toString()); + assertRejects("optionalUint64", "1234567890123456789012345"); + assertRejects("optionalUint64", "-1"); + + assertAccepts("optionalBool", "true"); + assertRejects("optionalBool", "1"); + assertRejects("optionalBool", "0"); + + assertAccepts("optionalFloat", String.valueOf(Float.MAX_VALUE)); + assertAccepts("optionalFloat", String.valueOf(-Float.MAX_VALUE)); + assertRejects("optionalFloat", String.valueOf(Double.MAX_VALUE)); + assertRejects("optionalFloat", String.valueOf(-Double.MAX_VALUE)); + + BigDecimal moreThanOne = new BigDecimal("1.000001"); + BigDecimal maxDouble = new BigDecimal(Double.MAX_VALUE); + BigDecimal minDouble = new BigDecimal(-Double.MAX_VALUE); + assertAccepts("optionalDouble", maxDouble.toString()); + assertAccepts("optionalDouble", minDouble.toString()); + assertRejects("optionalDouble", maxDouble.multiply(moreThanOne).toString()); + assertRejects("optionalDouble", minDouble.multiply(moreThanOne).toString()); + } + + public void testParserAcceptNull() throws Exception { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"optionalInt32\": null,\n" + + " \"optionalInt64\": null,\n" + + " \"optionalUint32\": null,\n" + + " \"optionalUint64\": null,\n" + + " \"optionalSint32\": null,\n" + + " \"optionalSint64\": null,\n" + + " \"optionalFixed32\": null,\n" + + " \"optionalFixed64\": null,\n" + + " \"optionalSfixed32\": null,\n" + + " \"optionalSfixed64\": null,\n" + + " \"optionalFloat\": null,\n" + + " \"optionalDouble\": null,\n" + + " \"optionalBool\": null,\n" + + " \"optionalString\": null,\n" + + " \"optionalBytes\": null,\n" + + " \"optionalNestedMessage\": null,\n" + + " \"optionalNestedEnum\": null,\n" + + " \"repeatedInt32\": null,\n" + + " \"repeatedInt64\": null,\n" + + " \"repeatedUint32\": null,\n" + + " \"repeatedUint64\": null,\n" + + " \"repeatedSint32\": null,\n" + + " \"repeatedSint64\": null,\n" + + " \"repeatedFixed32\": null,\n" + + " \"repeatedFixed64\": null,\n" + + " \"repeatedSfixed32\": null,\n" + + " \"repeatedSfixed64\": null,\n" + + " \"repeatedFloat\": null,\n" + + " \"repeatedDouble\": null,\n" + + " \"repeatedBool\": null,\n" + + " \"repeatedString\": null,\n" + + " \"repeatedBytes\": null,\n" + + " \"repeatedNestedMessage\": null,\n" + + " \"repeatedNestedEnum\": null\n" + + "}", builder); + TestAllTypes message = builder.build(); + assertEquals(TestAllTypes.getDefaultInstance(), message); + + // Repeated field elements can also be null. + builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"repeatedInt32\": [null, null],\n" + + " \"repeatedInt64\": [null, null],\n" + + " \"repeatedUint32\": [null, null],\n" + + " \"repeatedUint64\": [null, null],\n" + + " \"repeatedSint32\": [null, null],\n" + + " \"repeatedSint64\": [null, null],\n" + + " \"repeatedFixed32\": [null, null],\n" + + " \"repeatedFixed64\": [null, null],\n" + + " \"repeatedSfixed32\": [null, null],\n" + + " \"repeatedSfixed64\": [null, null],\n" + + " \"repeatedFloat\": [null, null],\n" + + " \"repeatedDouble\": [null, null],\n" + + " \"repeatedBool\": [null, null],\n" + + " \"repeatedString\": [null, null],\n" + + " \"repeatedBytes\": [null, null],\n" + + " \"repeatedNestedMessage\": [null, null],\n" + + " \"repeatedNestedEnum\": [null, null]\n" + + "}", builder); + message = builder.build(); + // "null" elements will be parsed to default values. + assertEquals(2, message.getRepeatedInt32Count()); + assertEquals(0, message.getRepeatedInt32(0)); + assertEquals(0, message.getRepeatedInt32(1)); + assertEquals(2, message.getRepeatedInt32Count()); + assertEquals(0, message.getRepeatedInt32(0)); + assertEquals(0, message.getRepeatedInt32(1)); + assertEquals(2, message.getRepeatedInt64Count()); + assertEquals(0, message.getRepeatedInt64(0)); + assertEquals(0, message.getRepeatedInt64(1)); + assertEquals(2, message.getRepeatedUint32Count()); + assertEquals(0, message.getRepeatedUint32(0)); + assertEquals(0, message.getRepeatedUint32(1)); + assertEquals(2, message.getRepeatedUint64Count()); + assertEquals(0, message.getRepeatedUint64(0)); + assertEquals(0, message.getRepeatedUint64(1)); + assertEquals(2, message.getRepeatedSint32Count()); + assertEquals(0, message.getRepeatedSint32(0)); + assertEquals(0, message.getRepeatedSint32(1)); + assertEquals(2, message.getRepeatedSint64Count()); + assertEquals(0, message.getRepeatedSint64(0)); + assertEquals(0, message.getRepeatedSint64(1)); + assertEquals(2, message.getRepeatedFixed32Count()); + assertEquals(0, message.getRepeatedFixed32(0)); + assertEquals(0, message.getRepeatedFixed32(1)); + assertEquals(2, message.getRepeatedFixed64Count()); + assertEquals(0, message.getRepeatedFixed64(0)); + assertEquals(0, message.getRepeatedFixed64(1)); + assertEquals(2, message.getRepeatedSfixed32Count()); + assertEquals(0, message.getRepeatedSfixed32(0)); + assertEquals(0, message.getRepeatedSfixed32(1)); + assertEquals(2, message.getRepeatedSfixed64Count()); + assertEquals(0, message.getRepeatedSfixed64(0)); + assertEquals(0, message.getRepeatedSfixed64(1)); + assertEquals(2, message.getRepeatedFloatCount()); + assertEquals(0f, message.getRepeatedFloat(0)); + assertEquals(0f, message.getRepeatedFloat(1)); + assertEquals(2, message.getRepeatedDoubleCount()); + assertEquals(0.0, message.getRepeatedDouble(0)); + assertEquals(0.0, message.getRepeatedDouble(1)); + assertEquals(2, message.getRepeatedBoolCount()); + assertFalse(message.getRepeatedBool(0)); + assertFalse(message.getRepeatedBool(1)); + assertEquals(2, message.getRepeatedStringCount()); + assertTrue(message.getRepeatedString(0).isEmpty()); + assertTrue(message.getRepeatedString(1).isEmpty()); + assertEquals(2, message.getRepeatedBytesCount()); + assertTrue(message.getRepeatedBytes(0).isEmpty()); + assertTrue(message.getRepeatedBytes(1).isEmpty()); + assertEquals(2, message.getRepeatedNestedMessageCount()); + assertEquals(NestedMessage.getDefaultInstance(), message.getRepeatedNestedMessage(0)); + assertEquals(NestedMessage.getDefaultInstance(), message.getRepeatedNestedMessage(1)); + assertEquals(2, message.getRepeatedNestedEnumCount()); + assertEquals(0, message.getRepeatedNestedEnumValue(0)); + assertEquals(0, message.getRepeatedNestedEnumValue(1)); + } + + public void testMapFields() throws Exception { + TestMap.Builder builder = TestMap.newBuilder(); + builder.getMutableInt32ToInt32Map().put(1, 10); + builder.getMutableInt64ToInt32Map().put(1234567890123456789L, 10); + builder.getMutableUint32ToInt32Map().put(2, 20); + builder.getMutableUint64ToInt32Map().put(2234567890123456789L, 20); + builder.getMutableSint32ToInt32Map().put(3, 30); + builder.getMutableSint64ToInt32Map().put(3234567890123456789L, 30); + builder.getMutableFixed32ToInt32Map().put(4, 40); + builder.getMutableFixed64ToInt32Map().put(4234567890123456789L, 40); + builder.getMutableSfixed32ToInt32Map().put(5, 50); + builder.getMutableSfixed64ToInt32Map().put(5234567890123456789L, 50); + builder.getMutableBoolToInt32Map().put(false, 6); + builder.getMutableStringToInt32Map().put("Hello", 10); + + builder.getMutableInt32ToInt64Map().put(1, 1234567890123456789L); + builder.getMutableInt32ToUint32Map().put(2, 20); + builder.getMutableInt32ToUint64Map().put(2, 2234567890123456789L); + builder.getMutableInt32ToSint32Map().put(3, 30); + builder.getMutableInt32ToSint64Map().put(3, 3234567890123456789L); + builder.getMutableInt32ToFixed32Map().put(4, 40); + builder.getMutableInt32ToFixed64Map().put(4, 4234567890123456789L); + builder.getMutableInt32ToSfixed32Map().put(5, 50); + builder.getMutableInt32ToSfixed64Map().put(5, 5234567890123456789L); + builder.getMutableInt32ToFloatMap().put(6, 1.5f); + builder.getMutableInt32ToDoubleMap().put(6, 1.25); + builder.getMutableInt32ToBoolMap().put(7, false); + builder.getMutableInt32ToStringMap().put(7, "World"); + builder.getMutableInt32ToBytesMap().put( + 8, ByteString.copyFrom(new byte[]{1, 2, 3})); + builder.getMutableInt32ToMessageMap().put( + 8, NestedMessage.newBuilder().setValue(1234).build()); + builder.getMutableInt32ToEnumMap().put(9, NestedEnum.BAR); + TestMap message = builder.build(); + + assertEquals( + "{\n" + + " \"int32ToInt32Map\": {\n" + + " \"1\": 10\n" + + " },\n" + + " \"int64ToInt32Map\": {\n" + + " \"1234567890123456789\": 10\n" + + " },\n" + + " \"uint32ToInt32Map\": {\n" + + " \"2\": 20\n" + + " },\n" + + " \"uint64ToInt32Map\": {\n" + + " \"2234567890123456789\": 20\n" + + " },\n" + + " \"sint32ToInt32Map\": {\n" + + " \"3\": 30\n" + + " },\n" + + " \"sint64ToInt32Map\": {\n" + + " \"3234567890123456789\": 30\n" + + " },\n" + + " \"fixed32ToInt32Map\": {\n" + + " \"4\": 40\n" + + " },\n" + + " \"fixed64ToInt32Map\": {\n" + + " \"4234567890123456789\": 40\n" + + " },\n" + + " \"sfixed32ToInt32Map\": {\n" + + " \"5\": 50\n" + + " },\n" + + " \"sfixed64ToInt32Map\": {\n" + + " \"5234567890123456789\": 50\n" + + " },\n" + + " \"boolToInt32Map\": {\n" + + " \"false\": 6\n" + + " },\n" + + " \"stringToInt32Map\": {\n" + + " \"Hello\": 10\n" + + " },\n" + + " \"int32ToInt64Map\": {\n" + + " \"1\": \"1234567890123456789\"\n" + + " },\n" + + " \"int32ToUint32Map\": {\n" + + " \"2\": 20\n" + + " },\n" + + " \"int32ToUint64Map\": {\n" + + " \"2\": \"2234567890123456789\"\n" + + " },\n" + + " \"int32ToSint32Map\": {\n" + + " \"3\": 30\n" + + " },\n" + + " \"int32ToSint64Map\": {\n" + + " \"3\": \"3234567890123456789\"\n" + + " },\n" + + " \"int32ToFixed32Map\": {\n" + + " \"4\": 40\n" + + " },\n" + + " \"int32ToFixed64Map\": {\n" + + " \"4\": \"4234567890123456789\"\n" + + " },\n" + + " \"int32ToSfixed32Map\": {\n" + + " \"5\": 50\n" + + " },\n" + + " \"int32ToSfixed64Map\": {\n" + + " \"5\": \"5234567890123456789\"\n" + + " },\n" + + " \"int32ToFloatMap\": {\n" + + " \"6\": 1.5\n" + + " },\n" + + " \"int32ToDoubleMap\": {\n" + + " \"6\": 1.25\n" + + " },\n" + + " \"int32ToBoolMap\": {\n" + + " \"7\": false\n" + + " },\n" + + " \"int32ToStringMap\": {\n" + + " \"7\": \"World\"\n" + + " },\n" + + " \"int32ToBytesMap\": {\n" + + " \"8\": \"AQID\"\n" + + " },\n" + + " \"int32ToMessageMap\": {\n" + + " \"8\": {\n" + + " \"value\": 1234\n" + + " }\n" + + " },\n" + + " \"int32ToEnumMap\": {\n" + + " \"9\": \"BAR\"\n" + + " }\n" + + "}", toJsonString(message)); + assertRoundTripEquals(message); + + // Test multiple entries. + builder = TestMap.newBuilder(); + builder.getMutableInt32ToInt32Map().put(1, 2); + builder.getMutableInt32ToInt32Map().put(3, 4); + message = builder.build(); + + assertEquals( + "{\n" + + " \"int32ToInt32Map\": {\n" + + " \"1\": 2,\n" + + " \"3\": 4\n" + + " }\n" + + "}", toJsonString(message)); + assertRoundTripEquals(message); + } + + public void testMapNullValueIsDefault() throws Exception { + TestMap.Builder builder = TestMap.newBuilder(); + mergeFromJson( + "{\n" + + " \"int32ToInt32Map\": {\"1\": null},\n" + + " \"int32ToMessageMap\": {\"2\": null}\n" + + "}", builder); + TestMap message = builder.build(); + assertTrue(message.getInt32ToInt32Map().containsKey(1)); + assertEquals(0, message.getInt32ToInt32Map().get(1).intValue()); + assertTrue(message.getInt32ToMessageMap().containsKey(2)); + assertEquals(0, message.getInt32ToMessageMap().get(2).getValue()); + } + + public void testParserAcceptNonQuotedObjectKey() throws Exception { + TestMap.Builder builder = TestMap.newBuilder(); + mergeFromJson( + "{\n" + + " int32ToInt32Map: {1: 2},\n" + + " stringToInt32Map: {hello: 3}\n" + + "}", builder); + TestMap message = builder.build(); + assertEquals(2, message.getInt32ToInt32Map().get(1).intValue()); + assertEquals(3, message.getStringToInt32Map().get("hello").intValue()); + } + + public void testWrappers() throws Exception { + TestWrappers.Builder builder = TestWrappers.newBuilder(); + builder.getBoolValueBuilder().setValue(false); + builder.getInt32ValueBuilder().setValue(0); + builder.getInt64ValueBuilder().setValue(0); + builder.getUint32ValueBuilder().setValue(0); + builder.getUint64ValueBuilder().setValue(0); + builder.getFloatValueBuilder().setValue(0.0f); + builder.getDoubleValueBuilder().setValue(0.0); + builder.getStringValueBuilder().setValue(""); + builder.getBytesValueBuilder().setValue(ByteString.EMPTY); + TestWrappers message = builder.build(); + + assertEquals( + "{\n" + + " \"int32Value\": 0,\n" + + " \"uint32Value\": 0,\n" + + " \"int64Value\": \"0\",\n" + + " \"uint64Value\": \"0\",\n" + + " \"floatValue\": 0.0,\n" + + " \"doubleValue\": 0.0,\n" + + " \"boolValue\": false,\n" + + " \"stringValue\": \"\",\n" + + " \"bytesValue\": \"\"\n" + + "}", toJsonString(message)); + assertRoundTripEquals(message); + + builder = TestWrappers.newBuilder(); + builder.getBoolValueBuilder().setValue(true); + builder.getInt32ValueBuilder().setValue(1); + builder.getInt64ValueBuilder().setValue(2); + builder.getUint32ValueBuilder().setValue(3); + builder.getUint64ValueBuilder().setValue(4); + builder.getFloatValueBuilder().setValue(5.0f); + builder.getDoubleValueBuilder().setValue(6.0); + builder.getStringValueBuilder().setValue("7"); + builder.getBytesValueBuilder().setValue(ByteString.copyFrom(new byte[]{8})); + message = builder.build(); + + assertEquals( + "{\n" + + " \"int32Value\": 1,\n" + + " \"uint32Value\": 3,\n" + + " \"int64Value\": \"2\",\n" + + " \"uint64Value\": \"4\",\n" + + " \"floatValue\": 5.0,\n" + + " \"doubleValue\": 6.0,\n" + + " \"boolValue\": true,\n" + + " \"stringValue\": \"7\",\n" + + " \"bytesValue\": \"CA==\"\n" + + "}", toJsonString(message)); + assertRoundTripEquals(message); + } + + public void testTimestamp() throws Exception { + TestTimestamp message = TestTimestamp.newBuilder() + .setTimestampValue(TimeUtil.parseTimestamp("1970-01-01T00:00:00Z")) + .build(); + + assertEquals( + "{\n" + + " \"timestampValue\": \"1970-01-01T00:00:00Z\"\n" + + "}", toJsonString(message)); + assertRoundTripEquals(message); + } + + public void testDuration() throws Exception { + TestDuration message = TestDuration.newBuilder() + .setDurationValue(TimeUtil.parseDuration("12345s")) + .build(); + + assertEquals( + "{\n" + + " \"durationValue\": \"12345s\"\n" + + "}", toJsonString(message)); + assertRoundTripEquals(message); + } + + public void testFieldMask() throws Exception { + TestFieldMask message = TestFieldMask.newBuilder() + .setFieldMaskValue(FieldMaskUtil.fromString("foo.bar,baz")) + .build(); + + assertEquals( + "{\n" + + " \"fieldMaskValue\": \"foo.bar,baz\"\n" + + "}", toJsonString(message)); + assertRoundTripEquals(message); + } + + public void testStruct() throws Exception { + // Build a struct with all possible values. + TestStruct.Builder builder = TestStruct.newBuilder(); + Struct.Builder structBuilder = builder.getStructValueBuilder(); + structBuilder.getMutableFields().put( + "null_value", Value.newBuilder().setNullValueValue(0).build()); + structBuilder.getMutableFields().put( + "number_value", Value.newBuilder().setNumberValue(1.25).build()); + structBuilder.getMutableFields().put( + "string_value", Value.newBuilder().setStringValue("hello").build()); + Struct.Builder subStructBuilder = Struct.newBuilder(); + subStructBuilder.getMutableFields().put( + "number_value", Value.newBuilder().setNumberValue(1234).build()); + structBuilder.getMutableFields().put( + "struct_value", Value.newBuilder().setStructValue(subStructBuilder.build()).build()); + ListValue.Builder listBuilder = ListValue.newBuilder(); + listBuilder.addValues(Value.newBuilder().setNumberValue(1.125).build()); + listBuilder.addValues(Value.newBuilder().setNullValueValue(0).build()); + structBuilder.getMutableFields().put( + "list_value", Value.newBuilder().setListValue(listBuilder.build()).build()); + TestStruct message = builder.build(); + + assertEquals( + "{\n" + + " \"structValue\": {\n" + + " \"null_value\": null,\n" + + " \"number_value\": 1.25,\n" + + " \"string_value\": \"hello\",\n" + + " \"struct_value\": {\n" + + " \"number_value\": 1234.0\n" + + " },\n" + + " \"list_value\": [1.125, null]\n" + + " }\n" + + "}", toJsonString(message)); + assertRoundTripEquals(message); + } + + public void testAnyFields() throws Exception { + TestAllTypes content = TestAllTypes.newBuilder().setOptionalInt32(1234).build(); + TestAny message = TestAny.newBuilder().setAnyValue(Any.pack(content)).build(); + + // A TypeRegistry must be provided in order to convert Any types. + try { + toJsonString(message); + fail("Exception is expected."); + } catch (IOException e) { + // Expected. + } + + JsonFormat.TypeRegistry registry = JsonFormat.TypeRegistry.newBuilder() + .add(TestAllTypes.getDescriptor()).build(); + JsonFormat.Printer printer = JsonFormat.printer().usingTypeRegistry(registry); + + assertEquals( + "{\n" + + " \"anyValue\": {\n" + + " \"@type\": \"type.googleapis.com/json_test.TestAllTypes\",\n" + + " \"optionalInt32\": 1234\n" + + " }\n" + + "}" , printer.print(message)); + assertRoundTripEquals(message, registry); + + + // Well-known types have a special formatting when embedded in Any. + // + // 1. Any in Any. + Any anyMessage = Any.pack(Any.pack(content)); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.Any\",\n" + + " \"value\": {\n" + + " \"@type\": \"type.googleapis.com/json_test.TestAllTypes\",\n" + + " \"optionalInt32\": 1234\n" + + " }\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + + // 2. Wrappers in Any. + anyMessage = Any.pack(Int32Value.newBuilder().setValue(12345).build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.Int32Value\",\n" + + " \"value\": 12345\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + anyMessage = Any.pack(UInt32Value.newBuilder().setValue(12345).build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.UInt32Value\",\n" + + " \"value\": 12345\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + anyMessage = Any.pack(Int64Value.newBuilder().setValue(12345).build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.Int64Value\",\n" + + " \"value\": \"12345\"\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + anyMessage = Any.pack(UInt64Value.newBuilder().setValue(12345).build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.UInt64Value\",\n" + + " \"value\": \"12345\"\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + anyMessage = Any.pack(FloatValue.newBuilder().setValue(12345).build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.FloatValue\",\n" + + " \"value\": 12345.0\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + anyMessage = Any.pack(DoubleValue.newBuilder().setValue(12345).build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.DoubleValue\",\n" + + " \"value\": 12345.0\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + anyMessage = Any.pack(BoolValue.newBuilder().setValue(true).build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.BoolValue\",\n" + + " \"value\": true\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + anyMessage = Any.pack(StringValue.newBuilder().setValue("Hello").build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.StringValue\",\n" + + " \"value\": \"Hello\"\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + anyMessage = Any.pack(BytesValue.newBuilder().setValue( + ByteString.copyFrom(new byte[]{1, 2})).build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.BytesValue\",\n" + + " \"value\": \"AQI=\"\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + + // 3. Timestamp in Any. + anyMessage = Any.pack(TimeUtil.parseTimestamp("1969-12-31T23:59:59Z")); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.Timestamp\",\n" + + " \"value\": \"1969-12-31T23:59:59Z\"\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + + // 4. Duration in Any + anyMessage = Any.pack(TimeUtil.parseDuration("12345.10s")); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n" + + " \"value\": \"12345.100s\"\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + + // 5. FieldMask in Any + anyMessage = Any.pack(FieldMaskUtil.fromString("foo.bar,baz")); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.FieldMask\",\n" + + " \"value\": \"foo.bar,baz\"\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + + // 6. Struct in Any + Struct.Builder structBuilder = Struct.newBuilder(); + structBuilder.getMutableFields().put( + "number", Value.newBuilder().setNumberValue(1.125).build()); + anyMessage = Any.pack(structBuilder.build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.Struct\",\n" + + " \"value\": {\n" + + " \"number\": 1.125\n" + + " }\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); + } + + public void testParserMissingTypeUrl() throws Exception { + try { + Any.Builder builder = Any.newBuilder(); + mergeFromJson( + "{\n" + + " \"optionalInt32\": 1234\n" + + "}", builder); + fail("Exception is expected."); + } catch (IOException e) { + // Expected. + } + } + + public void testParserUnexpectedTypeUrl() throws Exception { + try { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"@type\": \"type.googleapis.com/json_test.TestAllTypes\",\n" + + " \"optionalInt32\": 12345\n" + + "}", builder); + fail("Exception is expected."); + } catch (IOException e) { + // Expected. + } + } + + public void testParserRejectTrailingComma() throws Exception { + try { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"optionalInt32\": 12345,\n" + + "}", builder); + fail("Exception is expected."); + } catch (IOException e) { + // Expected. + } + + // TODO(xiaofeng): GSON allows trailing comma in arrays even after I set + // the JsonReader to non-lenient mode. If we want to enforce strict JSON + // compliance, we might want to switch to a different JSON parser or + // implement one by ourselves. + // try { + // TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + // JsonFormat.merge( + // "{\n" + // + " \"repeatedInt32\": [12345,]\n" + // + "}", builder); + // fail("Exception is expected."); + // } catch (IOException e) { + // // Expected. + // } + } + + public void testParserRejectInvalidBase64() throws Exception { + try { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"optionalBytes\": \"!@#$\"\n" + + "}", builder); + fail("Exception is expected."); + } catch (InvalidProtocolBufferException e) { + // Expected. + } + } + + public void testParserRejectInvalidEnumValue() throws Exception { + try { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"optionalNestedEnum\": \"XXX\"\n" + + "}", builder); + fail("Exception is expected."); + } catch (InvalidProtocolBufferException e) { + // Expected. + } + } +} diff --git a/java/util/src/test/java/com/google/protobuf/util/TimeUtilTest.java b/java/util/src/test/java/com/google/protobuf/util/TimeUtilTest.java new file mode 100644 index 0000000000..fe5617e115 --- /dev/null +++ b/java/util/src/test/java/com/google/protobuf/util/TimeUtilTest.java @@ -0,0 +1,439 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.google.protobuf.util; + +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; + +import junit.framework.TestCase; + +import org.junit.Assert; + +import java.text.ParseException; + +/** Unit tests for {@link TimeUtil}. */ +public class TimeUtilTest extends TestCase { + public void testTimestampStringFormat() throws Exception { + Timestamp start = TimeUtil.parseTimestamp("0001-01-01T00:00:00Z"); + Timestamp end = TimeUtil.parseTimestamp("9999-12-31T23:59:59.999999999Z"); + assertEquals(TimeUtil.TIMESTAMP_SECONDS_MIN, start.getSeconds()); + assertEquals(0, start.getNanos()); + assertEquals(TimeUtil.TIMESTAMP_SECONDS_MAX, end.getSeconds()); + assertEquals(999999999, end.getNanos()); + assertEquals("0001-01-01T00:00:00Z", TimeUtil.toString(start)); + assertEquals("9999-12-31T23:59:59.999999999Z", TimeUtil.toString(end)); + + Timestamp value = TimeUtil.parseTimestamp("1970-01-01T00:00:00Z"); + assertEquals(0, value.getSeconds()); + assertEquals(0, value.getNanos()); + + // Test negative timestamps. + value = TimeUtil.parseTimestamp("1969-12-31T23:59:59.999Z"); + assertEquals(-1, value.getSeconds()); + // Nano part is in the range of [0, 999999999] for Timestamp. + assertEquals(999000000, value.getNanos()); + + // Test that 3, 6, or 9 digits are used for the fractional part. + value = Timestamp.newBuilder().setNanos(10).build(); + assertEquals("1970-01-01T00:00:00.000000010Z", TimeUtil.toString(value)); + value = Timestamp.newBuilder().setNanos(10000).build(); + assertEquals("1970-01-01T00:00:00.000010Z", TimeUtil.toString(value)); + value = Timestamp.newBuilder().setNanos(10000000).build(); + assertEquals("1970-01-01T00:00:00.010Z", TimeUtil.toString(value)); + + // Test that parsing accepts timezone offsets. + value = TimeUtil.parseTimestamp("1970-01-01T00:00:00.010+08:00"); + assertEquals("1969-12-31T16:00:00.010Z", TimeUtil.toString(value)); + value = TimeUtil.parseTimestamp("1970-01-01T00:00:00.010-08:00"); + assertEquals("1970-01-01T08:00:00.010Z", TimeUtil.toString(value)); + } + + public void testTimetampInvalidFormat() throws Exception { + try { + // Value too small. + Timestamp value = Timestamp.newBuilder() + .setSeconds(TimeUtil.TIMESTAMP_SECONDS_MIN - 1).build(); + TimeUtil.toString(value); + Assert.fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + + try { + // Value too large. + Timestamp value = Timestamp.newBuilder() + .setSeconds(TimeUtil.TIMESTAMP_SECONDS_MAX + 1).build(); + TimeUtil.toString(value); + Assert.fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + + try { + // Invalid nanos value. + Timestamp value = Timestamp.newBuilder().setNanos(-1).build(); + TimeUtil.toString(value); + Assert.fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + + try { + // Invalid nanos value. + Timestamp value = Timestamp.newBuilder().setNanos(1000000000).build(); + TimeUtil.toString(value); + Assert.fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + + try { + // Value to small. + TimeUtil.parseTimestamp("0000-01-01T00:00:00Z"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Value to large. + TimeUtil.parseTimestamp("10000-01-01T00:00:00Z"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Missing 'T'. + TimeUtil.parseTimestamp("1970-01-01 00:00:00Z"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Missing 'Z'. + TimeUtil.parseTimestamp("1970-01-01T00:00:00"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Invalid offset. + TimeUtil.parseTimestamp("1970-01-01T00:00:00+0000"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Trailing text. + TimeUtil.parseTimestamp("1970-01-01T00:00:00Z0"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Invalid nanosecond value. + TimeUtil.parseTimestamp("1970-01-01T00:00:00.ABCZ"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + } + + public void testDurationStringFormat() throws Exception { + Timestamp start = TimeUtil.parseTimestamp("0001-01-01T00:00:00Z"); + Timestamp end = TimeUtil.parseTimestamp("9999-12-31T23:59:59.999999999Z"); + Duration duration = TimeUtil.distance(start, end); + assertEquals("315537897599.999999999s", TimeUtil.toString(duration)); + duration = TimeUtil.distance(end, start); + assertEquals("-315537897599.999999999s", TimeUtil.toString(duration)); + + // Generated output should contain 3, 6, or 9 fractional digits. + duration = Duration.newBuilder().setSeconds(1).build(); + assertEquals("1s", TimeUtil.toString(duration)); + duration = Duration.newBuilder().setNanos(10000000).build(); + assertEquals("0.010s", TimeUtil.toString(duration)); + duration = Duration.newBuilder().setNanos(10000).build(); + assertEquals("0.000010s", TimeUtil.toString(duration)); + duration = Duration.newBuilder().setNanos(10).build(); + assertEquals("0.000000010s", TimeUtil.toString(duration)); + + // Parsing accepts an fractional digits as long as they fit into nano + // precision. + duration = TimeUtil.parseDuration("0.1s"); + assertEquals(100000000, duration.getNanos()); + duration = TimeUtil.parseDuration("0.0001s"); + assertEquals(100000, duration.getNanos()); + duration = TimeUtil.parseDuration("0.0000001s"); + assertEquals(100, duration.getNanos()); + + // Duration must support range from -315,576,000,000s to +315576000000s + // which includes negative values. + duration = TimeUtil.parseDuration("315576000000.999999999s"); + assertEquals(315576000000L, duration.getSeconds()); + assertEquals(999999999, duration.getNanos()); + duration = TimeUtil.parseDuration("-315576000000.999999999s"); + assertEquals(-315576000000L, duration.getSeconds()); + assertEquals(-999999999, duration.getNanos()); + } + + public void testDurationInvalidFormat() throws Exception { + try { + // Value too small. + Duration value = Duration.newBuilder() + .setSeconds(TimeUtil.DURATION_SECONDS_MIN - 1).build(); + TimeUtil.toString(value); + Assert.fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + + try { + // Value too large. + Duration value = Duration.newBuilder() + .setSeconds(TimeUtil.DURATION_SECONDS_MAX + 1).build(); + TimeUtil.toString(value); + Assert.fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + + try { + // Invalid nanos value. + Duration value = Duration.newBuilder().setSeconds(1).setNanos(-1) + .build(); + TimeUtil.toString(value); + Assert.fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + + try { + // Invalid nanos value. + Duration value = Duration.newBuilder().setSeconds(-1).setNanos(1) + .build(); + TimeUtil.toString(value); + Assert.fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + + try { + // Value too small. + TimeUtil.parseDuration("-315576000001s"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Value too large. + TimeUtil.parseDuration("315576000001s"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Empty. + TimeUtil.parseDuration(""); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Missing "s". + TimeUtil.parseDuration("0"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Invalid trailing data. + TimeUtil.parseDuration("0s0"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + + try { + // Invalid prefix. + TimeUtil.parseDuration("--1s"); + Assert.fail("Exception is expected."); + } catch (ParseException e) { + // Expected. + } + } + + public void testTimestampConversion() throws Exception { + Timestamp timestamp = + TimeUtil.parseTimestamp("1970-01-01T00:00:01.111111111Z"); + assertEquals(1111111111, TimeUtil.toNanos(timestamp)); + assertEquals(1111111, TimeUtil.toMicros(timestamp)); + assertEquals(1111, TimeUtil.toMillis(timestamp)); + timestamp = TimeUtil.createTimestampFromNanos(1111111111); + assertEquals("1970-01-01T00:00:01.111111111Z", TimeUtil.toString(timestamp)); + timestamp = TimeUtil.createTimestampFromMicros(1111111); + assertEquals("1970-01-01T00:00:01.111111Z", TimeUtil.toString(timestamp)); + timestamp = TimeUtil.createTimestampFromMillis(1111); + assertEquals("1970-01-01T00:00:01.111Z", TimeUtil.toString(timestamp)); + + timestamp = TimeUtil.parseTimestamp("1969-12-31T23:59:59.111111111Z"); + assertEquals(-888888889, TimeUtil.toNanos(timestamp)); + assertEquals(-888889, TimeUtil.toMicros(timestamp)); + assertEquals(-889, TimeUtil.toMillis(timestamp)); + timestamp = TimeUtil.createTimestampFromNanos(-888888889); + assertEquals("1969-12-31T23:59:59.111111111Z", TimeUtil.toString(timestamp)); + timestamp = TimeUtil.createTimestampFromMicros(-888889); + assertEquals("1969-12-31T23:59:59.111111Z", TimeUtil.toString(timestamp)); + timestamp = TimeUtil.createTimestampFromMillis(-889); + assertEquals("1969-12-31T23:59:59.111Z", TimeUtil.toString(timestamp)); + } + + public void testDurationConversion() throws Exception { + Duration duration = TimeUtil.parseDuration("1.111111111s"); + assertEquals(1111111111, TimeUtil.toNanos(duration)); + assertEquals(1111111, TimeUtil.toMicros(duration)); + assertEquals(1111, TimeUtil.toMillis(duration)); + duration = TimeUtil.createDurationFromNanos(1111111111); + assertEquals("1.111111111s", TimeUtil.toString(duration)); + duration = TimeUtil.createDurationFromMicros(1111111); + assertEquals("1.111111s", TimeUtil.toString(duration)); + duration = TimeUtil.createDurationFromMillis(1111); + assertEquals("1.111s", TimeUtil.toString(duration)); + + duration = TimeUtil.parseDuration("-1.111111111s"); + assertEquals(-1111111111, TimeUtil.toNanos(duration)); + assertEquals(-1111111, TimeUtil.toMicros(duration)); + assertEquals(-1111, TimeUtil.toMillis(duration)); + duration = TimeUtil.createDurationFromNanos(-1111111111); + assertEquals("-1.111111111s", TimeUtil.toString(duration)); + duration = TimeUtil.createDurationFromMicros(-1111111); + assertEquals("-1.111111s", TimeUtil.toString(duration)); + duration = TimeUtil.createDurationFromMillis(-1111); + assertEquals("-1.111s", TimeUtil.toString(duration)); + } + + public void testTimeOperations() throws Exception { + Timestamp start = TimeUtil.parseTimestamp("0001-01-01T00:00:00Z"); + Timestamp end = TimeUtil.parseTimestamp("9999-12-31T23:59:59.999999999Z"); + + Duration duration = TimeUtil.distance(start, end); + assertEquals("315537897599.999999999s", TimeUtil.toString(duration)); + Timestamp value = TimeUtil.add(start, duration); + assertEquals(end, value); + value = TimeUtil.subtract(end, duration); + assertEquals(start, value); + + duration = TimeUtil.distance(end, start); + assertEquals("-315537897599.999999999s", TimeUtil.toString(duration)); + value = TimeUtil.add(end, duration); + assertEquals(start, value); + value = TimeUtil.subtract(start, duration); + assertEquals(end, value); + + // Result is larger than Long.MAX_VALUE. + try { + duration = TimeUtil.parseDuration("315537897599.999999999s"); + duration = TimeUtil.multiply(duration, 315537897599.999999999); + Assert.fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + + // Result is lesser than Long.MIN_VALUE. + try { + duration = TimeUtil.parseDuration("315537897599.999999999s"); + duration = TimeUtil.multiply(duration, -315537897599.999999999); + Assert.fail("Exception is expected."); + } catch (IllegalArgumentException e) { + // Expected. + } + + duration = TimeUtil.parseDuration("-1.125s"); + duration = TimeUtil.divide(duration, 2.0); + assertEquals("-0.562500s", TimeUtil.toString(duration)); + duration = TimeUtil.multiply(duration, 2.0); + assertEquals("-1.125s", TimeUtil.toString(duration)); + + duration = TimeUtil.add(duration, duration); + assertEquals("-2.250s", TimeUtil.toString(duration)); + + duration = TimeUtil.subtract(duration, TimeUtil.parseDuration("-1s")); + assertEquals("-1.250s", TimeUtil.toString(duration)); + + // Multiplications (with results larger than Long.MAX_VALUE in nanoseconds). + duration = TimeUtil.parseDuration("0.999999999s"); + assertEquals("315575999684.424s", + TimeUtil.toString(TimeUtil.multiply(duration, 315576000000L))); + duration = TimeUtil.parseDuration("-0.999999999s"); + assertEquals("-315575999684.424s", + TimeUtil.toString(TimeUtil.multiply(duration, 315576000000L))); + assertEquals("315575999684.424s", + TimeUtil.toString(TimeUtil.multiply(duration, -315576000000L))); + + // Divisions (with values larger than Long.MAX_VALUE in nanoseconds). + Duration d1 = TimeUtil.parseDuration("315576000000s"); + Duration d2 = TimeUtil.subtract(d1, TimeUtil.createDurationFromNanos(1)); + assertEquals(1, TimeUtil.divide(d1, d2)); + assertEquals(0, TimeUtil.divide(d2, d1)); + assertEquals("0.000000001s", TimeUtil.toString(TimeUtil.remainder(d1, d2))); + assertEquals("315575999999.999999999s", + TimeUtil.toString(TimeUtil.remainder(d2, d1))); + + // Divisions involving negative values. + // + // (-5) / 2 = -2, remainder = -1 + d1 = TimeUtil.parseDuration("-5s"); + d2 = TimeUtil.parseDuration("2s"); + assertEquals(-2, TimeUtil.divide(d1, d2)); + assertEquals(-2, TimeUtil.divide(d1, 2).getSeconds()); + assertEquals(-1, TimeUtil.remainder(d1, d2).getSeconds()); + // (-5) / (-2) = 2, remainder = -1 + d1 = TimeUtil.parseDuration("-5s"); + d2 = TimeUtil.parseDuration("-2s"); + assertEquals(2, TimeUtil.divide(d1, d2)); + assertEquals(2, TimeUtil.divide(d1, -2).getSeconds()); + assertEquals(-1, TimeUtil.remainder(d1, d2).getSeconds()); + // 5 / (-2) = -2, remainder = 1 + d1 = TimeUtil.parseDuration("5s"); + d2 = TimeUtil.parseDuration("-2s"); + assertEquals(-2, TimeUtil.divide(d1, d2)); + assertEquals(-2, TimeUtil.divide(d1, -2).getSeconds()); + assertEquals(1, TimeUtil.remainder(d1, d2).getSeconds()); + } +} diff --git a/java/util/src/test/java/com/google/protobuf/util/json_test.proto b/java/util/src/test/java/com/google/protobuf/util/json_test.proto new file mode 100644 index 0000000000..b2753af6bf --- /dev/null +++ b/java/util/src/test/java/com/google/protobuf/util/json_test.proto @@ -0,0 +1,158 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package json_test; + +option java_package = "com.google.protobuf.util"; +option java_outer_classname = "JsonTestProto"; + +import "google/protobuf/any.proto"; +import "google/protobuf/wrappers.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/struct.proto"; + +message TestAllTypes { + enum NestedEnum { + FOO = 0; + BAR = 1; + BAZ = 2; + } + message NestedMessage { + int32 value = 1; + } + + int32 optional_int32 = 1; + int64 optional_int64 = 2; + uint32 optional_uint32 = 3; + uint64 optional_uint64 = 4; + sint32 optional_sint32 = 5; + sint64 optional_sint64 = 6; + fixed32 optional_fixed32 = 7; + fixed64 optional_fixed64 = 8; + sfixed32 optional_sfixed32 = 9; + sfixed64 optional_sfixed64 = 10; + float optional_float = 11; + double optional_double = 12; + bool optional_bool = 13; + string optional_string = 14; + bytes optional_bytes = 15; + NestedMessage optional_nested_message = 18; + NestedEnum optional_nested_enum = 21; + + // Repeated + repeated int32 repeated_int32 = 31; + repeated int64 repeated_int64 = 32; + repeated uint32 repeated_uint32 = 33; + repeated uint64 repeated_uint64 = 34; + repeated sint32 repeated_sint32 = 35; + repeated sint64 repeated_sint64 = 36; + repeated fixed32 repeated_fixed32 = 37; + repeated fixed64 repeated_fixed64 = 38; + repeated sfixed32 repeated_sfixed32 = 39; + repeated sfixed64 repeated_sfixed64 = 40; + repeated float repeated_float = 41; + repeated double repeated_double = 42; + repeated bool repeated_bool = 43; + repeated string repeated_string = 44; + repeated bytes repeated_bytes = 45; + repeated NestedMessage repeated_nested_message = 48; + repeated NestedEnum repeated_nested_enum = 51; +} + +message TestMap { + // Instead of testing all combinations (too many), we only make sure all + // valid types have been used at least in one field as key and in one + // field as value. + map int32_to_int32_map = 1; + map int64_to_int32_map = 2; + map uint32_to_int32_map = 3; + map uint64_to_int32_map = 4; + map sint32_to_int32_map = 5; + map sint64_to_int32_map = 6; + map fixed32_to_int32_map = 7; + map fixed64_to_int32_map = 8; + map sfixed32_to_int32_map = 9; + map sfixed64_to_int32_map = 10; + map bool_to_int32_map = 11; + map string_to_int32_map = 12; + + map int32_to_int64_map = 101; + map int32_to_uint32_map = 102; + map int32_to_uint64_map = 103; + map int32_to_sint32_map = 104; + map int32_to_sint64_map = 105; + map int32_to_fixed32_map = 106; + map int32_to_fixed64_map = 107; + map int32_to_sfixed32_map = 108; + map int32_to_sfixed64_map = 109; + map int32_to_float_map = 110; + map int32_to_double_map = 111; + map int32_to_bool_map = 112; + map int32_to_string_map = 113; + map int32_to_bytes_map = 114; + map int32_to_message_map = 115; + map int32_to_enum_map = 116; +} + +message TestWrappers { + google.protobuf.Int32Value int32_value = 1; + google.protobuf.UInt32Value uint32_value = 2; + google.protobuf.Int64Value int64_value = 3; + google.protobuf.UInt64Value uint64_value = 4; + google.protobuf.FloatValue float_value = 5; + google.protobuf.DoubleValue double_value = 6; + google.protobuf.BoolValue bool_value = 7; + google.protobuf.StringValue string_value = 8; + google.protobuf.BytesValue bytes_value = 9; +} + +message TestTimestamp { + google.protobuf.Timestamp timestamp_value = 1; +} + +message TestDuration { + google.protobuf.Duration duration_value = 1; +} + +message TestFieldMask { + google.protobuf.FieldMask field_mask_value = 1; +} + +message TestStruct { + google.protobuf.Struct struct_value = 1; +} + +message TestAny { + google.protobuf.Any any_value = 1; +}