mirror of https://github.com/abpframework/abp.git
16 changed files with 538 additions and 5 deletions
@ -0,0 +1,30 @@ |
|||
using Microsoft.AspNetCore.Http; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Text.Formatting; |
|||
using Volo.ExtensionMethods; |
|||
|
|||
namespace Volo.Abp.AspNetCore.MultiTenancy |
|||
{ |
|||
public class DomainTenantResolver : HttpTenantResolverBase |
|||
{ |
|||
private readonly string _domainFormat; |
|||
|
|||
public DomainTenantResolver(string domainFormat) |
|||
{ |
|||
_domainFormat = domainFormat.RemovePreFix("http://", "https://"); |
|||
} |
|||
|
|||
protected override string GetTenantIdOrNameFromHttpContextOrNull(ITenantResolveContext context, HttpContext httpContext) |
|||
{ |
|||
var hostName = httpContext.Request.Host.Host.RemovePreFix("http://", "https://"); |
|||
var extractResult = FormattedStringValueExtracter.Extract(hostName, _domainFormat, true); |
|||
|
|||
if (!extractResult.IsMatch) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return extractResult.Matches[0].Value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.AspNetCore.MultiTenancy; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public static class MultiTenancyOptionsExtensions |
|||
{ |
|||
public static void AddDomainTenantResolver(this MultiTenancyOptions options, string domainFormat) |
|||
{ |
|||
options.TenantResolvers.Insert(0, new DomainTenantResolver(domainFormat)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
namespace Volo.Abp |
|||
{ |
|||
/// <summary>
|
|||
/// Can be used to store Name/Value (or Key/Value) pairs.
|
|||
/// </summary>
|
|||
//[Serializable]
|
|||
public class NameValue : NameValue<string> |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new <see cref="NameValue"/>.
|
|||
/// </summary>
|
|||
public NameValue() |
|||
{ |
|||
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="NameValue"/>.
|
|||
/// </summary>
|
|||
public NameValue(string name, string value) |
|||
{ |
|||
Name = name; |
|||
Value = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Can be used to store Name/Value (or Key/Value) pairs.
|
|||
/// </summary>
|
|||
//[Serializable]
|
|||
public class NameValue<T> |
|||
{ |
|||
/// <summary>
|
|||
/// Name.
|
|||
/// </summary>
|
|||
public string Name { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Value.
|
|||
/// </summary>
|
|||
public T Value { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="NameValue"/>.
|
|||
/// </summary>
|
|||
public NameValue() |
|||
{ |
|||
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="NameValue"/>.
|
|||
/// </summary>
|
|||
public NameValue(string name, T value) |
|||
{ |
|||
Name = name; |
|||
Value = value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
namespace Volo.Abp.Text.Formatting |
|||
{ |
|||
internal class FormatStringToken |
|||
{ |
|||
public string Text { get; private set; } |
|||
|
|||
public FormatStringTokenType Type { get; private set; } |
|||
|
|||
public FormatStringToken(string text, FormatStringTokenType type) |
|||
{ |
|||
Text = text; |
|||
Type = type; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.Text.Formatting |
|||
{ |
|||
internal enum FormatStringTokenType |
|||
{ |
|||
ConstantText, |
|||
DynamicValue |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Volo.Abp.Text.Formatting |
|||
{ |
|||
internal class FormatStringTokenizer |
|||
{ |
|||
public List<FormatStringToken> Tokenize(string format, bool includeBracketsForDynamicValues = false) |
|||
{ |
|||
var tokens = new List<FormatStringToken>(); |
|||
|
|||
var currentText = new StringBuilder(); |
|||
var inDynamicValue = false; |
|||
|
|||
for (var i = 0; i < format.Length; i++) |
|||
{ |
|||
var c = format[i]; |
|||
switch (c) |
|||
{ |
|||
case '{': |
|||
if (inDynamicValue) |
|||
{ |
|||
throw new FormatException("Incorrect syntax at char " + i + "! format string can not contain nested dynamic value expression!"); |
|||
} |
|||
|
|||
inDynamicValue = true; |
|||
|
|||
if (currentText.Length > 0) |
|||
{ |
|||
tokens.Add(new FormatStringToken(currentText.ToString(), FormatStringTokenType.ConstantText)); |
|||
currentText.Clear(); |
|||
} |
|||
|
|||
break; |
|||
case '}': |
|||
if (!inDynamicValue) |
|||
{ |
|||
throw new FormatException("Incorrect syntax at char " + i + "! These is no opening brackets for the closing bracket }."); |
|||
} |
|||
|
|||
inDynamicValue = false; |
|||
|
|||
if (currentText.Length <= 0) |
|||
{ |
|||
throw new FormatException("Incorrect syntax at char " + i + "! Brackets does not containt any chars."); |
|||
} |
|||
|
|||
var dynamicValue = currentText.ToString(); |
|||
if (includeBracketsForDynamicValues) |
|||
{ |
|||
dynamicValue = "{" + dynamicValue + "}"; |
|||
} |
|||
|
|||
tokens.Add(new FormatStringToken(dynamicValue, FormatStringTokenType.DynamicValue)); |
|||
currentText.Clear(); |
|||
|
|||
break; |
|||
default: |
|||
currentText.Append(c); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (inDynamicValue) |
|||
{ |
|||
throw new FormatException("There is no closing } char for an opened { char."); |
|||
} |
|||
|
|||
if (currentText.Length > 0) |
|||
{ |
|||
tokens.Add(new FormatStringToken(currentText.ToString(), FormatStringTokenType.ConstantText)); |
|||
} |
|||
|
|||
return tokens; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Linq; |
|||
using Volo.ExtensionMethods.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.Text.Formatting |
|||
{ |
|||
/// <summary>
|
|||
/// This class is used to extract dynamic values from a formatted string.
|
|||
/// It works as reverse of <see cref="string.Format(string,object)"/>
|
|||
/// </summary>
|
|||
/// <example>
|
|||
/// Say that str is "My name is Neo." and format is "My name is {name}.".
|
|||
/// Then Extract method gets "Neo" as "name".
|
|||
/// </example>
|
|||
public class FormattedStringValueExtracter |
|||
{ |
|||
/// <summary>
|
|||
/// Extracts dynamic values from a formatted string.
|
|||
/// </summary>
|
|||
/// <param name="str">String including dynamic values</param>
|
|||
/// <param name="format">Format of the string</param>
|
|||
/// <param name="ignoreCase">True, to search case-insensitive.</param>
|
|||
public static ExtractionResult Extract(string str, string format, bool ignoreCase = false) |
|||
{ |
|||
var stringComparison = ignoreCase |
|||
? StringComparison.OrdinalIgnoreCase |
|||
: StringComparison.Ordinal; |
|||
|
|||
if (str == format) |
|||
{ |
|||
return new ExtractionResult(true); |
|||
} |
|||
|
|||
var formatTokens = new FormatStringTokenizer().Tokenize(format); |
|||
if (formatTokens.IsNullOrEmpty()) |
|||
{ |
|||
return new ExtractionResult(str == ""); |
|||
} |
|||
|
|||
var result = new ExtractionResult(true); |
|||
|
|||
for (var i = 0; i < formatTokens.Count; i++) |
|||
{ |
|||
var currentToken = formatTokens[i]; |
|||
var previousToken = i > 0 ? formatTokens[i - 1] : null; |
|||
|
|||
if (currentToken.Type == FormatStringTokenType.ConstantText) |
|||
{ |
|||
if (i == 0) |
|||
{ |
|||
if (!str.StartsWith(currentToken.Text, stringComparison)) |
|||
{ |
|||
result.IsMatch = false; |
|||
return result; |
|||
} |
|||
|
|||
str = str.Substring(currentToken.Text.Length); |
|||
} |
|||
else |
|||
{ |
|||
var matchIndex = str.IndexOf(currentToken.Text, stringComparison); |
|||
if (matchIndex < 0) |
|||
{ |
|||
result.IsMatch = false; |
|||
return result; |
|||
} |
|||
|
|||
Debug.Assert(previousToken != null, "previousToken can not be null since i > 0 here"); |
|||
|
|||
result.Matches.Add(new NameValue(previousToken.Text, str.Substring(0, matchIndex))); |
|||
str = str.Substring(matchIndex + currentToken.Text.Length); |
|||
} |
|||
} |
|||
} |
|||
|
|||
var lastToken = formatTokens.Last(); |
|||
if (lastToken.Type == FormatStringTokenType.DynamicValue) |
|||
{ |
|||
result.Matches.Add(new NameValue(lastToken.Text, str)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Checks if given <see cref="str"/> fits to given <see cref="format"/>.
|
|||
/// Also gets extracted values.
|
|||
/// </summary>
|
|||
/// <param name="str">String including dynamic values</param>
|
|||
/// <param name="format">Format of the string</param>
|
|||
/// <param name="values">Array of extracted values if matched</param>
|
|||
/// <param name="ignoreCase">True, to search case-insensitive</param>
|
|||
/// <returns>True, if matched.</returns>
|
|||
public static bool IsMatch(string str, string format, out string[] values, bool ignoreCase = false) |
|||
{ |
|||
var result = Extract(str, format, ignoreCase); |
|||
if (!result.IsMatch) |
|||
{ |
|||
values = new string[0]; |
|||
return false; |
|||
} |
|||
|
|||
values = result.Matches.Select(m => m.Value).ToArray(); |
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Used as return value of <see cref="Extract"/> method.
|
|||
/// </summary>
|
|||
public class ExtractionResult |
|||
{ |
|||
/// <summary>
|
|||
/// Is fully matched.
|
|||
/// </summary>
|
|||
public bool IsMatch { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// List of matched dynamic values.
|
|||
/// </summary>
|
|||
public List<NameValue> Matches { get; private set; } |
|||
|
|||
internal ExtractionResult(bool isMatch) |
|||
{ |
|||
IsMatch = isMatch; |
|||
Matches = new List<NameValue>(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
using System; |
|||
using Shouldly; |
|||
using Volo.ExtensionMethods.Collections.Generic; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Text.Formatting |
|||
{ |
|||
public class FormattedStringTokenizer_Test |
|||
{ |
|||
[Fact] |
|||
public void Should_Throw_FormatException_For_Invalid_Format() |
|||
{ |
|||
Assert.Throws<FormatException>(() => new FormatStringTokenizer().Tokenize("a sample { wrong format")); |
|||
Assert.Throws<FormatException>(() => new FormatStringTokenizer().Tokenize("a sample {0{1}} wrong format")); |
|||
Assert.Throws<FormatException>(() => new FormatStringTokenizer().Tokenize("} wrong format")); |
|||
Assert.Throws<FormatException>(() => new FormatStringTokenizer().Tokenize("wrong {} format")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Tokenize_For_Valid_Format() |
|||
{ |
|||
TokenizeTest(""); |
|||
TokenizeTest("a sample {0} value", "a sample ", "{0}", " value"); |
|||
TokenizeTest("{0} is {name} at this {1}.", "{0}", " is ", "{name}", " at this ", "{1}", "."); |
|||
} |
|||
|
|||
private void TokenizeTest(string format, params string[] expectedTokens) |
|||
{ |
|||
var actualTokens = new FormatStringTokenizer().Tokenize(format); |
|||
if (expectedTokens.IsNullOrEmpty()) |
|||
{ |
|||
actualTokens.Count.ShouldBe(0); |
|||
return; |
|||
} |
|||
|
|||
actualTokens.Count.ShouldBe(expectedTokens.Length); |
|||
|
|||
for (var i = 0; i < actualTokens.Count; i++) |
|||
{ |
|||
var actualToken = actualTokens[i]; |
|||
var expectedToken = expectedTokens[i]; |
|||
|
|||
actualToken.Text.ShouldBe(expectedToken.Trim('{', '}')); |
|||
|
|||
if (expectedToken.StartsWith("{") && expectedToken.EndsWith("}")) |
|||
{ |
|||
actualToken.Type.ShouldBe(FormatStringTokenType.DynamicValue); |
|||
} |
|||
else |
|||
{ |
|||
actualToken.Type.ShouldBe(FormatStringTokenType.ConstantText); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Text.Formatting |
|||
{ |
|||
public class FormattedStringValueExtracter_Tests |
|||
{ |
|||
[Fact] |
|||
public void Test_Matched() |
|||
{ |
|||
Test_Matched( |
|||
"My name is Neo.", |
|||
"My name is {0}.", |
|||
new NameValue("0", "Neo") |
|||
); |
|||
|
|||
Test_Matched( |
|||
"User halil does not exist.", |
|||
"User {0} does not exist.", |
|||
new NameValue("0", "halil") |
|||
); |
|||
|
|||
Test_Matched( |
|||
"abp.io", |
|||
"{domain}", |
|||
new NameValue("domain", "abp.io") |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Test_Not_Matched() |
|||
{ |
|||
Test_Not_Matched( |
|||
"My name is Neo.", |
|||
"My name is Marry." |
|||
); |
|||
|
|||
Test_Not_Matched( |
|||
"Role {0} does not exist.", |
|||
"User name {0} is invalid, can only contain letters or digits." |
|||
); |
|||
|
|||
Test_Not_Matched( |
|||
"{0} cannot be null or empty.", |
|||
"Incorrect password." |
|||
); |
|||
|
|||
Test_Not_Matched( |
|||
"Incorrect password.", |
|||
"{0} cannot be null or empty." |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void IsMatch_Test() |
|||
{ |
|||
string[] values; |
|||
FormattedStringValueExtracter.IsMatch("User halil does not exist.", "User {0} does not exist.", out values).ShouldBe(true); |
|||
values[0].ShouldBe("halil"); |
|||
} |
|||
|
|||
private static void Test_Matched(string str, string format, params NameValue[] expectedPairs) |
|||
{ |
|||
var result = FormattedStringValueExtracter.Extract(str, format); |
|||
result.IsMatch.ShouldBe(true); |
|||
|
|||
if (expectedPairs == null) |
|||
{ |
|||
result.Matches.Count.ShouldBe(0); |
|||
return; |
|||
} |
|||
|
|||
result.Matches.Count.ShouldBe(expectedPairs.Length); |
|||
|
|||
for (int i = 0; i < expectedPairs.Length; i++) |
|||
{ |
|||
var actualMatch = result.Matches[i]; |
|||
var expectedPair = expectedPairs[i]; |
|||
|
|||
actualMatch.Name.ShouldBe(expectedPair.Name); |
|||
actualMatch.Value.ShouldBe(expectedPair.Value); |
|||
} |
|||
} |
|||
|
|||
private void Test_Not_Matched(string str, string format) |
|||
{ |
|||
var result = FormattedStringValueExtracter.Extract(str, format); |
|||
result.IsMatch.ShouldBe(false); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue