#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;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using Newtonsoft.Json.Utilities;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Newtonsoft.Json{
/// <summary>
/// Represents a reader that provides <see cref="JsonSchema"/> validation.
/// </summary>
public class JsonValidatingReader : JsonReader, IJsonLineInfo
{
private class SchemaScope
{
private readonly JTokenType _tokenType;
private readonly JsonSchemaModel _schema;
private readonly Dictionary<string, bool> _requiredProperties;
public string CurrentPropertyName { get; set; }
public int ArrayItemCount { get; set; }
public JsonSchemaModel Schema
{
get { return _schema; }
}
public Dictionary<string, bool> RequiredProperties
{
get { return _requiredProperties; }
}
public JTokenType TokenType
{
get { return _tokenType; }
}
public SchemaScope(JTokenType tokenType, JsonSchemaModel schema)
{
_tokenType = tokenType;
_schema = schema;
if (_schema != null && _schema.Properties != null)
_requiredProperties = GetRequiredProperties(_schema).Distinct().ToDictionary(p => p, p => false);
else
_requiredProperties = new Dictionary<string, bool>();
}
private IEnumerable<string> GetRequiredProperties(JsonSchemaModel schema)
{
return schema.Properties.Where(p => !p.Value.Optional).Select(p => p.Key);
}
}
private readonly JsonReader _reader;
private readonly Stack<SchemaScope> _stack;
private JsonSchema _schema;
private JsonSchemaModel _model;
private SchemaScope _currentScope;
/// <summary>
/// Sets an event handler for receiving schema validation errors.
/// </summary>
public event ValidationEventHandler ValidationEventHandler;
/// <summary>
/// Gets the text value of the current Json token.
/// </summary>
/// <value></value>
public override object Value
{
get { return _reader.Value; }
}
/// <summary>
/// Gets the depth of the current token in the JSON document.
/// </summary>
/// <value>The depth of the current token in the JSON document.</value>
public override int Depth
{
get { return _reader.Depth; }
}
/// <summary>
/// Gets the quotation mark character used to enclose the value of a string.
/// </summary>
/// <value></value>
public override char QuoteChar
{
get { return _reader.QuoteChar; }
protected internal set { }
}
/// <summary>
/// Gets the type of the current Json token.
/// </summary>
/// <value></value>
public override JsonToken TokenType
{
get { return _reader.TokenType; }
}
/// <summary>
/// Gets The Common Language Runtime (CLR) type for the current Json token.
/// </summary>
/// <value></value>
public override Type ValueType
{
get { return _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 JsonSchemaModel CurrentSchema
{
get { return _currentScope.Schema; }
}
private JsonSchemaModel CurrentMemberSchema
{
get
{
if (_currentScope == null)
return _model;
if (_currentScope.Schema == null)
return null;
switch (_currentScope.TokenType)
{
case JTokenType.None:
return _currentScope.Schema;
case JTokenType.Object:
if (_currentScope.CurrentPropertyName == null)
throw new Exception("CurrentPropertyName has not been set on scope.");
JsonSchemaModel propertySchema;
if (CurrentSchema.Properties != null && CurrentSchema.Properties.TryGetValue(_currentScope.CurrentPropertyName, out propertySchema))
return propertySchema;
return (CurrentSchema.AllowAdditionalProperties) ? CurrentSchema.AdditionalProperties : null;
case JTokenType.Array:
if (!CollectionUtils.IsNullOrEmpty(CurrentSchema.Items))
{
if (CurrentSchema.Items.Count == 1)
return CurrentSchema.Items[0];
if (CurrentSchema.Items.Count > (_currentScope.ArrayItemCount - 1))
return CurrentSchema.Items[_currentScope.ArrayItemCount - 1];
}
return (CurrentSchema.AllowAdditionalProperties) ? CurrentSchema.AdditionalProperties : null;
case JTokenType.Constructor:
return null;
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, lineInfo.LineNumber, lineInfo.LinePosition));
}
private void OnValidationEvent(JsonSchemaException exception)
{
ValidationEventHandler handler = ValidationEventHandler;
if (handler != null)
handler(this, new ValidationEventArgs(exception));
else
throw exception;
}
/// <summary>
/// Initializes a new instance of the <see cref="JsonValidatingReader"/> class that
/// validates the content returned from the given <see cref="JsonReader"/>.
/// </summary>
/// <param name="reader">The <see cref="JsonReader"/> to read from while validating.</param>
public JsonValidatingReader(JsonReader reader)
{
ValidationUtils.ArgumentNotNull(reader, "reader");
_reader = reader;
_stack = new Stack<SchemaScope>();
}
/// <summary>
/// Gets or sets the schema.
/// </summary>
/// <value>The schema.</value>
public JsonSchema Schema
{
get { return _schema; }
set
{
if (TokenType != JsonToken.None)
throw new Exception("Cannot change schema while validating JSON.");
_schema = value;
_model = null;
}
}
/// <summary>
/// Gets the <see cref="JsonReader"/> used to construct this <see cref="JsonValidatingReader"/>.
/// </summary>
/// <value>The <see cref="JsonReader"/> specified in the constructor.</value>
public JsonReader Reader
{
get { return _reader; }
}
private void ValidateInEnumAndNotDisallowed(JsonSchemaModel schema)
{
if (schema == null)
return;
JToken value = new JValue(_reader.Value);
if (schema.Enum != null)
{
if (!schema.Enum.ContainsValue(value, new JTokenEqualityComparer()))
RaiseError("Value {0} is not defined in enum.".FormatWith(CultureInfo.InvariantCulture, value),
schema);
}
JsonSchemaType? currentNodeType = GetCurrentNodeSchemaType();
if (currentNodeType != null)
{
if (JsonSchemaGenerator.HasFlag(schema.Disallow, currentNodeType.Value))
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;
}
}
/// <summary>
/// Reads the next JSON token from the stream as a <see cref="T:Byte[]"/>.
/// </summary>
/// <returns>
/// A <see cref="T:Byte[]"/> or a null reference if the next JSON token is null.
/// </returns>
public override byte[] ReadAsBytes()
{
byte[] data = _reader.ReadAsBytes();
ValidateCurrentToken();
return data;
}
/// <summary>
/// Reads the next JSON token from the stream.
/// </summary>
/// <returns>
/// true if the next token was read successfully; false if there are no more tokens to read.
/// </returns>
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);
}
switch (_reader.TokenType)
{
case JsonToken.StartObject:
ProcessValue();
JsonSchemaModel objectSchema = (ValidateObject(CurrentMemberSchema))
? CurrentMemberSchema
: null;
Push(new SchemaScope(JTokenType.Object, objectSchema));
break;
case JsonToken.StartArray:
ProcessValue();
JsonSchemaModel arraySchema = (ValidateArray(CurrentMemberSchema))
? CurrentMemberSchema
: null;
Push(new SchemaScope(JTokenType.Array, arraySchema));
break;
case JsonToken.StartConstructor:
Push(new SchemaScope(JTokenType.Constructor, null));
break;
case JsonToken.PropertyName:
ValidatePropertyName(CurrentSchema);
break;
case JsonToken.Raw:
break;
case JsonToken.Integer:
ProcessValue();
ValidateInteger(CurrentMemberSchema);
break;
case JsonToken.Float:
ProcessValue();
ValidateFloat(CurrentMemberSchema);
break;
case JsonToken.String:
ProcessValue();
ValidateString(CurrentMemberSchema);
break;
case JsonToken.Boolean:
ProcessValue();
ValidateBoolean(CurrentMemberSchema);
break;
case JsonToken.Null:
ProcessValue();
ValidateNull(CurrentMemberSchema);
break;
case JsonToken.Undefined:
break;
case JsonToken.EndObject:
ValidateEndObject(CurrentSchema);
Pop();
break;
case JsonToken.EndArray:
ValidateEndArray(CurrentSchema);
Pop();
break;
case JsonToken.EndConstructor:
Pop();
break;
case JsonToken.Date:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void ValidateEndObject(JsonSchemaModel schema)
{
if (schema == null)
return;
Dictionary<string, bool> requiredProperties = _currentScope.RequiredProperties;
if (requiredProperties != null)
{
List<string> unmatchedRequiredProperties =
requiredProperties.Where(kv => !kv.Value).Select(kv => kv.Key).ToList();
if (unmatchedRequiredProperties.Count > 0)
RaiseError("Non-optional properties are missing from object: {0}.".FormatWith(CultureInfo.InvariantCulture, string.Join(", ", unmatchedRequiredProperties.ToArray())), 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;
ValidateInEnumAndNotDisallowed(schema);
}
private void ValidateBoolean(JsonSchemaModel schema)
{
if (schema == null)
return;
if (!TestType(schema, JsonSchemaType.Boolean))
return;
ValidateInEnumAndNotDisallowed(schema);
}
private void ValidateString(JsonSchemaModel schema)
{
if (schema == null)
return;
if (!TestType(schema, JsonSchemaType.String))
return;
ValidateInEnumAndNotDisallowed(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;
ValidateInEnumAndNotDisallowed(schema);
long value = Convert.ToInt64(_reader.Value, CultureInfo.InvariantCulture);
if (schema.Maximum != null && value > schema.Maximum)
RaiseError("Integer {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Maximum), schema);
if (schema.Minimum != null && value < schema.Minimum)
RaiseError("Integer {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, value, schema.Minimum), schema);
}
private void ProcessValue()
{
if (_currentScope != null && _currentScope.TokenType == JTokenType.Array)
{
_currentScope.ArrayItemCount++;
if (CurrentSchema != null && CurrentSchema.Items != null && CurrentSchema.Items.Count > 1 && _currentScope.ArrayItemCount >= 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;
ValidateInEnumAndNotDisallowed(schema);
double value = Convert.ToDouble(_reader.Value, CultureInfo.InvariantCulture);
if (schema.Maximum != null && value > schema.Maximum)
RaiseError("Float {0} exceeds maximum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Maximum), schema);
if (schema.Minimum != null && value < schema.Minimum)
RaiseError("Float {0} is less than minimum value of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.Minimum), schema);
if (schema.MaximumDecimals != null && MathUtils.GetDecimalPlaces(value) > schema.MaximumDecimals)
RaiseError("Float {0} exceeds the maximum allowed number decimal places of {1}.".FormatWith(CultureInfo.InvariantCulture, JsonConvert.ToString(value), schema.MaximumDecimals), schema);
}
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.Properties != null && !schema.Properties.ContainsKey(propertyName))
{
IList<string> definedProperties = schema.Properties.Select(p => p.Key).ToList();
if (!schema.AllowAdditionalProperties && !definedProperties.Contains(propertyName))
{
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 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()
{
IJsonLineInfo lineInfo = _reader as IJsonLineInfo;
return (lineInfo != null) ? lineInfo.HasLineInfo() : false;
}
int IJsonLineInfo.LineNumber
{
get
{
IJsonLineInfo lineInfo = _reader as IJsonLineInfo;
return (lineInfo != null) ? lineInfo.LineNumber : 0;
}
}
int IJsonLineInfo.LinePosition
{
get
{
IJsonLineInfo lineInfo = _reader as IJsonLineInfo;
return (lineInfo != null) ? lineInfo.LinePosition : 0;
}
}
}
}
|