/* * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Nicolas Chapurlat <nchapurlat@nuxeo.com> */ package org.nuxeo.ecm.core.io.marshallers.json; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.codehaus.jackson.JsonNode; import org.junit.Assert; /** * Makes chained assertion on json. * * @since 7.2 */ public class JsonAssert { private JsonNode jsonNode; private JsonAssert(String json) throws IOException { jsonNode = JsonFactoryProvider.get().createJsonParser(json).readValueAsTree(); } private JsonAssert(JsonNode jsonNode) { this.jsonNode = jsonNode; } /** * Create an Json assertion on the given json string. * * @param json The json on which you want to make assertion. */ public static JsonAssert on(String json) throws IOException { return new JsonAssert(json); } /** * Gets the underlying {@link JsonNode} of the current {@link JsonAssert}. * * @return A {@link JsonNode} * @since 7.2 */ public JsonNode getNode() { return jsonNode; } /** * Prints the current json content. */ @Override public String toString() { return jsonNode.toString(); } /** * Gets a json assertion on the element at the given index. Works on array. * * @param index The index in the array. * @return A json assertion on the element of the array. * @since 7.2 */ public JsonAssert get(int index) throws IOException { return get("[" + index + "]"); } /** * Gets a json assertion on the element of which the path in the current {@link JsonAssert} is the given json path. * * @param jsonPath The json path: property or [index] or property.subProperty[index] or * [index].property[index][index] * @return A json assertion on the element targeted by the given path. * @since 7.2 */ public JsonAssert get(String jsonPath) throws IOException { // tokenize the path String SEPARATOR = "<SEP>"; String pattern = "[\\[\\]\\.]"; Iterator<String> tokens = Arrays.asList( jsonPath.replaceAll("(" + pattern + ")", SEPARATOR + "$1" + SEPARATOR).split(SEPARATOR)).iterator(); JsonNode jn = jsonNode; String read = ""; // iterates on tokens and navigate i json nodes while (tokens.hasNext()) { // ends when all token are read String token = tokens.next(); if (StringUtils.isBlank(token)) { continue; } switch (token) { case ".": // simple separator, ignore it read += token; break; case "[": // an index in a rray is expected, checks its an array an navigate in the array element read += token; if (!tokens.hasNext()) { throw new IOException("Invalid json parameter value : [ must be followed by a index and by ] :" + read); } // get the index Integer index = null; try { index = Integer.valueOf(tokens.next()); read += index; } catch (NumberFormatException e) { throw new IOException("Invalid json parameter value : [ must be followed by a index and by ] :" + read); } if (index < 0) { throw new IOException("Invalid json parameter value : [ must be followed by a index and by ] :" + read); } if (!tokens.hasNext() || !"]".equals(tokens.next())) { throw new IOException("Invalid json parameter value : [ must be followed by a index and by ] :" + read); } read += "]"; // checks its an array if (!jn.isArray()) { throw new IOException(read + " is not a array"); } // checks the index exists if (!jn.has(index)) { return null; } // navigate jn = jn.get(index); break; default: // a property, navigate in the property if the current its an abject // checks its an array if (!jn.isObject()) { throw new IOException(read + " is not an object"); } // checks the property exists if (!jn.has(token)) { return null; } read += token; // navigates jn = jn.get(token); break; } } // returns the new assertion return new JsonAssert(jn); } /** * Checks if the current element is an array which contains an element at the given index (starting 0). * * @param index The index to check. * @return A json assertion for the element at the given index. * @since 7.2 */ public JsonAssert has(int index) throws IOException { JsonAssert jsonAssert = get(index); Assert.assertNotNull("no field " + index, jsonAssert); return jsonAssert; } /** * Checks if the current element is an array which does not contains an element at the given index (starting 0). * * @param index The index to check. * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert hasNot(int index) throws IOException { Assert.assertNull("field is present", get(index)); return this; } /** * Checks if the current element has an element at in the given json path. see {@link #get(String)} * * @param index The index to check. * @return A json assertion for the element at the given json path. * @since 7.2 */ public JsonAssert has(String path) throws IOException { JsonAssert jsonAssert = get(path); Assert.assertNotNull("no field " + path, jsonAssert); return jsonAssert; } /** * Checks if the current element has an element at in the given json path. see {@link #get(String)} * * @param index The index to check. * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert hasNot(String path) throws IOException { Assert.assertNull("field is present", get(path)); return this; } /** * Checks the current element is null * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isNull() { Assert.assertTrue("value is not null", jsonNode.isNull()); return this; } /** * Checks that the current element is the empty string or null. * <p> * Useful for Oracle which confuses the two notions. * * @return The current json assertion for chaining. * @since 7.3 */ public JsonAssert isEmptyStringOrNull() { if (!jsonNode.isNull()) { isEquals(""); } return this; } /** * Checks the current is not null. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert notNull() { Assert.assertFalse("value is null", jsonNode.isNull()); return this; } /** * Checks the current is a text. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isText() { notNull(); Assert.assertTrue("not a text value", jsonNode.isTextual()); return this; } /** * @return The current text has the expected value. * @param expected The expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isEquals(String expected) { isText(); String value = jsonNode.getTextValue(); Assert.assertEquals(equalsMsg(expected, value), expected, value); return this; } /** * Checks the current text has not the given value. * * @param expected The not expected value. * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert notEquals(String expected) { isText(); Assert.assertNotEquals(notEqualsMsg(expected), expected, jsonNode.getTextValue()); return this; } /** * Checks the current is a boolean. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isBool() { notNull(); Assert.assertTrue("not a boolean value", jsonNode.isBoolean()); return this; } /** * Checks the current boolean is true. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isTrue() { isBool(); Assert.assertTrue("is not true", jsonNode.getBooleanValue()); return this; } /** * Checks the current boolean is false. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isFalse() { isBool(); Assert.assertFalse("is not false", jsonNode.getBooleanValue()); return this; } /** * Checks the current boolean has the expected value. * * @param expected The expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isEquals(boolean expected) { isBool(); boolean value = jsonNode.getBooleanValue(); Assert.assertEquals(equalsMsg(expected, value), expected, value); return this; } /** * Checks the current boolean has not the given value. * * @param expected The not expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isNotEquals(boolean expected) { isBool(); boolean value = jsonNode.getBooleanValue(); Assert.assertNotEquals(notEqualsMsg(expected), expected, value); return this; } /** * Checks the current is an integer. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isInt() { notNull(); Assert.assertTrue("not an int", jsonNode.isIntegralNumber()); return this; } /** * Checks the current integer has the expected value. * * @param expected The expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isEquals(int expected) { isInt(); int value = jsonNode.getIntValue(); Assert.assertEquals(equalsMsg(expected, value), expected, value); return this; } /** * Checks the current integer has not the given value. * * @param expected The not expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert notEquals(int expected) { isInt(); Assert.assertNotEquals(notEqualsMsg(expected), expected, jsonNode.getLongValue()); return this; } /** * Checks the current integer has the expected value. * * @param expected The expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isEquals(long expected) { isInt(); long value = jsonNode.getLongValue(); Assert.assertEquals(equalsMsg(expected, value), expected, value); return this; } /** * Checks the current integer has not the given value. * * @param expected The not expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert notEquals(long expected) { isInt(); Assert.assertNotEquals(notEqualsMsg(expected), expected, jsonNode.getLongValue()); return this; } /** * Checks the current is an floating point number. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isDouble() { notNull(); Assert.assertTrue("not a double", jsonNode.isFloatingPointNumber()); return this; } /** * Checks the current floating point number has the expected value. * * @param expected The expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isEquals(double expected, double delta) { isDouble(); double value = jsonNode.getDoubleValue(); Assert.assertEquals(equalsMsg(expected, value + " +- " + delta), expected, value, delta); return this; } /** * Checks the current floating point number has not the given value. * * @param expected The not expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert notEquals(double expected, double delta) { isDouble(); Assert.assertNotEquals(notEqualsMsg(expected + " +- " + delta), jsonNode.getDoubleValue(), delta); return this; } /** * Checks the current floating point number has the expected value. * * @param expected The expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isEquals(float expected, float delta) { isDouble(); double value = jsonNode.getDoubleValue(); Assert.assertEquals(equalsMsg(expected, value + " +- " + delta), expected, value, delta); return this; } /** * Checks the current floating point number has not the given value. * * @param expected The not expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert notEquals(float expected, float delta) { isDouble(); Assert.assertNotEquals(notEqualsMsg(expected + " +- " + delta), jsonNode.getDoubleValue(), delta); return this; } /** * Checks the current floating point number has the expected value. * * @param expected The expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isEquals(BigDecimal expected) { isDouble(); BigDecimal value = jsonNode.getDecimalValue(); Assert.assertEquals(equalsMsg(expected, value), expected, value); return this; } /** * Checks the current floating point number has not the given value. * * @param expected The not expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert notEquals(BigDecimal expected) { isDouble(); Assert.assertNotEquals(notEqualsMsg(expected), jsonNode.getDecimalValue()); return this; } /** * Checks the current is a binary. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isBinary() { notNull(); Assert.assertTrue("not a binary", jsonNode.isBinary()); return this; } /** * Checks the current binary has the expected value. * * @param expected The expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isEquals(byte[] expected) throws IOException { isBinary(); byte[] value = jsonNode.getBinaryValue(); Assert.assertEquals(equalsMsg(expected, value), expected, value); return this; } /** * Checks the current binary has not the given value. * * @param expected The not expected value * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert notEquals(byte[] expected) throws IOException { isBinary(); Assert.assertNotEquals(notEqualsMsg(expected), jsonNode.getBinaryValue()); return this; } /** * Checks the current is an object. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isObject() { notNull(); Assert.assertTrue("is not an object", jsonNode.isObject()); return this; } /** * Checks the current is not an object. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert notObject() { notNull(); Assert.assertTrue("is an object", jsonNode.isObject()); return this; } /** * Checks the current object has the given number of properties. * * @param count the expected number of properties. * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert properties(int count) { isObject(); int found = 0; // iterates to count properties Iterator<JsonNode> it = jsonNode.getElements(); while (it.hasNext()) { found++; it.next(); } Assert.assertEquals("Expected " + count + " children but found " + found, count, found); return this; } /** * Checks the current is an array. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert isArray() { notNull(); Assert.assertTrue("is not an array", jsonNode.isArray()); return this; } /** * Checks the current is not an array. * * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert notArray() { notNull(); Assert.assertTrue("is an array", jsonNode.isArray()); return this; } /** * Checks the current array has the given number of element. * * @param count the expected number of element. * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert length(int length) { isArray(); if (length == 0) { Assert.assertFalse("has more than 0 element : " + jsonNode.toString(), jsonNode.has(0)); return this; } Assert.assertTrue("has less than " + length + " elements", jsonNode.has(length - 1)); Assert.assertFalse("has more than " + length + " elements", jsonNode.has(length)); return this; } /** * Checks the current array contains exactly the given json as string. * * @param expecteds A set of json string. * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert contains(String... expecteds) { length(expecteds.length); JsonNode jn = null; Iterator<JsonNode> it = jsonNode.getElements(); Map<String, Integer> expectedMap = new HashMap<String, Integer>(); for (String value : expecteds) { Integer count = expectedMap.get(value); if (count == null) { count = 0; } count++; expectedMap.put(value, count); } Map<String, Integer> foundMap = new HashMap<String, Integer>(); List<String> founds = new ArrayList<String>(); while (it.hasNext()) { jn = it.next(); String value = jn.isNull() ? null : jn.getValueAsText(); founds.add(value); Integer count = foundMap.get(value); if (count == null) { count = 0; } count++; foundMap.put(value, count); } Assert.assertEquals("some value were not found or not expected: found=" + founds, expectedMap, foundMap); return this; } /** * Checks whether the node targeted by the given path contains all the given json strings. The path could be like * 'property' or 'property.subProperty'. If the path contains arrays, each sub elements would be checked. * <p> * Example: * * <pre> * JsonAssert json = JsonAssert.on("{ "element": [ { "name": "name1" }, { "name": "name1" }, { "name": "name2" } ] }"); * json.containsAll("element.name", "name1", "name1", "name2"); // works * json.containsAll("element.name", "name2", "name1", "name1"); // works * json.containsAll("element.name", "name1", "name2", "name1"); // works * json.containsAll("element.name", "toto"); // fail * json.containsAll("element.name"); // fail * json.containsAll("element.name", "name1", "name2"); // fail, even if there's just name1 and name2, it checks the length too. * json.containsAll("element.name", "name1", "name2", "name2"); // fail, name1 was found just one time, name2 was found 2 times * </pre> * * </p> * * @param path The targeted path. * @param values All the expected values. * @return The current json assertion for chaining. * @since 7.2 */ public JsonAssert childrenContains(String path, String... values) throws IOException { List<String> founds = getAll(path, jsonNode); Assert.assertEquals("found more or less element thant expected : found=" + founds, values.length, founds.size()); Map<String, Integer> expectedMap = new HashMap<String, Integer>(); for (String value : values) { Integer count = expectedMap.get(value); if (count == null) { count = 0; } count++; expectedMap.put(value, count); } Map<String, Integer> foundMap = new HashMap<String, Integer>(); for (String value : founds) { Integer count = foundMap.get(value); if (count == null) { count = 0; } count++; foundMap.put(value, count); } Assert.assertEquals("some value were not found or not expected: found=" + founds, expectedMap, foundMap); return this; } /** * utility for {@link #childrenContains(String, String...)} */ private List<String> getAll(String path, JsonNode node) throws IOException { List<String> result = new ArrayList<String>(); if (!node.isArray()) { int index = path.indexOf('.'); if (index < 0) { JsonNode el = node.get(path); if (el.isNull()) { result.add(null); } else { result.add(el.getValueAsText()); } } else { String token = path.substring(0, index); JsonNode el = node.get(token); String rest = path.substring(index + 1); result.addAll(getAll(rest, el)); } } else { Iterator<JsonNode> it = node.getElements(); while (it.hasNext()) { result.addAll(getAll(path, it.next())); } } return result; } private String equalsMsg(Object expected, Object value) { return "expected : " + expected + " but was " + value; } private String notEqualsMsg(Object expected) { return "is equals to expected : " + expected; } }