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