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