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.Splitter; 38 import com.google.common.primitives.Ints; 39 import com.google.protobuf.Descriptors.Descriptor; 40 import com.google.protobuf.Descriptors.FieldDescriptor; 41 import com.google.protobuf.FieldMask; 42 import com.google.protobuf.Internal; 43 import com.google.protobuf.Message; 44 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.List; 48 49 /** 50 * Utility helper functions to work with {@link com.google.protobuf.FieldMask}. 51 */ 52 public class FieldMaskUtil { 53 private static final String FIELD_PATH_SEPARATOR = ","; 54 private static final String FIELD_PATH_SEPARATOR_REGEX = ","; 55 private static final String FIELD_SEPARATOR_REGEX = "\\."; 56 FieldMaskUtil()57 private FieldMaskUtil() {} 58 59 /** 60 * Converts a FieldMask to a string. 61 */ toString(FieldMask fieldMask)62 public static String toString(FieldMask fieldMask) { 63 // TODO(xiaofeng): Consider using com.google.common.base.Joiner here instead. 64 StringBuilder result = new StringBuilder(); 65 boolean first = true; 66 for (String value : fieldMask.getPathsList()) { 67 if (value.isEmpty()) { 68 // Ignore empty paths. 69 continue; 70 } 71 if (first) { 72 first = false; 73 } else { 74 result.append(FIELD_PATH_SEPARATOR); 75 } 76 result.append(value); 77 } 78 return result.toString(); 79 } 80 81 /** 82 * Parses from a string to a FieldMask. 83 */ fromString(String value)84 public static FieldMask fromString(String value) { 85 // TODO(xiaofeng): Consider using com.google.common.base.Splitter here instead. 86 return fromStringList(null, Arrays.asList(value.split(FIELD_PATH_SEPARATOR_REGEX))); 87 } 88 89 /** 90 * Parses from a string to a FieldMask and validates all field paths. 91 * 92 * @throws IllegalArgumentException if any of the field path is invalid. 93 */ fromString(Class<? extends Message> type, String value)94 public static FieldMask fromString(Class<? extends Message> type, String value) { 95 // TODO(xiaofeng): Consider using com.google.common.base.Splitter here instead. 96 return fromStringList(type, Arrays.asList(value.split(FIELD_PATH_SEPARATOR_REGEX))); 97 } 98 99 /** 100 * Constructs a FieldMask for a list of field paths in a certain type. 101 * 102 * @throws IllegalArgumentException if any of the field path is not valid. 103 */ 104 // TODO(xiaofeng): Consider renaming fromStrings() fromStringList(Class<? extends Message> type, Iterable<String> paths)105 public static FieldMask fromStringList(Class<? extends Message> type, Iterable<String> paths) { 106 FieldMask.Builder builder = FieldMask.newBuilder(); 107 for (String path : paths) { 108 if (path.isEmpty()) { 109 // Ignore empty field paths. 110 continue; 111 } 112 if (type != null && !isValid(type, path)) { 113 throw new IllegalArgumentException(path + " is not a valid path for " + type); 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 /** 211 * Checks whether paths in a given fields mask are valid. 212 */ isValid(Descriptor descriptor, String path)213 public static boolean isValid(Descriptor descriptor, String path) { 214 String[] parts = path.split(FIELD_SEPARATOR_REGEX); 215 if (parts.length == 0) { 216 return false; 217 } 218 for (String name : parts) { 219 if (descriptor == null) { 220 return false; 221 } 222 FieldDescriptor field = descriptor.findFieldByName(name); 223 if (field == null) { 224 return false; 225 } 226 if (!field.isRepeated() && field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { 227 descriptor = field.getMessageType(); 228 } else { 229 descriptor = null; 230 } 231 } 232 return true; 233 } 234 235 /** 236 * Converts a FieldMask to its canonical form. In the canonical form of a 237 * FieldMask, all field paths are sorted alphabetically and redundant field 238 * paths are moved. 239 */ normalize(FieldMask mask)240 public static FieldMask normalize(FieldMask mask) { 241 return new FieldMaskTree(mask).toFieldMask(); 242 } 243 244 /** 245 * Creates a union of two or more FieldMasks. 246 */ union( FieldMask firstMask, FieldMask secondMask, FieldMask... otherMasks)247 public static FieldMask union( 248 FieldMask firstMask, FieldMask secondMask, FieldMask... otherMasks) { 249 FieldMaskTree maskTree = new FieldMaskTree(firstMask).mergeFromFieldMask(secondMask); 250 for (FieldMask mask : otherMasks) { 251 maskTree.mergeFromFieldMask(mask); 252 } 253 return maskTree.toFieldMask(); 254 } 255 256 /** 257 * Calculates the intersection of two FieldMasks. 258 */ intersection(FieldMask mask1, FieldMask mask2)259 public static FieldMask intersection(FieldMask mask1, FieldMask mask2) { 260 FieldMaskTree tree = new FieldMaskTree(mask1); 261 FieldMaskTree result = new FieldMaskTree(); 262 for (String path : mask2.getPathsList()) { 263 tree.intersectFieldPath(path, result); 264 } 265 return result.toFieldMask(); 266 } 267 268 /** 269 * Options to customize merging behavior. 270 */ 271 public static final class MergeOptions { 272 private boolean replaceMessageFields = false; 273 private boolean replaceRepeatedFields = false; 274 // TODO(b/28277137): change the default behavior to always replace primitive fields after 275 // fixing all failing TAP tests. 276 private boolean replacePrimitiveFields = false; 277 278 /** 279 * Whether to replace message fields (i.e., discard existing content in 280 * destination message fields) when merging. 281 * Default behavior is to merge the source message field into the 282 * destination message field. 283 */ replaceMessageFields()284 public boolean replaceMessageFields() { 285 return replaceMessageFields; 286 } 287 288 /** 289 * Whether to replace repeated fields (i.e., discard existing content in 290 * destination repeated fields) when merging. 291 * Default behavior is to append elements from source repeated field to the 292 * destination repeated field. 293 */ replaceRepeatedFields()294 public boolean replaceRepeatedFields() { 295 return replaceRepeatedFields; 296 } 297 298 /** 299 * Whether to replace primitive (non-repeated and non-message) fields in 300 * destination message fields with the source primitive fields (i.e., if the 301 * field is set in the source, the value is copied to the 302 * destination; if the field is unset in the source, the field is cleared 303 * from the destination) when merging. 304 * 305 * <p>Default behavior is to always set the value of the source primitive 306 * field to the destination primitive field, and if the source field is 307 * unset, the default value of the source field is copied to the 308 * destination. 309 */ replacePrimitiveFields()310 public boolean replacePrimitiveFields() { 311 return replacePrimitiveFields; 312 } 313 setReplaceMessageFields(boolean value)314 public void setReplaceMessageFields(boolean value) { 315 replaceMessageFields = value; 316 } 317 setReplaceRepeatedFields(boolean value)318 public void setReplaceRepeatedFields(boolean value) { 319 replaceRepeatedFields = value; 320 } 321 setReplacePrimitiveFields(boolean value)322 public void setReplacePrimitiveFields(boolean value) { 323 replacePrimitiveFields = value; 324 } 325 } 326 327 /** 328 * Merges fields specified by a FieldMask from one message to another with the 329 * specified merge options. 330 */ merge( FieldMask mask, Message source, Message.Builder destination, MergeOptions options)331 public static void merge( 332 FieldMask mask, Message source, Message.Builder destination, MergeOptions options) { 333 new FieldMaskTree(mask).merge(source, destination, options); 334 } 335 336 /** 337 * Merges fields specified by a FieldMask from one message to another. 338 */ merge(FieldMask mask, Message source, Message.Builder destination)339 public static void merge(FieldMask mask, Message source, Message.Builder destination) { 340 merge(mask, source, destination, new MergeOptions()); 341 } 342 } 343