From e976c0fa22b65a40858ca996e29c1b6b280aedbe Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Fri, 15 Jul 2016 10:01:02 -0700 Subject: [PATCH] Include all strings from GetAllStrings (#264) --- .../AssemblyResourceStringProvider.cs | 149 ++++++++++++++++++ .../Internal/IResourceStringProvider.cs | 13 ++ .../ResourceManagerStringLocalizer.cs | 87 +++------- ...sourceManagerWithCultureStringLocalizer.cs | 53 ++++++- .../ResourceManagerStringLocalizerTest.cs | 54 ++++++- 5 files changed, 280 insertions(+), 76 deletions(-) create mode 100644 src/Microsoft.Extensions.Localization/Internal/AssemblyResourceStringProvider.cs create mode 100644 src/Microsoft.Extensions.Localization/Internal/IResourceStringProvider.cs diff --git a/src/Microsoft.Extensions.Localization/Internal/AssemblyResourceStringProvider.cs b/src/Microsoft.Extensions.Localization/Internal/AssemblyResourceStringProvider.cs new file mode 100644 index 0000000000..1c57960271 --- /dev/null +++ b/src/Microsoft.Extensions.Localization/Internal/AssemblyResourceStringProvider.cs @@ -0,0 +1,149 @@ +// Copyright (c) .NET Foundation. 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; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Text; + +namespace Microsoft.Extensions.Localization.Internal +{ + public class AssemblyResourceStringProvider : IResourceStringProvider + { + private const string AssemblyElementDelimiter = ", "; + private static readonly string[] _assemblyElementDelimiterArray = new[] { AssemblyElementDelimiter }; + private static readonly char[] _assemblyEqualDelimiter = new[] { '=' }; + + private readonly AssemblyWrapper _assembly; + private readonly string _resourceBaseName; + private readonly IResourceNamesCache _resourceNamesCache; + + public AssemblyResourceStringProvider( + IResourceNamesCache resourceCache, + AssemblyWrapper resourceAssembly, + string resourceBaseName) + { + _resourceNamesCache = resourceCache; + _assembly = resourceAssembly; + _resourceBaseName = resourceBaseName; + } + + private string GetResourceCacheKey(CultureInfo culture) + { + var assemblyName = ApplyCultureToAssembly(culture); + + return $"Assembly={assemblyName};resourceName={_resourceBaseName}"; + } + + private string GetResourceName(CultureInfo culture) + { + var resourceStreamName = _resourceBaseName; + if (!string.IsNullOrEmpty(culture.Name)) + { + resourceStreamName += "." + culture.Name; + } + resourceStreamName += ".resources"; + + return resourceStreamName; + } + + private IList ThrowOrNull(CultureInfo culture, bool throwOnMissing) + { + if (throwOnMissing) + { + throw new MissingManifestResourceException( + Resources.FormatLocalization_MissingManifest(GetResourceName(culture))); + } + + return null; + } + + public IList GetAllResourceStrings(CultureInfo culture, bool throwOnMissing) + { + var cacheKey = GetResourceCacheKey(culture); + return _resourceNamesCache.GetOrAdd(cacheKey, _ => + { + var assembly = GetAssembly(culture); + if (assembly == null) + { + return ThrowOrNull(culture, throwOnMissing); + } + + var resourceStreamName = GetResourceName(culture); + using (var resourceStream = assembly.GetManifestResourceStream(resourceStreamName)) + { + if (resourceStream == null) + { + return ThrowOrNull(culture, throwOnMissing); + } + + using (var resources = new ResourceReader(resourceStream)) + { + var names = new List(); + foreach (DictionaryEntry entry in resources) + { + var resourceName = (string)entry.Key; + names.Add(resourceName); + } + return names; + } + } + }); + } + + protected virtual AssemblyWrapper GetAssembly(CultureInfo culture) + { + var assemblyString = ApplyCultureToAssembly(culture); + Assembly assembly; + try + { + assembly = Assembly.Load(new AssemblyName(assemblyString)); + } + catch (FileNotFoundException) + { + return null; + } + + return new AssemblyWrapper(assembly); + } + + // This is all a workaround for https://github.com/dotnet/coreclr/issues/6123 + private string ApplyCultureToAssembly(CultureInfo culture) + { + var builder = new StringBuilder(_assembly.FullName); + + var cultureName = string.IsNullOrEmpty(culture.Name) ? "neutral" : culture.Name; + var cultureString = $"Culture={cultureName}"; + + var cultureStartIndex = _assembly.FullName.IndexOf("Culture", StringComparison.OrdinalIgnoreCase); + if (cultureStartIndex < 0) + { + builder.Append(AssemblyElementDelimiter + cultureString); + } + else + { + var cultureEndIndex = _assembly.FullName.IndexOf( + AssemblyElementDelimiter, + cultureStartIndex, + StringComparison.Ordinal); + var cultureLength = cultureEndIndex - cultureStartIndex; + builder.Remove(cultureStartIndex, cultureLength); + builder.Insert(cultureStartIndex, cultureString); + } + + var firstSplit = _assembly.FullName.IndexOf(AssemblyElementDelimiter); + if (firstSplit < 0) + { + //Index of end of Assembly name + firstSplit = _assembly.FullName.Length; + } + builder.Insert(firstSplit, ".resources"); + + return builder.ToString(); + } + } +} diff --git a/src/Microsoft.Extensions.Localization/Internal/IResourceStringProvider.cs b/src/Microsoft.Extensions.Localization/Internal/IResourceStringProvider.cs new file mode 100644 index 0000000000..b74bd80eda --- /dev/null +++ b/src/Microsoft.Extensions.Localization/Internal/IResourceStringProvider.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.Extensions.Localization.Internal +{ + public interface IResourceStringProvider + { + IList GetAllResourceStrings(CultureInfo culture, bool throwOnMissing); + } +} diff --git a/src/Microsoft.Extensions.Localization/ResourceManagerStringLocalizer.cs b/src/Microsoft.Extensions.Localization/ResourceManagerStringLocalizer.cs index 80a6af7d6d..cccd45f1a0 100644 --- a/src/Microsoft.Extensions.Localization/ResourceManagerStringLocalizer.cs +++ b/src/Microsoft.Extensions.Localization/ResourceManagerStringLocalizer.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; @@ -13,8 +12,8 @@ using Microsoft.Extensions.Localization.Internal; namespace Microsoft.Extensions.Localization { /// - /// An that uses the and - /// to provide localized strings. + /// An that uses the and + /// to provide localized strings. /// /// This type is thread-safe. public class ResourceManagerStringLocalizer : IStringLocalizer @@ -22,22 +21,29 @@ namespace Microsoft.Extensions.Localization private readonly ConcurrentDictionary _missingManifestCache = new ConcurrentDictionary(); private readonly IResourceNamesCache _resourceNamesCache; private readonly ResourceManager _resourceManager; - private readonly AssemblyWrapper _resourceAssemblyWrapper; + private readonly IResourceStringProvider _resourceStringProvider; private readonly string _resourceBaseName; /// /// Creates a new . /// - /// The to read strings from. + /// The to read strings from. /// The that contains the strings as embedded resources. - /// The base name of the embedded resource in the that contains the strings. + /// The base name of the embedded resource that contains the strings. /// Cache of the list of strings for a given resource assembly name. public ResourceManagerStringLocalizer( ResourceManager resourceManager, Assembly resourceAssembly, string baseName, IResourceNamesCache resourceNamesCache) - : this(resourceManager, new AssemblyWrapper(resourceAssembly), baseName, resourceNamesCache) + : this( + resourceManager, + new AssemblyResourceStringProvider( + resourceNamesCache, + new AssemblyWrapper(resourceAssembly), + baseName), + baseName, + resourceNamesCache) { if (resourceAssembly == null) { @@ -50,7 +56,7 @@ namespace Microsoft.Extensions.Localization /// public ResourceManagerStringLocalizer( ResourceManager resourceManager, - AssemblyWrapper resourceAssemblyWrapper, + IResourceStringProvider resourceStringProvider, string baseName, IResourceNamesCache resourceNamesCache) { @@ -59,9 +65,9 @@ namespace Microsoft.Extensions.Localization throw new ArgumentNullException(nameof(resourceManager)); } - if (resourceAssemblyWrapper == null) + if (resourceStringProvider == null) { - throw new ArgumentNullException(nameof(resourceAssemblyWrapper)); + throw new ArgumentNullException(nameof(resourceStringProvider)); } if (baseName == null) @@ -74,7 +80,7 @@ namespace Microsoft.Extensions.Localization throw new ArgumentNullException(nameof(resourceNamesCache)); } - _resourceAssemblyWrapper = resourceAssemblyWrapper; + _resourceStringProvider = resourceStringProvider; _resourceManager = resourceManager; _resourceBaseName = baseName; _resourceNamesCache = resourceNamesCache; @@ -121,12 +127,12 @@ namespace Microsoft.Extensions.Localization return culture == null ? new ResourceManagerStringLocalizer( _resourceManager, - _resourceAssemblyWrapper.Assembly, + _resourceStringProvider, _resourceBaseName, _resourceNamesCache) : new ResourceManagerWithCultureStringLocalizer( _resourceManager, - _resourceAssemblyWrapper.Assembly, + _resourceStringProvider, _resourceBaseName, _resourceNamesCache, culture); @@ -151,14 +157,7 @@ namespace Microsoft.Extensions.Localization var resourceNames = includeParentCultures ? GetResourceNamesFromCultureHierarchy(culture) - : GetResourceNamesForCulture(culture); - - if (resourceNames == null && !includeParentCultures) - { - var resourceStreamName = GetResourceStreamName(culture); - throw new MissingManifestResourceException( - Resources.FormatLocalization_MissingManifest(resourceStreamName)); - } + : _resourceStringProvider.GetAllResourceStrings(culture, true); foreach (var name in resourceNames) { @@ -209,7 +208,7 @@ namespace Microsoft.Extensions.Localization while (true) { - var cultureResourceNames = GetResourceNamesForCulture(currentCulture); + var cultureResourceNames = _resourceStringProvider.GetAllResourceStrings(currentCulture, false); if (cultureResourceNames != null) { @@ -236,49 +235,5 @@ namespace Microsoft.Extensions.Localization return resourceNames; } - - private string GetResourceStreamName(CultureInfo culture) - { - var resourceStreamName = _resourceBaseName; - if (!string.IsNullOrEmpty(culture.Name)) - { - resourceStreamName += "." + culture.Name; - } - resourceStreamName += ".resources"; - - return resourceStreamName; - } - - private IList GetResourceNamesForCulture(CultureInfo culture) - { - var resourceStreamName = GetResourceStreamName(culture); - - var cacheKey = $"assembly={_resourceAssemblyWrapper.FullName};resourceStreamName={resourceStreamName}"; - - var cultureResourceNames = _resourceNamesCache.GetOrAdd(cacheKey, _ => - { - using (var cultureResourceStream = _resourceAssemblyWrapper.GetManifestResourceStream(resourceStreamName)) - { - if (cultureResourceStream == null) - { - return null; - } - - using (var resources = new ResourceReader(cultureResourceStream)) - { - var names = new List(); - foreach (DictionaryEntry entry in resources) - { - var resourceName = (string)entry.Key; - names.Add(resourceName); - } - return names; - } - } - - }); - - return cultureResourceNames; - } } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Localization/ResourceManagerWithCultureStringLocalizer.cs b/src/Microsoft.Extensions.Localization/ResourceManagerWithCultureStringLocalizer.cs index 507d295577..4ca2ed0748 100644 --- a/src/Microsoft.Extensions.Localization/ResourceManagerWithCultureStringLocalizer.cs +++ b/src/Microsoft.Extensions.Localization/ResourceManagerWithCultureStringLocalizer.cs @@ -6,12 +6,13 @@ using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Resources; +using Microsoft.Extensions.Localization.Internal; namespace Microsoft.Extensions.Localization { /// - /// An that uses the and - /// to provide localized strings for a specific . + /// An that uses the and + /// to provide localized strings for a specific . /// public class ResourceManagerWithCultureStringLocalizer : ResourceManagerStringLocalizer { @@ -20,9 +21,53 @@ namespace Microsoft.Extensions.Localization /// /// Creates a new . /// - /// The to read strings from. + /// The to read strings from. + /// The that can find the resources. + /// The base name of the embedded resource that contains the strings. + /// Cache of the list of strings for a given resource assembly name. + /// The specific to use. + internal ResourceManagerWithCultureStringLocalizer( + ResourceManager resourceManager, + IResourceStringProvider resourceStringProvider, + string baseName, + IResourceNamesCache resourceNamesCache, + CultureInfo culture) + : base(resourceManager, resourceStringProvider, baseName, resourceNamesCache) + { + if (resourceManager == null) + { + throw new ArgumentNullException(nameof(resourceManager)); + } + + if (resourceStringProvider == null) + { + throw new ArgumentNullException(nameof(resourceStringProvider)); + } + + if (baseName == null) + { + throw new ArgumentNullException(nameof(baseName)); + } + + if (resourceNamesCache == null) + { + throw new ArgumentNullException(nameof(resourceNamesCache)); + } + + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); + } + + _culture = culture; + } + + /// + /// Creates a new . + /// + /// The to read strings from. /// The that contains the strings as embedded resources. - /// The base name of the embedded resource in the that contains the strings. + /// The base name of the embedded resource that contains the strings. /// Cache of the list of strings for a given resource assembly name. /// The specific to use. public ResourceManagerWithCultureStringLocalizer( diff --git a/test/Microsoft.Extensions.Localization.Tests/ResourceManagerStringLocalizerTest.cs b/test/Microsoft.Extensions.Localization.Tests/ResourceManagerStringLocalizerTest.cs index 6cae4c48f4..c0ef4b9714 100644 --- a/test/Microsoft.Extensions.Localization.Tests/ResourceManagerStringLocalizerTest.cs +++ b/test/Microsoft.Extensions.Localization.Tests/ResourceManagerStringLocalizerTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. 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.Globalization; using System.IO; using System.Linq; @@ -21,8 +22,15 @@ namespace Microsoft.Extensions.Localization.Tests var baseName = "test"; var resourceAssembly = new TestAssemblyWrapper(); var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly); - var localizer1 = new ResourceManagerStringLocalizer(resourceManager, resourceAssembly, baseName, resourceNamesCache); - var localizer2 = new ResourceManagerStringLocalizer(resourceManager, resourceAssembly, baseName, resourceNamesCache); + var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceAssembly, baseName); + var localizer1 = new ResourceManagerStringLocalizer(resourceManager, + resourceStreamManager, + baseName, + resourceNamesCache); + var localizer2 = new ResourceManagerStringLocalizer(resourceManager, + resourceStreamManager, + baseName, + resourceNamesCache); // Act for (int i = 0; i < 5; i++) @@ -46,8 +54,19 @@ namespace Microsoft.Extensions.Localization.Tests var resourceAssembly2 = new TestAssemblyWrapper("Assembly2"); var resourceManager1 = new TestResourceManager(baseName, resourceAssembly1.Assembly); var resourceManager2 = new TestResourceManager(baseName, resourceAssembly2.Assembly); - var localizer1 = new ResourceManagerStringLocalizer(resourceManager1, resourceAssembly1, baseName, resourceNamesCache); - var localizer2 = new ResourceManagerStringLocalizer(resourceManager2, resourceAssembly2, baseName, resourceNamesCache); + var resourceStreamManager1 = new TestResourceStringProvider(resourceNamesCache, resourceAssembly1, baseName); + var resourceStreamManager2 = new TestResourceStringProvider(resourceNamesCache, resourceAssembly2, baseName); + + var localizer1 = new ResourceManagerStringLocalizer( + resourceManager1, + resourceStreamManager1, + baseName, + resourceNamesCache); + var localizer2 = new ResourceManagerStringLocalizer( + resourceManager2, + resourceStreamManager2, + baseName, + resourceNamesCache); // Act localizer1.GetAllStrings().ToList(); @@ -69,7 +88,12 @@ namespace Microsoft.Extensions.Localization.Tests var resourceNamesCache = new ResourceNamesCache(); var resourceAssembly = new TestAssemblyWrapper(); var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly); - var localizer = new ResourceManagerStringLocalizer(resourceManager, resourceAssembly, baseName, resourceNamesCache); + var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceAssembly, baseName); + var localizer = new ResourceManagerStringLocalizer( + resourceManager, + resourceStreamManager, + baseName, + resourceNamesCache); // Act // We have to access the result so it evaluates. @@ -144,12 +168,30 @@ namespace Microsoft.Extensions.Localization.Tests public TestResourceManager(string baseName, Assembly assembly) : base(baseName, assembly) { - } public override string GetString(string name, CultureInfo culture) => null; } + public class TestResourceStringProvider : AssemblyResourceStringProvider + { + private TestAssemblyWrapper _assemblyWrapper; + + public TestResourceStringProvider( + IResourceNamesCache resourceCache, + TestAssemblyWrapper assemblyWrapper, + string resourceBaseName) + : base(resourceCache, assemblyWrapper, resourceBaseName) + { + _assemblyWrapper = assemblyWrapper; + } + + protected override AssemblyWrapper GetAssembly(CultureInfo culture) + { + return _assemblyWrapper; + } + } + public class TestAssemblyWrapper : AssemblyWrapper { private readonly string _name;