aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-testing-harness/src/main/java/org/sonar/test/JsonComparison.java
blob: 82f83e43d230c2bd329f1479d07c5207fc25fc11 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/*
 * SonarQube
 * Copyright (C) 2009-2018 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;
    }
  }
}