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