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 static com.google.common.base.Preconditions.checkArgument; 34 35 import com.google.common.base.CaseFormat; 36 import com.google.common.base.Joiner; 37 import com.google.common.base.Optional; 38 import com.google.common.base.Splitter; 39 import com.google.common.primitives.Ints; 40 import com.google.errorprone.annotations.CanIgnoreReturnValue; 41 import com.google.protobuf.Descriptors.Descriptor; 42 import com.google.protobuf.Descriptors.FieldDescriptor; 43 import com.google.protobuf.FieldMask; 44 import com.google.protobuf.Internal; 45 import com.google.protobuf.Message; 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.List; 49 50 /** 51 * Utility helper functions to work with {@link com.google.protobuf.FieldMask}. 52 */ 53 public final class FieldMaskUtil { 54 private static final String FIELD_PATH_SEPARATOR = ","; 55 private static final String FIELD_PATH_SEPARATOR_REGEX = ","; 56 private static final String FIELD_SEPARATOR_REGEX = "\\."; 57 FieldMaskUtil()58 private FieldMaskUtil() {} 59 60 /** 61 * Converts a FieldMask to a string. 62 */ toString(FieldMask fieldMask)63 public static String toString(FieldMask fieldMask) { 64 // TODO(xiaofeng): Consider using com.google.common.base.Joiner here instead. 65 StringBuilder result = new StringBuilder(); 66 boolean first = true; 67 for (String value : fieldMask.getPathsList()) { 68 if (value.isEmpty()) { 69 // Ignore empty paths. 70 continue; 71 } 72 if (first) { 73 first = false; 74 } else { 75 result.append(FIELD_PATH_SEPARATOR); 76 } 77 result.append(value); 78 } 79 return result.toString(); 80 } 81 82 /** 83 * Parses from a string to a FieldMask. 84 */ fromString(String value)85 public static FieldMask fromString(String value) { 86 // TODO(xiaofeng): Consider using com.google.common.base.Splitter here instead. 87 return fromStringList(Arrays.asList(value.split(FIELD_PATH_SEPARATOR_REGEX))); 88 } 89 90 /** 91 * Parses from a string to a FieldMask and validates all field paths. 92 * 93 * @throws IllegalArgumentException if any of the field path is invalid. 94 */ fromString(Class<? extends Message> type, String value)95 public static FieldMask fromString(Class<? extends Message> type, String value) { 96 // TODO(xiaofeng): Consider using com.google.common.base.Splitter here instead. 97 return fromStringList(type, Arrays.asList(value.split(FIELD_PATH_SEPARATOR_REGEX))); 98 } 99 100 /** 101 * Constructs a FieldMask for a list of field paths in a certain type. 102 * 103 * @throws IllegalArgumentException if any of the field path is not valid. 104 */ 105 // TODO(xiaofeng): Consider renaming fromStrings() fromStringList(Class<? extends Message> type, Iterable<String> paths)106 public static FieldMask fromStringList(Class<? extends Message> type, Iterable<String> paths) { 107 return fromStringList(Internal.getDefaultInstance(type).getDescriptorForType(), paths); 108 } 109 110 /** 111 * Constructs a FieldMask for a list of field paths in a certain type. 112 * 113 * @throws IllegalArgumentException if any of the field path is not valid. 114 */ fromStringList(Descriptor descriptor, Iterable<String> paths)115 public static FieldMask fromStringList(Descriptor descriptor, Iterable<String> paths) { 116 return fromStringList(Optional.of(descriptor), paths); 117 } 118 119 /** 120 * Constructs a FieldMask for a list of field paths in a certain type. Does not validate the given 121 * paths. 122 */ fromStringList(Iterable<String> paths)123 public static FieldMask fromStringList(Iterable<String> paths) { 124 return fromStringList(Optional.<Descriptor>absent(), paths); 125 } 126 fromStringList(Optional<Descriptor> descriptor, Iterable<String> paths)127 private static FieldMask fromStringList(Optional<Descriptor> descriptor, Iterable<String> paths) { 128 FieldMask.Builder builder = FieldMask.newBuilder(); 129 for (String path : paths) { 130 if (path.isEmpty()) { 131 // Ignore empty field paths. 132 continue; 133 } 134 if (descriptor.isPresent() && !isValid(descriptor.get(), path)) { 135 throw new IllegalArgumentException( 136 path + " is not a valid path for " + descriptor.get().getFullName()); 137 } 138 builder.addPaths(path); 139 } 140 return builder.build(); 141 } 142 143 /** 144 * Constructs a FieldMask from the passed field numbers. 145 * 146 * @throws IllegalArgumentException if any of the fields are invalid for the message. 147 */ fromFieldNumbers(Class<? extends Message> type, int... fieldNumbers)148 public static FieldMask fromFieldNumbers(Class<? extends Message> type, int... fieldNumbers) { 149 return fromFieldNumbers(type, Ints.asList(fieldNumbers)); 150 } 151 152 /** 153 * Constructs a FieldMask from the passed field numbers. 154 * 155 * @throws IllegalArgumentException if any of the fields are invalid for the message. 156 */ fromFieldNumbers( Class<? extends Message> type, Iterable<Integer> fieldNumbers)157 public static FieldMask fromFieldNumbers( 158 Class<? extends Message> type, Iterable<Integer> fieldNumbers) { 159 Descriptor descriptor = Internal.getDefaultInstance(type).getDescriptorForType(); 160 161 FieldMask.Builder builder = FieldMask.newBuilder(); 162 for (Integer fieldNumber : fieldNumbers) { 163 FieldDescriptor field = descriptor.findFieldByNumber(fieldNumber); 164 checkArgument( 165 field != null, 166 String.format("%s is not a valid field number for %s.", fieldNumber, type)); 167 builder.addPaths(field.getName()); 168 } 169 return builder.build(); 170 } 171 172 /** 173 * Converts a field mask to a Proto3 JSON string, that is converting from snake case to camel 174 * case and joining all paths into one string with commas. 175 */ toJsonString(FieldMask fieldMask)176 public static String toJsonString(FieldMask fieldMask) { 177 List<String> paths = new ArrayList<String>(fieldMask.getPathsCount()); 178 for (String path : fieldMask.getPathsList()) { 179 if (path.isEmpty()) { 180 continue; 181 } 182 paths.add(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, path)); 183 } 184 return Joiner.on(FIELD_PATH_SEPARATOR).join(paths); 185 } 186 187 /** 188 * Converts a field mask from a Proto3 JSON string, that is splitting the paths along commas and 189 * converting from camel case to snake case. 190 */ fromJsonString(String value)191 public static FieldMask fromJsonString(String value) { 192 Iterable<String> paths = Splitter.on(FIELD_PATH_SEPARATOR).split(value); 193 FieldMask.Builder builder = FieldMask.newBuilder(); 194 for (String path : paths) { 195 if (path.isEmpty()) { 196 continue; 197 } 198 builder.addPaths(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, path)); 199 } 200 return builder.build(); 201 } 202 203 /** 204 * Checks whether paths in a given fields mask are valid. 205 */ isValid(Class<? extends Message> type, FieldMask fieldMask)206 public static boolean isValid(Class<? extends Message> type, FieldMask fieldMask) { 207 Descriptor descriptor = Internal.getDefaultInstance(type).getDescriptorForType(); 208 209 return isValid(descriptor, fieldMask); 210 } 211 212 /** 213 * Checks whether paths in a given fields mask are valid. 214 */ isValid(Descriptor descriptor, FieldMask fieldMask)215 public static boolean isValid(Descriptor descriptor, FieldMask fieldMask) { 216 for (String path : fieldMask.getPathsList()) { 217 if (!isValid(descriptor, path)) { 218 return false; 219 } 220 } 221 return true; 222 } 223 224 /** 225 * Checks whether a given field path is valid. 226 */ isValid(Class<? extends Message> type, String path)227 public static boolean isValid(Class<? extends Message> type, String path) { 228 Descriptor descriptor = Internal.getDefaultInstance(type).getDescriptorForType(); 229 230 return isValid(descriptor, path); 231 } 232 233 /** 234 * Checks whether paths in a given fields mask are valid. 235 */ isValid(Descriptor descriptor, String path)236 public static boolean isValid(Descriptor descriptor, String path) { 237 String[] parts = path.split(FIELD_SEPARATOR_REGEX); 238 if (parts.length == 0) { 239 return false; 240 } 241 for (String name : parts) { 242 if (descriptor == null) { 243 return false; 244 } 245 FieldDescriptor field = descriptor.findFieldByName(name); 246 if (field == null) { 247 return false; 248 } 249 if (!field.isRepeated() && field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { 250 descriptor = field.getMessageType(); 251 } else { 252 descriptor = null; 253 } 254 } 255 return true; 256 } 257 258 /** 259 * Converts a FieldMask to its canonical form. In the canonical form of a 260 * FieldMask, all field paths are sorted alphabetically and redundant field 261 * paths are removed. 262 */ normalize(FieldMask mask)263 public static FieldMask normalize(FieldMask mask) { 264 return new FieldMaskTree(mask).toFieldMask(); 265 } 266 267 /** 268 * Creates a union of two or more FieldMasks. 269 */ union( FieldMask firstMask, FieldMask secondMask, FieldMask... otherMasks)270 public static FieldMask union( 271 FieldMask firstMask, FieldMask secondMask, FieldMask... otherMasks) { 272 FieldMaskTree maskTree = new FieldMaskTree(firstMask).mergeFromFieldMask(secondMask); 273 for (FieldMask mask : otherMasks) { 274 maskTree.mergeFromFieldMask(mask); 275 } 276 return maskTree.toFieldMask(); 277 } 278 279 /** Subtracts {@code secondMask} and {@code otherMasks} from {@code firstMask}. */ subtract( FieldMask firstMask, FieldMask secondMask, FieldMask... otherMasks)280 public static FieldMask subtract( 281 FieldMask firstMask, FieldMask secondMask, FieldMask... otherMasks) { 282 FieldMaskTree maskTree = new FieldMaskTree(firstMask).removeFromFieldMask(secondMask); 283 for (FieldMask mask : otherMasks) { 284 maskTree.removeFromFieldMask(mask); 285 } 286 return maskTree.toFieldMask(); 287 } 288 289 /** 290 * Calculates the intersection of two FieldMasks. 291 */ intersection(FieldMask mask1, FieldMask mask2)292 public static FieldMask intersection(FieldMask mask1, FieldMask mask2) { 293 FieldMaskTree tree = new FieldMaskTree(mask1); 294 FieldMaskTree result = new FieldMaskTree(); 295 for (String path : mask2.getPathsList()) { 296 tree.intersectFieldPath(path, result); 297 } 298 return result.toFieldMask(); 299 } 300 301 /** 302 * Options to customize merging behavior. 303 */ 304 public static final class MergeOptions { 305 private boolean replaceMessageFields = false; 306 private boolean replaceRepeatedFields = false; 307 // TODO(b/28277137): change the default behavior to always replace primitive fields after 308 // fixing all failing TAP tests. 309 private boolean replacePrimitiveFields = false; 310 311 /** 312 * Whether to replace message fields (i.e., discard existing content in 313 * destination message fields). 314 */ replaceMessageFields()315 public boolean replaceMessageFields() { 316 return replaceMessageFields; 317 } 318 319 /** 320 * Whether to replace repeated fields (i.e., discard existing content in 321 * destination repeated fields). 322 */ replaceRepeatedFields()323 public boolean replaceRepeatedFields() { 324 return replaceRepeatedFields; 325 } 326 327 /** 328 * Whether to replace primitive (non-repeated and non-message) fields in 329 * destination message fields with the source primitive fields (i.e., clear 330 * destination field if source field is not set). 331 */ replacePrimitiveFields()332 public boolean replacePrimitiveFields() { 333 return replacePrimitiveFields; 334 } 335 336 /** 337 * Specify whether to replace message fields. Defaults to false. 338 * 339 * <p>If true, discard existing content in destination message fields when merging. 340 * 341 * <p>If false, merge the source message field into the destination message field. 342 */ 343 @CanIgnoreReturnValue setReplaceMessageFields(boolean value)344 public MergeOptions setReplaceMessageFields(boolean value) { 345 replaceMessageFields = value; 346 return this; 347 } 348 349 /** 350 * Specify whether to replace repeated fields. Defaults to false. 351 * 352 * <p>If true, discard existing content in destination repeated fields) when merging. 353 * 354 * <p>If false, append elements from source repeated field to the destination repeated field. 355 */ 356 @CanIgnoreReturnValue setReplaceRepeatedFields(boolean value)357 public MergeOptions setReplaceRepeatedFields(boolean value) { 358 replaceRepeatedFields = value; 359 return this; 360 } 361 362 /** 363 * Specify whether to replace primitive (non-repeated and non-message) fields in destination 364 * message fields with the source primitive fields. Defaults to false. 365 * 366 * <p>If true, set the value of the destination primitive field to the source primitive field if 367 * the source field is set, but clear the destination field otherwise. 368 * 369 * <p>If false, always set the value of the destination primitive field to the source primitive 370 * field, and if the source field is unset, the default value of the source field is copied to 371 * the destination. 372 */ 373 @CanIgnoreReturnValue setReplacePrimitiveFields(boolean value)374 public MergeOptions setReplacePrimitiveFields(boolean value) { 375 replacePrimitiveFields = value; 376 return this; 377 } 378 } 379 380 /** 381 * Merges fields specified by a FieldMask from one message to another with the specified merge 382 * options. The destination will remain unchanged if an empty FieldMask is provided. 383 */ merge( FieldMask mask, Message source, Message.Builder destination, MergeOptions options)384 public static void merge( 385 FieldMask mask, Message source, Message.Builder destination, MergeOptions options) { 386 new FieldMaskTree(mask).merge(source, destination, options); 387 } 388 389 /** 390 * Merges fields specified by a FieldMask from one message to another. 391 */ merge(FieldMask mask, Message source, Message.Builder destination)392 public static void merge(FieldMask mask, Message source, Message.Builder destination) { 393 merge(mask, source, destination, new MergeOptions()); 394 } 395 } 396