blob: 3f9bd478745fb5c21b0841ea517a902426c0767f [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 Skeet9ed6d4d2015-09-28 17:28:02 +0100173 if (!field.IsRepeated && !field.IsMap && !CanWriteSingleValue(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(": ");
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100185 WriteValue(builder, value);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100186 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 }
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100294
295 private void WriteValue(StringBuilder builder, object value)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100296 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100297 if (value == null)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100298 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100299 WriteNull(builder);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100300 }
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100301 else if (value is bool)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100302 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100303 builder.Append((bool) value ? "true" : "false");
304 }
305 else if (value is ByteString)
306 {
307 // Nothing in Base64 needs escaping
308 builder.Append('"');
309 builder.Append(((ByteString) value).ToBase64());
310 builder.Append('"');
311 }
312 else if (value is string)
313 {
314 WriteString(builder, (string) value);
315 }
316 else if (value is IDictionary)
317 {
318 WriteDictionary(builder, (IDictionary) value);
319 }
320 else if (value is IList)
321 {
322 WriteList(builder, (IList) value);
323 }
324 else if (value is int || value is uint)
325 {
326 IFormattable formattable = (IFormattable) value;
327 builder.Append(formattable.ToString("d", CultureInfo.InvariantCulture));
328 }
329 else if (value is long || value is ulong)
330 {
331 builder.Append('"');
332 IFormattable formattable = (IFormattable) value;
333 builder.Append(formattable.ToString("d", CultureInfo.InvariantCulture));
334 builder.Append('"');
335 }
336 else if (value is System.Enum)
337 {
338 WriteString(builder, value.ToString());
339 }
340 else if (value is float || value is double)
341 {
342 string text = ((IFormattable) value).ToString("r", CultureInfo.InvariantCulture);
343 if (text == "NaN" || text == "Infinity" || text == "-Infinity")
344 {
345 builder.Append('"');
346 builder.Append(text);
347 builder.Append('"');
348 }
349 else
350 {
351 builder.Append(text);
352 }
353 }
354 else if (value is IMessage)
355 {
356 IMessage message = (IMessage) value;
357 if (message.Descriptor.IsWellKnownType)
358 {
359 WriteWellKnownTypeValue(builder, message.Descriptor, value, true);
360 }
361 else
362 {
363 WriteMessage(builder, (IMessage) value);
364 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100365 }
366 else
367 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100368 throw new ArgumentException("Unable to format value of type " + value.GetType());
Jon Skeetf8c151f2015-07-03 11:56:29 +0100369 }
370 }
371
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100372 /// <summary>
373 /// Central interception point for well-known type formatting. Any well-known types which
Jon Skeet16e272e2015-07-31 13:22:15 +0100374 /// don't need special handling can fall back to WriteMessage. We avoid assuming that the
375 /// values are using the embedded well-known types, in order to allow for dynamic messages
376 /// in the future.
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100377 /// </summary>
Jon Skeet16e272e2015-07-31 13:22:15 +0100378 private void WriteWellKnownTypeValue(StringBuilder builder, MessageDescriptor descriptor, object value, bool inField)
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100379 {
Jon Skeete7caf152015-07-31 15:07:50 +0100380 if (value == null)
381 {
382 WriteNull(builder);
383 return;
384 }
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100385 // For wrapper types, the value will be the (possibly boxed) "native" value,
386 // 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 +0100387 if (descriptor.File == Int32Value.Descriptor.File)
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100388 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100389 WriteValue(builder, value);
Jon Skeet16e272e2015-07-31 13:22:15 +0100390 return;
391 }
Jon Skeete7caf152015-07-31 15:07:50 +0100392 if (descriptor.FullName == Timestamp.Descriptor.FullName)
Jon Skeet16e272e2015-07-31 13:22:15 +0100393 {
394 MaybeWrapInString(builder, value, WriteTimestamp, inField);
395 return;
396 }
Jon Skeete7caf152015-07-31 15:07:50 +0100397 if (descriptor.FullName == Duration.Descriptor.FullName)
Jon Skeet16e272e2015-07-31 13:22:15 +0100398 {
399 MaybeWrapInString(builder, value, WriteDuration, inField);
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100400 return;
401 }
Jon Skeet0e30de32015-08-03 11:43:07 +0100402 if (descriptor.FullName == FieldMask.Descriptor.FullName)
403 {
404 MaybeWrapInString(builder, value, WriteFieldMask, inField);
405 return;
406 }
Jon Skeete7caf152015-07-31 15:07:50 +0100407 if (descriptor.FullName == Struct.Descriptor.FullName)
408 {
409 WriteStruct(builder, (IMessage) value);
410 return;
411 }
412 if (descriptor.FullName == ListValue.Descriptor.FullName)
413 {
414 var fieldAccessor = descriptor.Fields[ListValue.ValuesFieldNumber].Accessor;
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100415 WriteList(builder, (IList) fieldAccessor.GetValue((IMessage) value));
Jon Skeete7caf152015-07-31 15:07:50 +0100416 return;
417 }
418 if (descriptor.FullName == Value.Descriptor.FullName)
419 {
420 WriteStructFieldValue(builder, (IMessage) value);
421 return;
422 }
Jon Skeet53c399a2015-07-20 19:24:31 +0100423 WriteMessage(builder, (IMessage) value);
Jon Skeetc9fd53a2015-07-20 11:48:24 +0100424 }
425
Jon Skeet16e272e2015-07-31 13:22:15 +0100426 /// <summary>
427 /// Some well-known types end up as string values... so they need wrapping in quotes, but only
428 /// when they're being used as fields within another message.
429 /// </summary>
430 private void MaybeWrapInString(StringBuilder builder, object value, Action<StringBuilder, IMessage> action, bool inField)
431 {
432 if (inField)
433 {
434 builder.Append('"');
435 action(builder, (IMessage) value);
436 builder.Append('"');
437 }
438 else
439 {
440 action(builder, (IMessage) value);
441 }
442 }
443
444 private void WriteTimestamp(StringBuilder builder, IMessage value)
445 {
446 // TODO: In the common case where this *is* using the built-in Timestamp type, we could
447 // avoid all the reflection at this point, by casting to Timestamp. In the interests of
448 // avoiding subtle bugs, don't do that until we've implemented DynamicMessage so that we can prove
449 // it still works in that case.
450 int nanos = (int) value.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.GetValue(value);
451 long seconds = (long) value.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.GetValue(value);
452
453 // Even if the original message isn't using the built-in classes, we can still build one... and then
454 // rely on it being normalized.
455 Timestamp normalized = Timestamp.Normalize(seconds, nanos);
456 // Use .NET's formatting for the value down to the second, including an opening double quote (as it's a string value)
457 DateTime dateTime = normalized.ToDateTime();
458 builder.Append(dateTime.ToString("yyyy'-'MM'-'dd'T'HH:mm:ss", CultureInfo.InvariantCulture));
Jon Skeet801b1692015-08-03 08:45:48 +0100459 AppendNanoseconds(builder, Math.Abs(normalized.Nanos));
Jon Skeet16e272e2015-07-31 13:22:15 +0100460 builder.Append('Z');
461 }
462
463 private void WriteDuration(StringBuilder builder, IMessage value)
464 {
Jon Skeet801b1692015-08-03 08:45:48 +0100465 // TODO: Same as for WriteTimestamp
Jon Skeet16e272e2015-07-31 13:22:15 +0100466 int nanos = (int) value.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.GetValue(value);
467 long seconds = (long) value.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.GetValue(value);
468
469 // Even if the original message isn't using the built-in classes, we can still build one... and then
470 // rely on it being normalized.
471 Duration normalized = Duration.Normalize(seconds, nanos);
472
473 // The seconds part will normally provide the minus sign if we need it, but not if it's 0...
474 if (normalized.Seconds == 0 && normalized.Nanos < 0)
475 {
476 builder.Append('-');
477 }
478
479 builder.Append(normalized.Seconds.ToString("d", CultureInfo.InvariantCulture));
Jon Skeet801b1692015-08-03 08:45:48 +0100480 AppendNanoseconds(builder, Math.Abs(normalized.Nanos));
481 builder.Append('s');
482 }
483
Jon Skeet0e30de32015-08-03 11:43:07 +0100484 private void WriteFieldMask(StringBuilder builder, IMessage value)
485 {
486 IList paths = (IList) value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(value);
487 AppendEscapedString(builder, string.Join(",", paths.Cast<string>().Select(ToCamelCase)));
488 }
489
Jon Skeet801b1692015-08-03 08:45:48 +0100490 /// <summary>
491 /// Appends a number of nanoseconds to a StringBuilder. Either 0 digits are added (in which
492 /// case no "." is appended), or 3 6 or 9 digits.
493 /// </summary>
494 private static void AppendNanoseconds(StringBuilder builder, int nanos)
495 {
Jon Skeet16e272e2015-07-31 13:22:15 +0100496 if (nanos != 0)
497 {
498 builder.Append('.');
499 // Output to 3, 6 or 9 digits.
500 if (nanos % 1000000 == 0)
501 {
502 builder.Append((nanos / 1000000).ToString("d", CultureInfo.InvariantCulture));
503 }
Jon Skeet801b1692015-08-03 08:45:48 +0100504 else if (nanos % 1000 == 0)
Jon Skeet16e272e2015-07-31 13:22:15 +0100505 {
506 builder.Append((nanos / 1000).ToString("d", CultureInfo.InvariantCulture));
507 }
508 else
509 {
510 builder.Append(nanos.ToString("d", CultureInfo.InvariantCulture));
511 }
512 }
Jon Skeet16e272e2015-07-31 13:22:15 +0100513 }
514
Jon Skeete7caf152015-07-31 15:07:50 +0100515 private void WriteStruct(StringBuilder builder, IMessage message)
516 {
517 builder.Append("{ ");
518 IDictionary fields = (IDictionary) message.Descriptor.Fields[Struct.FieldsFieldNumber].Accessor.GetValue(message);
519 bool first = true;
520 foreach (DictionaryEntry entry in fields)
521 {
522 string key = (string) entry.Key;
523 IMessage value = (IMessage) entry.Value;
524 if (string.IsNullOrEmpty(key) || value == null)
525 {
526 throw new InvalidOperationException("Struct fields cannot have an empty key or a null value.");
527 }
528
529 if (!first)
530 {
531 builder.Append(", ");
532 }
533 WriteString(builder, key);
534 builder.Append(": ");
535 WriteStructFieldValue(builder, value);
536 first = false;
537 }
538 builder.Append(first ? "}" : " }");
539 }
540
541 private void WriteStructFieldValue(StringBuilder builder, IMessage message)
542 {
543 var specifiedField = message.Descriptor.Oneofs[0].Accessor.GetCaseFieldDescriptor(message);
544 if (specifiedField == null)
545 {
546 throw new InvalidOperationException("Value message must contain a value for the oneof.");
547 }
548
549 object value = specifiedField.Accessor.GetValue(message);
550
551 switch (specifiedField.FieldNumber)
552 {
553 case Value.BoolValueFieldNumber:
554 case Value.StringValueFieldNumber:
555 case Value.NumberValueFieldNumber:
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100556 WriteValue(builder, value);
Jon Skeete7caf152015-07-31 15:07:50 +0100557 return;
558 case Value.StructValueFieldNumber:
559 case Value.ListValueFieldNumber:
560 // Structs and ListValues are nested messages, and already well-known types.
561 var nestedMessage = (IMessage) specifiedField.Accessor.GetValue(message);
562 WriteWellKnownTypeValue(builder, nestedMessage.Descriptor, nestedMessage, true);
563 return;
564 case Value.NullValueFieldNumber:
565 WriteNull(builder);
566 return;
567 default:
568 throw new InvalidOperationException("Unexpected case in struct field: " + specifiedField.FieldNumber);
569 }
570 }
571
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100572 internal void WriteList(StringBuilder builder, IList list)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100573 {
574 builder.Append("[ ");
575 bool first = true;
576 foreach (var value in list)
577 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100578 if (!CanWriteSingleValue(value))
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100579 {
580 continue;
581 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100582 if (!first)
583 {
584 builder.Append(", ");
585 }
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100586 WriteValue(builder, value);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100587 first = false;
588 }
589 builder.Append(first ? "]" : " ]");
590 }
591
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100592 internal void WriteDictionary(StringBuilder builder, IDictionary dictionary)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100593 {
594 builder.Append("{ ");
595 bool first = true;
Jon Skeetf8c151f2015-07-03 11:56:29 +0100596 // This will box each pair. Could use IDictionaryEnumerator, but that's ugly in terms of disposal.
597 foreach (DictionaryEntry pair in dictionary)
598 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100599 if (!CanWriteSingleValue(pair.Value))
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100600 {
601 continue;
602 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100603 if (!first)
604 {
605 builder.Append(", ");
606 }
607 string keyText;
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100608 if (pair.Key is string)
Jon Skeetf8c151f2015-07-03 11:56:29 +0100609 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100610 keyText = (string) pair.Key;
611 }
612 else if (pair.Key is bool)
613 {
614 keyText = (bool) pair.Key ? "true" : "false";
615 }
616 else if (pair.Key is int || pair.Key is uint | pair.Key is long || pair.Key is ulong)
617 {
618 keyText = ((IFormattable) pair.Key).ToString("d", CultureInfo.InvariantCulture);
619 }
620 else
621 {
622 if (pair.Key == null)
623 {
624 throw new ArgumentException("Dictionary has entry with null key");
625 }
626 throw new ArgumentException("Unhandled dictionary key type: " + pair.Key.GetType());
Jon Skeetf8c151f2015-07-03 11:56:29 +0100627 }
628 WriteString(builder, keyText);
629 builder.Append(": ");
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100630 WriteValue(builder, pair.Value);
Jon Skeetf8c151f2015-07-03 11:56:29 +0100631 first = false;
632 }
633 builder.Append(first ? "}" : " }");
634 }
635
636 /// <summary>
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100637 /// Returns whether or not a singular value can be represented in JSON.
638 /// Currently only relevant for enums, where unknown values can't be represented.
639 /// For repeated/map fields, this always returns true.
640 /// </summary>
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100641 private bool CanWriteSingleValue(object value)
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100642 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100643 if (value is System.Enum)
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100644 {
Jon Skeet9ed6d4d2015-09-28 17:28:02 +0100645 return System.Enum.IsDefined(value.GetType(), value);
Jon Skeet6ea9bc72015-07-10 14:05:52 +0100646 }
647 return true;
648 }
649
650 /// <summary>
Jon Skeetf8c151f2015-07-03 11:56:29 +0100651 /// Writes a string (including leading and trailing double quotes) to a builder, escaping as required.
652 /// </summary>
653 /// <remarks>
654 /// Other than surrogate pair handling, this code is mostly taken from src/google/protobuf/util/internal/json_escaping.cc.
655 /// </remarks>
656 private void WriteString(StringBuilder builder, string text)
657 {
658 builder.Append('"');
Jon Skeet0e30de32015-08-03 11:43:07 +0100659 AppendEscapedString(builder, text);
660 builder.Append('"');
661 }
662
663 /// <summary>
664 /// Appends the given text to the string builder, escaping as required.
665 /// </summary>
666 private void AppendEscapedString(StringBuilder builder, string text)
667 {
Jon Skeetf8c151f2015-07-03 11:56:29 +0100668 for (int i = 0; i < text.Length; i++)
669 {
670 char c = text[i];
671 if (c < 0xa0)
672 {
673 builder.Append(CommonRepresentations[c]);
674 continue;
675 }
676 if (char.IsHighSurrogate(c))
677 {
678 // Encountered first part of a surrogate pair.
679 // Check that we have the whole pair, and encode both parts as hex.
680 i++;
681 if (i == text.Length || !char.IsLowSurrogate(text[i]))
682 {
683 throw new ArgumentException("String contains low surrogate not followed by high surrogate");
684 }
685 HexEncodeUtf16CodeUnit(builder, c);
686 HexEncodeUtf16CodeUnit(builder, text[i]);
687 continue;
688 }
689 else if (char.IsLowSurrogate(c))
690 {
691 throw new ArgumentException("String contains high surrogate not preceded by low surrogate");
692 }
693 switch ((uint) c)
694 {
695 // These are not required by json spec
696 // but used to prevent security bugs in javascript.
697 case 0xfeff: // Zero width no-break space
698 case 0xfff9: // Interlinear annotation anchor
699 case 0xfffa: // Interlinear annotation separator
700 case 0xfffb: // Interlinear annotation terminator
701
702 case 0x00ad: // Soft-hyphen
703 case 0x06dd: // Arabic end of ayah
704 case 0x070f: // Syriac abbreviation mark
705 case 0x17b4: // Khmer vowel inherent Aq
706 case 0x17b5: // Khmer vowel inherent Aa
707 HexEncodeUtf16CodeUnit(builder, c);
708 break;
709
710 default:
711 if ((c >= 0x0600 && c <= 0x0603) || // Arabic signs
712 (c >= 0x200b && c <= 0x200f) || // Zero width etc.
713 (c >= 0x2028 && c <= 0x202e) || // Separators etc.
714 (c >= 0x2060 && c <= 0x2064) || // Invisible etc.
715 (c >= 0x206a && c <= 0x206f))
716 {
717 HexEncodeUtf16CodeUnit(builder, c);
718 }
719 else
720 {
721 // No handling of surrogates here - that's done earlier
722 builder.Append(c);
723 }
724 break;
725 }
726 }
Jon Skeet801b1692015-08-03 08:45:48 +0100727 }
Jon Skeetf8c151f2015-07-03 11:56:29 +0100728
729 private const string Hex = "0123456789abcdef";
730 private static void HexEncodeUtf16CodeUnit(StringBuilder builder, char c)
731 {
Jon Skeetf8c151f2015-07-03 11:56:29 +0100732 builder.Append("\\u");
733 builder.Append(Hex[(c >> 12) & 0xf]);
734 builder.Append(Hex[(c >> 8) & 0xf]);
735 builder.Append(Hex[(c >> 4) & 0xf]);
736 builder.Append(Hex[(c >> 0) & 0xf]);
737 }
738
739 /// <summary>
740 /// Settings controlling JSON formatting.
741 /// </summary>
742 public sealed class Settings
743 {
744 private static readonly Settings defaultInstance = new Settings(false);
745
746 /// <summary>
747 /// Default settings, as used by <see cref="JsonFormatter.Default"/>
748 /// </summary>
749 public static Settings Default { get { return defaultInstance; } }
750
751 private readonly bool formatDefaultValues;
752
753
754 /// <summary>
755 /// Whether fields whose values are the default for the field type (e.g. 0 for integers)
756 /// should be formatted (true) or omitted (false).
757 /// </summary>
758 public bool FormatDefaultValues { get { return formatDefaultValues; } }
759
Jon Skeet811fc892015-08-04 15:58:39 +0100760 /// <summary>
761 /// Creates a new <see cref="Settings"/> object with the specified formatting of default values.
762 /// </summary>
763 /// <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 +0100764 public Settings(bool formatDefaultValues)
765 {
766 this.formatDefaultValues = formatDefaultValues;
767 }
768 }
769 }
770}