• 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   /** 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