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 }