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