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