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