Skip to content

Commit

Permalink
Save Shell Config SubSections (#14490)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtkech authored Oct 18, 2023
1 parent 8dca6ea commit a74b4bd
Show file tree
Hide file tree
Showing 16 changed files with 418 additions and 213 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,19 @@ public static IConfigurationBuilder AddTenantJsonFile(this IConfigurationBuilder
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddTenantJsonFile(this IConfigurationBuilder builder, Action<TenantJsonConfigurationSource>? configureSource)
=> builder.Add(configureSource);

/// <summary>
/// Adds a JSON configuration source to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="stream">The <see cref="Stream"/> to read the json configuration data from.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddTenantJsonStream(this IConfigurationBuilder builder, Stream stream)
{
// ThrowHelper.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(builder);

return builder.Add<TenantJsonStreamConfigurationSource>(s => s.Stream = stream);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.IO;
using System.Text.Json;
using OrchardCore.Environment.Shell.Configuration.Internal;

namespace Microsoft.Extensions.Configuration.Json
{
Expand All @@ -19,18 +19,7 @@ public TenantJsonConfigurationProvider(TenantJsonConfigurationSource source) : b
/// Loads the JSON data from a stream.
/// </summary>
/// <param name="stream">The stream to read.</param>
public override void Load(Stream stream)
{
try
{
Data = JsonConfigurationFileParser.Parse(stream);
}
catch (JsonException e)
{
// throw new FormatException(SR.Error_JSONParseError, e);
throw new FormatException("Could not parse the JSON file.", e);
}
}
public override void Load(Stream stream) => Data = JsonConfigurationParser.Parse(stream);

/// <summary>
/// Dispose the provider.
Expand All @@ -40,8 +29,11 @@ protected override void Dispose(bool disposing)
{
base.Dispose(true);

// OC: Will be part of 'FileConfigurationProvider'.
(Source.FileProvider as IDisposable)?.Dispose();
// OC: Will be part of 'FileConfigurationProvider' in a future version.
// if (Source.OwnsFileProvider)
{
(Source.FileProvider as IDisposable)?.Dispose();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using OrchardCore.Environment.Shell.Configuration.Internal;

namespace Microsoft.Extensions.Configuration.Json
{
/// <summary>
/// Loads configuration key/values from a json stream into a provider.
/// </summary>
public class TenantJsonStreamConfigurationProvider : StreamConfigurationProvider
{
/// <summary>
/// Constructor.
/// </summary>
/// <param name="source">The <see cref="TenantJsonStreamConfigurationSource"/>.</param>
public TenantJsonStreamConfigurationProvider(TenantJsonStreamConfigurationSource source) : base(source) { }

/// <summary>
/// Loads json configuration key/values from a stream into a provider.
/// </summary>
/// <param name="stream">The json <see cref="Stream"/> to load configuration data from.</param>
public override void Load(Stream stream)
{
Data = JsonConfigurationParser.Parse(stream);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Extensions.Configuration.Json
{
/// <summary>
/// Represents a JSON file as an <see cref="IConfigurationSource"/>.
/// </summary>
public class TenantJsonStreamConfigurationSource : StreamConfigurationSource
{
/// <summary>
/// Builds the <see cref="TenantJsonStreamConfigurationProvider"/> for this source.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
/// <returns>An <see cref="TenantJsonStreamConfigurationProvider"/></returns>
public override IConfigurationProvider Build(IConfigurationBuilder builder)
=> new TenantJsonStreamConfigurationProvider(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace OrchardCore.Environment.Shell.Configuration.Internal;

public static class ConfigurationExtensions
{
public static JObject ToJObject(this IConfiguration configuration)
{
var jToken = ToJToken(configuration);
if (jToken is not JObject jObject)
{
throw new FormatException($"Top level JSON element must be an object. Instead, {jToken.Type} was found.");
}

return jObject;
}

public static JToken ToJToken(this IConfiguration configuration)
{
JArray jArray = null;
JObject jObject = null;

foreach (var child in configuration.GetChildren())
{
if (int.TryParse(child.Key, out var index))
{
if (jObject is not null)
{
throw new FormatException($"Can't use the numeric key '{child.Key}' inside an object.");
}

jArray ??= new JArray();
if (index > jArray.Count)
{
// Inserting null values is useful to override arrays items,
// it allows to keep non null items at the right position.
for (var i = jArray.Count; i < index; i++)
{
jArray.Add(JValue.CreateNull());
}
}

if (child.GetChildren().Any())
{
jArray.Add(ToJToken(child));
}
else
{
jArray.Add(child.Value);
}
}
else
{
if (jArray is not null)
{
throw new FormatException($"Can't use the non numeric key '{child.Key}' inside an array.");
}

jObject ??= new JObject();
if (child.GetChildren().Any())
{
jObject.Add(child.Key, ToJToken(child));
}
else
{
jObject.Add(child.Key, child.Value);
}
}
}

return jArray as JToken ?? jObject ?? new JObject();
}

public static JObject ToJObject(this IDictionary<string, string> configurationData)
{
var configuration = new ConfigurationBuilder()
.Add(new UpdatableDataProvider(configurationData))
.Build();

using var disposable = configuration as IDisposable;

return configuration.ToJObject();
}

public static async Task<IDictionary<string, string>> ToConfigurationDataAsync(this JObject jConfiguration)
{
if (jConfiguration is null)
{
return new Dictionary<string, string>();
}

var configurationString = await jConfiguration.ToStringAsync(Formatting.None);
using var ms = new MemoryStream(Encoding.UTF8.GetBytes(configurationString));

return await JsonConfigurationParser.ParseAsync(ms);
}

public static async Task<string> ToStringAsync(this JObject jConfiguration, Formatting formatting = Formatting.Indented)
{
jConfiguration ??= new JObject();

using var sw = new StringWriter(CultureInfo.InvariantCulture);
using var jw = new JsonTextWriter(sw) { Formatting = formatting };

await jConfiguration.WriteToAsync(jw);

return sw.ToString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;

#nullable enable

namespace OrchardCore.Environment.Shell.Configuration.Internal;

public sealed class JsonConfigurationParser
{
private JsonConfigurationParser() { }

private readonly Dictionary<string, string?> _data = new(StringComparer.OrdinalIgnoreCase);
private readonly Stack<string> _paths = new();

public static IDictionary<string, string?> Parse(Stream input)
=> new JsonConfigurationParser().ParseStream(input);

public static Task<IDictionary<string, string?>> ParseAsync(Stream input)
=> new JsonConfigurationParser().ParseStreamAsync(input);

private IDictionary<string, string?> ParseStream(Stream input)
{
var jsonDocumentOptions = new JsonDocumentOptions
{
CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};

try
{
using (var reader = new StreamReader(input))
using (var doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions))
{
if (doc.RootElement.ValueKind != JsonValueKind.Object)
{
throw new FormatException($"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found.");
}

VisitObjectElement(doc.RootElement);
}

return _data;
}
catch (JsonException e)
{
throw new FormatException("Could not parse the JSON document.", e);
}
}

private async Task<IDictionary<string, string?>> ParseStreamAsync(Stream input)
{
var jsonDocumentOptions = new JsonDocumentOptions
{
CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};

try
{
using (var doc = await JsonDocument.ParseAsync(input, jsonDocumentOptions))
{
if (doc.RootElement.ValueKind != JsonValueKind.Object)
{
throw new FormatException($"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found.");
}

VisitObjectElement(doc.RootElement);
}

return _data;
}
catch (JsonException e)
{
throw new FormatException("Could not parse the JSON document.", e);
}
}

private void VisitObjectElement(JsonElement element)
{
var isEmpty = true;

foreach (var property in element.EnumerateObject())
{
isEmpty = false;
EnterContext(property.Name);
VisitValue(property.Value);
ExitContext();
}

SetNullIfElementIsEmpty(isEmpty);
}

private void VisitArrayElement(JsonElement element)
{
var index = 0;

foreach (var arrayElement in element.EnumerateArray())
{
EnterContext(index.ToString());
VisitValue(arrayElement, visitArray: true);
ExitContext();
index++;
}

SetNullIfElementIsEmpty(isEmpty: index == 0);
}

private void SetNullIfElementIsEmpty(bool isEmpty)
{
if (isEmpty && _paths.Count > 0)
{
_data[_paths.Peek()] = null;
}
}

private void VisitValue(JsonElement value, bool visitArray = false)
{
Debug.Assert(_paths.Count > 0);

switch (value.ValueKind)
{
case JsonValueKind.Object:
VisitObjectElement(value);
break;

case JsonValueKind.Array:
VisitArrayElement(value);
break;

case JsonValueKind.Number:
case JsonValueKind.String:
case JsonValueKind.True:
case JsonValueKind.False:
case JsonValueKind.Null:

// Skipping null values is useful to override array items,
// it allows to keep non null items at the right position.
if (visitArray && value.ValueKind == JsonValueKind.Null)
{
break;
}

var key = _paths.Peek();
if (_data.ContainsKey(key))
{
throw new FormatException($"A duplicate key '{key}' was found.");
}
_data[key] = value.ToString();
break;

default:
throw new FormatException($"Unsupported JSON token '{value.ValueKind}' was found.");
}
}

private void EnterContext(string context) =>
_paths.Push(_paths.Count > 0 ?
_paths.Peek() + ConfigurationPath.KeyDelimiter + context :
context);

private void ExitContext() => _paths.Pop();
}
Loading

0 comments on commit a74b4bd

Please sign in to comment.