• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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