From 1d9154f32f0e94545431dca623268ab48a860302 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 19 Nov 2013 11:18:26 -0800 Subject: [PATCH] -JsonPath++ --- .../Linq/JsonPath/JPathExecuteTests.cs | 36 +++- .../Linq/JsonPath/JPathParseTests.cs | 190 +++++++++++++----- .../Linq/JsonPath/QueryExpressionTests.cs | 131 ++++++++++++ .../Newtonsoft.Json.Tests.csproj | 1 + .../Linq/JsonPath/AndFilter.cs | 23 +++ Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs | 166 +++++++++------ Src/Newtonsoft.Json/Linq/JsonPath/OrFilter.cs | 31 +++ .../Linq/JsonPath/QueryFilter.cs | 140 ++++++++----- Src/Newtonsoft.Json/Newtonsoft.Json.csproj | 2 + 9 files changed, 560 insertions(+), 160 deletions(-) create mode 100644 Src/Newtonsoft.Json.Tests/Linq/JsonPath/QueryExpressionTests.cs create mode 100644 Src/Newtonsoft.Json/Linq/JsonPath/AndFilter.cs create mode 100644 Src/Newtonsoft.Json/Linq/JsonPath/OrFilter.cs diff --git a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs index 0e31faad..89bddb4b 100644 --- a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs +++ b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathExecuteTests.cs @@ -25,7 +25,9 @@ using System; using System.Collections.Generic; +using System.Numerics; using Newtonsoft.Json.Linq.JsonPath; +using Newtonsoft.Json.Tests.Bson; #if !NETFX_CORE using NUnit.Framework; #else @@ -404,6 +406,19 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath Assert.AreEqual(3, (int)t[1]); } + [Test] + public void MultipleQueries() + { + JArray a = new JArray(1, 2, 3, 4, 5, 6, 7, 8, 9); + + // json path does item based evaluation - http://www.sitepen.com/blog/2008/03/17/jsonpath-support/ + // first query resolves array to ints + // int has no children to query + IList t = a.SelectTokens("[?(@ <> 1)][?(@ <> 4)][?(@ < 7)]").ToList(); + Assert.IsNotNull(t); + Assert.AreEqual(0, t.Count); + } + [Test] public void GreaterQuery() { @@ -419,20 +434,37 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath Assert.IsTrue(JToken.DeepEquals(new JObject(new JProperty("hi", 3)), t[1])); } + [Test] + public void GreaterQueryBigInteger() + { + JArray a = new JArray( + new JObject(new JProperty("hi", new BigInteger(1))), + new JObject(new JProperty("hi", new BigInteger(2))), + new JObject(new JProperty("hi", new BigInteger(3)))); + + IList t = a.SelectTokens("[ ?( @.hi > 1 ) ]").ToList(); + Assert.IsNotNull(t); + Assert.AreEqual(2, t.Count); + Assert.IsTrue(JToken.DeepEquals(new JObject(new JProperty("hi", 2)), t[0])); + Assert.IsTrue(JToken.DeepEquals(new JObject(new JProperty("hi", 3)), t[1])); + } + [Test] public void GreaterOrEqualQuery() { JArray a = new JArray( new JObject(new JProperty("hi", 1)), new JObject(new JProperty("hi", 2)), + new JObject(new JProperty("hi", 2.0)), new JObject(new JProperty("hi", 3))); IList t = a.SelectTokens("[ ?( @.hi >= 1 ) ]").ToList(); Assert.IsNotNull(t); - Assert.AreEqual(3, t.Count); + Assert.AreEqual(4, t.Count); Assert.IsTrue(JToken.DeepEquals(new JObject(new JProperty("hi", 1)), t[0])); Assert.IsTrue(JToken.DeepEquals(new JObject(new JProperty("hi", 2)), t[1])); - Assert.IsTrue(JToken.DeepEquals(new JObject(new JProperty("hi", 3)), t[2])); + Assert.IsTrue(JToken.DeepEquals(new JObject(new JProperty("hi", 2.0)), t[2])); + Assert.IsTrue(JToken.DeepEquals(new JObject(new JProperty("hi", 3)), t[3])); } [Test] diff --git a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathParseTests.cs b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathParseTests.cs index c20aa08d..877b5a6f 100644 --- a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathParseTests.cs +++ b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/JPathParseTests.cs @@ -109,6 +109,14 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath () => { new JPath("$ .Blah"); }); } + [Test] + public void NoFieldNameAfterDot() + { + ExceptionAssert.Throws( + @"Unexpected end while parsing path.", + () => { new JPath("$.Blah."); }); + } + [Test] public void RootWithBadWhitespace2() { @@ -215,11 +223,10 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath JPath path = new JPath("Blah[ ?( @..name ) ]"); Assert.AreEqual(2, path.Filters.Count); Assert.AreEqual("Blah", ((FieldFilter)path.Filters[0]).Name); - List expressions = ((QueryFilter)path.Filters[1]).Expression; - Assert.AreEqual(1, expressions.Count); - Assert.AreEqual(QueryOperator.Exists, expressions[0].Operator); - Assert.AreEqual(1, expressions[0].Path.Count); - Assert.AreEqual("name", ((ScanFilter)expressions[0].Path[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.AreEqual(QueryOperator.Exists, expressions.Operator); + Assert.AreEqual(1, expressions.Path.Count); + Assert.AreEqual("name", ((ScanFilter)expressions.Path[0]).Name); } [Test] @@ -228,10 +235,9 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath JPath path = new JPath("Blah[ ?( @.name=='hi' ) ]"); Assert.AreEqual(2, path.Filters.Count); Assert.AreEqual("Blah", ((FieldFilter)path.Filters[0]).Name); - List expressions = ((QueryFilter)path.Filters[1]).Expression; - Assert.AreEqual(1, expressions.Count); - Assert.AreEqual(QueryOperator.Equals, expressions[0].Operator); - Assert.AreEqual("hi", (string)expressions[0].Value); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.AreEqual(QueryOperator.Equals, expressions.Operator); + Assert.AreEqual("hi", (string)expressions.Value); } [Test] @@ -240,10 +246,9 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath JPath path = new JPath(@"Blah[ ?( @.name=='h\'i' ) ]"); Assert.AreEqual(2, path.Filters.Count); Assert.AreEqual("Blah", ((FieldFilter)path.Filters[0]).Name); - List expressions = ((QueryFilter)path.Filters[1]).Expression; - Assert.AreEqual(1, expressions.Count); - Assert.AreEqual(QueryOperator.Equals, expressions[0].Operator); - Assert.AreEqual("h'i", (string)expressions[0].Value); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.AreEqual(QueryOperator.Equals, expressions.Operator); + Assert.AreEqual("h'i", (string)expressions.Value); } [Test] @@ -252,10 +257,9 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath JPath path = new JPath(@"Blah[ ?( @.name=='h\\i' ) ]"); Assert.AreEqual(2, path.Filters.Count); Assert.AreEqual("Blah", ((FieldFilter)path.Filters[0]).Name); - List expressions = ((QueryFilter)path.Filters[1]).Expression; - Assert.AreEqual(1, expressions.Count); - Assert.AreEqual(QueryOperator.Equals, expressions[0].Operator); - Assert.AreEqual("h\\i", (string)expressions[0].Value); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.AreEqual(QueryOperator.Equals, expressions.Operator); + Assert.AreEqual("h\\i", (string)expressions.Value); } [Test] @@ -272,10 +276,9 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath JPath path = new JPath("Blah[ ?( @.name==false ) ]"); Assert.AreEqual(2, path.Filters.Count); Assert.AreEqual("Blah", ((FieldFilter)path.Filters[0]).Name); - List expressions = ((QueryFilter)path.Filters[1]).Expression; - Assert.AreEqual(1, expressions.Count); - Assert.AreEqual(QueryOperator.Equals, expressions[0].Operator); - Assert.AreEqual(false, (bool)expressions[0].Value); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.AreEqual(QueryOperator.Equals, expressions.Operator); + Assert.AreEqual(false, (bool)expressions.Value); } [Test] @@ -284,10 +287,9 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath JPath path = new JPath("Blah[ ?( @.name==true ) ]"); Assert.AreEqual(2, path.Filters.Count); Assert.AreEqual("Blah", ((FieldFilter)path.Filters[0]).Name); - List expressions = ((QueryFilter)path.Filters[1]).Expression; - Assert.AreEqual(1, expressions.Count); - Assert.AreEqual(QueryOperator.Equals, expressions[0].Operator); - Assert.AreEqual(true, (bool)expressions[0].Value); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.AreEqual(QueryOperator.Equals, expressions.Operator); + Assert.AreEqual(true, (bool)expressions.Value); } [Test] @@ -296,98 +298,184 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath JPath path = new JPath("Blah[ ?( @.name==null ) ]"); Assert.AreEqual(2, path.Filters.Count); Assert.AreEqual("Blah", ((FieldFilter)path.Filters[0]).Name); - List expressions = ((QueryFilter)path.Filters[1]).Expression; - Assert.AreEqual(1, expressions.Count); - Assert.AreEqual(QueryOperator.Equals, expressions[0].Operator); - Assert.AreEqual(null, expressions[0].Value.Value); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.AreEqual(QueryOperator.Equals, expressions.Operator); + Assert.AreEqual(null, expressions.Value.Value); } [Test] public void FilterWithScan() { JPath path = new JPath("[?(@..name<>null)]"); - List expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual("name", ((ScanFilter)expressions[0].Path[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual("name", ((ScanFilter)expressions.Path[0]).Name); } [Test] public void FilterWithNotEquals() { JPath path = new JPath("[?(@.name<>null)]"); - List expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual(QueryOperator.NotEquals, expressions[0].Operator); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(QueryOperator.NotEquals, expressions.Operator); } [Test] public void FilterWithNotEquals2() { JPath path = new JPath("[?(@.name!=null)]"); - List expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual(QueryOperator.NotEquals, expressions[0].Operator); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(QueryOperator.NotEquals, expressions.Operator); } [Test] public void FilterWithLessThan() { JPath path = new JPath("[?(@.name expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual(QueryOperator.LessThan, expressions[0].Operator); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(QueryOperator.LessThan, expressions.Operator); } [Test] public void FilterWithLessThanOrEquals() { JPath path = new JPath("[?(@.name<=null)]"); - List expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual(QueryOperator.LessThanOrEquals, expressions[0].Operator); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(QueryOperator.LessThanOrEquals, expressions.Operator); } [Test] public void FilterWithGreaterThan() { JPath path = new JPath("[?(@.name>null)]"); - List expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual(QueryOperator.GreaterThan, expressions[0].Operator); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(QueryOperator.GreaterThan, expressions.Operator); } [Test] public void FilterWithGreaterThanOrEquals() { JPath path = new JPath("[?(@.name>=null)]"); - List expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual(QueryOperator.GreaterThanOrEquals, expressions[0].Operator); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(QueryOperator.GreaterThanOrEquals, expressions.Operator); } [Test] public void FilterWithInteger() { JPath path = new JPath("[?(@.name>=12)]"); - List expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual(12, (int)expressions[0].Value); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(12, (int)expressions.Value); } [Test] public void FilterWithNegativeInteger() { JPath path = new JPath("[?(@.name>=-12)]"); - List expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual(-12, (int)expressions[0].Value); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(-12, (int)expressions.Value); } [Test] public void FilterWithFloat() { JPath path = new JPath("[?(@.name>=12.1)]"); - List expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual(12.1d, (double)expressions[0].Value); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(12.1d, (double)expressions.Value); + } + + [Test] + public void FilterExistWithAnd() + { + JPath path = new JPath("[?(@.name&&@.title)]"); + CompositeExpression expressions = (CompositeExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(QueryOperator.And, expressions.Operator); + Assert.AreEqual(2, expressions.Expressions.Count); + Assert.AreEqual("name", ((FieldFilter)((BooleanQueryExpression)expressions.Expressions[0]).Path[0]).Name); + Assert.AreEqual(QueryOperator.Exists, expressions.Expressions[0].Operator); + Assert.AreEqual("title", ((FieldFilter)((BooleanQueryExpression)expressions.Expressions[1]).Path[0]).Name); + Assert.AreEqual(QueryOperator.Exists, expressions.Expressions[1].Operator); + } + + [Test] + public void FilterExistWithAndOr() + { + JPath path = new JPath("[?(@.name&&@.title||@.pie)]"); + CompositeExpression andExpression = (CompositeExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(QueryOperator.And, andExpression.Operator); + Assert.AreEqual(2, andExpression.Expressions.Count); + Assert.AreEqual("name", ((FieldFilter)((BooleanQueryExpression)andExpression.Expressions[0]).Path[0]).Name); + Assert.AreEqual(QueryOperator.Exists, andExpression.Expressions[0].Operator); + + CompositeExpression orExpression = (CompositeExpression)andExpression.Expressions[1]; + Assert.AreEqual(2, orExpression.Expressions.Count); + Assert.AreEqual("title", ((FieldFilter)((BooleanQueryExpression)orExpression.Expressions[0]).Path[0]).Name); + Assert.AreEqual(QueryOperator.Exists, orExpression.Expressions[0].Operator); + Assert.AreEqual("pie", ((FieldFilter)((BooleanQueryExpression)orExpression.Expressions[1]).Path[0]).Name); + Assert.AreEqual(QueryOperator.Exists, orExpression.Expressions[1].Operator); + } + + [Test] + public void BadOr1() + { + ExceptionAssert.Throws("Unexpected character while parsing path query: )", + () => new JPath("[?(@.name||)]")); + } + + [Test] + public void BaddOr2() + { + ExceptionAssert.Throws("Unexpected character while parsing path query: |", + () => new JPath("[?(@.name|)]")); + } + + [Test] + public void BaddOr3() + { + ExceptionAssert.Throws("Unexpected character while parsing path query: |", + () => new JPath("[?(@.name|")); + } + + [Test] + public void BaddOr4() + { + ExceptionAssert.Throws("Path ended with open query.", + () => new JPath("[?(@.name||")); + } + + [Test] + public void NoAtAfterOr() + { + ExceptionAssert.Throws("Unexpected character while parsing path query: s", + () => new JPath("[?(@.name||s")); + } + + [Test] + public void NoPathAfterAt() + { + ExceptionAssert.Throws(@"Path ended with open query.", + () => new JPath("[?(@.name||@")); + } + + [Test] + public void NoPathAfterDot() + { + ExceptionAssert.Throws(@"Unexpected end while parsing path.", + () => new JPath("[?(@.name||@.")); + } + + [Test] + public void NoPathAfterDot2() + { + ExceptionAssert.Throws(@"Unexpected end while parsing path.", + () => new JPath("[?(@.name||@.)]")); } [Test] public void FilterWithFloatExp() { JPath path = new JPath("[?(@.name>=5.56789e+0)]"); - List expressions = ((QueryFilter)path.Filters[0]).Expression; - Assert.AreEqual(5.56789e+0, (double)expressions[0].Value); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.AreEqual(5.56789e+0, (double)expressions.Value); } [Test] diff --git a/Src/Newtonsoft.Json.Tests/Linq/JsonPath/QueryExpressionTests.cs b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/QueryExpressionTests.cs new file mode 100644 index 00000000..f918bd13 --- /dev/null +++ b/Src/Newtonsoft.Json.Tests/Linq/JsonPath/QueryExpressionTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq.JsonPath; +using NUnit.Framework; + +namespace Newtonsoft.Json.Tests.Linq.JsonPath +{ + [TestFixture] + public class QueryExpressionTests : TestFixtureBase + { + [Test] + public void AndExpressionTest() + { + CompositeExpression compositeExpression = new CompositeExpression + { + Operator = QueryOperator.And, + Expressions = new List + { + new BooleanQueryExpression + { + Operator = QueryOperator.Exists, + Path = new List + { + new FieldFilter + { + Name = "FirstName" + } + } + }, + new BooleanQueryExpression + { + Operator = QueryOperator.Exists, + Path = new List + { + new FieldFilter + { + Name = "LastName" + } + } + } + } + }; + + JObject o1 = new JObject + { + {"Title","Title!"}, + {"FirstName", "FirstName!"}, + {"LastName", "LastName!"} + }; + + Assert.IsTrue(compositeExpression.IsMatch(o1)); + + JObject o2 = new JObject + { + {"Title","Title!"}, + {"FirstName", "FirstName!"} + }; + + Assert.IsFalse(compositeExpression.IsMatch(o2)); + + JObject o3 = new JObject + { + {"Title","Title!"} + }; + + Assert.IsFalse(compositeExpression.IsMatch(o3)); + } + + [Test] + public void OrExpressionTest() + { + CompositeExpression compositeExpression = new CompositeExpression + { + Operator = QueryOperator.Or, + Expressions = new List + { + new BooleanQueryExpression + { + Operator = QueryOperator.Exists, + Path = new List + { + new FieldFilter + { + Name = "FirstName" + } + } + }, + new BooleanQueryExpression + { + Operator = QueryOperator.Exists, + Path = new List + { + new FieldFilter + { + Name = "LastName" + } + } + } + } + }; + + JObject o1 = new JObject + { + {"Title","Title!"}, + {"FirstName", "FirstName!"}, + {"LastName", "LastName!"} + }; + + Assert.IsTrue(compositeExpression.IsMatch(o1)); + + JObject o2 = new JObject + { + {"Title","Title!"}, + {"FirstName", "FirstName!"} + }; + + Assert.IsTrue(compositeExpression.IsMatch(o2)); + + JObject o3 = new JObject + { + {"Title","Title!"} + }; + + Assert.IsFalse(compositeExpression.IsMatch(o3)); + } + } +} diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj index dab7a83d..3abf477c 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj @@ -211,6 +211,7 @@ + diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/AndFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/AndFilter.cs new file mode 100644 index 00000000..effb2cd3 --- /dev/null +++ b/Src/Newtonsoft.Json/Linq/JsonPath/AndFilter.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Newtonsoft.Json.Linq.JsonPath +{ + internal class AndFilter : PathFilter + { + public List Filters { get; set; } + + public override IEnumerable ExecuteFilter(IEnumerable current, bool errorWhenNoMatch) + { + foreach (PathFilter f in Filters) + { + current = f.ExecuteFilter(current, errorWhenNoMatch).ToList(); + } + + return current; + } + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs b/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs index 8e80e574..c83c5251 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/JPath.cs @@ -85,6 +85,7 @@ namespace Newtonsoft.Json.Linq.JsonPath { bool scan = false; bool followingIndexer = false; + bool followingDot = false; bool ended = false; while (_currentIndex < _expression.Length && !ended) @@ -107,6 +108,7 @@ namespace Newtonsoft.Json.Linq.JsonPath _currentIndex++; currentPartStartIndex = _currentIndex; followingIndexer = true; + followingDot = false; break; case ']': case ')': @@ -135,9 +137,10 @@ namespace Newtonsoft.Json.Linq.JsonPath _currentIndex++; currentPartStartIndex = _currentIndex; followingIndexer = false; + followingDot = true; break; default: - if (query && (currentChar == '=' || currentChar == '<' || currentChar == '!' || currentChar == '>')) + if (query && (currentChar == '=' || currentChar == '<' || currentChar == '!' || currentChar == '>' || currentChar == '|' || currentChar == '&')) { ended = true; } @@ -152,6 +155,8 @@ namespace Newtonsoft.Json.Linq.JsonPath } } + bool atPathEnd = (_currentIndex == _expression.Length); + if (_currentIndex > currentPartStartIndex) { string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex).TrimEnd(); @@ -160,8 +165,14 @@ namespace Newtonsoft.Json.Linq.JsonPath PathFilter filter = (scan) ? (PathFilter)new ScanFilter() { Name = member } : new FieldFilter() { Name = member }; filters.Add(filter); } + else + { + // no field name following dot in path and at end of base path/query + if (followingDot && (atPathEnd || query)) + throw new JsonException("Unexpected end while parsing path."); + } - return (_currentIndex == _expression.Length); + return atPathEnd; } private PathFilter ParseIndexer(char indexerOpenChar) @@ -344,75 +355,118 @@ namespace Newtonsoft.Json.Linq.JsonPath _currentIndex++; - List expressions = new List(); + QueryExpression expression = ParseExpression(); - while (true) + _currentIndex++; + EnsureLength("Path ended with open indexer."); + EatWhitespace(); + + if (_expression[_currentIndex] != indexerCloseChar) + throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]); + + return new QueryFilter { - expressions.Add(ParseExpression()); - - if (_expression[_currentIndex] == ')') - { - _currentIndex++; - EnsureLength("Path ended with open indexer."); - EatWhitespace(); - - if (_expression[_currentIndex] != indexerCloseChar) - throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]); - - return new QueryFilter - { - Expression = expressions - }; - } - else - { - _currentIndex++; - } - } + Expression = expression + }; } private QueryExpression ParseExpression() { - EatWhitespace(); + QueryExpression rootExpression = null; + CompositeExpression parentExpression = null; - if (_expression[_currentIndex] != '@') - throw new JsonException("Unexpected character while parsing path query: " + _expression[_currentIndex]); - - _currentIndex++; - - List expressionPath = new List(); - - if (ParsePath(expressionPath, _currentIndex, true)) - throw new JsonException("Path ended with open query."); - - EatWhitespace(); - EnsureLength("Path ended with open query."); - - QueryOperator op; - object value = null; - if (_expression[_currentIndex] == ')') + while (_currentIndex < _expression.Length) { - op = QueryOperator.Exists; - } - else - { - op = ParseOperator(); + EatWhitespace(); + + if (_expression[_currentIndex] != '@') + throw new JsonException("Unexpected character while parsing path query: " + _expression[_currentIndex]); + + _currentIndex++; + + List expressionPath = new List(); + + if (ParsePath(expressionPath, _currentIndex, true)) + throw new JsonException("Path ended with open query."); EatWhitespace(); EnsureLength("Path ended with open query."); - value = ParseValue(); + QueryOperator op; + object value = null; + if (_expression[_currentIndex] == ')' + || _expression[_currentIndex] == '|' + || _expression[_currentIndex] == '&') + { + op = QueryOperator.Exists; + } + else + { + op = ParseOperator(); - EatWhitespace(); - EnsureLength("Path ended with open query."); + EatWhitespace(); + EnsureLength("Path ended with open query."); + + value = ParseValue(); + + EatWhitespace(); + EnsureLength("Path ended with open query."); + } + + BooleanQueryExpression booleanExpression = new BooleanQueryExpression + { + Path = expressionPath, + Operator = op, + Value = (op != QueryOperator.Exists) ? new JValue(value) : null + }; + + if (_expression[_currentIndex] == ')') + { + if (parentExpression != null) + { + parentExpression.Expressions.Add(booleanExpression); + return rootExpression; + } + + return booleanExpression; + } + if (_expression[_currentIndex] == '&' && Match("&&")) + { + if (parentExpression == null || parentExpression.Operator != QueryOperator.And) + { + CompositeExpression andExpression = new CompositeExpression { Operator = QueryOperator.And }; + + if (parentExpression != null) + parentExpression.Expressions.Add(andExpression); + + parentExpression = andExpression; + + if (rootExpression == null) + rootExpression = parentExpression; + } + + parentExpression.Expressions.Add(booleanExpression); + } + if (_expression[_currentIndex] == '|' && Match("||")) + { + if (parentExpression == null || parentExpression.Operator != QueryOperator.Or) + { + CompositeExpression orExpression = new CompositeExpression { Operator = QueryOperator.Or }; + + if (parentExpression != null) + parentExpression.Expressions.Add(orExpression); + + parentExpression = orExpression; + + if (rootExpression == null) + rootExpression = parentExpression; + } + + parentExpression.Expressions.Add(booleanExpression); + } } - return new QueryExpression - { - Path = expressionPath, - Operator = op, - Value = (op != QueryOperator.Exists) ? new JValue(value) : null - }; + throw new JsonException("Path ended with open query."); } private object ParseValue() diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/OrFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/OrFilter.cs new file mode 100644 index 00000000..12601ce1 --- /dev/null +++ b/Src/Newtonsoft.Json/Linq/JsonPath/OrFilter.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Newtonsoft.Json.Linq.JsonPath +{ + internal class OrFilter : PathFilter + { + public List Filters { get; set; } + + public override IEnumerable ExecuteFilter(IEnumerable current, bool errorWhenNoMatch) + { + List cachedCurrent = current.ToList(); + List remaining = cachedCurrent.ToList(); + List matches = new List(); + int previousResultCount = 0; + + foreach (PathFilter f in Filters) + { + matches.AddRange(f.ExecuteFilter(remaining, errorWhenNoMatch)); + + for (int i = previousResultCount; i < matches.Count; i++) + { + remaining.Remove(matches[i]); + } + previousResultCount = matches.Count; + } + + return cachedCurrent.Where(matches.Contains); + } + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Linq/JsonPath/QueryFilter.cs b/Src/Newtonsoft.Json/Linq/JsonPath/QueryFilter.cs index d06b96b1..106ca42b 100644 --- a/Src/Newtonsoft.Json/Linq/JsonPath/QueryFilter.cs +++ b/Src/Newtonsoft.Json/Linq/JsonPath/QueryFilter.cs @@ -13,19 +13,103 @@ namespace Newtonsoft.Json.Linq.JsonPath LessThan, LessThanOrEquals, GreaterThan, - GreaterThanOrEquals + GreaterThanOrEquals, + And, + Or } - internal class QueryExpression + internal abstract class QueryExpression + { + public QueryOperator Operator { get; set; } + + public abstract bool IsMatch(JToken t); + } + + internal class CompositeExpression : QueryExpression + { + public List Expressions { get; set; } + + public CompositeExpression() + { + Expressions = new List(); + } + + public override bool IsMatch(JToken t) + { + switch (Operator) + { + case QueryOperator.And: + foreach (QueryExpression e in Expressions) + { + if (!e.IsMatch(t)) + return false; + } + return true; + case QueryOperator.Or: + foreach (QueryExpression e in Expressions) + { + if (e.IsMatch(t)) + return true; + } + return false; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + internal class BooleanQueryExpression : QueryExpression { public List Path { get; set; } - public QueryOperator Operator { get; set; } public JValue Value { get; set; } + + public override bool IsMatch(JToken t) + { + IEnumerable pathResult = JPath.Evaluate(Path, t, false); + + foreach (JToken r in pathResult) + { + JValue v = r as JValue; + switch (Operator) + { + case QueryOperator.Equals: + if (v != null && v.Equals(Value)) + return true; + break; + case QueryOperator.NotEquals: + if (v != null && !v.Equals(Value)) + return true; + break; + case QueryOperator.GreaterThan: + if (v != null && v.CompareTo(Value) > 0) + return true; + break; + case QueryOperator.GreaterThanOrEquals: + if (v != null && v.CompareTo(Value) >= 0) + return true; + break; + case QueryOperator.LessThan: + if (v != null && v.CompareTo(Value) < 0) + return true; + break; + case QueryOperator.LessThanOrEquals: + if (v != null && v.CompareTo(Value) <= 0) + return true; + break; + case QueryOperator.Exists: + return true; + default: + throw new ArgumentOutOfRangeException(); + } + } + + return false; + } } internal class QueryFilter : PathFilter { - public List Expression { get; set; } + public QueryExpression Expression { get; set; } public override IEnumerable ExecuteFilter(IEnumerable current, bool errorWhenNoMatch) { @@ -33,56 +117,10 @@ namespace Newtonsoft.Json.Linq.JsonPath { foreach (JToken v in t) { - if (Evaluate(errorWhenNoMatch, v)) + if (Expression.IsMatch(v)) yield return v; } } } - - private bool Evaluate(bool errorWhenNoMatch, JToken value) - { - foreach (QueryExpression expression in Expression) - { - IEnumerable pathResult = JPath.Evaluate(expression.Path, value, errorWhenNoMatch); - - foreach (JToken r in pathResult) - { - JValue v = r as JValue; - switch (expression.Operator) - { - case QueryOperator.Equals: - if (v != null && v.Equals(expression.Value)) - return true; - break; - case QueryOperator.NotEquals: - if (v != null && !v.Equals(expression.Value)) - return true; - break; - case QueryOperator.GreaterThan: - if (v != null && v.CompareTo(expression.Value) > 0) - return true; - break; - case QueryOperator.GreaterThanOrEquals: - if (v != null && v.CompareTo(expression.Value) >= 0) - return true; - break; - case QueryOperator.LessThan: - if (v != null && v.CompareTo(expression.Value) < 0) - return true; - break; - case QueryOperator.LessThanOrEquals: - if (v != null && v.CompareTo(expression.Value) <= 0) - return true; - break; - case QueryOperator.Exists: - return true; - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - return false; - } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.csproj index f08cb031..f6c11f07 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.csproj @@ -111,6 +111,7 @@ + @@ -120,6 +121,7 @@ +