• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Protocol Buffers - Google's data interchange format
2 // Copyright 2008 Google Inc.  All rights reserved.
3 // https://developers.google.com/protocol-buffers/
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions are
7 // met:
8 //
9 //     * Redistributions of source code must retain the above copyright
10 // notice, this list of conditions and the following disclaimer.
11 //     * Redistributions in binary form must reproduce the above
12 // copyright notice, this list of conditions and the following disclaimer
13 // in the documentation and/or other materials provided with the
14 // distribution.
15 //     * Neither the name of Google Inc. nor the names of its
16 // contributors may be used to endorse or promote products derived from
17 // this software without specific prior written permission.
18 //
19 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 
31 package com.google.protobuf.util;
32 
33 import com.google.errorprone.annotations.CanIgnoreReturnValue;
34 import com.google.protobuf.Descriptors.Descriptor;
35 import com.google.protobuf.Descriptors.FieldDescriptor;
36 import com.google.protobuf.FieldMask;
37 import com.google.protobuf.Message;
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.Map.Entry;
41 import java.util.SortedMap;
42 import java.util.TreeMap;
43 import java.util.logging.Logger;
44 
45 /**
46  * A tree representation of a FieldMask. Each leaf node in this tree represent
47  * a field path in the FieldMask.
48  *
49  * <p>For example, FieldMask "foo.bar,foo.baz,bar.baz" as a tree will be:
50  * <pre>
51  *   [root] -+- foo -+- bar
52  *           |       |
53  *           |       +- baz
54  *           |
55  *           +- bar --- baz
56  * </pre>
57  *
58  * <p>By representing FieldMasks with this tree structure we can easily convert
59  * a FieldMask to a canonical form, merge two FieldMasks, calculate the
60  * intersection to two FieldMasks and traverse all fields specified by the
61  * FieldMask in a message tree.
62  */
63 final class FieldMaskTree {
64   private static final Logger logger = Logger.getLogger(FieldMaskTree.class.getName());
65 
66   private static final String FIELD_PATH_SEPARATOR_REGEX = "\\.";
67 
68   private static final class Node {
69     final SortedMap<String, Node> children = new TreeMap<String, Node>();
70   }
71 
72   private final Node root = new Node();
73 
74   /**
75    * Creates an empty FieldMaskTree.
76    */
FieldMaskTree()77   FieldMaskTree() {}
78 
79   /**
80    * Creates a FieldMaskTree for a given FieldMask.
81    */
FieldMaskTree(FieldMask mask)82   FieldMaskTree(FieldMask mask) {
83     mergeFromFieldMask(mask);
84   }
85 
86   @Override
toString()87   public String toString() {
88     return FieldMaskUtil.toString(toFieldMask());
89   }
90 
91   /**
92    * Adds a field path to the tree. In a FieldMask, every field path matches the specified field as
93    * well as all its sub-fields. For example, a field path "foo.bar" matches field "foo.bar" and
94    * also "foo.bar.baz", etc. When adding a field path to the tree, redundant sub-paths will be
95    * removed. That is, after adding "foo.bar" to the tree, "foo.bar.baz" will be removed if it
96    * exists, which will turn the tree node for "foo.bar" to a leaf node. Likewise, if the field path
97    * to add is a sub-path of an existing leaf node, nothing will be changed in the tree.
98    */
99   @CanIgnoreReturnValue
addFieldPath(String path)100   FieldMaskTree addFieldPath(String path) {
101     String[] parts = path.split(FIELD_PATH_SEPARATOR_REGEX);
102     if (parts.length == 0) {
103       return this;
104     }
105     Node node = root;
106     boolean createNewBranch = false;
107     // Find the matching node in the tree.
108     for (String part : parts) {
109       // Check whether the path matches an existing leaf node.
110       if (!createNewBranch && node != root && node.children.isEmpty()) {
111         // The path to add is a sub-path of an existing leaf node.
112         return this;
113       }
114       if (node.children.containsKey(part)) {
115         node = node.children.get(part);
116       } else {
117         createNewBranch = true;
118         Node tmp = new Node();
119         node.children.put(part, tmp);
120         node = tmp;
121       }
122     }
123     // Turn the matching node into a leaf node (i.e., remove sub-paths).
124     node.children.clear();
125     return this;
126   }
127 
128   /** Merges all field paths in a FieldMask into this tree. */
129   @CanIgnoreReturnValue
mergeFromFieldMask(FieldMask mask)130   FieldMaskTree mergeFromFieldMask(FieldMask mask) {
131     for (String path : mask.getPathsList()) {
132       addFieldPath(path);
133     }
134     return this;
135   }
136 
137   /**
138    * Converts this tree to a FieldMask.
139    */
toFieldMask()140   FieldMask toFieldMask() {
141     if (root.children.isEmpty()) {
142       return FieldMask.getDefaultInstance();
143     }
144     List<String> paths = new ArrayList<String>();
145     getFieldPaths(root, "", paths);
146     return FieldMask.newBuilder().addAllPaths(paths).build();
147   }
148 
149   /**
150    * Gathers all field paths in a sub-tree.
151    */
getFieldPaths(Node node, String path, List<String> paths)152   private void getFieldPaths(Node node, String path, List<String> paths) {
153     if (node.children.isEmpty()) {
154       paths.add(path);
155       return;
156     }
157     for (Entry<String, Node> entry : node.children.entrySet()) {
158       String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
159       getFieldPaths(entry.getValue(), childPath, paths);
160     }
161   }
162 
163   /**
164    * Adds the intersection of this tree with the given {@code path} to {@code output}.
165    */
intersectFieldPath(String path, FieldMaskTree output)166   void intersectFieldPath(String path, FieldMaskTree output) {
167     if (root.children.isEmpty()) {
168       return;
169     }
170     String[] parts = path.split(FIELD_PATH_SEPARATOR_REGEX);
171     if (parts.length == 0) {
172       return;
173     }
174     Node node = root;
175     for (String part : parts) {
176       if (node != root && node.children.isEmpty()) {
177         // The given path is a sub-path of an existing leaf node in the tree.
178         output.addFieldPath(path);
179         return;
180       }
181       if (node.children.containsKey(part)) {
182         node = node.children.get(part);
183       } else {
184         return;
185       }
186     }
187     // We found a matching node for the path. All leaf children of this matching
188     // node is in the intersection.
189     List<String> paths = new ArrayList<String>();
190     getFieldPaths(node, path, paths);
191     for (String value : paths) {
192       output.addFieldPath(value);
193     }
194   }
195 
196   /**
197    * Merges all fields specified by this FieldMaskTree from {@code source} to {@code destination}.
198    */
merge(Message source, Message.Builder destination, FieldMaskUtil.MergeOptions options)199   void merge(Message source, Message.Builder destination, FieldMaskUtil.MergeOptions options) {
200     if (source.getDescriptorForType() != destination.getDescriptorForType()) {
201       throw new IllegalArgumentException("Cannot merge messages of different types.");
202     }
203     if (root.children.isEmpty()) {
204       return;
205     }
206     merge(root, "", source, destination, options);
207   }
208 
209   /**
210    * Merges all fields specified by a sub-tree from {@code source} to {@code destination}.
211    */
merge( Node node, String path, Message source, Message.Builder destination, FieldMaskUtil.MergeOptions options)212   private void merge(
213       Node node,
214       String path,
215       Message source,
216       Message.Builder destination,
217       FieldMaskUtil.MergeOptions options) {
218     if (source.getDescriptorForType() != destination.getDescriptorForType()) {
219       throw new IllegalArgumentException(
220           String.format(
221               "source (%s) and destination (%s) descriptor must be equal",
222               source.getDescriptorForType(), destination.getDescriptorForType()));
223     }
224 
225     Descriptor descriptor = source.getDescriptorForType();
226     for (Entry<String, Node> entry : node.children.entrySet()) {
227       FieldDescriptor field = descriptor.findFieldByName(entry.getKey());
228       if (field == null) {
229         logger.warning(
230             "Cannot find field \""
231                 + entry.getKey()
232                 + "\" in message type "
233                 + descriptor.getFullName());
234         continue;
235       }
236       if (!entry.getValue().children.isEmpty()) {
237         if (field.isRepeated() || field.getJavaType() != FieldDescriptor.JavaType.MESSAGE) {
238           logger.warning(
239               "Field \""
240                   + field.getFullName()
241                   + "\" is not a "
242                   + "singular message field and cannot have sub-fields.");
243           continue;
244         }
245         if (!source.hasField(field) && !destination.hasField(field)) {
246           // If the message field is not present in both source and destination, skip recursing
247           // so we don't create unnecessary empty messages.
248           continue;
249         }
250         String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
251         Message.Builder childBuilder = ((Message) destination.getField(field)).toBuilder();
252         merge(entry.getValue(), childPath, (Message) source.getField(field), childBuilder, options);
253         destination.setField(field, childBuilder.buildPartial());
254         continue;
255       }
256       if (field.isRepeated()) {
257         if (options.replaceRepeatedFields()) {
258           destination.setField(field, source.getField(field));
259         } else {
260           for (Object element : (List) source.getField(field)) {
261             destination.addRepeatedField(field, element);
262           }
263         }
264       } else {
265         if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
266           if (options.replaceMessageFields()) {
267             if (!source.hasField(field)) {
268               destination.clearField(field);
269             } else {
270               destination.setField(field, source.getField(field));
271             }
272           } else {
273             if (source.hasField(field)) {
274               destination.setField(
275                   field,
276                   ((Message) destination.getField(field))
277                       .toBuilder()
278                       .mergeFrom((Message) source.getField(field))
279                       .build());
280             }
281           }
282         } else {
283           if (source.hasField(field) || !options.replacePrimitiveFields()) {
284             destination.setField(field, source.getField(field));
285           } else {
286             destination.clearField(field);
287           }
288         }
289       }
290     }
291   }
292 }
293