|
@ -50,9 +50,19 @@ namespace Apewer |
|
|
|
|
|
|
|
|
#endregion
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
#region 构造。
|
|
|
#region 消息
|
|
|
|
|
|
|
|
|
|
|
|
private const string ValueIsNotSupported = "指定的值类型不受支持"; |
|
|
|
|
|
private const string IsNotJsonObject = "当前实例不是 Json 对象。"; |
|
|
|
|
|
private const string IsNotJsonArray = "当前实例不是 Json 数组。"; |
|
|
|
|
|
private const string InvalidIndex = "未指定有效的索引。"; |
|
|
|
|
|
private const string IndexLessZero = "指定的索引小于 0,无效。"; |
|
|
|
|
|
private const string IndexGraterMax = "指定的索引超出了最大值,无效。"; |
|
|
|
|
|
private const string IndexGraterCount = "指定的索引超出了数量,无效。"; |
|
|
|
|
|
|
|
|
#region 基础。
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region JToken
|
|
|
|
|
|
|
|
|
[NonSerialized] |
|
|
[NonSerialized] |
|
|
private JToken _jtoken = null; |
|
|
private JToken _jtoken = null; |
|
@ -69,8 +79,57 @@ namespace Apewer |
|
|
[NonSerialized] |
|
|
[NonSerialized] |
|
|
private JValue _jvalue = null; |
|
|
private JValue _jvalue = null; |
|
|
|
|
|
|
|
|
|
|
|
static JToken ToJToken(object value) |
|
|
|
|
|
{ |
|
|
|
|
|
if (value == null) return new JValue(null, JTokenType.Null); |
|
|
|
|
|
|
|
|
|
|
|
var type = value.GetType(); |
|
|
|
|
|
if (RuntimeUtility.IsNullableType(type)) value = RuntimeUtility.GetNullableValue(value); |
|
|
|
|
|
|
|
|
|
|
|
if (value == null) return new JValue(null, JTokenType.Null); |
|
|
|
|
|
if (value is Json json) return json?._jtoken ?? new JValue(null, JTokenType.Null); |
|
|
|
|
|
if (value is DateTime dt) return new JValue(SerializeDateTime(dt)); |
|
|
|
|
|
|
|
|
|
|
|
if (value is string) return new JValue(value); |
|
|
|
|
|
if (value is bool) return new JValue(value); |
|
|
|
|
|
|
|
|
|
|
|
if (value is byte) return new JValue(value); |
|
|
|
|
|
if (value is sbyte) return new JValue(value); |
|
|
|
|
|
if (value is short) return new JValue(value); |
|
|
|
|
|
if (value is ushort) return new JValue(value); |
|
|
|
|
|
if (value is int) return new JValue(value); |
|
|
|
|
|
if (value is uint) return new JValue(value); |
|
|
|
|
|
if (value is long) return new JValue(value); |
|
|
|
|
|
if (value is ulong uint64) return (uint64 > int.MaxValue) ? new JValue(uint64.ToString()) : new JValue(Convert.ToInt64(uint64)); |
|
|
|
|
|
|
|
|
|
|
|
if (value is float) return new JValue(value); |
|
|
|
|
|
if (value is double) return new JValue(value); |
|
|
|
|
|
if (value is decimal) return new JValue(value); |
|
|
|
|
|
|
|
|
|
|
|
var from = From(value); |
|
|
|
|
|
if (from != null) return from._jtoken; |
|
|
|
|
|
throw new ArgumentException(ValueIsNotSupported); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
static object ParseJToken(JToken jtoken) |
|
|
|
|
|
{ |
|
|
|
|
|
if (jtoken == null) return null; |
|
|
|
|
|
if (jtoken is JValue jvalue) return jvalue.Value; |
|
|
|
|
|
if (jtoken is JObject) return new Json(jtoken); |
|
|
|
|
|
if (jtoken is JArray) return new Json(jtoken); |
|
|
|
|
|
if (jtoken is JProperty jproperty) |
|
|
|
|
|
{ |
|
|
|
|
|
var value = jproperty.Value; |
|
|
|
|
|
if (value == null) return null; |
|
|
|
|
|
return ParseJToken(value); |
|
|
|
|
|
} |
|
|
|
|
|
throw new InvalidOperationException($"Json 类型 {jtoken.Type} 不支持解析。"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
#endregion
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 构造。
|
|
|
|
|
|
|
|
|
#region Reset
|
|
|
#region Reset
|
|
|
|
|
|
|
|
|
/// <summary>重置当前对象为空。</summary>
|
|
|
/// <summary>重置当前对象为空。</summary>
|
|
@ -190,19 +249,10 @@ namespace Apewer |
|
|
#region 属性。
|
|
|
#region 属性。
|
|
|
|
|
|
|
|
|
/// <summary>用于兼容 SimpleJson 的操作。</summary>
|
|
|
/// <summary>用于兼容 SimpleJson 的操作。</summary>
|
|
|
public string this[string name] |
|
|
public string this[string name] { get => GetProperty(name)?.ToString() ?? ""; set => SetProperty(name, value ?? ""); } |
|
|
{ |
|
|
|
|
|
get |
|
|
/// <summary>获取或设置数组的元素。</summary>
|
|
|
{ |
|
|
public object this[int index] { get => GetItem(index); set => SetItem(index, value); } |
|
|
var property = GetProperty(name); |
|
|
|
|
|
if (property == null) return ""; |
|
|
|
|
|
return property.ToString(); |
|
|
|
|
|
} |
|
|
|
|
|
set |
|
|
|
|
|
{ |
|
|
|
|
|
SetProperty(name, value ?? ""); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private JTokenType TokenType |
|
|
private JTokenType TokenType |
|
|
{ |
|
|
{ |
|
@ -244,9 +294,7 @@ namespace Apewer |
|
|
public string Type { get { return TokenType.ToString(); } } |
|
|
public string Type { get { return TokenType.ToString(); } } |
|
|
|
|
|
|
|
|
/// <summary>实例有效。</summary>
|
|
|
/// <summary>实例有效。</summary>
|
|
|
public |
|
|
public bool Available { get { return _jtoken != null && TokenType != JTokenType.None; } } |
|
|
bool Available |
|
|
|
|
|
{ get { return _jtoken != null && TokenType != JTokenType.None; } } |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>获取当前实例的值,当为 Json 格式时缩进。</summary>
|
|
|
/// <summary>获取当前实例的值,当为 Json 格式时缩进。</summary>
|
|
|
public string Lucid { get { return ToString(true); } } |
|
|
public string Lucid { get { return ToString(true); } } |
|
@ -921,6 +969,37 @@ namespace Apewer |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>获取数组的元素。</summary>
|
|
|
|
|
|
/// <exception cref="ArgumentOutOfRangeException" />
|
|
|
|
|
|
/// <exception cref="InvalidOperationException" />
|
|
|
|
|
|
public object GetItem(int index) |
|
|
|
|
|
{ |
|
|
|
|
|
if (!IsArray) throw new InvalidOperationException(IsNotJsonArray); |
|
|
|
|
|
|
|
|
|
|
|
if (index < 0) throw new ArgumentOutOfRangeException(nameof(index), IndexLessZero); |
|
|
|
|
|
var jarray = _jtoken as JArray; |
|
|
|
|
|
if (index >= jarray.Count) throw new ArgumentOutOfRangeException(nameof(index), IndexGraterCount); |
|
|
|
|
|
|
|
|
|
|
|
var item = jarray[index]; |
|
|
|
|
|
return ParseJToken(item); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>设置数组的元素。</summary>
|
|
|
|
|
|
/// <exception cref="ArgumentException" />
|
|
|
|
|
|
/// <exception cref="ArgumentOutOfRangeException" />
|
|
|
|
|
|
/// <exception cref="InvalidOperationException" />
|
|
|
|
|
|
public void SetItem(int index, object value) |
|
|
|
|
|
{ |
|
|
|
|
|
if (!IsArray) throw new InvalidOperationException(IsNotJsonArray); |
|
|
|
|
|
|
|
|
|
|
|
if (index < 0) throw new ArgumentOutOfRangeException(nameof(index), IndexLessZero); |
|
|
|
|
|
var jarray = _jtoken as JArray; |
|
|
|
|
|
while (jarray.Count < index + 1) jarray.Add(null); |
|
|
|
|
|
|
|
|
|
|
|
var jtoken = ToJToken(value); |
|
|
|
|
|
jarray.SetItem(index, jtoken); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
#endregion
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
#region Property
|
|
|
#region Property
|
|
@ -1817,13 +1896,17 @@ namespace Apewer |
|
|
{ |
|
|
{ |
|
|
if (pt.Equals(typeof(DateTime))) |
|
|
if (pt.Equals(typeof(DateTime))) |
|
|
{ |
|
|
{ |
|
|
try |
|
|
if (AllowException) |
|
|
{ |
|
|
{ |
|
|
setter.Invoke(entity, new object[] { DeserializeDateTime(value as string) }); |
|
|
setter.Invoke(entity, new object[] { DeserializeDateTime(value) }); |
|
|
} |
|
|
} |
|
|
catch (Exception exception) |
|
|
else |
|
|
{ |
|
|
{ |
|
|
if (AllowException) throw exception; |
|
|
try |
|
|
|
|
|
{ |
|
|
|
|
|
setter.Invoke(entity, new object[] { DeserializeDateTime(value) }); |
|
|
|
|
|
} |
|
|
|
|
|
catch { } |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
else if (pt.Equals(typeof(string))) |
|
|
else if (pt.Equals(typeof(string))) |
|
@ -1888,39 +1971,202 @@ namespace Apewer |
|
|
/// <summary></summary>
|
|
|
/// <summary></summary>
|
|
|
public override bool TryGetMember(GetMemberBinder binder, out object result) |
|
|
public override bool TryGetMember(GetMemberBinder binder, out object result) |
|
|
{ |
|
|
{ |
|
|
var contains = false; |
|
|
if (!IsObject) throw new InvalidOperationException(IsNotJsonObject); |
|
|
if (IsObject) |
|
|
|
|
|
|
|
|
var contains = _jobject.ContainsKey(binder.Name); |
|
|
|
|
|
if (!contains) |
|
|
{ |
|
|
{ |
|
|
var property = GetProperty(binder.Name); |
|
|
result = null; |
|
|
contains = property != null; |
|
|
return true; |
|
|
result = contains ? property.Value : null; |
|
|
|
|
|
return contains; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
result = null; |
|
|
var jtoken = _jobject.GetValue(binder.Name); |
|
|
return contains; |
|
|
result = ParseJToken(jtoken); |
|
|
|
|
|
return true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// <summary></summary>
|
|
|
/// <summary></summary>
|
|
|
public override bool TrySetMember(SetMemberBinder binder, object value) |
|
|
public override bool TrySetMember(SetMemberBinder binder, object value) |
|
|
{ |
|
|
{ |
|
|
var name = binder.Name; |
|
|
if (!IsObject) throw new InvalidOperationException(IsNotJsonObject); |
|
|
|
|
|
|
|
|
if (value == null) return SetProperty(name); |
|
|
|
|
|
if (value is Json) return SetProperty(name, (Json)value); |
|
|
|
|
|
if (value is string) return SetProperty(name, (string)value); |
|
|
|
|
|
if (value is bool) return SetProperty(name, (bool)value); |
|
|
|
|
|
if (value is byte) return SetProperty(name, (byte)value); |
|
|
|
|
|
if (value is sbyte) return SetProperty(name, (sbyte)value); |
|
|
|
|
|
if (value is short) return SetProperty(name, (short)value); |
|
|
|
|
|
if (value is ushort) return SetProperty(name, (ushort)value); |
|
|
|
|
|
if (value is int) return SetProperty(name, (int)value); |
|
|
|
|
|
if (value is uint) return SetProperty(name, (uint)value); |
|
|
|
|
|
if (value is long) return SetProperty(name, (long)value); |
|
|
|
|
|
if (value is float) return SetProperty(name, (float)value); |
|
|
|
|
|
if (value is double) return SetProperty(name, (double)value); |
|
|
|
|
|
if (value is decimal) return SetProperty(name, (decimal)value); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var contains = _jobject.ContainsKey(binder.Name); |
|
|
|
|
|
if (contains) |
|
|
|
|
|
{ |
|
|
|
|
|
_jobject[binder.Name] = ToJToken(value); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
_jobject.Add(binder.Name, ToJToken(value)); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Class<T> DynamicIndex<T>(object[] indexes, Func<string, T> stringCallback, Func<int, T> intCallback) |
|
|
|
|
|
{ |
|
|
|
|
|
var index = indexes[0]; |
|
|
|
|
|
if (index != null) |
|
|
|
|
|
{ |
|
|
|
|
|
if (IsObject) |
|
|
|
|
|
{ |
|
|
|
|
|
if (index is string name) return new Class<T>(stringCallback.Invoke(name)); |
|
|
|
|
|
} |
|
|
|
|
|
if (IsArray) |
|
|
|
|
|
{ |
|
|
|
|
|
if (index is int || index is short || index is byte || index is ushort || index is sbyte) |
|
|
|
|
|
{ |
|
|
|
|
|
var int32 = int.Parse(index.ToString()); |
|
|
|
|
|
if (int32 < 0) throw new ArgumentOutOfRangeException("index", IndexLessZero); |
|
|
|
|
|
return new Class<T>(intCallback(int32)); |
|
|
|
|
|
} |
|
|
|
|
|
else if (index is long int64) |
|
|
|
|
|
{ |
|
|
|
|
|
if (int64 < 0) throw new ArgumentOutOfRangeException("index", IndexLessZero); |
|
|
|
|
|
if (int64 > int.MaxValue) throw new ArgumentOutOfRangeException("index", IndexGraterMax); |
|
|
|
|
|
var int32 = Convert.ToInt32(int64); |
|
|
|
|
|
return new Class<T>(intCallback(int32)); |
|
|
|
|
|
} |
|
|
|
|
|
else if (index is uint uint32) |
|
|
|
|
|
{ |
|
|
|
|
|
if (uint32 > int.MaxValue) throw new ArgumentOutOfRangeException("index", IndexGraterMax); |
|
|
|
|
|
var int32 = Convert.ToInt32(uint32); |
|
|
|
|
|
return new Class<T>(intCallback(int32)); |
|
|
|
|
|
} |
|
|
|
|
|
else if (index is ulong uint64) |
|
|
|
|
|
{ |
|
|
|
|
|
if (uint64 > int.MaxValue) throw new ArgumentOutOfRangeException("index", IndexGraterMax); |
|
|
|
|
|
var int32 = Convert.ToInt32(uint64); |
|
|
|
|
|
return new Class<T>(intCallback(int32)); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
throw new InvalidOperationException(InvalidIndex); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary></summary>
|
|
|
|
|
|
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) |
|
|
|
|
|
{ |
|
|
|
|
|
var box = DynamicIndex(indexes, name => |
|
|
|
|
|
{ |
|
|
|
|
|
var property = GetProperty(name); |
|
|
|
|
|
return ParseJToken(property?._jtoken); |
|
|
|
|
|
}, index => |
|
|
|
|
|
{ |
|
|
|
|
|
return GetItem(index); |
|
|
|
|
|
}); |
|
|
|
|
|
if (box == null) |
|
|
|
|
|
{ |
|
|
|
|
|
result = null; |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
result = box.Value; |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary></summary>
|
|
|
|
|
|
public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) |
|
|
|
|
|
{ |
|
|
|
|
|
var box = DynamicIndex(indexes, name => |
|
|
|
|
|
{ |
|
|
|
|
|
_jobject[name] = ToJToken(value); |
|
|
|
|
|
return true; |
|
|
|
|
|
}, index => |
|
|
|
|
|
{ |
|
|
|
|
|
SetItem(index, value); |
|
|
|
|
|
return true; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
return box != null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary></summary>
|
|
|
|
|
|
public override bool TryInvoke(InvokeBinder binder, object[] args, out object result) |
|
|
|
|
|
{ |
|
|
|
|
|
switch (binder.CallInfo.ArgumentNames.First().Lower()) |
|
|
|
|
|
{ |
|
|
|
|
|
case "tostring": |
|
|
|
|
|
if (args == null) |
|
|
|
|
|
{ |
|
|
|
|
|
result = ToString(); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
switch (args.Length) |
|
|
|
|
|
{ |
|
|
|
|
|
case 0: |
|
|
|
|
|
result = ToString(); |
|
|
|
|
|
return true; |
|
|
|
|
|
case 1: |
|
|
|
|
|
if (args[0] == null) |
|
|
|
|
|
{ |
|
|
|
|
|
result = null; |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
if (args[0] is bool indented) |
|
|
|
|
|
{ |
|
|
|
|
|
result = ToString(indented); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
else if (args[0] is byte int8) |
|
|
|
|
|
{ |
|
|
|
|
|
result = ToString(int8 == 1); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
else if (args[0] is short int16) |
|
|
|
|
|
{ |
|
|
|
|
|
result = ToString(int16 == 1); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
else if (args[0] is int int32) |
|
|
|
|
|
{ |
|
|
|
|
|
result = ToString(int32 == 1); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
else if (args[0] is long int64) |
|
|
|
|
|
{ |
|
|
|
|
|
result = ToString(int64 == 1); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
result = null; |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
default: |
|
|
|
|
|
result = null; |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
default: |
|
|
|
|
|
result = null; |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary></summary>
|
|
|
|
|
|
public override bool TryConvert(ConvertBinder binder, out object result) |
|
|
|
|
|
{ |
|
|
|
|
|
if (binder.Type.Equals(typeof(Json))) |
|
|
|
|
|
{ |
|
|
|
|
|
result = this; |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
if (binder.Type.Equals(typeof(string))) |
|
|
|
|
|
{ |
|
|
|
|
|
result = ToString(); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
result = null; |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@ -2247,29 +2493,40 @@ namespace Apewer |
|
|
|
|
|
|
|
|
#region DateTime
|
|
|
#region DateTime
|
|
|
|
|
|
|
|
|
|
|
|
static Func<DateTime, string> _datetime_serializer = null; |
|
|
|
|
|
static Func<object, DateTime> _datetime_deserializer = null; |
|
|
|
|
|
|
|
|
/// <summary>自定义 DateTime 序列化程序。</summary>
|
|
|
/// <summary>自定义 DateTime 序列化程序。</summary>
|
|
|
public static Func<DateTime, string> DateTimeSerializer { get; set; } |
|
|
public static Func<DateTime, string> DateTimeSerializer { get => _datetime_serializer; set => _datetime_serializer = value; } |
|
|
|
|
|
|
|
|
/// <summary>自定义 DateTime 反序列化程序。</summary>
|
|
|
/// <summary>自定义 DateTime 反序列化程序。</summary>
|
|
|
public static Func<string, DateTime> DateTimeDeserializer { get; set; } |
|
|
public static Func<object, DateTime> DateTimeDeserializer { get => _datetime_deserializer; set => _datetime_deserializer = value; } |
|
|
|
|
|
|
|
|
/// <summary>序列化 DateTime 实例。</summary>
|
|
|
/// <summary>序列化 DateTime 实例。</summary>
|
|
|
public static string SerializeDateTime(DateTime dateTime) |
|
|
public static string SerializeDateTime(DateTime dateTime) |
|
|
{ |
|
|
{ |
|
|
var serializer = DateTimeSerializer; |
|
|
var serializer = _datetime_serializer; |
|
|
if (serializer != null) return serializer.Invoke(dateTime); |
|
|
if (serializer != null) return serializer.Invoke(dateTime); |
|
|
|
|
|
|
|
|
return ClockUtility.Lucid(dateTime); |
|
|
return ClockUtility.Lucid(dateTime); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// <summary>序列化 DateTime 实例。</summary>
|
|
|
/// <summary>序列化 DateTime 实例。</summary>
|
|
|
public static DateTime DeserializeDateTime(string text) |
|
|
public static DateTime DeserializeDateTime(object value) |
|
|
{ |
|
|
{ |
|
|
var deserializer = DateTimeDeserializer; |
|
|
var deserializer = _datetime_deserializer; |
|
|
if (deserializer != null) return deserializer.Invoke(text); |
|
|
if (deserializer != null) return deserializer.Invoke(value); |
|
|
|
|
|
|
|
|
|
|
|
try |
|
|
|
|
|
{ |
|
|
|
|
|
if (value is DateTime dt) return dt; |
|
|
|
|
|
|
|
|
var ndt = ClockUtility.Parse(text); |
|
|
var text = value.ToString(); |
|
|
return ndt == null ? default(DateTime) : ndt.Value; |
|
|
var parse = ClockUtility.Parse(text); |
|
|
|
|
|
if (parse != null) return parse.Value; |
|
|
|
|
|
} |
|
|
|
|
|
catch { } |
|
|
|
|
|
return default; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
#endregion
|
|
|
#endregion
|
|
|