OLD | NEW |
| (Empty) |
1 #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 | |
33 using System; | |
34 using System.Collections; | |
35 using System.Globalization; | |
36 using System.Text; | |
37 using Google.Protobuf.Reflection; | |
38 using Google.Protobuf.WellKnownTypes; | |
39 using System.Linq; | |
40 | |
41 namespace 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 reflectio
n-based, | |
52 /// it's not as quick as baking calls into generated messages - but is a sim
pler 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(Setting
s.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 definitio
n. | |
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 | |
117 /// <summary> | |
118 /// Creates a new formatted with the given settings. | |
119 /// </summary> | |
120 /// <param name="settings">The settings.</param> | |
121 public JsonFormatter(Settings settings) | |
122 { | |
123 this.settings = settings; | |
124 } | |
125 | |
126 /// <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> | |
131 public string Format(IMessage message) | |
132 { | |
133 Preconditions.CheckNotNull(message, "message"); | |
134 StringBuilder builder = new StringBuilder(); | |
135 if (message.Descriptor.IsWellKnownType) | |
136 { | |
137 WriteWellKnownTypeValue(builder, message.Descriptor, message, fa
lse); | |
138 } | |
139 else | |
140 { | |
141 WriteMessage(builder, message); | |
142 } | |
143 return builder.ToString(); | |
144 } | |
145 | |
146 private void WriteMessage(StringBuilder builder, IMessage message) | |
147 { | |
148 if (message == null) | |
149 { | |
150 WriteNull(builder); | |
151 return; | |
152 } | |
153 builder.Append("{ "); | |
154 var fields = message.Descriptor.Fields; | |
155 bool first = true; | |
156 // First non-oneof fields | |
157 foreach (var field in fields.InFieldNumberOrder()) | |
158 { | |
159 var accessor = field.Accessor; | |
160 // Oneofs are written later | |
161 if (field.ContainingOneof != null && field.ContainingOneof.Acces
sor.GetCaseFieldDescriptor(message) != field) | |
162 { | |
163 continue; | |
164 } | |
165 // Omit default values unless we're asked to format them, or the
y're oneofs (where the default | |
166 // value is still formatted regardless, because that's how we pr
eserve the oneof case). | |
167 object value = accessor.GetValue(message); | |
168 if (field.ContainingOneof == null && !settings.FormatDefaultValu
es && IsDefaultValue(accessor, value)) | |
169 { | |
170 continue; | |
171 } | |
172 // Omit awkward (single) values such as unknown enum values | |
173 if (!field.IsRepeated && !field.IsMap && !CanWriteSingleValue(ac
cessor.Descriptor, value)) | |
174 { | |
175 continue; | |
176 } | |
177 | |
178 // Okay, all tests complete: let's write the field value... | |
179 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; | |
187 } | |
188 builder.Append(first ? "}" : " }"); | |
189 } | |
190 | |
191 // Converted from src/google/protobuf/util/internal/utility.cc ToCamelCa
se | |
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 des
criptor, 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.Inv
ariantCulture)); | |
335 break; | |
336 } | |
337 case FieldType.Enum: | |
338 EnumValueDescriptor enumValue = descriptor.EnumType.FindValu
eByNumber((int) value); | |
339 // We will already have validated that this is a known value
. | |
340 WriteString(builder, enumValue.Name); | |
341 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.Inv
ariantCulture)); | |
351 builder.Append('"'); | |
352 break; | |
353 } | |
354 case FieldType.Double: | |
355 case FieldType.Float: | |
356 string text = ((IFormattable) value).ToString("r", CultureIn
fo.InvariantCulture); | |
357 if (text == "NaN" || text == "Infinity" || text == "-Infinit
y") | |
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... | |
370 if (descriptor.MessageType.IsWellKnownType) | |
371 { | |
372 WriteWellKnownTypeValue(builder, descriptor.MessageType,
value, true); | |
373 } | |
374 else | |
375 { | |
376 WriteMessage(builder, (IMessage) value); | |
377 } | |
378 break; | |
379 default: | |
380 throw new ArgumentException("Invalid field type: " + descrip
tor.FieldType); | |
381 } | |
382 } | |
383 | |
384 /// <summary> | |
385 /// Central interception point for well-known type formatting. Any well-
known types which | |
386 /// 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 fo
r dynamic messages | |
388 /// in the future. | |
389 /// </summary> | |
390 private void WriteWellKnownTypeValue(StringBuilder builder, MessageDescr
iptor descriptor, object value, bool inField) | |
391 { | |
392 if (value == null) | |
393 { | |
394 WriteNull(builder); | |
395 return; | |
396 } | |
397 // 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 Valu
e field for the wrapper type. | |
399 if (descriptor.File == Int32Value.Descriptor.File) | |
400 { | |
401 WriteSingleValue(builder, descriptor.FindFieldByNumber(1), value
); | |
402 return; | |
403 } | |
404 if (descriptor.FullName == Timestamp.Descriptor.FullName) | |
405 { | |
406 MaybeWrapInString(builder, value, WriteTimestamp, inField); | |
407 return; | |
408 } | |
409 if (descriptor.FullName == Duration.Descriptor.FullName) | |
410 { | |
411 MaybeWrapInString(builder, value, WriteDuration, inField); | |
412 return; | |
413 } | |
414 if (descriptor.FullName == FieldMask.Descriptor.FullName) | |
415 { | |
416 MaybeWrapInString(builder, value, WriteFieldMask, inField); | |
417 return; | |
418 } | |
419 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.ValuesFieldNumbe
r].Accessor; | |
427 WriteList(builder, fieldAccessor, (IList) fieldAccessor.GetValue
((IMessage) value)); | |
428 return; | |
429 } | |
430 if (descriptor.FullName == Value.Descriptor.FullName) | |
431 { | |
432 WriteStructFieldValue(builder, (IMessage) value); | |
433 return; | |
434 } | |
435 WriteMessage(builder, (IMessage) value); | |
436 } | |
437 | |
438 /// <summary> | |
439 /// Some well-known types end up as string values... so they need wrappi
ng 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, Acti
on<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 Times
tamp 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 Dynam
icMessage 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.SecondsField
Number].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", Cultur
eInfo.InvariantCulture)); | |
471 AppendNanoseconds(builder, Math.Abs(normalized.Nanos)); | |
472 builder.Append('Z'); | |
473 } | |
474 | |
475 private void WriteDuration(StringBuilder builder, IMessage value) | |
476 { | |
477 // TODO: Same as for WriteTimestamp | |
478 int nanos = (int) value.Descriptor.Fields[Duration.NanosFieldNumber]
.Accessor.GetValue(value); | |
479 long seconds = (long) value.Descriptor.Fields[Duration.SecondsFieldN
umber].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.Invarian
tCulture)); | |
492 AppendNanoseconds(builder, Math.Abs(normalized.Nanos)); | |
493 builder.Append('s'); | |
494 } | |
495 | |
496 private void WriteFieldMask(StringBuilder builder, IMessage value) | |
497 { | |
498 IList paths = (IList) value.Descriptor.Fields[FieldMask.PathsFieldNu
mber].Accessor.GetValue(value); | |
499 AppendEscapedString(builder, string.Join(",", paths.Cast<string>().S
elect(ToCamelCase))); | |
500 } | |
501 | |
502 /// <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 { | |
508 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.I
nvariantCulture)); | |
515 } | |
516 else if (nanos % 1000 == 0) | |
517 { | |
518 builder.Append((nanos / 1000).ToString("d", CultureInfo.Inva
riantCulture)); | |
519 } | |
520 else | |
521 { | |
522 builder.Append(nanos.ToString("d", CultureInfo.InvariantCult
ure)); | |
523 } | |
524 } | |
525 } | |
526 | |
527 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 ha
ve 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 messa
ge) | |
554 { | |
555 var specifiedField = message.Descriptor.Oneofs[0].Accessor.GetCaseFi
eldDescriptor(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 w
ell-known types. | |
573 var nestedMessage = (IMessage) specifiedField.Accessor.GetVa
lue(message); | |
574 WriteWellKnownTypeValue(builder, nestedMessage.Descriptor, n
estedMessage, true); | |
575 return; | |
576 case Value.NullValueFieldNumber: | |
577 WriteNull(builder); | |
578 return; | |
579 default: | |
580 throw new InvalidOperationException("Unexpected case in stru
ct field: " + specifiedField.FieldNumber); | |
581 } | |
582 } | |
583 | |
584 private void WriteList(StringBuilder builder, IFieldAccessor accessor, I
List list) | |
585 { | |
586 builder.Append("[ "); | |
587 bool first = true; | |
588 foreach (var value in list) | |
589 { | |
590 if (!CanWriteSingleValue(accessor.Descriptor, value)) | |
591 { | |
592 continue; | |
593 } | |
594 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 acces
sor, IDictionary dictionary) | |
605 { | |
606 builder.Append("{ "); | |
607 bool first = true; | |
608 FieldDescriptor keyType = accessor.Descriptor.MessageType.FindFieldB
yNumber(1); | |
609 FieldDescriptor valueType = accessor.Descriptor.MessageType.FindFiel
dByNumber(2); | |
610 // This will box each pair. Could use IDictionaryEnumerator, but tha
t's ugly in terms of disposal. | |
611 foreach (DictionaryEntry pair in dictionary) | |
612 { | |
613 if (!CanWriteSingleValue(valueType, pair.Value)) | |
614 { | |
615 continue; | |
616 } | |
617 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", Cultur
eInfo.InvariantCulture); | |
641 break; | |
642 default: | |
643 throw new ArgumentException("Invalid key type: " + keyTy
pe.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> | |
654 /// Returns whether or not a singular value can be represented in JSON. | |
655 /// Currently only relevant for enums, where unknown values can't be rep
resented. | |
656 /// For repeated/map fields, this always returns true. | |
657 /// </summary> | |
658 private bool CanWriteSingleValue(FieldDescriptor descriptor, object valu
e) | |
659 { | |
660 if (descriptor.FieldType == FieldType.Enum) | |
661 { | |
662 EnumValueDescriptor enumValue = descriptor.EnumType.FindValueByN
umber((int) value); | |
663 return enumValue != null; | |
664 } | |
665 return true; | |
666 } | |
667 | |
668 /// <summary> | |
669 /// 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 s
rc/google/protobuf/util/internal/json_escaping.cc. | |
673 /// </remarks> | |
674 private void WriteString(StringBuilder builder, string text) | |
675 { | |
676 builder.Append('"'); | |
677 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 { | |
686 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 surroga
te 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 earl
ier | |
740 builder.Append(c); | |
741 } | |
742 break; | |
743 } | |
744 } | |
745 } | |
746 | |
747 private const string Hex = "0123456789abcdef"; | |
748 private static void HexEncodeUtf16CodeUnit(StringBuilder builder, char c
) | |
749 { | |
750 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(fals
e); | |
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 | |
778 /// <summary> | |
779 /// Creates a new <see cref="Settings"/> object with the specified f
ormatting 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> | |
782 public Settings(bool formatDefaultValues) | |
783 { | |
784 this.formatDefaultValues = formatDefaultValues; | |
785 } | |
786 } | |
787 } | |
788 } | |
OLD | NEW |