blob: 12bbdfdde921cfacf310828468e38014418e2f54 [file] [log] [blame]
Jon Skeetf8c151f2015-07-03 11:56:29 +01001#region Copyright notice and license
2// Protocol Buffers - Google's data interchange format
3// Copyright 2015 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
33using System;
34using System.Collections;
35using System.Globalization;
36using System.Text;
Jon Skeet9f37de92015-07-14 10:24:52 +010037using Google.Protobuf.Reflection;
Jon Skeetc9fd53a2015-07-20 11:48:24 +010038using Google.Protobuf.WellKnownTypes;
Jon Skeet0e30de32015-08-03 11:43:07 +010039using System.Linq;
Jon Skeetf8c151f2015-07-03 11:56:29 +010040
41namespace Google.Protobuf
42{
43 /// <summary>
44 /// Reflection-based converter from messages to JSON.
45 /// </summary>
46 /// <remarks>
47 /// <para>
48 /// Instances of this class are thread-safe, with no mutable state.
49 /// </para>
50 /// <para>
51 /// This is a simple start to get JSON formatting working. As it's reflection-based,
52 /// it's not as quick as baking calls into generated messages - but is a simpler implementation.
53 /// (This code is generally not heavily optimized.)
54 /// </para>
55 /// </remarks>
56 public sealed class JsonFormatter
57 {
58 private static JsonFormatter defaultInstance = new JsonFormatter(Settings.Default);
59
60 /// <summary>
61 /// Returns a formatter using the default settings.
62 /// </summary>
63 public static JsonFormatter Default { get { return defaultInstance; } }
64
65 /// <summary>
66 /// The JSON representation of the first 160 characters of Unicode.
67 /// Empty strings are replaced by the static constructor.
68 /// </summary>
69 private static readonly string[] CommonRepresentations = {
70 // C0 (ASCII and derivatives) control characters
71 "\\u0000", "\\u0001", "\\u0002", "\\u0003", // 0x00
72 "\\u0004", "\\u0005", "\\u0006", "\\u0007",
73 "\\b", "\\t", "\\n", "\\u000b",
74 "\\f", "\\r", "\\u000e", "\\u000f",
75 "\\u0010", "\\u0011", "\\u0012", "\\u0013", // 0x10
76 "\\u0014", "\\u0015", "\\u0016", "\\u0017",
77 "\\u0018", "\\u0019", "\\u001a", "\\u001b",
78 "\\u001c", "\\u001d", "\\u001e", "\\u001f",
79 // Escaping of " and \ are required by www.json.org string definition.
80 // Escaping of < and > are required for HTML security.
81 "", "", "\\\"", "", "", "", "", "", // 0x20
82 "", "", "", "", "", "", "", "",
83 "", "", "", "", "", "", "", "", // 0x30
84 "", "", "", "", "\\u003c", "", "\\u003e", "",
85 "", "", "", "", "", "", "", "", // 0x40
86 "", "", "", "", "", "", "", "",
87 "", "", "", "", "", "", "", "", // 0x50
88 "", "", "", "", "\\\\", "", "", "",
89 "", "", "", "", "", "", "", "", // 0x60
90 "", "", "", "", "", "", "", "",
91 "", "", "", "", "", "", "", "", // 0x70
92 "", "", "", "", "", "", "", "\\u007f",
93 // C1 (ISO 8859 and Unicode) extended control characters
94 "\\u0080", "\\u0081", "\\u0082", "\\u0083", // 0x80
95 "\\u0084", "\\u0085", "\\u0086", "\\u0087",
96 "\\u0088", "\\u0089", "\\u008a", "\\u008b",
97 "\\u008c", "\\u008d", "\\u008e", "\\u008f",
98 "\\u0090", "\\u0091", "\\u0092", "\\u0093", // 0x90
99 "\\u0094", "\\u0095", "\\u0096", "\\u0097",
100 "\\u0098", "\\u0099", "\\u009a", "\\u009b",
101 "\\u009c", "\\u009d", "\\u009e", "\\u009f"
102 };
103
104 static JsonFormatter()
105 {
106 for (int i = 0; i < CommonRepresentations.Length; i++)
107 {
108 if (CommonRepresentations[i] == "")
109 {
110 CommonRepresentations[i] = ((char) i).ToString();
111 }
112 }
113 }
114
115 private readonly Settings settings;
116
Jon Skeet811fc892015-08-04 15:58:39 +0100117 /// <summary>
118 /// Creates a new formatted with the given settings.
119 /// </summary>
120 /// <param name="settings">The settings.</param>
Jon Skeetf8c151f2015-07-03 11:56:29 +0100121 public JsonFormatter(Settings settings)
122 {
123 this.settings = settings;
124 }
125
Jon Skeet811fc892015-08-04 15:58:39 +0100126 /// <summary>
127 /// Formats the specified message as JSON.
128 /// </summary>
129 /// <param name="message">The message to format.</param>
130 /// <returns>The formatted message.</returns>
Jon Skeet53c399a2015-07-20 19:24:31 +0100131 public string Format(IMessage message)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100132 {
Jon Skeet68380f02015-07-30 13:03:45 +0100133 Preconditions.CheckNotNull(message, "message");
Jon Skeetf8c151f2015-07-03 11:56:29 +0100134 StringBuilder builder = new StringBuilder();
Jon Skeet16e272e2015-07-31 13:22:15 +0100135 if (message.Descriptor.IsWellKnownType)
136 {
137 WriteWellKnownTypeValue(builder, message.Descriptor, message, false);
138 }
139 else
140 {
141 WriteMessage(builder, message);
142 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100143 return builder.ToString();
144 }
145
Jon Skeet53c399a2015-07-20 19:24:31 +0100146 private void WriteMessage(StringBuilder builder, IMessage message)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100147 {
148 if (message == null)
149 {
150 WriteNull(builder);
151 return;
152 }
153 builder.Append("{ ");
Jon Skeet53c399a2015-07-20 19:24:31 +0100154 var fields = message.Descriptor.Fields;
Jon Skeetf8c151f2015-07-03 11:56:29 +0100155 bool first = true;
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100156 // First non-oneof fields
Jon Skeetc1c6b2d2015-07-22 19:57:29 +0100157 foreach (var field in fields.InFieldNumberOrder())
Jon Skeetf8c151f2015-07-03 11:56:29 +0100158 {
Jon Skeet53c399a2015-07-20 19:24:31 +0100159 var accessor = field.Accessor;
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100160 // Oneofs are written later
Jon Skeet4fed0b52015-07-31 10:33:31 +0100161 if (field.ContainingOneof != null && field.ContainingOneof.Accessor.GetCaseFieldDescriptor(message) != field)
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100162 {
163 continue;
164 }
Jon Skeet4fed0b52015-07-31 10:33:31 +0100165 // Omit default values unless we're asked to format them, or they're oneofs (where the default
166 // value is still formatted regardless, because that's how we preserve the oneof case).
Jon Skeetf8c151f2015-07-03 11:56:29 +0100167 object value = accessor.GetValue(message);
Jon Skeet4fed0b52015-07-31 10:33:31 +0100168 if (field.ContainingOneof == null && !settings.FormatDefaultValues && IsDefaultValue(accessor, value))
Jon Skeetf8c151f2015-07-03 11:56:29 +0100169 {
170 continue;
171 }
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100172 // Omit awkward (single) values such as unknown enum values
Jon Skeet53c399a2015-07-20 19:24:31 +0100173 if (!field.IsRepeated && !field.IsMap && !CanWriteSingleValue(accessor.Descriptor, value))
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100174 {
175 continue;
176 }
177
178 // Okay, all tests complete: let's write the field value...
Jon Skeetf8c151f2015-07-03 11:56:29 +0100179 if (!first)
180 {
181 builder.Append(", ");
182 }
183 WriteString(builder, ToCamelCase(accessor.Descriptor.Name));
184 builder.Append(": ");
185 WriteValue(builder, accessor, value);
186 first = false;
Jon Skeet4fed0b52015-07-31 10:33:31 +0100187 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100188 builder.Append(first ? "}" : " }");
189 }
190
191 // Converted from src/google/protobuf/util/internal/utility.cc ToCamelCase
192 internal static string ToCamelCase(string input)
193 {
194 bool capitalizeNext = false;
195 bool wasCap = true;
196 bool isCap = false;
197 bool firstWord = true;
198 StringBuilder result = new StringBuilder(input.Length);
199
200 for (int i = 0; i < input.Length; i++, wasCap = isCap)
201 {
202 isCap = char.IsUpper(input[i]);
203 if (input[i] == '_')
204 {
205 capitalizeNext = true;
206 if (result.Length != 0)
207 {
208 firstWord = false;
209 }
210 continue;
211 }
212 else if (firstWord)
213 {
214 // Consider when the current character B is capitalized,
215 // first word ends when:
216 // 1) following a lowercase: "...aB..."
217 // 2) followed by a lowercase: "...ABc..."
218 if (result.Length != 0 && isCap &&
219 (!wasCap || (i + 1 < input.Length && char.IsLower(input[i + 1]))))
220 {
221 firstWord = false;
222 }
223 else
224 {
225 result.Append(char.ToLowerInvariant(input[i]));
226 continue;
227 }
228 }
229 else if (capitalizeNext)
230 {
231 capitalizeNext = false;
232 if (char.IsLower(input[i]))
233 {
234 result.Append(char.ToUpperInvariant(input[i]));
235 continue;
236 }
237 }
238 result.Append(input[i]);
239 }
240 return result.ToString();
241 }
242
243 private static void WriteNull(StringBuilder builder)
244 {
245 builder.Append("null");
246 }
247
248 private static bool IsDefaultValue(IFieldAccessor accessor, object value)
249 {
250 if (accessor.Descriptor.IsMap)
251 {
252 IDictionary dictionary = (IDictionary) value;
253 return dictionary.Count == 0;
254 }
255 if (accessor.Descriptor.IsRepeated)
256 {
257 IList list = (IList) value;
258 return list.Count == 0;
259 }
260 switch (accessor.Descriptor.FieldType)
261 {
262 case FieldType.Bool:
263 return (bool) value == false;
264 case FieldType.Bytes:
265 return (ByteString) value == ByteString.Empty;
266 case FieldType.String:
267 return (string) value == "";
268 case FieldType.Double:
269 return (double) value == 0.0;
270 case FieldType.SInt32:
271 case FieldType.Int32:
272 case FieldType.SFixed32:
273 case FieldType.Enum:
274 return (int) value == 0;
275 case FieldType.Fixed32:
276 case FieldType.UInt32:
277 return (uint) value == 0;
278 case FieldType.Fixed64:
279 case FieldType.UInt64:
280 return (ulong) value == 0;
281 case FieldType.SFixed64:
282 case FieldType.Int64:
283 case FieldType.SInt64:
284 return (long) value == 0;
285 case FieldType.Float:
286 return (float) value == 0f;
287 case FieldType.Message:
288 case FieldType.Group: // Never expect to get this, but...
289 return value == null;
290 default:
291 throw new ArgumentException("Invalid field type");
292 }
293 }
294
295 private void WriteValue(StringBuilder builder, IFieldAccessor accessor, object value)
296 {
297 if (accessor.Descriptor.IsMap)
298 {
299 WriteDictionary(builder, accessor, (IDictionary) value);
300 }
301 else if (accessor.Descriptor.IsRepeated)
302 {
303 WriteList(builder, accessor, (IList) value);
304 }
305 else
306 {
307 WriteSingleValue(builder, accessor.Descriptor, value);
308 }
309 }
310
311 private void WriteSingleValue(StringBuilder builder, FieldDescriptor descriptor, object value)
312 {
313 switch (descriptor.FieldType)
314 {
315 case FieldType.Bool:
316 builder.Append((bool) value ? "true" : "false");
317 break;
318 case FieldType.Bytes:
319 // Nothing in Base64 needs escaping
320 builder.Append('"');
321 builder.Append(((ByteString) value).ToBase64());
322 builder.Append('"');
323 break;
324 case FieldType.String:
325 WriteString(builder, (string) value);
326 break;
327 case FieldType.Fixed32:
328 case FieldType.UInt32:
329 case FieldType.SInt32:
330 case FieldType.Int32:
331 case FieldType.SFixed32:
332 {
333 IFormattable formattable = (IFormattable) value;
334 builder.Append(formattable.ToString("d", CultureInfo.InvariantCulture));
335 break;
336 }
337 case FieldType.Enum:
338 EnumValueDescriptor enumValue = descriptor.EnumType.FindValueByNumber((int) value);
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100339 // We will already have validated that this is a known value.
340 WriteString(builder, enumValue.Name);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100341 break;
342 case FieldType.Fixed64:
343 case FieldType.UInt64:
344 case FieldType.SFixed64:
345 case FieldType.Int64:
346 case FieldType.SInt64:
347 {
348 builder.Append('"');
349 IFormattable formattable = (IFormattable) value;
350 builder.Append(formattable.ToString("d", CultureInfo.InvariantCulture));
351 builder.Append('"');
352 break;
353 }
354 case FieldType.Double:
355 case FieldType.Float:
356 string text = ((IFormattable) value).ToString("r", CultureInfo.InvariantCulture);
357 if (text == "NaN" || text == "Infinity" || text == "-Infinity")
358 {
359 builder.Append('"');
360 builder.Append(text);
361 builder.Append('"');
362 }
363 else
364 {
365 builder.Append(text);
366 }
367 break;
368 case FieldType.Message:
369 case FieldType.Group: // Never expect to get this, but...
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100370 if (descriptor.MessageType.IsWellKnownType)
371 {
Jon Skeet16e272e2015-07-31 13:22:15 +0100372 WriteWellKnownTypeValue(builder, descriptor.MessageType, value, true);
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100373 }
374 else
375 {
Jon Skeet53c399a2015-07-20 19:24:31 +0100376 WriteMessage(builder, (IMessage) value);
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100377 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100378 break;
379 default:
380 throw new ArgumentException("Invalid field type: " + descriptor.FieldType);
381 }
382 }
383
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100384 /// <summary>
385 /// Central interception point for well-known type formatting. Any well-known types which
Jon Skeet16e272e2015-07-31 13:22:15 +0100386 /// don't need special handling can fall back to WriteMessage. We avoid assuming that the
387 /// values are using the embedded well-known types, in order to allow for dynamic messages
388 /// in the future.
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100389 /// </summary>
Jon Skeet16e272e2015-07-31 13:22:15 +0100390 private void WriteWellKnownTypeValue(StringBuilder builder, MessageDescriptor descriptor, object value, bool inField)
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100391 {
Jon Skeete7caf152015-07-31 15:07:50 +0100392 if (value == null)
393 {
394 WriteNull(builder);
395 return;
396 }
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100397 // For wrapper types, the value will be the (possibly boxed) "native" value,
398 // so we can write it as if we were unconditionally writing the Value field for the wrapper type.
Jon Skeete7caf152015-07-31 15:07:50 +0100399 if (descriptor.File == Int32Value.Descriptor.File)
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100400 {
Jon Skeet16e272e2015-07-31 13:22:15 +0100401 WriteSingleValue(builder, descriptor.FindFieldByNumber(1), value);
402 return;
403 }
Jon Skeete7caf152015-07-31 15:07:50 +0100404 if (descriptor.FullName == Timestamp.Descriptor.FullName)
Jon Skeet16e272e2015-07-31 13:22:15 +0100405 {
406 MaybeWrapInString(builder, value, WriteTimestamp, inField);
407 return;
408 }
Jon Skeete7caf152015-07-31 15:07:50 +0100409 if (descriptor.FullName == Duration.Descriptor.FullName)
Jon Skeet16e272e2015-07-31 13:22:15 +0100410 {
411 MaybeWrapInString(builder, value, WriteDuration, inField);
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100412 return;
413 }
Jon Skeet0e30de32015-08-03 11:43:07 +0100414 if (descriptor.FullName == FieldMask.Descriptor.FullName)
415 {
416 MaybeWrapInString(builder, value, WriteFieldMask, inField);
417 return;
418 }
Jon Skeete7caf152015-07-31 15:07:50 +0100419 if (descriptor.FullName == Struct.Descriptor.FullName)
420 {
421 WriteStruct(builder, (IMessage) value);
422 return;
423 }
424 if (descriptor.FullName == ListValue.Descriptor.FullName)
425 {
426 var fieldAccessor = descriptor.Fields[ListValue.ValuesFieldNumber].Accessor;
Jon Skeet6f300442015-08-06 14:29:34 +0100427 WriteList(builder, fieldAccessor, (IList) fieldAccessor.GetValue((IMessage) value));
Jon Skeete7caf152015-07-31 15:07:50 +0100428 return;
429 }
430 if (descriptor.FullName == Value.Descriptor.FullName)
431 {
432 WriteStructFieldValue(builder, (IMessage) value);
433 return;
434 }
Jon Skeet53c399a2015-07-20 19:24:31 +0100435 WriteMessage(builder, (IMessage) value);
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100436 }
437
Jon Skeet16e272e2015-07-31 13:22:15 +0100438 /// <summary>
439 /// Some well-known types end up as string values... so they need wrapping in quotes, but only
440 /// when they're being used as fields within another message.
441 /// </summary>
442 private void MaybeWrapInString(StringBuilder builder, object value, Action<StringBuilder, IMessage> action, bool inField)
443 {
444 if (inField)
445 {
446 builder.Append('"');
447 action(builder, (IMessage) value);
448 builder.Append('"');
449 }
450 else
451 {
452 action(builder, (IMessage) value);
453 }
454 }
455
456 private void WriteTimestamp(StringBuilder builder, IMessage value)
457 {
458 // TODO: In the common case where this *is* using the built-in Timestamp type, we could
459 // avoid all the reflection at this point, by casting to Timestamp. In the interests of
460 // avoiding subtle bugs, don't do that until we've implemented DynamicMessage so that we can prove
461 // it still works in that case.
462 int nanos = (int) value.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.GetValue(value);
463 long seconds = (long) value.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.GetValue(value);
464
465 // Even if the original message isn't using the built-in classes, we can still build one... and then
466 // rely on it being normalized.
467 Timestamp normalized = Timestamp.Normalize(seconds, nanos);
468 // Use .NET's formatting for the value down to the second, including an opening double quote (as it's a string value)
469 DateTime dateTime = normalized.ToDateTime();
470 builder.Append(dateTime.ToString("yyyy'-'MM'-'dd'T'HH:mm:ss", CultureInfo.InvariantCulture));
Jon Skeet801b1692015-08-03 08:45:48 +0100471 AppendNanoseconds(builder, Math.Abs(normalized.Nanos));
Jon Skeet16e272e2015-07-31 13:22:15 +0100472 builder.Append('Z');
473 }
474
475 private void WriteDuration(StringBuilder builder, IMessage value)
476 {
Jon Skeet801b1692015-08-03 08:45:48 +0100477 // TODO: Same as for WriteTimestamp
Jon Skeet16e272e2015-07-31 13:22:15 +0100478 int nanos = (int) value.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.GetValue(value);
479 long seconds = (long) value.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.GetValue(value);
480
481 // Even if the original message isn't using the built-in classes, we can still build one... and then
482 // rely on it being normalized.
483 Duration normalized = Duration.Normalize(seconds, nanos);
484
485 // The seconds part will normally provide the minus sign if we need it, but not if it's 0...
486 if (normalized.Seconds == 0 && normalized.Nanos < 0)
487 {
488 builder.Append('-');
489 }
490
491 builder.Append(normalized.Seconds.ToString("d", CultureInfo.InvariantCulture));
Jon Skeet801b1692015-08-03 08:45:48 +0100492 AppendNanoseconds(builder, Math.Abs(normalized.Nanos));
493 builder.Append('s');
494 }
495
Jon Skeet0e30de32015-08-03 11:43:07 +0100496 private void WriteFieldMask(StringBuilder builder, IMessage value)
497 {
498 IList paths = (IList) value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(value);
499 AppendEscapedString(builder, string.Join(",", paths.Cast<string>().Select(ToCamelCase)));
500 }
501
Jon Skeet801b1692015-08-03 08:45:48 +0100502 /// <summary>
503 /// Appends a number of nanoseconds to a StringBuilder. Either 0 digits are added (in which
504 /// case no "." is appended), or 3 6 or 9 digits.
505 /// </summary>
506 private static void AppendNanoseconds(StringBuilder builder, int nanos)
507 {
Jon Skeet16e272e2015-07-31 13:22:15 +0100508 if (nanos != 0)
509 {
510 builder.Append('.');
511 // Output to 3, 6 or 9 digits.
512 if (nanos % 1000000 == 0)
513 {
514 builder.Append((nanos / 1000000).ToString("d", CultureInfo.InvariantCulture));
515 }
Jon Skeet801b1692015-08-03 08:45:48 +0100516 else if (nanos % 1000 == 0)
Jon Skeet16e272e2015-07-31 13:22:15 +0100517 {
518 builder.Append((nanos / 1000).ToString("d", CultureInfo.InvariantCulture));
519 }
520 else
521 {
522 builder.Append(nanos.ToString("d", CultureInfo.InvariantCulture));
523 }
524 }
Jon Skeet16e272e2015-07-31 13:22:15 +0100525 }
526
Jon Skeete7caf152015-07-31 15:07:50 +0100527 private void WriteStruct(StringBuilder builder, IMessage message)
528 {
529 builder.Append("{ ");
530 IDictionary fields = (IDictionary) message.Descriptor.Fields[Struct.FieldsFieldNumber].Accessor.GetValue(message);
531 bool first = true;
532 foreach (DictionaryEntry entry in fields)
533 {
534 string key = (string) entry.Key;
535 IMessage value = (IMessage) entry.Value;
536 if (string.IsNullOrEmpty(key) || value == null)
537 {
538 throw new InvalidOperationException("Struct fields cannot have an empty key or a null value.");
539 }
540
541 if (!first)
542 {
543 builder.Append(", ");
544 }
545 WriteString(builder, key);
546 builder.Append(": ");
547 WriteStructFieldValue(builder, value);
548 first = false;
549 }
550 builder.Append(first ? "}" : " }");
551 }
552
553 private void WriteStructFieldValue(StringBuilder builder, IMessage message)
554 {
555 var specifiedField = message.Descriptor.Oneofs[0].Accessor.GetCaseFieldDescriptor(message);
556 if (specifiedField == null)
557 {
558 throw new InvalidOperationException("Value message must contain a value for the oneof.");
559 }
560
561 object value = specifiedField.Accessor.GetValue(message);
562
563 switch (specifiedField.FieldNumber)
564 {
565 case Value.BoolValueFieldNumber:
566 case Value.StringValueFieldNumber:
567 case Value.NumberValueFieldNumber:
568 WriteSingleValue(builder, specifiedField, value);
569 return;
570 case Value.StructValueFieldNumber:
571 case Value.ListValueFieldNumber:
572 // Structs and ListValues are nested messages, and already well-known types.
573 var nestedMessage = (IMessage) specifiedField.Accessor.GetValue(message);
574 WriteWellKnownTypeValue(builder, nestedMessage.Descriptor, nestedMessage, true);
575 return;
576 case Value.NullValueFieldNumber:
577 WriteNull(builder);
578 return;
579 default:
580 throw new InvalidOperationException("Unexpected case in struct field: " + specifiedField.FieldNumber);
581 }
582 }
583
Jon Skeetf8c151f2015-07-03 11:56:29 +0100584 private void WriteList(StringBuilder builder, IFieldAccessor accessor, IList list)
585 {
586 builder.Append("[ ");
587 bool first = true;
588 foreach (var value in list)
589 {
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100590 if (!CanWriteSingleValue(accessor.Descriptor, value))
591 {
592 continue;
593 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100594 if (!first)
595 {
596 builder.Append(", ");
597 }
598 WriteSingleValue(builder, accessor.Descriptor, value);
599 first = false;
600 }
601 builder.Append(first ? "]" : " ]");
602 }
603
604 private void WriteDictionary(StringBuilder builder, IFieldAccessor accessor, IDictionary dictionary)
605 {
606 builder.Append("{ ");
607 bool first = true;
608 FieldDescriptor keyType = accessor.Descriptor.MessageType.FindFieldByNumber(1);
609 FieldDescriptor valueType = accessor.Descriptor.MessageType.FindFieldByNumber(2);
610 // This will box each pair. Could use IDictionaryEnumerator, but that's ugly in terms of disposal.
611 foreach (DictionaryEntry pair in dictionary)
612 {
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100613 if (!CanWriteSingleValue(valueType, pair.Value))
614 {
615 continue;
616 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100617 if (!first)
618 {
619 builder.Append(", ");
620 }
621 string keyText;
622 switch (keyType.FieldType)
623 {
624 case FieldType.String:
625 keyText = (string) pair.Key;
626 break;
627 case FieldType.Bool:
628 keyText = (bool) pair.Key ? "true" : "false";
629 break;
630 case FieldType.Fixed32:
631 case FieldType.Fixed64:
632 case FieldType.SFixed32:
633 case FieldType.SFixed64:
634 case FieldType.Int32:
635 case FieldType.Int64:
636 case FieldType.SInt32:
637 case FieldType.SInt64:
638 case FieldType.UInt32:
639 case FieldType.UInt64:
640 keyText = ((IFormattable) pair.Key).ToString("d", CultureInfo.InvariantCulture);
641 break;
642 default:
643 throw new ArgumentException("Invalid key type: " + keyType.FieldType);
644 }
645 WriteString(builder, keyText);
646 builder.Append(": ");
647 WriteSingleValue(builder, valueType, pair.Value);
648 first = false;
649 }
650 builder.Append(first ? "}" : " }");
651 }
652
653 /// <summary>
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100654 /// Returns whether or not a singular value can be represented in JSON.
655 /// Currently only relevant for enums, where unknown values can't be represented.
656 /// For repeated/map fields, this always returns true.
657 /// </summary>
658 private bool CanWriteSingleValue(FieldDescriptor descriptor, object value)
659 {
660 if (descriptor.FieldType == FieldType.Enum)
661 {
662 EnumValueDescriptor enumValue = descriptor.EnumType.FindValueByNumber((int) value);
663 return enumValue != null;
664 }
665 return true;
666 }
667
668 /// <summary>
Jon Skeetf8c151f2015-07-03 11:56:29 +0100669 /// Writes a string (including leading and trailing double quotes) to a builder, escaping as required.
670 /// </summary>
671 /// <remarks>
672 /// Other than surrogate pair handling, this code is mostly taken from src/google/protobuf/util/internal/json_escaping.cc.
673 /// </remarks>
674 private void WriteString(StringBuilder builder, string text)
675 {
676 builder.Append('"');
Jon Skeet0e30de32015-08-03 11:43:07 +0100677 AppendEscapedString(builder, text);
678 builder.Append('"');
679 }
680
681 /// <summary>
682 /// Appends the given text to the string builder, escaping as required.
683 /// </summary>
684 private void AppendEscapedString(StringBuilder builder, string text)
685 {
Jon Skeetf8c151f2015-07-03 11:56:29 +0100686 for (int i = 0; i < text.Length; i++)
687 {
688 char c = text[i];
689 if (c < 0xa0)
690 {
691 builder.Append(CommonRepresentations[c]);
692 continue;
693 }
694 if (char.IsHighSurrogate(c))
695 {
696 // Encountered first part of a surrogate pair.
697 // Check that we have the whole pair, and encode both parts as hex.
698 i++;
699 if (i == text.Length || !char.IsLowSurrogate(text[i]))
700 {
701 throw new ArgumentException("String contains low surrogate not followed by high surrogate");
702 }
703 HexEncodeUtf16CodeUnit(builder, c);
704 HexEncodeUtf16CodeUnit(builder, text[i]);
705 continue;
706 }
707 else if (char.IsLowSurrogate(c))
708 {
709 throw new ArgumentException("String contains high surrogate not preceded by low surrogate");
710 }
711 switch ((uint) c)
712 {
713 // These are not required by json spec
714 // but used to prevent security bugs in javascript.
715 case 0xfeff: // Zero width no-break space
716 case 0xfff9: // Interlinear annotation anchor
717 case 0xfffa: // Interlinear annotation separator
718 case 0xfffb: // Interlinear annotation terminator
719
720 case 0x00ad: // Soft-hyphen
721 case 0x06dd: // Arabic end of ayah
722 case 0x070f: // Syriac abbreviation mark
723 case 0x17b4: // Khmer vowel inherent Aq
724 case 0x17b5: // Khmer vowel inherent Aa
725 HexEncodeUtf16CodeUnit(builder, c);
726 break;
727
728 default:
729 if ((c >= 0x0600 && c <= 0x0603) || // Arabic signs
730 (c >= 0x200b && c <= 0x200f) || // Zero width etc.
731 (c >= 0x2028 && c <= 0x202e) || // Separators etc.
732 (c >= 0x2060 && c <= 0x2064) || // Invisible etc.
733 (c >= 0x206a && c <= 0x206f))
734 {
735 HexEncodeUtf16CodeUnit(builder, c);
736 }
737 else
738 {
739 // No handling of surrogates here - that's done earlier
740 builder.Append(c);
741 }
742 break;
743 }
744 }
Jon Skeet801b1692015-08-03 08:45:48 +0100745 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100746
747 private const string Hex = "0123456789abcdef";
748 private static void HexEncodeUtf16CodeUnit(StringBuilder builder, char c)
749 {
Jon Skeetf8c151f2015-07-03 11:56:29 +0100750 builder.Append("\\u");
751 builder.Append(Hex[(c >> 12) & 0xf]);
752 builder.Append(Hex[(c >> 8) & 0xf]);
753 builder.Append(Hex[(c >> 4) & 0xf]);
754 builder.Append(Hex[(c >> 0) & 0xf]);
755 }
756
757 /// <summary>
758 /// Settings controlling JSON formatting.
759 /// </summary>
760 public sealed class Settings
761 {
762 private static readonly Settings defaultInstance = new Settings(false);
763
764 /// <summary>
765 /// Default settings, as used by <see cref="JsonFormatter.Default"/>
766 /// </summary>
767 public static Settings Default { get { return defaultInstance; } }
768
769 private readonly bool formatDefaultValues;
770
771
772 /// <summary>
773 /// Whether fields whose values are the default for the field type (e.g. 0 for integers)
774 /// should be formatted (true) or omitted (false).
775 /// </summary>
776 public bool FormatDefaultValues { get { return formatDefaultValues; } }
777
Jon Skeet811fc892015-08-04 15:58:39 +0100778 /// <summary>
779 /// Creates a new <see cref="Settings"/> object with the specified formatting of default values.
780 /// </summary>
781 /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
Jon Skeetf8c151f2015-07-03 11:56:29 +0100782 public Settings(bool formatDefaultValues)
783 {
784 this.formatDefaultValues = formatDefaultValues;
785 }
786 }
787 }
788}