#region License // Copyright (c) 2007 James Newton-King // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. #endregion using System; using System.Collections.Generic; #if HAVE_BIG_INTEGER using System.Numerics; #endif using LC.Newtonsoft.Json.Linq; using LC.Newtonsoft.Json.Schema; using LC.Newtonsoft.Json.Utilities; using System.Globalization; using System.Text.RegularExpressions; using System.IO; #if !HAVE_LINQ using LC.Newtonsoft.Json.Utilities.LinqBridge; #else using System.Linq; #endif namespace LC.Newtonsoft.Json { /// /// /// Represents a reader that provides validation. /// /// /// JSON Schema validation has been moved to its own package. See https://www.newtonsoft.com/jsonschema for more details. /// /// [Obsolete("JSON Schema validation has been moved to its own package. See https://www.newtonsoft.com/jsonschema for more details.")] public class JsonValidatingReader : JsonReader, IJsonLineInfo { private class SchemaScope { private readonly JTokenType _tokenType; private readonly IList _schemas; private readonly Dictionary _requiredProperties; public string CurrentPropertyName { get; set; } public int ArrayItemCount { get; set; } public bool IsUniqueArray { get; } public IList UniqueArrayItems { get; } public JTokenWriter CurrentItemWriter { get; set; } public IList Schemas => _schemas; public Dictionary RequiredProperties => _requiredProperties; public JTokenType TokenType => _tokenType; public SchemaScope(JTokenType tokenType, IList schemas) { _tokenType = tokenType; _schemas = schemas; _requiredProperties = schemas.SelectMany(GetRequiredProperties).Distinct().ToDictionary(p => p, p => false); if (tokenType == JTokenType.Array && schemas.Any(s => s.UniqueItems)) { IsUniqueArray = true; UniqueArrayItems = new List(); } } private IEnumerable GetRequiredProperties(JsonSchemaModel schema) { if (schema?.Properties == null) { return Enumerable.Empty(); } return schema.Properties.Where(p => p.Value.Required).Select(p => p.Key); } } private readonly JsonReader _reader; private readonly Stack _stack; private JsonSchema _schema; private JsonSchemaModel _model; private SchemaScope _currentScope; /// /// Sets an event handler for receiving schema validation errors. /// public event ValidationEventHandler ValidationEventHandler; /// /// Gets the text value of the current JSON token. /// /// public override object Value => _reader.Value; /// /// Gets the depth of the current token in the JSON document. /// /// The depth of the current token in the JSON document. public override int Depth => _reader.Depth; /// /// Gets the path of the current JSON token. /// public override string Path => _reader.Path; /// /// Gets the quotation mark character used to enclose the value of a string. /// /// public override char QuoteChar { get { return _reader.QuoteChar; } protected internal set { } } /// /// Gets the type of the current JSON token. /// /// public override JsonToken TokenType => _reader.TokenType; /// /// Gets the .NET type for the current JSON token. /// /// public override Type ValueType => _reader.ValueType; private void Push(SchemaScope scope) { _stack.Push(scope); _currentScope = scope; } private SchemaScope Pop() { SchemaScope poppedScope = _stack.Pop(); _currentScope = (_stack.Count != 0) ? _stack.Peek() : null; return poppedScope; } private IList CurrentSchemas => _currentScope.Schemas; private static readonly IList EmptySchemaList = new List(); private IList CurrentMemberSchemas { get { if (_currentScope == null) { return new List(new[] { _model }); } if (_currentScope.Schemas == null || _currentScope.Schemas.Count == 0) { return EmptySchemaList; } switch (_currentScope.TokenType) { case JTokenType.None: return _currentScope.Schemas; case JTokenType.Object: { if (_currentScope.CurrentPropertyName == null) { throw new JsonReaderException("CurrentPropertyName has not been set on scope."); } IList schemas = new List(); foreach (JsonSchemaModel schema in CurrentSchemas) { if (schema.Properties != null && schema.Properties.TryGetValue(_currentScope.CurrentPropertyName, out JsonSchemaModel propertySchema)) { schemas.Add(propertySchema); } if (schema.PatternProperties != null) { foreach (KeyValuePair patternProperty in schema.PatternProperties) { if (Regex.IsMatch(_currentScope.CurrentPropertyName, patternProperty.Key)) { schemas.Add(patternProperty.Value); } } } if (schemas.Count == 0 && schema.AllowAdditionalProperties && schema.AdditionalProperties != null) { schemas.Add(schema.AdditionalProperties); } } return schemas; } case JTokenType.Array: { IList schemas = new List(); foreach (JsonSchemaModel schema in CurrentSchemas) { if (!schema.PositionalItemsValidation) { if (schema.Items != null && schema.Items.Count > 0) { schemas.Add(schema.Items[0]); } } else { if (schema.Items != null && schema.Items.Count > 0) { if (schema.Items.Count > (_currentScope.ArrayItemCount - 1)) { schemas.Add(schema.Items[_currentScope.ArrayItemCount - 1]); } } if (schema.AllowAdditionalItems && schema.AdditionalItems != null) { schemas.Add(schema.AdditionalItems); } } } return schemas; } case JTokenType.Constructor: return EmptySchemaList; default: throw new ArgumentOutOfRangeException("TokenType", "Unexpected token type: {0}".FormatWith(CultureInfo.InvariantCulture, _currentScope.TokenType)); } } } private void RaiseError(string message, JsonSchemaModel schema) { IJsonLineInfo lineInfo = this; string exceptionMessage = (lineInfo.HasLineInfo()) ? message + " Line {0}, position {1}.".FormatWith(CultureInfo.InvariantCulture, lineInfo.LineNumber, lineInfo.LinePosition) : message; OnValidationEvent(new JsonSchemaException(exceptionMessage, null, Path, lineInfo.LineNumber, lineInfo.LinePosition)); } private void OnValidationEvent(JsonSchemaException exception) { ValidationEventHandler handler = ValidationEventHandler; if (handler != null) { handler(this, new ValidationEventArgs(exception)); } else { throw exception; } } /// /// Initializes a new instance of the class that /// validates the content returned from the given . /// /// The to read from while validating. public JsonValidatingReader(JsonReader reader) { ValidationUtils.ArgumentNotNull(reader, nameof(reader)); _reader = reader; _stack = new Stack(); } /// /// Gets or sets the schema. /// /// The schema. public JsonSchema Schema { get => _schema; set { if (TokenType != JsonToken.None) { throw new InvalidOperationException("Cannot change schema while validating JSON."); } _schema = value; _model = null; } } /// /// Gets the used to construct this . /// /// The specified in the constructor. public JsonReader Reader => _reader; /// /// Changes the reader's state to . /// If is set to true, the underlying is also closed. /// public override void Close() { base.Close(); if (CloseInput) { _reader?.Close(); } } private void ValidateNotDisallowed(JsonSchemaModel schema) { if (schema == null) { return; } JsonSchemaType? currentNodeType = GetCurrentNodeSchemaType(); if (currentNodeType != null) { if (JsonSchemaGenerator.HasFlag(schema.Disallow, currentNodeType.GetValueOrDefault())) { RaiseError("Type {0} is disallowed.".FormatWith(CultureInfo.InvariantCulture, currentNodeType), schema); } } } private JsonSchemaType? GetCurrentNodeSchemaType() { switch (_reader.TokenType) { case JsonToken.StartObject: return JsonSchemaType.Object; case JsonToken.StartArray: return JsonSchemaType.Array; case JsonToken.Integer: return JsonSchemaType.Integer; case JsonToken.Float: return JsonSchemaType.Float; case JsonToken.String: return JsonSchemaType.String; case JsonToken.Boolean: return JsonSchemaType.Boolean; case JsonToken.Null: return JsonSchemaType.Null; default: return null; } } /// /// Reads the next JSON token from the underlying as a of . /// /// A of . public override int? ReadAsInt32() { int? i = _reader.ReadAsInt32(); ValidateCurrentToken(); return i; } /// /// Reads the next JSON token from the underlying as a []. /// /// /// A [] or null if the next JSON token is null. /// public override byte[] ReadAsBytes() { byte[] data = _reader.ReadAsBytes(); ValidateCurrentToken(); return data; } /// /// Reads the next JSON token from the underlying as a of . /// /// A of . public override decimal? ReadAsDecimal() { decimal? d = _reader.ReadAsDecimal(); ValidateCurrentToken(); return d; } /// /// Reads the next JSON token from the underlying as a of . /// /// A of . public override double? ReadAsDouble() { double? d = _reader.ReadAsDouble(); ValidateCurrentToken(); return d; } /// /// Reads the next JSON token from the underlying as a of . /// /// A of . public override bool? ReadAsBoolean() { bool? b = _reader.ReadAsBoolean(); ValidateCurrentToken(); return b; } /// /// Reads the next JSON token from the underlying as a . /// /// A . This method will return null at the end of an array. public override string ReadAsString() { string s = _reader.ReadAsString(); ValidateCurrentToken(); return s; } /// /// Reads the next JSON token from the underlying as a of . /// /// A of . This method will return null at the end of an array. public override DateTime? ReadAsDateTime() { DateTime? dateTime = _reader.ReadAsDateTime(); ValidateCurrentToken(); return dateTime; } #if HAVE_DATE_TIME_OFFSET /// /// Reads the next JSON token from the underlying as a of . /// /// A of . public override DateTimeOffset? ReadAsDateTimeOffset() { DateTimeOffset? dateTimeOffset = _reader.ReadAsDateTimeOffset(); ValidateCurrentToken(); return dateTimeOffset; } #endif /// /// Reads the next JSON token from the underlying . /// /// /// true if the next token was read successfully; false if there are no more tokens to read. /// public override bool Read() { if (!_reader.Read()) { return false; } if (_reader.TokenType == JsonToken.Comment) { return true; } ValidateCurrentToken(); return true; } private void ValidateCurrentToken() { // first time validate has been called. build model if (_model == null) { JsonSchemaModelBuilder builder = new JsonSchemaModelBuilder(); _model = builder.Build(_schema); if (!JsonTokenUtils.IsStartToken(_reader.TokenType)) { Push(new SchemaScope(JTokenType.None, CurrentMemberSchemas)); } } switch (_reader.TokenType) { case JsonToken.StartObject: ProcessValue(); IList objectSchemas = CurrentMemberSchemas.Where(ValidateObject).ToList(); Push(new SchemaScope(JTokenType.Object, objectSchemas)); WriteToken(CurrentSchemas); break; case JsonToken.StartArray: ProcessValue(); IList arraySchemas = CurrentMemberSchemas.Where(ValidateArray).ToList(); Push(new SchemaScope(JTokenType.Array, arraySchemas)); WriteToken(CurrentSchemas); break; case JsonToken.StartConstructor: ProcessValue(); Push(new SchemaScope(JTokenType.Constructor, null)); WriteToken(CurrentSchemas); break; case JsonToken.PropertyName: WriteToken(CurrentSchemas); foreach (JsonSchemaModel schema in CurrentSchemas) { ValidatePropertyName(schema); } break; case JsonToken.Raw: ProcessValue(); break; case JsonToken.Integer: ProcessValue(); WriteToken(CurrentMemberSchemas); foreach (JsonSchemaModel schema in CurrentMemberSchemas) { ValidateInteger(schema); } break; case JsonToken.Float: ProcessValue(); WriteToken(CurrentMemberSchemas); foreach (JsonSchemaModel schema in CurrentMemberSchemas) { ValidateFloat(schema); } break; case JsonToken.String: ProcessValue(); WriteToken(CurrentMemberSchemas); foreach (JsonSchemaModel schema in CurrentMemberSchemas) { ValidateString(schema); } break; case JsonToken.Boolean: ProcessValue(); WriteToken(CurrentMemberSchemas); foreach (JsonSchemaModel schema in CurrentMemberSchemas) { ValidateBoolean(schema); } break; case JsonToken.Null: ProcessValue(); WriteToken(CurrentMemberSchemas); foreach (JsonSchemaModel schema in CurrentMemberSchemas) { ValidateNull(schema); } break; case JsonToken.EndObject: WriteToken(CurrentSchemas); foreach (JsonSchemaModel schema in CurrentSchemas) { ValidateEndObject(schema); } Pop(); break; case JsonToken.EndArray: WriteToken(CurrentSchemas); foreach (JsonSchemaModel schema in CurrentSchemas) { ValidateEndArray(schema); } Pop(); break; case JsonToken.EndConstructor: WriteToken(CurrentSchemas); Pop(); break; case JsonToken.Undefined: case JsonToken.Date: case JsonToken.Bytes: // these have no equivalent in JSON schema WriteToken(CurrentMemberSchemas); break; case JsonToken.None: // no content, do nothing break; default: throw new ArgumentOutOfRangeException(); } } private void WriteToken(IList schemas) { foreach (SchemaScope schemaScope in _stack) { bool isInUniqueArray = (schemaScope.TokenType == JTokenType.Array && schemaScope.IsUniqueArray && schemaScope.ArrayItemCount > 0); if (isInUniqueArray || schemas.Any(s => s.Enum != null)) { if (schemaScope.CurrentItemWriter == null) { if (JsonTokenUtils.IsEndToken(_reader.TokenType)) { continue; } schemaScope.CurrentItemWriter = new JTokenWriter(); } schemaScope.CurrentItemWriter.WriteToken(_reader, false); // finished writing current item if (schemaScope.CurrentItemWriter.Top == 0 && _reader.TokenType != JsonToken.PropertyName) { JToken finishedItem = schemaScope.CurrentItemWriter.Token; // start next item with new writer schemaScope.CurrentItemWriter = null; if (isInUniqueArray) { if (schemaScope.UniqueArrayItems.Contains(finishedItem, JToken.EqualityComparer)) { RaiseError("Non-unique array item at index {0}.".FormatWith(CultureInfo.InvariantCulture, schemaScope.ArrayItemCount - 1), schemaScope.Schemas.First(s => s.UniqueItems)); } schemaScope.UniqueArrayItems.Add(finishedItem); } else if (schemas.Any(s => s.Enum != null)) { foreach (JsonSchemaModel schema in schemas) { if (schema.Enum != null) { if (!schema.Enum.ContainsValue(finishedItem, JToken.EqualityComparer)) { StringWriter sw = new StringWriter(CultureInfo.InvariantCulture); finishedItem.WriteTo(new JsonTextWriter(sw)); RaiseError("Value {0} is not defined in enum.".FormatWith(CultureInfo.InvariantCulture, sw.ToString()), schema); } } } } } } } } private void ValidateEndObject(JsonSchemaModel schema) { if (schema == null) { return; } Dictionary requiredProperties = _currentScope.RequiredProperties; if (requiredProperties != null && requiredProperties.Values.Any(v => !v)) { IEnumerable unmatchedRequiredProperties = requiredProperties.Where(kv => !kv.Value).Select(kv => kv.Key); RaiseError("Required properties are missing from object: {0}.".FormatWith(CultureInfo.InvariantCulture, string.Join(", ", unmatchedRequiredProperties #if !HAVE_STRING_JOIN_WITH_ENUMERABLE .ToArray() #endif )), schema); } } private void ValidateEndArray(JsonSchemaModel schema) { if (schema == null) { return; } int arrayItemCount = _currentScope.ArrayItemCount; if (schema.MaximumItems != null && arrayItemCount > schema.MaximumItems) { RaiseError("Array item count {0} exceeds maximum count of {1}.".FormatWith(CultureInfo.InvariantCulture, arrayItemCount, schema.MaximumItems), schema); } if (schema.MinimumItems != null && arrayItemCount < schema.MinimumItems) { RaiseError("Array item count {0} is less than minimum count of {1}.".FormatWith(CultureInfo.InvariantCulture, arrayItemCount, schema.MinimumItems), schema); } } private void ValidateNull(JsonSchemaModel schema) { if (schema == null) { return; } if (!TestType(schema, JsonSchemaType.Null)) { return; } ValidateNotDisallowed(schema); } private void ValidateBoolean(JsonSchemaModel schema) { if (schema == null) { return; } if (!TestType(schema, JsonSchemaType.Boolean)) { return; } ValidateNotDisallowed(schema); } private void ValidateString(JsonSchemaModel schema) { if (schema == null) { return; } if (!TestType(schema, JsonSchemaType.String)) { return; } ValidateNotDisallowed(schema); string value = _reader.Value.ToString(); if (schema.MaximumLength != null && value.Length > schema.MaximumLength) { RaiseError("String '{0}' exceeds maximum length of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.MaximumLength), schema); } if (schema.MinimumLength != null && value.Length < schema.MinimumLength) { RaiseError("String '{0}' is less than minimum length of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.MinimumLength), schema); } if (schema.Patterns != null) { foreach (string pattern in schema.Patterns) { if (!Regex.IsMatch(value, pattern)) { RaiseError("String '{0}' does not match regex pattern '{1}'.".FormatWith(CultureInfo.InvariantCulture, value, pattern), schema); } } } } private void ValidateInteger(JsonSchemaModel schema) { if (schema == null) { return; } if (!TestType(schema, JsonSchemaType.Integer)) { return; } ValidateNotDisallowed(schema); object value = _reader.Value; if (schema.Maximum != null) { if (JValue.Compare(JTokenType.Integer, value, schema.Maximum) > 0) { RaiseError("Integer {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema); } if (schema.ExclusiveMaximum && JValue.Compare(JTokenType.Integer, value, schema.Maximum) == 0) { RaiseError("Integer {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema); } } if (schema.Minimum != null) { if (JValue.Compare(JTokenType.Integer, value, schema.Minimum) < 0) { RaiseError("Integer {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema); } if (schema.ExclusiveMinimum && JValue.Compare(JTokenType.Integer, value, schema.Minimum) == 0) { RaiseError("Integer {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema); } } if (schema.DivisibleBy != null) { bool notDivisible; #if HAVE_BIG_INTEGER if (value is BigInteger i) { // not that this will lose any decimal point on DivisibleBy // so manually raise an error if DivisibleBy is not an integer and value is not zero bool divisibleNonInteger = !Math.Abs(schema.DivisibleBy.Value - Math.Truncate(schema.DivisibleBy.Value)).Equals(0); if (divisibleNonInteger) { notDivisible = i != 0; } else { notDivisible = i % new BigInteger(schema.DivisibleBy.Value) != 0; } } else #endif { notDivisible = !IsZero(Convert.ToInt64(value, CultureInfo.InvariantCulture) % schema.DivisibleBy.GetValueOrDefault()); } if (notDivisible) { RaiseError("Integer {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema); } } } private void ProcessValue() { if (_currentScope != null && _currentScope.TokenType == JTokenType.Array) { _currentScope.ArrayItemCount++; foreach (JsonSchemaModel currentSchema in CurrentSchemas) { // if there is positional validation and the array index is past the number of item validation schemas and there are no additional items then error if (currentSchema != null && currentSchema.PositionalItemsValidation && !currentSchema.AllowAdditionalItems && (currentSchema.Items == null || _currentScope.ArrayItemCount - 1 >= currentSchema.Items.Count)) { RaiseError("Index {0} has not been defined and the schema does not allow additional items.".FormatWith(CultureInfo.InvariantCulture, _currentScope.ArrayItemCount), currentSchema); } } } } private void ValidateFloat(JsonSchemaModel schema) { if (schema == null) { return; } if (!TestType(schema, JsonSchemaType.Float)) { return; } ValidateNotDisallowed(schema); double value = Convert.ToDouble(_reader.Value, CultureInfo.InvariantCulture); if (schema.Maximum != null) { if (value > schema.Maximum) { RaiseError("Float {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema); } if (schema.ExclusiveMaximum && value == schema.Maximum) { RaiseError("Float {0} equals maximum value of {1} and exclusive maximum is true.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema); } } if (schema.Minimum != null) { if (value < schema.Minimum) { RaiseError("Float {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema); } if (schema.ExclusiveMinimum && value == schema.Minimum) { RaiseError("Float {0} equals minimum value of {1} and exclusive minimum is true.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema); } } if (schema.DivisibleBy != null) { double remainder = FloatingPointRemainder(value, schema.DivisibleBy.GetValueOrDefault()); if (!IsZero(remainder)) { RaiseError("Float {0} is not evenly divisible by {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.DivisibleBy), schema); } } } private static double FloatingPointRemainder(double dividend, double divisor) { return dividend - Math.Floor(dividend / divisor) * divisor; } private static bool IsZero(double value) { const double epsilon = 2.2204460492503131e-016; return Math.Abs(value) < 20.0 * epsilon; } private void ValidatePropertyName(JsonSchemaModel schema) { if (schema == null) { return; } string propertyName = Convert.ToString(_reader.Value, CultureInfo.InvariantCulture); if (_currentScope.RequiredProperties.ContainsKey(propertyName)) { _currentScope.RequiredProperties[propertyName] = true; } if (!schema.AllowAdditionalProperties) { bool propertyDefinied = IsPropertyDefinied(schema, propertyName); if (!propertyDefinied) { RaiseError("Property '{0}' has not been defined and the schema does not allow additional properties.".FormatWith(CultureInfo.InvariantCulture, propertyName), schema); } } _currentScope.CurrentPropertyName = propertyName; } private bool IsPropertyDefinied(JsonSchemaModel schema, string propertyName) { if (schema.Properties != null && schema.Properties.ContainsKey(propertyName)) { return true; } if (schema.PatternProperties != null) { foreach (string pattern in schema.PatternProperties.Keys) { if (Regex.IsMatch(propertyName, pattern)) { return true; } } } return false; } private bool ValidateArray(JsonSchemaModel schema) { if (schema == null) { return true; } return (TestType(schema, JsonSchemaType.Array)); } private bool ValidateObject(JsonSchemaModel schema) { if (schema == null) { return true; } return (TestType(schema, JsonSchemaType.Object)); } private bool TestType(JsonSchemaModel currentSchema, JsonSchemaType currentType) { if (!JsonSchemaGenerator.HasFlag(currentSchema.Type, currentType)) { RaiseError("Invalid type. Expected {0} but got {1}.".FormatWith(CultureInfo.InvariantCulture, currentSchema.Type, currentType), currentSchema); return false; } return true; } bool IJsonLineInfo.HasLineInfo() { return _reader is IJsonLineInfo lineInfo && lineInfo.HasLineInfo(); } int IJsonLineInfo.LineNumber => (_reader is IJsonLineInfo lineInfo) ? lineInfo.LineNumber : 0; int IJsonLineInfo.LinePosition => (_reader is IJsonLineInfo lineInfo) ? lineInfo.LinePosition : 0; } }