Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Framework.ConfigurationModel.Json
{
internal class JsonConfigurationFileParser
{
private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private readonly Stack<string> _context = new Stack<string>();
private string _currentPath;

private JsonTextReader _reader;

public IDictionary<string, string> Parse(Stream input)
{
_data.Clear();
_reader = new JsonTextReader(new StreamReader(input));
_reader.DateParseHandling = DateParseHandling.None;

var jsonConfig = JObject.Load(_reader);

VisitJObject(jsonConfig);

return _data;
}

private void VisitJObject(JObject jObject)
{
foreach (var property in jObject.Properties())
{
EnterContext(property.Name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this syntax. Much more clear than my previous approach 👍

VisitProperty(property);
ExitContext();
}
}

private void VisitProperty(JProperty property)
{
VisitToken(property.Value);
}

private void VisitToken(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
VisitJObject(token.Value<JObject>());
break;

case JTokenType.Array:
VisitArray(token.Value<JArray>());
break;

case JTokenType.Integer:
case JTokenType.Float:
case JTokenType.String:
case JTokenType.Boolean:
case JTokenType.Bytes:
case JTokenType.Raw:
case JTokenType.Null:
VisitPrimitive(token);
break;

default:
throw new FormatException(Resources.FormatError_UnsupportedJSONToken(
_reader.TokenType,
_reader.Path,
_reader.LineNumber,
_reader.LinePosition));
}
}

private void VisitArray(JArray array)
{
for (int index = 0; index < array.Count; index++)
{
EnterContext(index.ToString());
VisitToken(array[index]);
ExitContext();
}
}

private void VisitPrimitive(JToken data)
{
var key = _currentPath;

if (_data.ContainsKey(key))
{
throw new FormatException(Resources.FormatError_KeyIsDuplicated(key));
}
_data[key] = data.ToString();
}

private void EnterContext(string context)
{
_context.Push(context);
_currentPath = string.Join(":", _context.Reverse());
}

private void ExitContext()
{
_context.Pop();
_currentPath = string.Join(":", _context.Reverse());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.Framework.ConfigurationModel.Json;
using Newtonsoft.Json;

namespace Microsoft.Framework.ConfigurationModel
{
Expand Down Expand Up @@ -78,129 +77,8 @@ public override void Load()

internal void Load(Stream stream)
{
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

using (var reader = new JsonTextReader(new StreamReader(stream)))
{
var startObjectCount = 0;

// Dates are parsed as strings
reader.DateParseHandling = DateParseHandling.None;

// Move to the first token
reader.Read();

SkipComments(reader);

if (reader.TokenType != JsonToken.StartObject)
{
throw new FormatException(Resources.FormatError_RootMustBeAnObject(reader.Path,
reader.LineNumber, reader.LinePosition));
}

do
{
SkipComments(reader);

switch (reader.TokenType)
{
case JsonToken.StartObject:
startObjectCount++;
break;

case JsonToken.EndObject:
startObjectCount--;
break;

// Keys in key-value pairs
case JsonToken.PropertyName:
break;

// Values in key-value pairs
case JsonToken.Integer:
case JsonToken.Float:
case JsonToken.String:
case JsonToken.Boolean:
case JsonToken.Bytes:
case JsonToken.Raw:
case JsonToken.Null:
var key = GetKey(reader.Path);

if (data.ContainsKey(key))
{
throw new FormatException(Resources.FormatError_KeyIsDuplicated(key));
}
data[key] = reader.Value.ToString();
break;

// End of file
case JsonToken.None:
{
throw new FormatException(Resources.FormatError_UnexpectedEnd(reader.Path,
reader.LineNumber, reader.LinePosition));
}

default:
{
// Unsupported elements: Array, Constructor, Undefined
throw new FormatException(Resources.FormatError_UnsupportedJSONToken(
reader.TokenType, reader.Path, reader.LineNumber, reader.LinePosition));
}
}

reader.Read();

} while (startObjectCount > 0);
}

Data = data;
}

private string GetKey(string jsonPath)
{
var pathSegments = new List<string>();
var index = 0;

while (index < jsonPath.Length)
{
// If the JSON element contains '.' in its name, JSON.net escapes that element as ['element']
// while getting its Path. So before replacing '.' => ':' to represent JSON hierarchy, here
// we skip a '.' => ':' conversion if the element is not enclosed with in ['..'].
var start = jsonPath.IndexOf("['", index);

if (start < 0)
{
// No more ['. Skip till end of string.
pathSegments.Add(jsonPath.
Substring(index).
Replace('.', ':'));
break;
}
else
{
if (start > index)
{
pathSegments.Add(
jsonPath
.Substring(index, start - index) // Anything between the previous [' and '].
.Replace('.', ':'));
}

var endIndex = jsonPath.IndexOf("']", start);
pathSegments.Add(jsonPath.Substring(start + 2, endIndex - start - 2));
index = endIndex + 2;
}
}

return string.Join(string.Empty, pathSegments);
}

private void SkipComments(JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment)
{
reader.Read();
}
JsonConfigurationFileParser parser = new JsonConfigurationFileParser();
Data = parser.Parse(stream);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,6 @@
<data name="Error_KeyIsDuplicated" xml:space="preserve">
<value>A duplicate key '{0}' was found.</value>
</data>
<data name="Error_RootMustBeAnObject" xml:space="preserve">
<value>Only an object can be the root. Path '{0}', line {1} position {2}.</value>
</data>
<data name="Error_UnexpectedEnd" xml:space="preserve">
<value>Unexpected end when parsing JSON. Path '{0}', line {1} position {2}.</value>
</data>
<data name="Error_UnsupportedJSONToken" xml:space="preserve">
<value>Unsupported JSON token '{0}' was found. Path '{1}', line {2} position {3}.</value>
</data>
Expand Down
6 changes: 5 additions & 1 deletion src/Microsoft.Framework.ConfigurationModel.Json/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"frameworks": {
"net45": { },
"dnx451": { },
"dnxcore50": { }
"dnxcore50": {
"dependencies": {
"System.Dynamic.Runtime": "4.0.10-*"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why's this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, json.net requires it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, I should clarify: when I use a particular type from json.net, the compiler complains about that missing reference

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug in Json.NET's nuspec?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to file a bug on this for Json.NET? And a bug for us to track removing this? This doesn't seem right.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will investigate this to see if it is an actual bug

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;

namespace Microsoft.Framework.ConfigurationModel
{
public class ConfigurationKeyComparer : IComparer<string>
{
private const char Separator = ':';

public static ConfigurationKeyComparer Instance { get; } = new ConfigurationKeyComparer();

public int Compare(string x, string y)
{
var xParts = x?.Split(Separator) ?? new string[0];
var yParts = y?.Split(Separator) ?? new string[0];

// Compare each part until we get two parts that are not equal
for (int i = 0; i < Math.Min(xParts.Length, yParts.Length); i++)
{
x = xParts[i];
y = yParts[i];

var value1 = 0;
var value2 = 0;

var xIsInt = x != null && int.TryParse(x, out value1);
var yIsInt = y != null && int.TryParse(y, out value2);

int result = 0;

if (!xIsInt && !yIsInt)
{
// Both are strings
result = string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
}
else if (xIsInt && yIsInt)
{
// Both are int
result = value1 - value2;
}
else
{
// Only one of them is int
result = xIsInt ? -1 : 1;
}

if (result != 0)
{
// One of them is different
return result;
}
}

// If we get here, the common parts are equal.
// If they are of the same length, then they are totally identical
return xParts.Length - yParts.Length;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: InternalsVisibleTo("Microsoft.Framework.ConfigurationModel.Test")]
[assembly: InternalsVisibleTo("Microsoft.Framework.ConfigurationModel.Json.Test")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IVT is to be used only for the unit test assembly of this assembly. Make the type(s) in question public and move to a .Internal namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the PR that fixes this: #188

[assembly: NeutralResourcesLanguage("en-US")]
[assembly: AssemblyMetadata("Serviceable", "True")]
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public virtual IEnumerable<string> ProduceSubKeys(IEnumerable<string> earlierKey
return Data
.Where(kv => kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.Select(kv => Segment(kv.Key, prefix, delimiter))
.Concat(earlierKeys);
.Concat(earlierKeys)
.OrderBy(k => k, ConfigurationKeyComparer.Instance);
}

private static string Segment(string key, string prefix, string delimiter)
Expand Down
Loading