• 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.common.base.Splitter;
34 import com.google.errorprone.annotations.CanIgnoreReturnValue;
35 import com.google.protobuf.Descriptors.Descriptor;
36 import com.google.protobuf.Descriptors.FieldDescriptor;
37 import com.google.protobuf.FieldMask;
38 import com.google.protobuf.GeneratedMessage;
39 import com.google.protobuf.Message;
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Map.Entry;
43 import java.util.SortedMap;
44 import java.util.TreeMap;
45 import java.util.logging.Logger;
46 
47 /**
48  * A tree representation of a FieldMask. Each leaf node in this tree represent
49  * a field path in the FieldMask.
50  *
51  * <p>For example, FieldMask "foo.bar,foo.baz,bar.baz" as a tree will be:
52  * <pre>
53  *   [root] -+- foo -+- bar
54  *           |       |
55  *           |       +- baz
56  *           |
57  *           +- bar --- baz
58  * </pre>
59  *
60  * <p>By representing FieldMasks with this tree structure we can easily convert
61  * a FieldMask to a canonical form, merge two FieldMasks, calculate the
62  * intersection to two FieldMasks and traverse all fields specified by the
63  * FieldMask in a message tree.
64  */
65 final class FieldMaskTree {
66   private static final Logger logger = Logger.getLogger(FieldMaskTree.class.getName());
67 
68   private static final String FIELD_PATH_SEPARATOR_REGEX = "\\.";
69 
70   private static final class Node {
71     final SortedMap<String, Node> children = new TreeMap<>();
72   }
73 
74   private final Node root = new Node();
75 
76   /**
77    * Creates an empty FieldMaskTree.
78    */
FieldMaskTree()79   FieldMaskTree() {}
80 
81   /**
82    * Creates a FieldMaskTree for a given FieldMask.
83    */
FieldMaskTree(FieldMask mask)84   FieldMaskTree(FieldMask mask) {
85     mergeFromFieldMask(mask);
86   }
87 
88   @Override
toString()89   public String toString() {
90     return FieldMaskUtil.toString(toFieldMask());
91   }
92 
93   /**
94    * Adds a field path to the tree. In a FieldMask, every field path matches the specified field as
95    * well as all its sub-fields. For example, a field path "foo.bar" matches field "foo.bar" and
96    * also "foo.bar.baz", etc. When adding a field path to the tree, redundant sub-paths will be
97    * removed. That is, after adding "foo.bar" to the tree, "foo.bar.baz" will be removed if it
98    * exists, which will turn the tree node for "foo.bar" to a leaf node. Likewise, if the field path
99    * to add is a sub-path of an existing leaf node, nothing will be changed in the tree.
100    */
101   @CanIgnoreReturnValue
102   @SuppressWarnings("StringSplitter")
addFieldPath(String path)103   FieldMaskTree addFieldPath(String path) {
104     String[] parts = path.split(FIELD_PATH_SEPARATOR_REGEX);
105     if (parts.length == 0) {
106       return this;
107     }
108     Node node = root;
109     boolean createNewBranch = false;
110     // Find the matching node in the tree.
111     for (String part : parts) {
112       // Check whether the path matches an existing leaf node.
113       if (!createNewBranch && node != root && node.children.isEmpty()) {
114         // The path to add is a sub-path of an existing leaf node.
115         return this;
116       }
117       if (node.children.containsKey(part)) {
118         node = node.children.get(part);
119       } else {
120         createNewBranch = true;
121         Node tmp = new Node();
122         node.children.put(part, tmp);
123         node = tmp;
124       }
125     }
126     // Turn the matching node into a leaf node (i.e., remove sub-paths).
127     node.children.clear();
128     return this;
129   }
130 
131   /** Merges all field paths in a FieldMask into this tree. */
132   @CanIgnoreReturnValue
mergeFromFieldMask(FieldMask mask)133   FieldMaskTree mergeFromFieldMask(FieldMask mask) {
134     for (String path : mask.getPathsList()) {
135       addFieldPath(path);
136     }
137     return this;
138   }
139 
140   /**
141    * Removes {@code path} from the tree.
142    *
143    * <ul>
144    *   When removing a field path from the tree:
145    *   <li>All sub-paths will be removed. That is, after removing "foo.bar" from the tree,
146    *       "foo.bar.baz" will be removed.
147    *   <li>If all children of a node has been removed, the node itself will be removed as well.
148    *       That is, if "foo" only has one child "bar" and "foo.bar" only has one child "baz",
149    *       removing "foo.bar.barz" would remove both "foo" and "foo.bar".
150    *       If "foo" has both "bar" and "qux" as children, removing "foo.bar" would leave the path
151    *       "foo.qux" intact.
152    *   <li>If the field path to remove is a non-exist sub-path, nothing will be changed.
153    * </ul>
154    */
155   @CanIgnoreReturnValue
removeFieldPath(String path)156   FieldMaskTree removeFieldPath(String path) {
157     List<String> parts = Splitter.onPattern(FIELD_PATH_SEPARATOR_REGEX).splitToList(path);
158     if (parts.isEmpty()) {
159       return this;
160     }
161     removeFieldPath(root, parts, 0);
162     return this;
163   }
164 
165   /**
166    * Removes {@code parts} from {@code node} recursively.
167    *
168    * @return a boolean value indicating whether current {@code node} should be removed.
169    */
170   @CanIgnoreReturnValue
removeFieldPath(Node node, List<String> parts, int index)171   private static boolean removeFieldPath(Node node, List<String> parts, int index) {
172     String key = parts.get(index);
173 
174     // Base case 1: path not match.
175     if (!node.children.containsKey(key)) {
176       return false;
177     }
178     // Base case 2: last element in parts.
179     if (index == parts.size() - 1) {
180       node.children.remove(key);
181       return node.children.isEmpty();
182     }
183     // Recursive remove sub-path.
184     if (removeFieldPath(node.children.get(key), parts, index + 1)) {
185       node.children.remove(key);
186     }
187     return node.children.isEmpty();
188   }
189 
190   /** Removes all field paths in {@code mask} from this tree. */
191   @CanIgnoreReturnValue
removeFromFieldMask(FieldMask mask)192   FieldMaskTree removeFromFieldMask(FieldMask mask) {
193     for (String path : mask.getPathsList()) {
194       removeFieldPath(path);
195     }
196     return this;
197   }
198 
199   /**
200    * Converts this tree to a FieldMask.
201    */
toFieldMask()202   FieldMask toFieldMask() {
203     if (root.children.isEmpty()) {
204       return FieldMask.getDefaultInstance();
205     }
206     List<String> paths = new ArrayList<>();
207     getFieldPaths(root, "", paths);
208     return FieldMask.newBuilder().addAllPaths(paths).build();
209   }
210 
211   /** Gathers all field paths in a sub-tree. */
getFieldPaths(Node node, String path, List<String> paths)212   private static void getFieldPaths(Node node, String path, List<String> paths) {
213     if (node.children.isEmpty()) {
214       paths.add(path);
215       return;
216     }
217     for (Entry<String, Node> entry : node.children.entrySet()) {
218       String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
219       getFieldPaths(entry.getValue(), childPath, paths);
220     }
221   }
222 
223   /**
224    * Adds the intersection of this tree with the given {@code path} to {@code output}.
225    */
intersectFieldPath(String path, FieldMaskTree output)226   void intersectFieldPath(String path, FieldMaskTree output) {
227     if (root.children.isEmpty()) {
228       return;
229     }
230     String[] parts = path.split(FIELD_PATH_SEPARATOR_REGEX);
231     if (parts.length == 0) {
232       return;
233     }
234     Node node = root;
235     for (String part : parts) {
236       if (node != root && node.children.isEmpty()) {
237         // The given path is a sub-path of an existing leaf node in the tree.
238         output.addFieldPath(path);
239         return;
240       }
241       if (node.children.containsKey(part)) {
242         node = node.children.get(part);
243       } else {
244         return;
245       }
246     }
247     // We found a matching node for the path. All leaf children of this matching
248     // node is in the intersection.
249     List<String> paths = new ArrayList<>();
250     getFieldPaths(node, path, paths);
251     for (String value : paths) {
252       output.addFieldPath(value);
253     }
254   }
255 
256   /**
257    * Merges all fields specified by this FieldMaskTree from {@code source} to {@code destination}.
258    */
merge(Message source, Message.Builder destination, FieldMaskUtil.MergeOptions options)259   void merge(Message source, Message.Builder destination, FieldMaskUtil.MergeOptions options) {
260     if (source.getDescriptorForType() != destination.getDescriptorForType()) {
261       throw new IllegalArgumentException("Cannot merge messages of different types.");
262     }
263     if (root.children.isEmpty()) {
264       return;
265     }
266     merge(root, "", source, destination, options);
267   }
268 
269   /** Merges all fields specified by a sub-tree from {@code source} to {@code destination}. */
merge( Node node, String path, Message source, Message.Builder destination, FieldMaskUtil.MergeOptions options)270   private static void merge(
271       Node node,
272       String path,
273       Message source,
274       Message.Builder destination,
275       FieldMaskUtil.MergeOptions options) {
276     if (source.getDescriptorForType() != destination.getDescriptorForType()) {
277       throw new IllegalArgumentException(
278           String.format(
279               "source (%s) and destination (%s) descriptor must be equal",
280               source.getDescriptorForType(), destination.getDescriptorForType()));
281     }
282 
283     Descriptor descriptor = source.getDescriptorForType();
284     for (Entry<String, Node> entry : node.children.entrySet()) {
285       FieldDescriptor field = descriptor.findFieldByName(entry.getKey());
286       if (field == null) {
287         logger.warning(
288             "Cannot find field \""
289                 + entry.getKey()
290                 + "\" in message type "
291                 + descriptor.getFullName());
292         continue;
293       }
294       if (!entry.getValue().children.isEmpty()) {
295         if (field.isRepeated() || field.getJavaType() != FieldDescriptor.JavaType.MESSAGE) {
296           logger.warning(
297               "Field \""
298                   + field.getFullName()
299                   + "\" is not a "
300                   + "singular message field and cannot have sub-fields.");
301           continue;
302         }
303         if (!source.hasField(field) && !destination.hasField(field)) {
304           // If the message field is not present in both source and destination, skip recursing
305           // so we don't create unnecessary empty messages.
306           continue;
307         }
308         // This is a mess because of java proto API 1 still hanging around.
309         Message.Builder childBuilder =
310             destination instanceof GeneratedMessage.Builder
311                 ? destination.getFieldBuilder(field)
312                 : ((Message) destination.getField(field)).toBuilder();
313         merge(entry.getValue(), path, (Message) source.getField(field), childBuilder, options);
314         destination.setField(field, childBuilder.buildPartial());
315         continue;
316       }
317       if (field.isRepeated()) {
318         if (options.replaceRepeatedFields()) {
319           destination.setField(field, source.getField(field));
320         } else {
321           for (Object element : (List) source.getField(field)) {
322             destination.addRepeatedField(field, element);
323           }
324         }
325       } else {
326         if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
327           if (options.replaceMessageFields()) {
328             if (!source.hasField(field)) {
329               destination.clearField(field);
330             } else {
331               destination.setField(field, source.getField(field));
332             }
333           } else {
334             if (source.hasField(field)) {
335               destination.setField(
336                   field,
337                   ((Message) destination.getField(field))
338                       .toBuilder()
339                       .mergeFrom((Message) source.getField(field))
340                       .build());
341             }
342           }
343         } else {
344           if (source.hasField(field) || !options.replacePrimitiveFields()) {
345             destination.setField(field, source.getField(field));
346           } else {
347             destination.clearField(field);
348           }
349         }
350       }
351     }
352   }
353 }
354