/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.skyscreamer.jsonassert;
import java.text.MessageFormat;
import org.json.JSONArray;
import org.json.JSONException;
import org.skyscreamer.jsonassert.comparator.JSONComparator;
/**
* <p>A value matcher for arrays. This operates like STRICT_ORDER array match,
* however if expected array has less elements than actual array the matching
* process loops through the expected array to get expected elements for the
* additional actual elements. In general the expected array will contain a
* single element which is matched against each actual array element in turn.
* This allows simple verification of constant array element components and
* coupled with RegularExpressionValueMatcher can be used to match specific
* array element components against a regular expression pattern. As a convenience to reduce syntactic complexity of expected string, if the
* expected object is not an array, a one element expected array is created
* containing whatever is provided as the expected value.</p>
*
* <p>Some examples of typical usage idioms listed below.</p>
*
* <p>Assuming JSON to be verified is held in String variable ARRAY_OF_JSONOBJECTS and contains:</p>
*
* <pre>{@code
* {a:[{background:white, id:1, type:row},
* {background:grey, id:2, type:row},
* {background:white, id:3, type:row},
* {background:grey, id:4, type:row}]}
* }</pre>
*
* <p>then:</p>
*
* <p>To verify that the 'id' attribute of first element of array 'a' is '1':</p>
*
* <pre>{@code
* JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);
* Customization customization = new Customization("a", new ArrayValueMatcher<Object>(comparator, 0));
* JSONAssert.assertEquals("{a:[{id:1}]}", ARRAY_OF_JSONOBJECTS,
* new CustomComparator(JSONCompareMode.LENIENT, customization));
* }</pre>
*
* <p>To simplify complexity of expected JSON string, the value <code>"a:[{id:1}]}"</code> may be replaced by <code>"a:{id:1}}"</code></p>
*
* <p>To verify that the 'type' attribute of second and third elements of array 'a' is 'row':</p>
*
* <pre>{@code
* JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);
* Customization customization = new Customization("a", new ArrayValueMatcher<Object>(comparator, 1, 2));
* JSONAssert.assertEquals("{a:[{type:row}]}", ARRAY_OF_JSONOBJECTS,
* new CustomComparator(JSONCompareMode.LENIENT, customization));
* }</pre>
*
* <p>To verify that the 'type' attribute of every element of array 'a' is 'row':</p>
*
* <pre>{@code
* JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);
* Customization customization = new Customization("a", new ArrayValueMatcher<Object>(comparator));
* JSONAssert.assertEquals("{a:[{type:row}]}", ARRAY_OF_JSONOBJECTS,
* new CustomComparator(JSONCompareMode.LENIENT, customization));
* }</pre>
*
* <p>To verify that the 'id' attribute of every element of array 'a' matches regular expression '\d+'. This requires a custom comparator to specify regular expression to be used to validate each array element, hence the array of Customization instances:</p>
*
* <pre>{@code
* // get length of array we will verify
* int aLength = ((JSONArray)((JSONObject)JSONParser.parseJSON(ARRAY_OF_JSONOBJECTS)).get("a")).length();
* // create array of customizations one for each array element
* RegularExpressionValueMatcher<Object> regExValueMatcher =
* new RegularExpressionValueMatcher<Object>("\\d+"); // matches one or more digits
* Customization[] customizations = new Customization[aLength];
* for (int i=0; i<aLength; i++) {
* String contextPath = "a["+i+"].id";
* customizations[i] = new Customization(contextPath, regExValueMatcher);
* }
* CustomComparator regExComparator = new CustomComparator(JSONCompareMode.STRICT_ORDER, customizations);
* ArrayValueMatcher<Object> regExArrayValueMatcher = new ArrayValueMatcher<Object>(regExComparator);
* Customization regExArrayValueCustomization = new Customization("a", regExArrayValueMatcher);
* CustomComparator regExCustomArrayValueComparator =
* new CustomComparator(JSONCompareMode.STRICT_ORDER, new Customization[] { regExArrayValueCustomization });
* JSONAssert.assertEquals("{a:[{id:X}]}", ARRAY_OF_JSONOBJECTS, regExCustomArrayValueComparator);
* }</pre>
*
* <p>To verify that the 'background' attribute of every element of array 'a' alternates between 'white' and 'grey' starting with first element 'background' being 'white':</p>
*
* <pre>{@code
* JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);
* Customization customization = new Customization("a", new ArrayValueMatcher<Object>(comparator));
* JSONAssert.assertEquals("{a:[{background:white},{background:grey}]}", ARRAY_OF_JSONOBJECTS,
* new CustomComparator(JSONCompareMode.LENIENT, customization));
* }</pre>
*
* <p>Assuming JSON to be verified is held in String variable ARRAY_OF_JSONARRAYS and contains:</p>
*
* <code>{a:[[6,7,8], [9,10,11], [12,13,14], [19,20,21,22]]}</code>
*
* <p>then:</p>
*
* <p>To verify that the first three elements of JSON array 'a' are JSON arrays of length 3:</p>
*
* <pre>{@code
* JSONComparator comparator = new ArraySizeComparator(JSONCompareMode.STRICT_ORDER);
* Customization customization = new Customization("a", new ArrayValueMatcher<Object>(comparator, 0, 2));
* JSONAssert.assertEquals("{a:[[3]]}", ARRAY_OF_JSONARRAYS, new CustomComparator(JSONCompareMode.LENIENT, customization));
* }</pre>
*
* <p>NOTE: simplified expected JSON strings are not possible in this case as ArraySizeComparator does not support them.</p>
*
* <p>To verify that the second elements of JSON array 'a' is a JSON array whose first element has the value 9:</p>
*
* <pre>{@code
* JSONComparator innerComparator = new DefaultComparator(JSONCompareMode.LENIENT);
* Customization innerCustomization = new Customization("a[1]", new ArrayValueMatcher<Object>(innerComparator, 0));
* JSONComparator comparator = new CustomComparator(JSONCompareMode.LENIENT, innerCustomization);
* Customization customization = new Customization("a", new ArrayValueMatcher<Object>(comparator, 1));
* JSONAssert.assertEquals("{a:[[9]]}", ARRAY_OF_JSONARRAYS, new CustomComparator(JSONCompareMode.LENIENT, customization));
* }</pre>
*
* <p>To simplify complexity of expected JSON string, the value <code>"{a:[[9]]}"</code> may be replaced by <code>"{a:[9]}"</code> or <code>"{a:9}"</code></p>
*
* @author Duncan Mackinder
*
*/
public class ArrayValueMatcher<T> implements LocationAwareValueMatcher<T> {
private final JSONComparator comparator;
private final int from;
private final int to;
/**
* Create ArrayValueMatcher to match every element in actual array against
* elements taken in sequence from expected array, repeating from start of
* expected array if necessary.
*
* @param comparator
* comparator to use to compare elements
*/
public ArrayValueMatcher(JSONComparator comparator) {
this(comparator, 0, Integer.MAX_VALUE);
}
/**
* Create ArrayValueMatcher to match specified element in actual array
* against first element of expected array.
*
* @param comparator
* comparator to use to compare elements
* @param index
* index of the array element to be compared
*/
public ArrayValueMatcher(JSONComparator comparator, int index) {
this(comparator, index, index);
}
/**
* Create ArrayValueMatcher to match every element in specified range
* (inclusive) from actual array against elements taken in sequence from
* expected array, repeating from start of expected array if necessary.
*
* @param comparator
* comparator to use to compare elements
* @param from first element in actual array to compared
* @param to last element in actual array to compared
*/
public ArrayValueMatcher(JSONComparator comparator, int from, int to) {
assert comparator != null : "comparator null";
assert from >= 0 : MessageFormat.format("from({0}) < 0", from);
assert to >= from : MessageFormat.format("to({0}) < from({1})", to,
from);
this.comparator = comparator;
this.from = from;
this.to = to;
}
@Override
/*
* NOTE: method defined as required by ValueMatcher interface but will never
* be called so defined simply to indicate match failure
*/
public boolean equal(T o1, T o2) {
return false;
}
@Override
public boolean equal(String prefix, T actual, T expected, JSONCompareResult result) {
if (!(actual instanceof JSONArray)) {
throw new IllegalArgumentException("ArrayValueMatcher applied to non-array actual value");
}
try {
JSONArray actualArray = (JSONArray) actual;
JSONArray expectedArray = expected instanceof JSONArray ? (JSONArray) expected: new JSONArray(new Object[] { expected });
int first = Math.max(0, from);
int last = Math.min(actualArray.length() - 1, to);
int expectedLen = expectedArray.length();
for (int i = first; i <= last; i++) {
String elementPrefix = MessageFormat.format("{0}[{1}]", prefix, i);
Object actualElement = actualArray.get(i);
Object expectedElement = expectedArray.get((i - first) % expectedLen);
comparator.compareValues(elementPrefix, expectedElement, actualElement, result);
}
// any failures have already been passed to result, so return true
return true;
}
catch (JSONException e) {
return false;
}
}
}