Resvolution - ShortStringHelperResolver

This commit is contained in:
Stephan
2016-08-24 19:30:33 +02:00
parent 376dec2fed
commit 3707db6c21
19 changed files with 412 additions and 514 deletions

View File

@@ -4,7 +4,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Globalization;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
namespace Umbraco.Core.Strings
@@ -19,30 +18,17 @@ namespace Umbraco.Core.Strings
/// </remarks>
public class DefaultShortStringHelper : IShortStringHelper
{
private readonly IUmbracoSettingsSection _umbracoSettings;
#region Ctor and vars
[Obsolete("Use the other ctor that specifies all dependencies")]
public DefaultShortStringHelper()
public DefaultShortStringHelper(IUmbracoSettingsSection settings)
{
_umbracoSettings = _umbracoSettings;
InitializeLegacyUrlReplaceCharacters();
_config = new DefaultShortStringHelperConfig().WithDefault(settings);
}
public DefaultShortStringHelper(IUmbracoSettingsSection umbracoSettings)
// clones the config so it cannot be changed at runtime
public DefaultShortStringHelper(DefaultShortStringHelperConfig config)
{
_umbracoSettings = umbracoSettings;
InitializeLegacyUrlReplaceCharacters();
}
/// <summary>
/// Freezes the helper so it can prevents its configuration from being modified.
/// </summary>
/// <remarks>Will be called by <c>ShortStringHelperResolver</c> when resolution freezes.</remarks>
public void Freeze()
{
_frozen = true;
_config = config.Clone();
}
// see notes for CleanAsciiString
@@ -50,9 +36,7 @@ namespace Umbraco.Core.Strings
//const string ValidStringCharactersSource = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
//readonly static char[] ValidStringCharacters;
private CultureInfo _defaultCulture = CultureInfo.InvariantCulture;
private bool _frozen;
private readonly Dictionary<CultureInfo, Dictionary<CleanStringType, Config>> _configs = new Dictionary<CultureInfo, Dictionary<CleanStringType, Config>>();
private readonly DefaultShortStringHelperConfig _config;
// see notes for CleanAsciiString
//static DefaultShortStringHelper()
@@ -64,27 +48,6 @@ namespace Umbraco.Core.Strings
#region Filters
private readonly Dictionary<string, string> _urlReplaceCharacters = new Dictionary<string, string>();
private void InitializeLegacyUrlReplaceCharacters()
{
foreach (var node in _umbracoSettings.RequestHandler.CharCollection)
{
if(string.IsNullOrEmpty(node.Char) == false)
_urlReplaceCharacters[node.Char] = node.Replacement;
}
}
/// <summary>
/// Returns a new string in which characters have been replaced according to the Umbraco settings UrlReplaceCharacters.
/// </summary>
/// <param name="s">The string to filter.</param>
/// <returns>The filtered string.</returns>
public string ApplyUrlReplaceCharacters(string s)
{
return s.ReplaceMany(_urlReplaceCharacters);
}
// ok to be static here because it's not configureable in any way
private static readonly char[] InvalidFileNameChars =
Path.GetInvalidFileNameChars()
@@ -97,194 +60,6 @@ namespace Umbraco.Core.Strings
return InvalidFileNameChars.Contains(c) == false;
}
public static string CutMaxLength(string text, int length)
{
return text.Length <= length ? text : text.Substring(0, length);
}
#endregion
#region Configuration
private void EnsureNotFrozen()
{
if (_frozen)
throw new InvalidOperationException("Cannot configure the helper once it is frozen.");
}
/// <summary>
/// Sets a default culture.
/// </summary>
/// <param name="culture">The default culture.</param>
/// <returns>The short string helper.</returns>
public DefaultShortStringHelper WithDefaultCulture(CultureInfo culture)
{
EnsureNotFrozen();
_defaultCulture = culture;
return this;
}
public DefaultShortStringHelper WithConfig(Config config)
{
return WithConfig(_defaultCulture, CleanStringType.RoleMask, config);
}
public DefaultShortStringHelper WithConfig(CleanStringType stringRole, Config config)
{
return WithConfig(_defaultCulture, stringRole, config);
}
public DefaultShortStringHelper WithConfig(CultureInfo culture, CleanStringType stringRole, Config config)
{
if (config == null)
throw new ArgumentNullException("config");
EnsureNotFrozen();
if (_configs.ContainsKey(culture) == false)
_configs[culture] = new Dictionary<CleanStringType, Config>();
_configs[culture][stringRole] = config.Clone(); // clone so it can't be changed
return this;
}
/// <summary>
/// Sets the default configuration.
/// </summary>
/// <returns>The short string helper.</returns>
public DefaultShortStringHelper WithDefaultConfig()
{
return WithConfig(CleanStringType.UrlSegment, new Config
{
PreFilter = ApplyUrlReplaceCharacters,
PostFilter = x => CutMaxLength(x, 240),
IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore
StringType = (_umbracoSettings.RequestHandler.ConvertUrlsToAscii ? CleanStringType.Ascii : CleanStringType.Utf8) | CleanStringType.LowerCase,
BreakTermsOnUpper = false,
Separator = '-'
}).WithConfig(CleanStringType.FileName, new Config
{
PreFilter = ApplyUrlReplaceCharacters,
IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore
StringType = CleanStringType.Utf8 | CleanStringType.LowerCase,
BreakTermsOnUpper = false,
Separator = '-'
}).WithConfig(CleanStringType.Alias, new Config
{
PreFilter = ApplyUrlReplaceCharacters,
IsTerm = (c, leading) => leading
? char.IsLetter(c) // only letters
: (char.IsLetterOrDigit(c) || c == '_'), // letter, digit or underscore
StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase,
BreakTermsOnUpper = false
}).WithConfig(CleanStringType.UnderscoreAlias, new Config
{
PreFilter = ApplyUrlReplaceCharacters,
IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore
StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase,
BreakTermsOnUpper = false
}).WithConfig(CleanStringType.ConvertCase, new Config
{
PreFilter = null,
IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore
StringType = CleanStringType.Ascii,
BreakTermsOnUpper = true
});
}
public sealed class Config
{
public Config()
{
StringType = CleanStringType.Utf8 | CleanStringType.Unchanged;
PreFilter = null;
PostFilter = null;
IsTerm = (c, leading) => leading ? char.IsLetter(c) : char.IsLetterOrDigit(c);
BreakTermsOnUpper = false;
CutAcronymOnNonUpper = false;
GreedyAcronyms = false;
Separator = Char.MinValue;
}
public Config Clone()
{
return new Config
{
PreFilter = PreFilter,
PostFilter = PostFilter,
IsTerm = IsTerm,
StringType = StringType,
BreakTermsOnUpper = BreakTermsOnUpper,
CutAcronymOnNonUpper = CutAcronymOnNonUpper,
GreedyAcronyms = GreedyAcronyms,
Separator = Separator
};
}
public Func<string, string> PreFilter { get; set; }
public Func<string, string> PostFilter { get; set; }
public Func<char, bool, bool> IsTerm { get; set; }
public CleanStringType StringType { get; set; }
// indicate whether an uppercase within a term eg "fooBar" is to break
// into a new term, or to be considered as part of the current term
public bool BreakTermsOnUpper { get; set; }
// indicate whether a non-uppercase within an acronym eg "FOOBar" is to cut
// the acronym (at "B" or "a" depending on GreedyAcronyms) or to give
// up the acronym and treat the term as a word
public bool CutAcronymOnNonUpper { get; set; }
// indicates whether acronyms parsing is greedy ie whether "FOObar" is
// "FOO" + "bar" (greedy) or "FO" + "Obar" (non-greedy)
public bool GreedyAcronyms { get; set; }
// the separator char
// but then how can we tell we dont want any?
public char Separator { get; set; }
// extends the config
public CleanStringType StringTypeExtend(CleanStringType stringType)
{
var st = StringType;
foreach (var mask in new[] { CleanStringType.CaseMask, CleanStringType.CodeMask })
{
var a = stringType & mask;
if (a == 0) continue;
st = st & ~mask; // clear what we have
st = st | a; // set the new value
}
return st;
}
internal static readonly Config NotConfigured = new Config();
}
private Config GetConfig(CleanStringType stringType, CultureInfo culture)
{
stringType = stringType & CleanStringType.RoleMask;
Dictionary<CleanStringType, Config> config;
if (_configs.ContainsKey(culture))
{
config = _configs[culture];
if (config.ContainsKey(stringType)) // have we got a config for _that_ role?
return config[stringType];
if (config.ContainsKey(CleanStringType.RoleMask)) // have we got a generic config for _all_ roles?
return config[CleanStringType.RoleMask];
}
else if (_configs.ContainsKey(_defaultCulture))
{
config = _configs[_defaultCulture];
if (config.ContainsKey(stringType)) // have we got a config for _that_ role?
return config[stringType];
if (config.ContainsKey(CleanStringType.RoleMask)) // have we got a generic config for _all_ roles?
return config[CleanStringType.RoleMask];
}
return Config.NotConfigured;
}
#endregion
#region JavaScript
@@ -333,8 +108,7 @@ function validateSafeAlias(input, value, immediate, callback) {{
/// </summary>
public string GetShortStringServicesJavaScript(string controllerPath)
{
return string.Format(SssjsFormat,
_umbracoSettings.Content.ForceSafeAliases ? "true" : "false", controllerPath);
return string.Format(SssjsFormat, _config.ForceSafeAliases ? "true" : "false", controllerPath);
}
#endregion
@@ -352,7 +126,7 @@ function validateSafeAlias(input, value, immediate, callback) {{
/// </remarks>
public virtual string CleanStringForSafeAlias(string text)
{
return CleanStringForSafeAlias(text, _defaultCulture);
return CleanStringForSafeAlias(text, _config.DefaultCulture);
}
/// <summary>
@@ -380,7 +154,7 @@ function validateSafeAlias(input, value, immediate, callback) {{
/// </remarks>
public virtual string CleanStringForUrlSegment(string text)
{
return CleanStringForUrlSegment(text, _defaultCulture);
return CleanStringForUrlSegment(text, _config.DefaultCulture);
}
/// <summary>
@@ -406,7 +180,7 @@ function validateSafeAlias(input, value, immediate, callback) {{
/// <remarks>Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but that issue is not documented.</remarks>
public virtual string CleanStringForSafeFileName(string text)
{
return CleanStringForSafeFileName(text, _defaultCulture);
return CleanStringForSafeFileName(text, _config.DefaultCulture);
}
/// <summary>
@@ -452,43 +226,43 @@ function validateSafeAlias(input, value, immediate, callback) {{
// - Leading digits are removed.
// - Many consecutive separators are folded into one unique separator.
const byte StateBreak = 1;
const byte StateUp = 2;
const byte StateWord = 3;
const byte StateAcronym = 4;
private const byte StateBreak = 1;
private const byte StateUp = 2;
private const byte StateWord = 3;
private const byte StateAcronym = 4;
/// <summary>
/// Cleans a string.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="stringType">A flag indicating the target casing and encoding of the string. By default,
/// <param name="stringType">A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.</param>
/// <returns>The clean string.</returns>
/// <remarks>The string is cleaned in the context of the default culture.</remarks>
public string CleanString(string text, CleanStringType stringType)
{
return CleanString(text, stringType, _defaultCulture, null);
return CleanString(text, stringType, _config.DefaultCulture, null);
}
/// <summary>
/// Cleans a string, using a specified separator.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="stringType">A flag indicating the target casing and encoding of the string. By default,
/// <param name="stringType">A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.</param>
/// <param name="separator">The separator.</param>
/// <returns>The clean string.</returns>
/// <remarks>The string is cleaned in the context of the default culture.</remarks>
public string CleanString(string text, CleanStringType stringType, char separator)
{
return CleanString(text, stringType, _defaultCulture, separator);
return CleanString(text, stringType, _config.DefaultCulture, separator);
}
/// <summary>
/// Cleans a string in the context of a specified culture.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="stringType">A flag indicating the target casing and encoding of the string. By default,
/// <param name="stringType">A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.</param>
/// <param name="culture">The culture.</param>
/// <returns>The clean string.</returns>
@@ -501,7 +275,7 @@ function validateSafeAlias(input, value, immediate, callback) {{
/// Cleans a string in the context of a specified culture, using a specified separator.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="stringType">A flag indicating the target casing and encoding of the string. By default,
/// <param name="stringType">A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.</param>
/// <param name="separator">The separator.</param>
/// <param name="culture">The culture.</param>
@@ -514,13 +288,11 @@ function validateSafeAlias(input, value, immediate, callback) {{
protected virtual string CleanString(string text, CleanStringType stringType, CultureInfo culture, char? separator)
{
// be safe
if (text == null)
throw new ArgumentNullException("text");
if (culture == null)
throw new ArgumentNullException("culture");
if (text == null) throw new ArgumentNullException(nameof(text));
if (culture == null) throw new ArgumentNullException(nameof(culture));
// get config
var config = GetConfig(stringType, culture);
var config = _config.For(stringType, culture);
stringType = config.StringTypeExtend(stringType);
// apply defaults
@@ -542,8 +314,8 @@ function validateSafeAlias(input, value, immediate, callback) {{
// recode
var codeType = stringType & CleanStringType.CodeMask;
text = codeType == CleanStringType.Ascii
? Utf8ToAsciiConverter.ToAsciiString(text)
text = codeType == CleanStringType.Ascii
? Utf8ToAsciiConverter.ToAsciiString(text)
: RemoveSurrogatePairs(text);
// clean
@@ -552,7 +324,7 @@ function validateSafeAlias(input, value, immediate, callback) {{
// apply post-filter
if (config.PostFilter != null)
text = config.PostFilter(text);
return text;
}
@@ -584,7 +356,7 @@ function validateSafeAlias(input, value, immediate, callback) {{
// that the utf8 version. Micro-optimizing sometimes isn't such a good idea.
// note: does NOT support surrogate pairs in text
internal string CleanCodeString(string text, CleanStringType caseType, char separator, CultureInfo culture, Config config)
internal string CleanCodeString(string text, CleanStringType caseType, char separator, CultureInfo culture, DefaultShortStringHelperConfig.Config config)
{
int opos = 0, ipos = 0;
var state = StateBreak;
@@ -822,12 +594,12 @@ function validateSafeAlias(input, value, immediate, callback) {{
{
term = term.Substring(i);
term.CopyTo(0, output, opos, term.Length);
opos += term.Length;
opos += term.Length;
}
break;
default:
throw new ArgumentOutOfRangeException("caseType");
throw new ArgumentOutOfRangeException(nameof(caseType));
}
}
@@ -847,7 +619,7 @@ function validateSafeAlias(input, value, immediate, callback) {{
{
// be safe
if (text == null)
throw new ArgumentNullException("text");
throw new ArgumentNullException(nameof(text));
var input = text.ToCharArray();
var output = new char[input.Length * 2];
@@ -898,12 +670,10 @@ function validateSafeAlias(input, value, immediate, callback) {{
public virtual string ReplaceMany(string text, IDictionary<string, string> replacements)
{
// be safe
if (text == null)
throw new ArgumentNullException("text");
if (replacements == null)
throw new ArgumentNullException("replacements");
if (text == null) throw new ArgumentNullException(nameof(text));
if (replacements == null) throw new ArgumentNullException(nameof(replacements));
// Have done various tests, implementing my own "super fast" state machine to handle
// Have done various tests, implementing my own "super fast" state machine to handle
// replacement of many items, or via regexes, but on short strings and not too
// many replacements (which prob. is going to be our case) nothing can beat this...
// (at least with safe and checked code -- we don't want unsafe/unchecked here)
@@ -924,10 +694,8 @@ function validateSafeAlias(input, value, immediate, callback) {{
public virtual string ReplaceMany(string text, char[] chars, char replacement)
{
// be safe
if (text == null)
throw new ArgumentNullException("text");
if (chars == null)
throw new ArgumentNullException("chars");
if (text == null) throw new ArgumentNullException(nameof(text));
if (chars == null) throw new ArgumentNullException(nameof(chars));
// see note above