-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Save Shell Config SubSections (#14490)
- Loading branch information
Showing
16 changed files
with
418 additions
and
213 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
29 changes: 29 additions & 0 deletions
29
...ore.Abstractions/Modules/Overrides/Configuration/TenantJsonStreamConfigurationProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
...dCore.Abstractions/Modules/Overrides/Configuration/TenantJsonStreamConfigurationSource.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
118 changes: 118 additions & 0 deletions
118
...hardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/ConfigurationExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
167 changes: 167 additions & 0 deletions
167
...hardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/JsonConfigurationParser.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
Oops, something went wrong.