/* * SonarQube * Copyright (C) 2009-2017 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.test; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import static java.util.Collections.synchronizedSet; @ThreadSafe class JsonComparison { private boolean strictTimezone = false; private boolean strictArrayOrder = false; private Set<String> ignoredFields = synchronizedSet(new HashSet<String>()); boolean isStrictTimezone() { return strictTimezone; } JsonComparison withTimezone() { this.strictTimezone = true; return this; } boolean isStrictArrayOrder() { return strictArrayOrder; } JsonComparison withStrictArrayOrder() { this.strictArrayOrder = true; return this; } JsonComparison setIgnoredFields(String... ignoredFields) { Collections.addAll(this.ignoredFields, ignoredFields); return this; } boolean areSimilar(String expected, String actual) { Object expectedJson = parse(expected); Object actualJson = parse(actual); return compare(expectedJson, actualJson); } private Object parse(String s) { try { JSONParser parser = new JSONParser(); return parser.parse(s); } catch (Exception e) { throw new IllegalStateException("Invalid JSON: " + s, e); } } private boolean compare(@Nullable Object expectedObject, @Nullable Object actualObject) { if (expectedObject == null) { return actualObject == null; } if (actualObject == null) { // expected non-null, got null return false; } if (expectedObject.getClass() != actualObject.getClass()) { return false; } if (expectedObject instanceof JSONArray) { return compareArrays((JSONArray) expectedObject, (JSONArray) actualObject); } if (expectedObject instanceof JSONObject) { return compareObjects((JSONObject) expectedObject, (JSONObject) actualObject); } if (expectedObject instanceof String) { return compareStrings((String) expectedObject, (String) actualObject); } if (expectedObject instanceof Number) { return compareNumbers((Number) expectedObject, (Number) actualObject); } return compareBooleans((Boolean) expectedObject, (Boolean) actualObject); } private boolean compareBooleans(Boolean expected, Boolean actual) { return expected.equals(actual); } private boolean compareNumbers(Number expected, Number actual) { double d1 = expected.doubleValue(); double d2 = actual.doubleValue(); if (Double.compare(d1, d2) == 0) { return true; } return Math.abs(d1 - d2) <= 0.0000001; } private boolean compareStrings(String expected, String actual) { if (!strictTimezone) { // two instants with different timezones are considered as identical (2015-01-01T13:00:00+0100 and 2015-01-01T12:00:00+0000) Date expectedDate = tryParseDate(expected); Date actualDate = tryParseDate(actual); if (expectedDate != null && actualDate != null) { return expectedDate.getTime() == actualDate.getTime(); } } return expected.equals(actual); } private boolean compareArrays(JSONArray expected, JSONArray actual) { if (strictArrayOrder) { return compareArraysByStrictOrder(expected, actual); } return compareArraysByLenientOrder(expected, actual); } private boolean compareArraysByStrictOrder(JSONArray expected, JSONArray actual) { if (expected.size() != actual.size()) { return false; } for (int index = 0; index < expected.size(); index++) { Object expectedElt = expected.get(index); Object actualElt = actual.get(index); if (!compare(expectedElt, actualElt)) { return false; } } return true; } private boolean compareArraysByLenientOrder(JSONArray expected, JSONArray actual) { if (expected.size() > actual.size()) { return false; } List remainingActual = new ArrayList(actual); for (Object expectedElement : expected) { // element can be null boolean found = false; for (Object actualElement : remainingActual) { if (compare(expectedElement, actualElement)) { found = true; remainingActual.remove(actualElement); break; } } if (!found) { return false; } } return remainingActual.isEmpty(); } private boolean compareObjects(JSONObject expectedMap, JSONObject actualMap) { // each key-value of expected map must exist in actual map for (Map.Entry<Object, Object> expectedEntry : (Set<Map.Entry<Object, Object>>) expectedMap.entrySet()) { Object key = expectedEntry.getKey(); if (shouldIgnoreField(key)) { continue; } if (!actualMap.containsKey(key)) { return false; } if (!compare(expectedEntry.getValue(), actualMap.get(key))) { return false; } } return true; } private boolean shouldIgnoreField(Object key) { return key instanceof String && ignoredFields.contains((String) key); } @CheckForNull private static Date tryParseDate(String s) { try { return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(s); } catch (ParseException ignored) { // not a datetime return null; } } }