diff --git a/Src/Newtonsoft.Json.Tests/Linq/JPathTests.cs b/Src/Newtonsoft.Json.Tests/Linq/JPathTests.cs index a4cf505d..e719ed74 100644 --- a/Src/Newtonsoft.Json.Tests/Linq/JPathTests.cs +++ b/Src/Newtonsoft.Json.Tests/Linq/JPathTests.cs @@ -50,7 +50,23 @@ namespace Newtonsoft.Json.Tests.Linq { JPath path = new JPath("Blah"); Assert.AreEqual(1, path.Parts.Count); - Assert.AreEqual("Blah", path.Parts[0]); + Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name); + } + + [Test] + public void SinglePropertyWithRoot() + { + JPath path = new JPath("$.Blah"); + Assert.AreEqual(1, path.Parts.Count); + Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name); + } + + [Test] + public void SingleScanWithRoot() + { + JPath path = new JPath("$..Blah"); + Assert.AreEqual(1, path.Parts.Count); + Assert.AreEqual("Blah", ((ScanFilter)path.Parts[0]).Name); } [Test] @@ -58,8 +74,17 @@ namespace Newtonsoft.Json.Tests.Linq { JPath path = new JPath("Blah.Two"); Assert.AreEqual(2, path.Parts.Count); - Assert.AreEqual("Blah", path.Parts[0]); - Assert.AreEqual("Two", path.Parts[1]); + Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name); + Assert.AreEqual("Two", ((FieldFilter)path.Parts[1]).Name); + } + + [Test] + public void OnePropertyOneScan() + { + JPath path = new JPath("Blah..Two"); + Assert.AreEqual(2, path.Parts.Count); + Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name); + Assert.AreEqual("Two", ((ScanFilter)path.Parts[1]).Name); } [Test] @@ -67,21 +92,34 @@ namespace Newtonsoft.Json.Tests.Linq { JPath path = new JPath("Blah[0]"); Assert.AreEqual(2, path.Parts.Count); - Assert.AreEqual("Blah", path.Parts[0]); - Assert.AreEqual(0, path.Parts[1]); + Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name); + Assert.AreEqual(0, ((ArrayIndexFilter)path.Parts[1]).Index); + } + + [Test] + public void SinglePropertyAndFilter() + { + JPath path = new JPath("Blah[?(@.name=='hi')]"); + Assert.AreEqual(2, path.Parts.Count); + Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name); + List expressions = ((QueryFilter)path.Parts[1]).Expression; + Assert.AreEqual(1, expressions.Count); } [Test] public void MultiplePropertiesAndIndexers() { - JPath path = new JPath("Blah[0].Two.Three[1].Four"); + JPath path = new JPath("Blah[0]..Two.Three[1].Four"); Assert.AreEqual(6, path.Parts.Count); - Assert.AreEqual("Blah", path.Parts[0]); - Assert.AreEqual(0, path.Parts[1]); - Assert.AreEqual("Two", path.Parts[2]); - Assert.AreEqual("Three", path.Parts[3]); - Assert.AreEqual(1, path.Parts[4]); - Assert.AreEqual("Four", path.Parts[5]); + Assert.AreEqual(FilterType.Field, path.Parts[0].Type); + Assert.AreEqual("Blah", ((FieldFilter) path.Parts[0]).Name); + Assert.AreEqual(FilterType.ArrayIndex, path.Parts[1].Type); + Assert.AreEqual(0, ((ArrayIndexFilter) path.Parts[1]).Index); + Assert.AreEqual(FilterType.Scan, path.Parts[2].Type); + Assert.AreEqual("Two", ((ScanFilter)path.Parts[2]).Name); + Assert.AreEqual("Three", ((FieldFilter)path.Parts[3]).Name); + Assert.AreEqual(1, ((ArrayIndexFilter)path.Parts[4]).Index); + Assert.AreEqual("Four", ((FieldFilter)path.Parts[5]).Name); } [Test] @@ -96,36 +134,46 @@ namespace Newtonsoft.Json.Tests.Linq public void UnclosedIndexer() { ExceptionAssert.Throws( - @"Path ended with open indexer. Expected ]", + @"Path ended with open indexer.", () => { new JPath("Blah[0"); }); } - [Test] - public void AdditionalDots() - { - JPath path = new JPath(".Blah..[0]..Two.Three....[1].Four."); - Assert.AreEqual(6, path.Parts.Count); - Assert.AreEqual("Blah", path.Parts[0]); - Assert.AreEqual(0, path.Parts[1]); - Assert.AreEqual("Two", path.Parts[2]); - Assert.AreEqual("Three", path.Parts[3]); - Assert.AreEqual(1, path.Parts[4]); - Assert.AreEqual("Four", path.Parts[5]); - } + //[Test] + //public void AdditionalDots() + //{ + // JPath path = new JPath(".Blah..[0]..Two.Three....[1].Four."); + // Assert.AreEqual(6, path.Parts.Count); + // Assert.AreEqual("Blah", path.Parts[0]); + // Assert.AreEqual(0, path.Parts[1]); + // Assert.AreEqual("Two", path.Parts[2]); + // Assert.AreEqual("Three", path.Parts[3]); + // Assert.AreEqual(1, path.Parts[4]); + // Assert.AreEqual("Four", path.Parts[5]); + //} [Test] public void IndexerOnly() { JPath path = new JPath("[111119990]"); Assert.AreEqual(1, path.Parts.Count); - Assert.AreEqual(111119990, path.Parts[0]); + Assert.AreEqual(111119990, ((ArrayIndexFilter)path.Parts[0]).Index); + } + + [Test] + public void MultipleIndexes() + { + JPath path = new JPath("[111119990,3]"); + Assert.AreEqual(1, path.Parts.Count); + Assert.AreEqual(2, ((ArrayMultipleIndexFilter)path.Parts[0]).Indexes.Count); + Assert.AreEqual(111119990, ((ArrayMultipleIndexFilter)path.Parts[0]).Indexes[0]); + Assert.AreEqual(3, ((ArrayMultipleIndexFilter)path.Parts[0]).Indexes[1]); } [Test] public void EmptyIndexer() { ExceptionAssert.Throws( - "Empty path indexer.", + "Array index expected.", () => { new JPath("[]"); }); } @@ -142,10 +190,10 @@ namespace Newtonsoft.Json.Tests.Linq { JPath path = new JPath("[1][0][0][" + int.MaxValue + "]"); Assert.AreEqual(4, path.Parts.Count); - Assert.AreEqual(1, path.Parts[0]); - Assert.AreEqual(0, path.Parts[1]); - Assert.AreEqual(0, path.Parts[2]); - Assert.AreEqual(int.MaxValue, path.Parts[3]); + Assert.AreEqual(1, ((ArrayIndexFilter)path.Parts[0]).Index); + Assert.AreEqual(0, ((ArrayIndexFilter)path.Parts[1]).Index); + Assert.AreEqual(0, ((ArrayIndexFilter)path.Parts[2]).Index); + Assert.AreEqual(int.MaxValue, ((ArrayIndexFilter)path.Parts[3]).Index); } [Test] @@ -168,6 +216,18 @@ namespace Newtonsoft.Json.Tests.Linq Assert.AreEqual(1, (int)t); } + [Test] + public void QuoteName() + { + JObject o = new JObject( + new JProperty("Blah", 1)); + + JToken t = o.SelectToken("['Blah']"); + Assert.IsNotNull(t); + Assert.AreEqual(JTokenType.Integer, t.Type); + Assert.AreEqual(1, (int)t); + } + [Test] public void EvaluateMissingProperty() { @@ -269,6 +329,19 @@ namespace Newtonsoft.Json.Tests.Linq Assert.AreEqual(2, (int)t); } + [Test] + public void EvaluateArrayMultipleIndexes() + { + JArray a = new JArray(1, 2, 3, 4); + + IEnumerable t = a.SelectTokens("[1,2,0]"); + Assert.IsNotNull(t); + Assert.AreEqual(3, t.Count()); + Assert.AreEqual(2, (int)t.ElementAt(0)); + Assert.AreEqual(3, (int)t.ElementAt(1)); + Assert.AreEqual(1, (int)t.ElementAt(2)); + } + [Test] public void EvaluateSinglePropertyReturningArray() { diff --git a/Src/Newtonsoft.Json/Linq/JPath.cs b/Src/Newtonsoft.Json/Linq/JPath.cs index 188ec6ca..c2f6886b 100644 --- a/Src/Newtonsoft.Json/Linq/JPath.cs +++ b/Src/Newtonsoft.Json/Linq/JPath.cs @@ -26,14 +26,92 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Newtonsoft.Json.Utilities; namespace Newtonsoft.Json.Linq { + internal enum FilterType + { + ArrayIndex, + ArrayMultipleIndex, + ArraySlice, + Field, + Scan, + Query + } + + internal abstract class PathFilter + { + public abstract FilterType Type { get; } + } + + internal class ArrayIndexFilter : PathFilter + { + public int Index { get; set; } + + public override FilterType Type + { + get { return FilterType.ArrayIndex; } + } + } + + internal class ArrayMultipleIndexFilter : PathFilter + { + public List Indexes { get; set; } + + public override FilterType Type + { + get { return FilterType.ArrayMultipleIndex; } + } + } + + internal class ArraySliceFilter : PathFilter + { + public int? Start { get; set; } + public int? End { get; set; } + public int? Skip { get; set; } + + public override FilterType Type + { + get { return FilterType.ArraySlice; } + } + } + + internal class FieldFilter : PathFilter + { + public string Name { get; set; } + + public override FilterType Type + { + get { return FilterType.Field; } + } + } + + internal class ScanFilter : PathFilter + { + public string Name { get; set; } + + public override FilterType Type + { + get { return FilterType.Scan; } + } + } + + internal class QueryFilter : PathFilter + { + public List Expression { get; set; } + + public override FilterType Type + { + get { return FilterType.Query; } + } + } + internal class JPath { private readonly string _expression; - public List Parts { get; private set; } + public List Parts { get; private set; } private int _currentIndex; @@ -41,7 +119,7 @@ namespace Newtonsoft.Json.Linq { ValidationUtils.ArgumentNotNull(expression, "expression"); _expression = expression; - Parts = new List(); + Parts = new List(); ParseMain(); } @@ -50,6 +128,34 @@ namespace Newtonsoft.Json.Linq { int currentPartStartIndex = _currentIndex; bool followingIndexer = false; + bool scan = false; + + EnsureLength("Empty path."); + + if (_expression[_currentIndex] == '$') + { + _currentIndex++; + + EnsureLength("Unexpected end while parsing path."); + + if (_expression[_currentIndex] != '.') + throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]); + + _currentIndex++; + + EnsureLength("Unexpected end while parsing path."); + + if (_expression[_currentIndex] == '.') + { + scan = true; + _currentIndex++; + currentPartStartIndex = 3; + } + else + { + currentPartStartIndex = 2; + } + } while (_currentIndex < _expression.Length) { @@ -62,7 +168,9 @@ namespace Newtonsoft.Json.Linq if (_currentIndex > currentPartStartIndex) { string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex); - Parts.Add(member); + PathFilter filter = (scan) ? (PathFilter)new ScanFilter() { Name = member } : new FieldFilter() { Name = member }; + Parts.Add(filter); + scan = false; } ParseIndexer(currentChar); @@ -76,7 +184,14 @@ namespace Newtonsoft.Json.Linq if (_currentIndex > currentPartStartIndex) { string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex); - Parts.Add(member); + PathFilter filter = (scan) ? (PathFilter)new ScanFilter() { Name = member } : new FieldFilter() { Name = member }; + Parts.Add(filter); + scan = false; + } + if (_currentIndex + 1 < _expression.Length && _expression[_currentIndex + 1] == '.') + { + scan = true; + _currentIndex++; } currentPartStartIndex = _currentIndex + 1; followingIndexer = false; @@ -93,7 +208,8 @@ namespace Newtonsoft.Json.Linq if (_currentIndex > currentPartStartIndex) { string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex); - Parts.Add(member); + PathFilter filter = (scan) ? (PathFilter)new ScanFilter() { Name = member } : new FieldFilter() { Name = member }; + Parts.Add(filter); } } @@ -102,23 +218,74 @@ namespace Newtonsoft.Json.Linq _currentIndex++; char indexerCloseChar = (indexerOpenChar == '[') ? ']' : ')'; - int indexerStart = _currentIndex; - int indexerLength = 0; - bool indexerClosed = false; + + EnsureLength("Path ended with open indexer."); + + if (_expression[_currentIndex] == '\'') + { + ParseQuotedField(indexerCloseChar); + } + else if (_expression[_currentIndex] == '?') + { + ParseQuery(indexerCloseChar); + } + else + { + ParseArrayIndexer(indexerCloseChar); + } + } + + private void ParseArrayIndexer(char indexerCloseChar) + { + int start = _currentIndex; + List indexes = null; + int? startIndex = null; + int? endIndex = null; + int? step = null; while (_currentIndex < _expression.Length) { char currentCharacter = _expression[_currentIndex]; - if (char.IsDigit(currentCharacter)) + + if (currentCharacter == indexerCloseChar) { - indexerLength++; + int length = _currentIndex - start; + + if (length == 0) + throw new JsonException("Array index expected."); + + string indexer = _expression.Substring(start, length); + int index = Convert.ToInt32(indexer, CultureInfo.InvariantCulture); + + if (indexes != null) + { + indexes.Add(index); + Parts.Add(new ArrayMultipleIndexFilter { Indexes = indexes }); + } + else + { + Parts.Add(new ArrayIndexFilter { Index = index }); + } + + return; } - else if (currentCharacter == indexerCloseChar) + + if (currentCharacter == ',') { - indexerClosed = true; - break; + int length = _currentIndex - start; + + if (length == 0) + throw new JsonException("Array index expected."); + + if (indexes == null) + indexes = new List(); + + string indexer = _expression.Substring(start, length); + indexes.Add(Convert.ToInt32(indexer, CultureInfo.InvariantCulture)); + + start = _currentIndex + 1; } - else + else if (!char.IsDigit(currentCharacter)) { throw new JsonException("Unexpected character while parsing path indexer: " + currentCharacter); } @@ -126,83 +293,209 @@ namespace Newtonsoft.Json.Linq _currentIndex++; } - if (!indexerClosed) - throw new JsonException("Path ended with open indexer. Expected " + indexerCloseChar); - - if (indexerLength == 0) - throw new JsonException("Empty path indexer."); - - string indexer = _expression.Substring(indexerStart, indexerLength); - Parts.Add(Convert.ToInt32(indexer, CultureInfo.InvariantCulture)); + throw new JsonException("Path ended with open indexer."); } - internal JToken Evaluate(JToken root, bool errorWhenNoMatch) + private void ParseQuery(char indexerCloseChar) { - JToken current = root; + _currentIndex++; + EnsureLength("Path ended with open indexer."); - foreach (object part in Parts) + if (_expression[_currentIndex] != '(') + throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]); + + List expressions = new List(); + + while (true) { - string propertyName = part as string; - if (propertyName != null) + ParseExpression(); + expressions.Add(string.Empty); + + if (_expression[_currentIndex] == ')') { - JObject o = current as JObject; - if (o != null) + _currentIndex++; + EnsureLength("Path ended with open indexer."); + if (_expression[_currentIndex] != indexerCloseChar) + throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]); + + Parts.Add(new QueryFilter { - current = o[propertyName]; - - if (current == null && errorWhenNoMatch) - throw new JsonException("Property '{0}' does not exist on JObject.".FormatWith(CultureInfo.InvariantCulture, propertyName)); - } - else - { - if (errorWhenNoMatch) - throw new JsonException("Property '{0}' not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, propertyName, current.GetType().Name)); - - return null; - } + Expression = expressions + }); + return; } else { - int index = (int)part; + _currentIndex++; + } + } + } - JArray a = current as JArray; - JConstructor c = current as JConstructor; + private void ParseExpression() + { + _currentIndex++; + while (_currentIndex < _expression.Length) + { + char currentCharacter = _expression[_currentIndex]; - if (a != null) - { - if (a.Count <= index) - { - if (errorWhenNoMatch) - throw new JsonException("Index {0} outside the bounds of JArray.".FormatWith(CultureInfo.InvariantCulture, index)); + if (currentCharacter == ')') + { + return; + } + else + { + _currentIndex++; + } + } + } - return null; - } + private void ParseQuotedField(char indexerCloseChar) + { + _currentIndex++; + int start = _currentIndex; - current = a[index]; - } - else if (c != null) - { - if (c.Count <= index) - { - if (errorWhenNoMatch) - throw new JsonException("Index {0} outside the bounds of JConstructor.".FormatWith(CultureInfo.InvariantCulture, index)); + while (_currentIndex < _expression.Length) + { + char currentCharacter = _expression[_currentIndex]; + if (currentCharacter == '\'') + { + int length = _currentIndex - start; - return null; - } + if (length == 0) + throw new JsonException("Empty path indexer."); - current = c[index]; - } - else - { - if (errorWhenNoMatch) - throw new JsonException("Index {0} not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, index, current.GetType().Name)); + // check that character after the quote is to close the index + _currentIndex++; + EnsureLength("Path ended with open indexer."); + if (_expression[_currentIndex] != indexerCloseChar) + throw new JsonException("Unexpected character while parsing path indexer: " + currentCharacter); - return null; - } + string indexer = _expression.Substring(start, length); + Parts.Add(new FieldFilter { Name = indexer }); + + return; + } + + _currentIndex++; + } + + throw new JsonException("Path ended with open indexer."); + } + + private void EnsureLength(string message) + { + if (_currentIndex >= _expression.Length) + throw new JsonException(message); + } + + internal IEnumerable Evaluate(JToken root, bool errorWhenNoMatch) + { + IEnumerable current = new [] { root }; + + foreach (PathFilter filter in Parts) + { + switch (filter.Type) + { + case FilterType.ArrayIndex: + current = ExecuteArrayIndexFilter(current, errorWhenNoMatch, (ArrayIndexFilter)filter); + break; + case FilterType.ArrayMultipleIndex: + current = ExecuteArrayMultipleIndexFilter(current, errorWhenNoMatch, (ArrayMultipleIndexFilter)filter); + break; + case FilterType.Field: + current = ExecuteFieldFilter(current, errorWhenNoMatch, (FieldFilter)filter); + break; + default: + throw new ArgumentOutOfRangeException(); } } return current; } + + private IEnumerable ExecuteArrayMultipleIndexFilter(IEnumerable current, bool errorWhenNoMatch, ArrayMultipleIndexFilter arrayMultipleIndexFilter) + { + foreach (JToken t in current) + { + foreach (int i in arrayMultipleIndexFilter.Indexes) + { + JToken v = GetTokenIndex(t, errorWhenNoMatch, i); + + if (v != null) + yield return v; + } + } + } + + private static IEnumerable ExecuteFieldFilter(IEnumerable current, bool errorWhenNoMatch, FieldFilter fieldFilter) + { + foreach (JToken t in current) + { + JObject o = t as JObject; + if (o != null) + { + JToken v = o[fieldFilter.Name]; + + if (v == null && errorWhenNoMatch) + throw new JsonException("Property '{0}' does not exist on JObject.".FormatWith(CultureInfo.InvariantCulture, fieldFilter.Name)); + + yield return v; + } + else + { + if (errorWhenNoMatch) + throw new JsonException("Property '{0}' not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, fieldFilter.Name, t.GetType().Name)); + } + } + } + + private static IEnumerable ExecuteArrayIndexFilter(IEnumerable current, bool errorWhenNoMatch, ArrayIndexFilter arrayIndexFilter) + { + foreach (JToken t in current) + { + JToken v = GetTokenIndex(t, errorWhenNoMatch, arrayIndexFilter.Index); + + if (v != null) + yield return v; + } + } + + private static JToken GetTokenIndex(JToken t, bool errorWhenNoMatch, int index) + { + JArray a = t as JArray; + JConstructor c = t as JConstructor; + + if (a != null) + { + if (a.Count <= index) + { + if (errorWhenNoMatch) + throw new JsonException("Index {0} outside the bounds of JArray.".FormatWith(CultureInfo.InvariantCulture, index)); + + return null; + } + + return a[index]; + } + else if (c != null) + { + if (c.Count <= index) + { + if (errorWhenNoMatch) + throw new JsonException("Index {0} outside the bounds of JConstructor.".FormatWith(CultureInfo.InvariantCulture, index)); + + return null; + } + + return c[index]; + } + else + { + if (errorWhenNoMatch) + throw new JsonException("Index {0} not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, index, t.GetType().Name)); + + return null; + } + } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Linq/JToken.cs b/Src/Newtonsoft.Json/Linq/JToken.cs index ffce1d22..544b1b45 100644 --- a/Src/Newtonsoft.Json/Linq/JToken.cs +++ b/Src/Newtonsoft.Json/Linq/JToken.cs @@ -1932,6 +1932,23 @@ namespace Newtonsoft.Json.Linq /// A flag to indicate whether an error should be thrown if no token is found. /// The that matches the object path. public JToken SelectToken(string path, bool errorWhenNoMatch) + { + JPath p = new JPath(path); + + // todo change this to a foreach + List result = p.Evaluate(this, errorWhenNoMatch).ToList(); + if (result.Count > 1) + throw new Exception("TODO"); + + return result.SingleOrDefault(); + } + + public IEnumerable SelectTokens(string path) + { + return SelectTokens(path, false); + } + + public IEnumerable SelectTokens(string path, bool errorWhenNoMatch) { JPath p = new JPath(path); return p.Evaluate(this, errorWhenNoMatch);