1 /*
2 * SPDX-FileCopyrightText: none
3 * SPDX-License-Identifier: CC0-1.0
4 */
5
6 package gov.nist.secauto.metaschema.core.model.util;
7
8 import com.fasterxml.jackson.core.JsonLocation;
9 import com.fasterxml.jackson.core.JsonParser;
10 import com.fasterxml.jackson.core.JsonToken;
11
12 import gov.nist.secauto.metaschema.core.util.CustomCollectors;
13 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
14
15 import org.apache.logging.log4j.LogManager;
16 import org.apache.logging.log4j.Logger;
17 import org.json.JSONObject;
18 import org.json.JSONTokener;
19
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.Reader;
23 import java.net.URI;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.List;
27
28 import edu.umd.cs.findbugs.annotations.NonNull;
29 import edu.umd.cs.findbugs.annotations.Nullable;
30
31 /**
32 * Provides utility functions to support reading and writing JSON, and for
33 * producing error and warning messages.
34 */
35 public final class JsonUtil {
36 private static final Logger LOGGER = LogManager.getLogger(JsonUtil.class);
37
38 private JsonUtil() {
39 // disable construction
40 }
41
42 /**
43 * Parse the input stream into a JSON object.
44 *
45 * @param is
46 * the input stream to parse
47 * @return the JSON object
48 */
49 @NonNull
50 public static JSONObject toJsonObject(@NonNull InputStream is) {
51 return new JSONObject(new JSONTokener(is));
52 }
53
54 /**
55 * Parse the reader into a JSON object.
56 *
57 * @param reader
58 * the reader to parse
59 * @return the JSON object
60 */
61 @NonNull
62 public static JSONObject toJsonObject(@NonNull Reader reader) {
63 return new JSONObject(new JSONTokener(reader));
64 }
65
66 /**
67 * Generate an informational string describing the token at the current location
68 * of the provided {@code parser}.
69 *
70 * @param parser
71 * the JSON parser
72 * @param resource
73 * the resource being parsed
74 * @return the informational string
75 * @throws IOException
76 * if an error occurred while getting the information from the parser
77 */
78 @SuppressWarnings("null")
79 @NonNull
80 public static String toString(
81 @NonNull JsonParser parser,
82 @NonNull URI resource) throws IOException {
83 return new StringBuilder(32)
84 .append(parser.currentToken().name())
85 .append(" '")
86 .append(parser.getText())
87 .append('\'')
88 .append(generateLocationMessage(parser, resource))
89 .toString();
90 }
91
92 /**
93 * Generate an informational string describing the provided {@code location}.
94 *
95 * @param location
96 * a JSON parser location
97 * @return the informational string
98 */
99 @SuppressWarnings("null")
100 @NonNull
101 public static String toString(@NonNull JsonLocation location) {
102 return new StringBuilder(8)
103 .append(location.getLineNr())
104 .append(':')
105 .append(location.getColumnNr())
106 .toString();
107 }
108
109 /**
110 * Advance the parser to the next location matching the provided token.
111 *
112 * @param parser
113 * the JSON parser
114 * @param resource
115 * the resource being parsed
116 * @param token
117 * the expected token
118 * @return the current token or {@code null} if no tokens remain in the stream
119 * @throws IOException
120 * if an error occurred while parsing the JSON
121 */
122 @Nullable
123 public static JsonToken advanceTo(
124 @NonNull JsonParser parser,
125 @NonNull URI resource,
126 @NonNull JsonToken token) throws IOException {
127 JsonToken currentToken = null;
128 while (parser.hasCurrentToken() && (currentToken = parser.currentToken()) != token) {
129 currentToken = parser.nextToken();
130 if (LOGGER.isWarnEnabled()) {
131 LOGGER.warn("skipping over: {}",
132 toString(parser, resource));
133 }
134 }
135 return currentToken;
136 }
137
138 /**
139 * Skip the next JSON value in the stream.
140 *
141 * @param parser
142 * the JSON parser
143 * @param resource
144 * the resource being parsed
145 * @return the current token or {@code null} if no tokens remain in the stream
146 * @throws IOException
147 * if an error occurred while parsing the JSON
148 */
149 @SuppressWarnings({
150 "resource", // parser not owned
151 "PMD.CyclomaticComplexity" // acceptable
152 })
153 @Nullable
154 public static JsonToken skipNextValue(
155 @NonNull JsonParser parser,
156 @NonNull URI resource) throws IOException {
157
158 JsonToken currentToken = parser.currentToken();
159 // skip the field name
160 if (currentToken == JsonToken.FIELD_NAME) {
161 currentToken = parser.nextToken();
162 }
163
164 switch (currentToken) {
165 case START_ARRAY:
166 case START_OBJECT:
167 case VALUE_EMBEDDED_OBJECT:
168 parser.skipChildren();
169 break;
170 case VALUE_FALSE:
171 case VALUE_NULL:
172 case VALUE_NUMBER_FLOAT:
173 case VALUE_NUMBER_INT:
174 case VALUE_STRING:
175 case VALUE_TRUE:
176 // do nothing
177 break;
178 case FIELD_NAME:
179 case END_OBJECT:
180 case END_ARRAY:
181 case NOT_AVAILABLE:
182 // error
183 String msg = String.format("Unhandled JsonToken %s.",
184 toString(parser, resource));
185 LOGGER.error(msg);
186 throw new UnsupportedOperationException(msg);
187 }
188
189 // advance past the value
190 return parser.nextToken();
191 }
192 //
193 // @SuppressWarnings("PMD.CyclomaticComplexity") // acceptable
194 // private static boolean checkEndOfValue(@NonNull JsonParser parser, @NonNull
195 // JsonToken startToken) {
196 // JsonToken currentToken = parser.getCurrentToken();
197 //
198 // boolean retval;
199 // switch (startToken) { // NOPMD - intentional fall through
200 // case START_OBJECT:
201 // retval = JsonToken.END_OBJECT.equals(currentToken);
202 // break;
203 // case START_ARRAY:
204 // retval = JsonToken.END_ARRAY.equals(currentToken);
205 // break;
206 // case VALUE_EMBEDDED_OBJECT:
207 // case VALUE_FALSE:
208 // case VALUE_NULL:
209 // case VALUE_NUMBER_FLOAT:
210 // case VALUE_NUMBER_INT:
211 // case VALUE_STRING:
212 // case VALUE_TRUE:
213 // retval = true;
214 // break;
215 // default:
216 // retval = false;
217 // }
218 // return retval;
219 // }
220
221 /**
222 * Ensure that the current token is one of the provided tokens.
223 * <p>
224 * Note: This uses a Java assertion to support debugging in a whay that doesn't
225 * impact parser performance during production operation.
226 *
227 * @param parser
228 * the JSON parser
229 * @param resource
230 * the resource being parsed
231 * @param expectedTokens
232 * the tokens for which one is expected to match against the current
233 * token
234 */
235 public static void assertCurrent(
236 @NonNull JsonParser parser,
237 @NonNull URI resource,
238 @NonNull JsonToken... expectedTokens) {
239 JsonToken current = parser.currentToken();
240 assert Arrays.stream(expectedTokens)
241 .anyMatch(expected -> expected == current) : generateExpectedMessage(
242 parser,
243 resource,
244 expectedTokens,
245 parser.currentToken());
246 }
247
248 // public static void assertCurrentIsFieldValue(@NonNull JsonParser parser) {
249 // JsonToken token = parser.currentToken();
250 // assert token.isStructStart() || token.isScalarValue() : String.format(
251 // "Expected a START_OBJECT, START_ARRAY, or VALUE_xxx token, but found
252 // JsonToken '%s'%s.",
253 // token,
254 // generateLocationMessage(parser));
255 // }
256
257 /**
258 * Ensure that the current token is the one expected and then advance the token
259 * stream.
260 *
261 * @param parser
262 * the JSON parser
263 * @param resource
264 * the resource being parsed
265 * @param expectedToken
266 * the expected token
267 * @return the next token
268 * @throws IOException
269 * if an error occurred while reading the token stream
270 */
271 @Nullable
272 public static JsonToken assertAndAdvance(
273 @NonNull JsonParser parser,
274 @NonNull URI resource,
275 @NonNull JsonToken expectedToken)
276 throws IOException {
277 JsonToken token = parser.currentToken();
278 assert token == expectedToken : generateExpectedMessage(
279 parser,
280 resource,
281 expectedToken,
282 token);
283 return parser.nextToken();
284 }
285
286 /**
287 * Advance the token stream, then ensure that the current token is the one
288 * expected.
289 *
290 * @param parser
291 * the JSON parser
292 * @param resource
293 * the resource being parsed
294 * @param expectedToken
295 * the expected token
296 * @return the next token
297 * @throws IOException
298 * if an error occurred while reading the token stream
299 */
300 @Nullable
301 public static JsonToken advanceAndAssert(
302 @NonNull JsonParser parser,
303 @NonNull URI resource,
304 @NonNull JsonToken expectedToken)
305 throws IOException {
306 JsonToken token = parser.nextToken();
307 assert token == expectedToken : generateExpectedMessage(
308 parser,
309 resource,
310 expectedToken,
311 token);
312 return token;
313 }
314
315 /**
316 * Generate a message intended for error reporting based on a presumed token.
317 *
318 * @param parser
319 * the JSON parser
320 * @param resource
321 * the resource being parsed
322 * @param expectedToken
323 * the expected token
324 * @param actualToken
325 * the actual token found
326 * @return the message string
327 */
328 @NonNull
329 private static String generateExpectedMessage(
330 @NonNull JsonParser parser,
331 @NonNull URI resource,
332 @NonNull JsonToken expectedToken,
333 JsonToken actualToken) {
334 return ObjectUtils.notNull(
335 String.format("Expected JsonToken '%s', but found JsonToken '%s'%s.",
336 expectedToken,
337 actualToken,
338 generateLocationMessage(parser, resource)));
339 }
340
341 /**
342 * Generate a message intended for error reporting based on a presumed set of
343 * tokens.
344 *
345 * @param parser
346 * the JSON parser
347 * @param resource
348 * the resource being parsed
349 * @param expectedTokens
350 * the set of expected tokens, one of which was expected to match the
351 * actual token
352 * @param actualToken
353 * the actual token found
354 * @return the message string
355 */
356 @NonNull
357 private static String generateExpectedMessage(
358 @NonNull JsonParser parser,
359 @NonNull URI resource,
360 @NonNull JsonToken[] expectedTokens,
361 JsonToken actualToken) {
362 List<JsonToken> expectedTokensList = ObjectUtils.notNull(Arrays.asList(expectedTokens));
363 return generateExpectedMessage(parser, resource, expectedTokensList, actualToken);
364 }
365
366 /**
367 * Generate a message intended for error reporting based on a presumed set of
368 * tokens.
369 *
370 * @param parser
371 * the JSON parser
372 * @param resource
373 * the resource being parsed
374 * @param expectedTokens
375 * the set of expected tokens, one of which was expected to match the
376 * actual token
377 * @param actualToken
378 * the actual token found
379 * @return the message string
380 */
381 @NonNull
382 private static String generateExpectedMessage(
383 @NonNull JsonParser parser,
384 @NonNull URI resource,
385 @NonNull Collection<JsonToken> expectedTokens,
386 JsonToken actualToken) {
387 return ObjectUtils.notNull(
388 String.format("Expected JsonToken(s) '%s', but found JsonToken '%s'%s.",
389 expectedTokens.stream().map(Enum::name).collect(CustomCollectors.joiningWithOxfordComma("or")),
390 actualToken,
391 generateLocationMessage(parser, resource)));
392 }
393
394 /**
395 * Generate a location string for the current location in the JSON token stream.
396 *
397 * @param parser
398 * the JSON parser
399 * @param resource
400 * the resource being parsed
401 * @return the location string
402 */
403 @NonNull
404 public static CharSequence generateLocationMessage(@NonNull JsonParser parser, @NonNull URI resource) {
405 JsonLocation location = parser.currentLocation();
406 return location == null
407 ? " in '" + resource.toString() + "'"
408 : generateLocationMessage(location, resource);
409 }
410
411 /**
412 * Generate a location string for the current location in the JSON token stream.
413 *
414 * @param location
415 * a JSON token stream location
416 * @param resource
417 * the resource being parsed
418 * @return the location string
419 */
420 @SuppressWarnings("null")
421 @NonNull
422 public static CharSequence generateLocationMessage(@NonNull JsonLocation location, @NonNull URI resource) {
423 return new StringBuilder()
424 .append(" in '")
425 .append(resource.toString())
426 .append("' at '")
427 .append(location.getLineNr())
428 .append(':')
429 .append(location.getColumnNr())
430 .append('\'');
431 }
432 }