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