Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save Shell Config SubSections #14490

Merged
merged 23 commits into from
Oct 18, 2023
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
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