• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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   /**
280    * Subtracts {@code secondMask} and {@code otherMasks} from {@code firstMask}.
281    *
282    * <p>This method disregards proto structure. That is, if {@code firstMask} is "foo" and {@code
283    * secondMask} is "foo.bar", the response will always be "foo" without considering the internal
284    * proto structure of message "foo".
285    */
subtract( FieldMask firstMask, FieldMask secondMask, FieldMask... otherMasks)286   public static FieldMask subtract(
287       FieldMask firstMask, FieldMask secondMask, FieldMask... otherMasks) {
288     FieldMaskTree maskTree = new FieldMaskTree(firstMask).removeFromFieldMask(secondMask);
289     for (FieldMask mask : otherMasks) {
290       maskTree.removeFromFieldMask(mask);
291     }
292     return maskTree.toFieldMask();
293   }
294 
295   /**
296    * Calculates the intersection of two FieldMasks.
297    */
intersection(FieldMask mask1, FieldMask mask2)298   public static FieldMask intersection(FieldMask mask1, FieldMask mask2) {
299     FieldMaskTree tree = new FieldMaskTree(mask1);
300     FieldMaskTree result = new FieldMaskTree();
301     for (String path : mask2.getPathsList()) {
302       tree.intersectFieldPath(path, result);
303     }
304     return result.toFieldMask();
305   }
306 
307   /**
308    * Options to customize merging behavior.
309    */
310   public static final class MergeOptions {
311     private boolean replaceMessageFields = false;
312     private boolean replaceRepeatedFields = false;
313     // TODO(b/28277137): change the default behavior to always replace primitive fields after
314     // fixing all failing TAP tests.
315     private boolean replacePrimitiveFields = false;
316 
317     /**
318      * Whether to replace message fields (i.e., discard existing content in
319      * destination message fields).
320      */
replaceMessageFields()321     public boolean replaceMessageFields() {
322       return replaceMessageFields;
323     }
324 
325     /**
326      * Whether to replace repeated fields (i.e., discard existing content in
327      * destination repeated fields).
328      */
replaceRepeatedFields()329     public boolean replaceRepeatedFields() {
330       return replaceRepeatedFields;
331     }
332 
333     /**
334      * Whether to replace primitive (non-repeated and non-message) fields in
335      * destination message fields with the source primitive fields (i.e., clear
336      * destination field if source field is not set).
337      */
replacePrimitiveFields()338     public boolean replacePrimitiveFields() {
339       return replacePrimitiveFields;
340     }
341 
342     /**
343      * Specify whether to replace message fields. Defaults to false.
344      *
345      * <p>If true, discard existing content in destination message fields when merging.
346      *
347      * <p>If false, merge the source message field into the destination message field.
348      */
349     @CanIgnoreReturnValue
setReplaceMessageFields(boolean value)350     public MergeOptions setReplaceMessageFields(boolean value) {
351       replaceMessageFields = value;
352       return this;
353     }
354 
355     /**
356      * Specify whether to replace repeated fields. Defaults to false.
357      *
358      * <p>If true, discard existing content in destination repeated fields) when merging.
359      *
360      * <p>If false, append elements from source repeated field to the destination repeated field.
361      */
362     @CanIgnoreReturnValue
setReplaceRepeatedFields(boolean value)363     public MergeOptions setReplaceRepeatedFields(boolean value) {
364       replaceRepeatedFields = value;
365       return this;
366     }
367 
368     /**
369      * Specify whether to replace primitive (non-repeated and non-message) fields in destination
370      * message fields with the source primitive fields. Defaults to false.
371      *
372      * <p>If true, set the value of the destination primitive field to the source primitive field if
373      * the source field is set, but clear the destination field otherwise.
374      *
375      * <p>If false, always set the value of the destination primitive field to the source primitive
376      * field, and if the source field is unset, the default value of the source field is copied to
377      * the destination.
378      */
379     @CanIgnoreReturnValue
setReplacePrimitiveFields(boolean value)380     public MergeOptions setReplacePrimitiveFields(boolean value) {
381       replacePrimitiveFields = value;
382       return this;
383     }
384   }
385 
386   /**
387    * Merges fields specified by a FieldMask from one message to another with the specified merge
388    * options. The destination will remain unchanged if an empty FieldMask is provided.
389    */
merge( FieldMask mask, Message source, Message.Builder destination, MergeOptions options)390   public static void merge(
391       FieldMask mask, Message source, Message.Builder destination, MergeOptions options) {
392     new FieldMaskTree(mask).merge(source, destination, options);
393   }
394 
395   /**
396    * Merges fields specified by a FieldMask from one message to another.
397    */
merge(FieldMask mask, Message source, Message.Builder destination)398   public static void merge(FieldMask mask, Message source, Message.Builder destination) {
399     merge(mask, source, destination, new MergeOptions());
400   }
401 }
402