/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using ThirdParty.Json.LitJson; using Amazon.Runtime.Internal.Util; namespace Amazon.Runtime.Internal.Transform { /// /// Wraps a json string for unmarshalling. /// /// Each Read() operation gets the next token. /// TestExpression() is used to match the current key-chain /// to an xpath expression. The general pattern looks like this: /// /// JsonUnmarshallerContext context = new JsonUnmarshallerContext(jsonString); /// while (context.Read()) /// { /// if (context.IsKey) /// { /// if (context.TestExpresion("path/to/element")) /// { /// myObject.stringMember = stringUnmarshaller.GetInstance().Unmarshall(context); /// continue; /// } /// } /// } /// /// public class JsonUnmarshallerContext : UnmarshallerContext { private const string DELIMITER = "/"; #region Private members private StreamReader streamReader = null; private JsonReader jsonReader = null; private JsonPathStack stack = new JsonPathStack(); private string currentField; private JsonToken? currentToken = null; private bool disposed = false; private bool wasPeeked = false; #endregion #region Constructors /// /// Wrap the jsonstring for unmarshalling. /// /// Stream that contains the JSON for unmarshalling /// If set to true, maintains a copy of the complete response body constraint to log response size as the stream is being read. /// Response data coming back from the request /// If set to true, maintains a copy of the complete response body as the stream is being read. public JsonUnmarshallerContext( Stream responseStream, bool maintainResponseBody, IWebResponseData responseData, bool isException = false) : this(responseStream, maintainResponseBody, responseData, isException, null) { } /// /// Wrap the jsonstring for unmarshalling. /// /// Stream that contains the JSON for unmarshalling /// If set to true, maintains a copy of the complete response body constraint to log response size as the stream is being read. /// Response data coming back from the request /// If set to true, maintains a copy of the complete response body as the stream is being read. /// Context for the request that produced this response public JsonUnmarshallerContext( Stream responseStream, bool maintainResponseBody, IWebResponseData responseData, bool isException, IRequestContext requestContext) { if (isException) { this.WrappingStream = new CachingWrapperStream(responseStream); } else if (maintainResponseBody) { this.WrappingStream = new CachingWrapperStream(responseStream, AWSConfigs.LoggingConfig.LogResponsesSizeLimit); } if (isException || maintainResponseBody) { responseStream = this.WrappingStream; } this.WebResponseData = responseData; this.MaintainResponseBody = maintainResponseBody; this.IsException = isException; //if the json unmarshaller context is being called internally without there being a http response then the response data would be null if(responseData != null) { long contentLength; bool parsedContentLengthHeader = long.TryParse(responseData.GetHeaderValue("Content-Length"), out contentLength); // Temporary work around checking Content-Encoding for an issue with NetStandard on Linux returning Content-Length for a gzipped response. // Causing the SDK to attempt a CRC check over the gzipped response data with a CRC value for the uncompressed value. // The Content-Encoding check can be removed with the following github issue is shipped. // https://github.com/dotnet/corefx/issues/6796 if (parsedContentLengthHeader && responseData.ContentLength.Equals(contentLength) && string.IsNullOrEmpty(responseData.GetHeaderValue("Content-Encoding"))) { base.SetupCRCStream(responseData, responseStream, contentLength); base.SetupFlexibleChecksumStream(responseData, CrcStream ?? responseStream, contentLength, requestContext); } } if (this.FlexibleChecksumStream != null) // either just flexible checksum, or flexible checksum wrapping the older CRC stream streamReader = new StreamReader(this.FlexibleChecksumStream); else if (this.CrcStream != null) streamReader = new StreamReader(this.CrcStream); else streamReader = new StreamReader(responseStream); jsonReader = new JsonReader(streamReader); } #endregion #region Overrides /// /// Are we at the start of the json document. /// public override bool IsStartOfDocument { get { return (CurrentTokenType == JsonToken.None) && (!streamReader.EndOfStream); } } /// /// Is the current token the end of an object /// public override bool IsEndElement { get { return CurrentTokenType == JsonToken.ObjectEnd; } } /// /// Is the current token the start of an object /// public override bool IsStartElement { get { return CurrentTokenType == JsonToken.ObjectStart; } } /// /// Returns the element depth of the parser's current position in the json /// document being parsed. /// public override int CurrentDepth { get { return this.stack.CurrentDepth; } } /// /// The current Json path that is being unmarshalled. /// public override string CurrentPath { get { return this.stack.CurrentPath; } } /// /// Reads to the next token in the json document, and updates the context /// accordingly. /// /// /// True if a token was read, false if there are no more tokens to read. /// public override bool Read() { if (wasPeeked) { wasPeeked = false; return currentToken == null; } bool result = jsonReader.Read(); if (result) { currentToken = jsonReader.Token; UpdateContext(); } else { currentToken = null; } wasPeeked = false; return result; } /// /// Peeks at the next token. This peek implementation /// reads the next token and makes the subsequent Read() return the same data. /// If Peek is called successively, it will return the same data. /// Only the first one calls Read(), subsequent calls /// will return the same data until a Read() call is made. /// /// Token to peek. /// Returns true if the peeked token matches given token. public bool Peek(JsonToken token) { if (wasPeeked) return currentToken != null && currentToken == token; if (Read()) { wasPeeked = true; return currentToken == token; } return false; } /// /// Returns the text contents of the current token being parsed. /// /// /// The text contents of the current token being parsed. /// public override string ReadText() { object data = jsonReader.Value; string text; switch (currentToken) { case JsonToken.Null: text = null; break; case JsonToken.String: case JsonToken.PropertyName: text = data as string; break; case JsonToken.Boolean: case JsonToken.Int: case JsonToken.UInt: case JsonToken.Long: case JsonToken.ULong: IFormattable iformattable = data as IFormattable; if (iformattable != null) text = iformattable.ToString(null, CultureInfo.InvariantCulture); else text = data.ToString(); break; case JsonToken.Double: var formattable = data as IFormattable; if (formattable != null) text = formattable.ToString("R", CultureInfo.InvariantCulture); else text = data.ToString(); break; default: throw new AmazonClientException( "We expected a VALUE token but got: " + currentToken); } return text; } #endregion #region Public properties /// /// The type of the current token /// public JsonToken CurrentTokenType { get { return currentToken.Value; } } #endregion #region Internal methods/properties /// /// Get the base stream of the jsonStream. /// public Stream Stream { get { return streamReader.BaseStream; } } /// /// Peeks at the next (non-whitespace) character in the jsonStream. /// /// The next (non-whitespace) character in the jsonStream, or -1 if at the end. public int Peek() { // Per MSDN documentation on StreamReader.Peek(), it's perfectly acceptable to cast // int returned by Peek() to char. unchecked { while (Char.IsWhiteSpace((char) StreamPeek())) { streamReader.Read(); } } return StreamPeek(); } #endregion #region Private methods /// /// Peeks at the next character in the stream. /// If the data isn't buffered into the StreamReader (Peek() returns -1), /// we flush the buffered data and try one more time. /// /// private int StreamPeek() { int peek = streamReader.Peek(); if (peek == -1) { streamReader.DiscardBufferedData(); peek = streamReader.Peek(); } return peek; } private void UpdateContext() { if (!currentToken.HasValue) return; if (currentToken.Value == JsonToken.ObjectStart || currentToken.Value == JsonToken.ArrayStart) { // Push '/' for object start and array start. stack.Push(new PathSegment { SegmentType = PathSegmentType.Delimiter, Value = DELIMITER }); } else if (currentToken.Value == JsonToken.ObjectEnd || currentToken.Value == JsonToken.ArrayEnd) { if (stack.Peek().SegmentType == PathSegmentType.Delimiter) { // Pop '/' associated with corresponding object start and array start. stack.Pop(); if (stack.Count > 0 && stack.Peek().SegmentType != PathSegmentType.Delimiter) { // Pop the property name associated with the // object or array if present. // e.g. {"a":["1","2","3"]} stack.Pop(); } } currentField = null; } else if (currentToken.Value == JsonToken.PropertyName) { string t = ReadText(); // Push property name, it's appended to the stack's CurrentPath, // it this does not affect the depth. stack.Push(new PathSegment { SegmentType = PathSegmentType.Value, Value = t }); } else if (currentToken.Value != JsonToken.None && stack.Peek().SegmentType != PathSegmentType.Delimiter) { // Pop if you encounter a simple data type or null // This will pop the property name associated with it in cases like {"a":"b"}. // Exclude the case where it's a value in an array so we dont end poping the start of array and // property name e.g. {"a":["1","2","3"]} stack.Pop(); } } #endregion public JsonData ToJsonData() { var data = JsonMapper.ToObject(jsonReader); if (stack.Count > 0) stack.Pop(); return data; } protected override void Dispose(bool disposing) { if (!disposed) { if (disposing) { if (streamReader != null) { streamReader.Dispose(); streamReader = null; } } disposed = true; } base.Dispose(disposing); } private enum PathSegmentType { Value, Delimiter } private struct PathSegment { internal PathSegmentType SegmentType { get; set; } internal string Value { get; set; } } private class JsonPathStack { private Stack stack = new Stack(); int currentDepth = 0; private StringBuilder stackStringBuilder = new StringBuilder(128); private string stackString; public int CurrentDepth { get { return this.currentDepth; } } public string CurrentPath { get { if (this.stackString == null) this.stackString = this.stackStringBuilder.ToString(); return this.stackString; } } internal void Push(PathSegment segment) { if (segment.SegmentType == PathSegmentType.Delimiter) { currentDepth++; } stackStringBuilder.Append(segment.Value); stackString = null; stack.Push(segment); } internal PathSegment Pop() { var segment = this.stack.Pop(); if (segment.SegmentType == PathSegmentType.Delimiter) { currentDepth--; } stackStringBuilder.Remove(stackStringBuilder.Length - segment.Value.Length, segment.Value.Length); stackString = null; return segment; } internal PathSegment Peek() { return this.stack.Peek(); } public int Count { get { return this.stack.Count; } } } } }