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