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