| // 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. |
| * |
| * <p>For example, FieldMask "foo.bar,foo.baz,bar.baz" as a tree will be: |
| * <pre> |
| * [root] -+- foo -+- bar |
| * | | |
| * | +- baz |
| * | |
| * +- bar --- baz |
| * </pre> |
| * |
| * <p>By representing FieldMasks with this tree structure we can easily convert |
| * a FieldMask to a canonical form, merge two FieldMasks, calculate the |
| * intersection to two FieldMasks and traverse all fields specified by the |
| * FieldMask in a message tree. |
| */ |
| 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<String, Node> children = new TreeMap<String, Node>(); |
| } |
| |
| 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<String> paths = new ArrayList<String>(); |
| getFieldPaths(root, "", paths); |
| return FieldMask.newBuilder().addAllPaths(paths).build(); |
| } |
| |
| /** Gathers all field paths in a sub-tree. */ |
| private void getFieldPaths(Node node, String path, List<String> paths) { |
| if (node.children.isEmpty()) { |
| paths.add(path); |
| return; |
| } |
| for (Entry<String, Node> entry : node.children.entrySet()) { |
| String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey(); |
| getFieldPaths(entry.getValue(), childPath, paths); |
| } |
| } |
| |
| /** |
| * Adds the intersection of this tree with the given {@code path} to |
| * {@code output}. |
| */ |
| 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<String> paths = new ArrayList<String>(); |
| getFieldPaths(node, path, paths); |
| for (String value : paths) { |
| output.addFieldPath(value); |
| } |
| } |
| |
| /** |
| * Merges all fields specified by this FieldMaskTree from {@code source} to |
| * {@code destination}. |
| */ |
| 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<String, Node> entry : node.children.entrySet()) { |
| FieldDescriptor field = descriptor.findFieldByName(entry.getKey()); |
| if (field == null) { |
| logger.warning( |
| "Cannot find field \"" |
| + entry.getKey() |
| + "\" in message type " |
| + descriptor.getFullName()); |
| continue; |
| } |
| if (!entry.getValue().children.isEmpty()) { |
| if (field.isRepeated() || field.getJavaType() != FieldDescriptor.JavaType.MESSAGE) { |
| logger.warning( |
| "Field \"" |
| + field.getFullName() |
| + "\" is not a " |
| + "singluar message field and cannot have sub-fields."); |
| continue; |
| } |
| String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey(); |
| merge( |
| entry.getValue(), |
| childPath, |
| (Message) source.getField(field), |
| destination.getFieldBuilder(field), |
| options); |
| continue; |
| } |
| if (field.isRepeated()) { |
| if (options.replaceRepeatedFields()) { |
| destination.setField(field, source.getField(field)); |
| } else { |
| for (Object element : (List) source.getField(field)) { |
| destination.addRepeatedField(field, element); |
| } |
| } |
| } else { |
| if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { |
| if (options.replaceMessageFields()) { |
| if (!source.hasField(field)) { |
| destination.clearField(field); |
| } else { |
| destination.setField(field, source.getField(field)); |
| } |
| } else { |
| if (source.hasField(field)) { |
| destination.getFieldBuilder(field).mergeFrom((Message) source.getField(field)); |
| } |
| } |
| } else { |
| if (source.hasField(field) || !options.replacePrimitiveFields()) { |
| destination.setField(field, source.getField(field)); |
| } else { |
| destination.clearField(field); |
| } |
| } |
| } |
| } |
| } |
| } |