• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 #region Copyright notice and license
2 // Protocol Buffers - Google's data interchange format
3 // Copyright 2016 Google Inc.  All rights reserved.
4 // https://developers.google.com/protocol-buffers/
5 //
6 // Redistribution and use in source and binary forms, with or without
7 // modification, are permitted provided that the following conditions are
8 // met:
9 //
10 //     * Redistributions of source code must retain the above copyright
11 // notice, this list of conditions and the following disclaimer.
12 //     * Redistributions in binary form must reproduce the above
13 // copyright notice, this list of conditions and the following disclaimer
14 // in the documentation and/or other materials provided with the
15 // distribution.
16 //     * Neither the name of Google Inc. nor the names of its
17 // contributors may be used to endorse or promote products derived from
18 // this software without specific prior written permission.
19 //
20 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 #endregion
32 
33 using System;
34 using System.Collections;
35 using System.Collections.Generic;
36 using System.IO;
37 using System.Linq;
38 using Google.Protobuf.Reflection;
39 
40 namespace Google.Protobuf.WellKnownTypes
41 {
42     // Manually-written partial class for the FieldMask well-known type.
43     public partial class FieldMask : ICustomDiagnosticMessage
44     {
45         private const char FIELD_PATH_SEPARATOR = ',';
46         private const char FIELD_SEPARATOR_REGEX = '.';
47 
48         /// <summary>
49         /// Converts a field mask specified by paths to a string.
50         /// </summary>
51         /// <remarks>
52         /// If the value is a normalized duration in the range described in <c>field_mask.proto</c>,
53         /// <paramref name="diagnosticOnly"/> is ignored. Otherwise, if the parameter is <c>true</c>,
54         /// a JSON object with a warning is returned; if it is <c>false</c>, an <see cref="InvalidOperationException"/> is thrown.
55         /// </remarks>
56         /// <param name="paths">Paths in the field mask</param>
57         /// <param name="diagnosticOnly">Determines the handling of non-normalized values</param>
58         /// <exception cref="InvalidOperationException">The represented field mask is invalid, and <paramref name="diagnosticOnly"/> is <c>false</c>.</exception>
ToJson(IList<string> paths, bool diagnosticOnly)59         internal static string ToJson(IList<string> paths, bool diagnosticOnly)
60         {
61             var firstInvalid = paths.FirstOrDefault(p => !IsPathValid(p));
62             if (firstInvalid == null)
63             {
64                 var writer = new StringWriter();
65 #if NET35
66                 var query = paths.Select(JsonFormatter.ToJsonName);
67                 JsonFormatter.WriteString(writer, string.Join(",", query.ToArray()));
68 #else
69                 JsonFormatter.WriteString(writer, string.Join(",", paths.Select(JsonFormatter.ToJsonName)));
70 #endif
71                 return writer.ToString();
72             }
73             else
74             {
75                 if (diagnosticOnly)
76                 {
77                     var writer = new StringWriter();
78                     writer.Write("{ \"@warning\": \"Invalid FieldMask\", \"paths\": ");
79                     JsonFormatter.Default.WriteList(writer, (IList)paths);
80                     writer.Write(" }");
81                     return writer.ToString();
82                 }
83                 else
84                 {
85                     throw new InvalidOperationException($"Invalid field mask to be converted to JSON: {firstInvalid}");
86                 }
87             }
88         }
89 
90         /// <summary>
91         /// Returns a string representation of this <see cref="FieldMask"/> for diagnostic purposes.
92         /// </summary>
93         /// <remarks>
94         /// Normally the returned value will be a JSON string value (including leading and trailing quotes) but
95         /// when the value is non-normalized or out of range, a JSON object representation will be returned
96         /// instead, including a warning. This is to avoid exceptions being thrown when trying to
97         /// diagnose problems - the regular JSON formatter will still throw an exception for non-normalized
98         /// values.
99         /// </remarks>
100         /// <returns>A string representation of this value.</returns>
ToDiagnosticString()101         public string ToDiagnosticString()
102         {
103             return ToJson(Paths, true);
104         }
105 
106         /// <summary>
107         /// Parses from a string to a FieldMask.
108         /// </summary>
FromString(string value)109         public static FieldMask FromString(string value)
110         {
111             return FromStringEnumerable<Empty>(new List<string>(value.Split(FIELD_PATH_SEPARATOR)));
112         }
113 
114         /// <summary>
115         /// Parses from a string to a FieldMask and validates all field paths.
116         /// </summary>
117         /// <typeparam name="T">The type to validate the field paths against.</typeparam>
118         public static FieldMask FromString<T>(string value) where T : IMessage
119         {
120             return FromStringEnumerable<T>(new List<string>(value.Split(FIELD_PATH_SEPARATOR)));
121         }
122 
123         /// <summary>
124         /// Constructs a FieldMask for a list of field paths in a certain type.
125         /// </summary>
126         /// <typeparam name="T">The type to validate the field paths against.</typeparam>
127         public static FieldMask FromStringEnumerable<T>(IEnumerable<string> paths) where T : IMessage
128         {
129             var mask = new FieldMask();
130             foreach (var path in paths)
131             {
132                 if (path.Length == 0)
133                 {
134                     // Ignore empty field paths.
135                     continue;
136                 }
137 
138                 if (typeof(T) != typeof(Empty)
139                     && !IsValid<T>(path))
140                 {
141                     throw new InvalidProtocolBufferException(path + " is not a valid path for " + typeof(T));
142                 }
143 
144                 mask.Paths.Add(path);
145             }
146 
147             return mask;
148         }
149 
150         /// <summary>
151         /// Constructs a FieldMask from the passed field numbers.
152         /// </summary>
153         /// <typeparam name="T">The type to validate the field paths against.</typeparam>
154         public static FieldMask FromFieldNumbers<T>(params int[] fieldNumbers) where T : IMessage
155         {
156             return FromFieldNumbers<T>((IEnumerable<int>)fieldNumbers);
157         }
158 
159         /// <summary>
160         /// Constructs a FieldMask from the passed field numbers.
161         /// </summary>
162         /// <typeparam name="T">The type to validate the field paths against.</typeparam>
163         public static FieldMask FromFieldNumbers<T>(IEnumerable<int> fieldNumbers) where T : IMessage
164         {
165             var descriptor = Activator.CreateInstance<T>().Descriptor;
166 
167             var mask = new FieldMask();
168             foreach (var fieldNumber in fieldNumbers)
169             {
170                 var field = descriptor.FindFieldByNumber(fieldNumber);
171                 if (field == null)
172                 {
173                     throw new ArgumentNullException($"{fieldNumber} is not a valid field number for {descriptor.Name}");
174                 }
175 
176                 mask.Paths.Add(field.Name);
177             }
178 
179             return mask;
180         }
181 
182         /// <summary>
183         /// Checks whether the given path is valid for a field mask.
184         /// </summary>
185         /// <returns>true if the path is valid; false otherwise</returns>
IsPathValid(string input)186         private static bool IsPathValid(string input)
187         {
188             for (int i = 0; i < input.Length; i++)
189             {
190                 char c = input[i];
191                 if (c >= 'A' && c <= 'Z')
192                 {
193                     return false;
194                 }
195                 if (c == '_' && i < input.Length - 1)
196                 {
197                     char next = input[i + 1];
198                     if (next < 'a' || next > 'z')
199                     {
200                         return false;
201                     }
202                 }
203             }
204             return true;
205         }
206 
207         /// <summary>
208         /// Checks whether paths in a given fields mask are valid.
209         /// </summary>
210         /// <typeparam name="T">The type to validate the field paths against.</typeparam>
211         public static bool IsValid<T>(FieldMask fieldMask) where T : IMessage
212         {
213             var descriptor = Activator.CreateInstance<T>().Descriptor;
214 
215             return IsValid(descriptor, fieldMask);
216         }
217 
218         /// <summary>
219         /// Checks whether paths in a given fields mask are valid.
220         /// </summary>
IsValid(MessageDescriptor descriptor, FieldMask fieldMask)221         public static bool IsValid(MessageDescriptor descriptor, FieldMask fieldMask)
222         {
223             foreach (var path in fieldMask.Paths)
224             {
225                 if (!IsValid(descriptor, path))
226                 {
227                     return false;
228                 }
229             }
230 
231             return true;
232         }
233 
234         /// <summary>
235         /// Checks whether a given field path is valid.
236         /// </summary>
237         /// <typeparam name="T">The type to validate the field paths against.</typeparam>
238         public static bool IsValid<T>(string path) where T : IMessage
239         {
240             var descriptor = Activator.CreateInstance<T>().Descriptor;
241 
242             return IsValid(descriptor, path);
243         }
244 
245         /// <summary>
246         /// Checks whether paths in a given fields mask are valid.
247         /// </summary>
IsValid(MessageDescriptor descriptor, string path)248         public static bool IsValid(MessageDescriptor descriptor, string path)
249         {
250             var parts = path.Split(FIELD_SEPARATOR_REGEX);
251             if (parts.Length == 0)
252             {
253                 return false;
254             }
255 
256             foreach (var name in parts)
257             {
258                 var field = descriptor?.FindFieldByName(name);
259                 if (field == null)
260                 {
261                     return false;
262                 }
263 
264                 if (!field.IsRepeated
265                     && field.FieldType == FieldType.Message)
266                 {
267                     descriptor = field.MessageType;
268                 }
269                 else
270                 {
271                     descriptor = null;
272                 }
273             }
274 
275             return true;
276         }
277 
278         /// <summary>
279         /// Converts this FieldMask to its canonical form. In the canonical form of a
280         /// FieldMask, all field paths are sorted alphabetically and redundant field
281         /// paths are removed.
282         /// </summary>
Normalize()283         public FieldMask Normalize()
284         {
285             return new FieldMaskTree(this).ToFieldMask();
286         }
287 
288         /// <summary>
289         /// Creates a union of two or more FieldMasks.
290         /// </summary>
Union(params FieldMask[] otherMasks)291         public FieldMask Union(params FieldMask[] otherMasks)
292         {
293             var maskTree = new FieldMaskTree(this);
294             foreach (var mask in otherMasks)
295             {
296                 maskTree.MergeFromFieldMask(mask);
297             }
298 
299             return maskTree.ToFieldMask();
300         }
301 
302         /// <summary>
303         /// Calculates the intersection of two FieldMasks.
304         /// </summary>
Intersection(FieldMask additionalMask)305         public FieldMask Intersection(FieldMask additionalMask)
306         {
307             var tree = new FieldMaskTree(this);
308             var result = new FieldMaskTree();
309             foreach (var path in additionalMask.Paths)
310             {
311                 tree.IntersectFieldPath(path, result);
312             }
313 
314             return result.ToFieldMask();
315         }
316 
317         /// <summary>
318         /// Merges fields specified by this FieldMask from one message to another with the
319         /// specified merge options.
320         /// </summary>
Merge(IMessage source, IMessage destination, MergeOptions options)321         public void Merge(IMessage source, IMessage destination, MergeOptions options)
322         {
323             new FieldMaskTree(this).Merge(source, destination, options);
324         }
325 
326         /// <summary>
327         /// Merges fields specified by this FieldMask from one message to another.
328         /// </summary>
Merge(IMessage source, IMessage destination)329         public void Merge(IMessage source, IMessage destination)
330         {
331             Merge(source, destination, new MergeOptions());
332         }
333 
334         /// <summary>
335         /// Options to customize merging behavior.
336         /// </summary>
337         public sealed class MergeOptions
338         {
339             /// <summary>
340             /// Whether to replace message fields(i.e., discard existing content in
341             /// destination message fields) when merging.
342             /// Default behavior is to merge the source message field into the
343             /// destination message field.
344             /// </summary>
345             public bool ReplaceMessageFields { get; set; } = false;
346 
347             /// <summary>
348             /// Whether to replace repeated fields (i.e., discard existing content in
349             /// destination repeated fields) when merging.
350             /// Default behavior is to append elements from source repeated field to the
351             /// destination repeated field.
352             /// </summary>
353             public bool ReplaceRepeatedFields { get; set; } = false;
354 
355             /// <summary>
356             /// Whether to replace primitive (non-repeated and non-message) fields in
357             /// destination message fields with the source primitive fields (i.e., if the
358             /// field is set in the source, the value is copied to the
359             /// destination; if the field is unset in the source, the field is cleared
360             /// from the destination) when merging.
361             ///
362             /// Default behavior is to always set the value of the source primitive
363             /// field to the destination primitive field, and if the source field is
364             /// unset, the default value of the source field is copied to the
365             /// destination.
366             /// </summary>
367             public bool ReplacePrimitiveFields { get; set; } = false;
368         }
369     }
370 }
371