View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.metaschema.databind.io.json;
28  
29  import com.fasterxml.jackson.core.JsonParser;
30  import com.fasterxml.jackson.core.JsonToken;
31  
32  import gov.nist.secauto.metaschema.core.model.util.JsonUtil;
33  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
34  import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding;
35  import gov.nist.secauto.metaschema.databind.model.IBoundFieldValueInstance;
36  import gov.nist.secauto.metaschema.databind.model.IBoundFlagInstance;
37  import gov.nist.secauto.metaschema.databind.model.IBoundNamedInstance;
38  import gov.nist.secauto.metaschema.databind.model.IBoundNamedModelInstance;
39  import gov.nist.secauto.metaschema.databind.model.IClassBinding;
40  import gov.nist.secauto.metaschema.databind.model.IFieldClassBinding;
41  import gov.nist.secauto.metaschema.databind.model.info.IDataTypeHandler;
42  import gov.nist.secauto.metaschema.databind.model.info.IModelPropertyInfo;
43  import gov.nist.secauto.metaschema.databind.model.info.IPropertyCollector;
44  
45  import org.apache.logging.log4j.LogManager;
46  import org.apache.logging.log4j.Logger;
47  
48  import java.io.IOException;
49  import java.util.HashMap;
50  import java.util.Map;
51  
52  import edu.umd.cs.findbugs.annotations.NonNull;
53  import edu.umd.cs.findbugs.annotations.Nullable;
54  
55  public class MetaschemaJsonReader
56      implements IJsonParsingContext {
57    private static final Logger LOGGER = LogManager.getLogger(MetaschemaJsonReader.class);
58  
59    @NonNull
60    private final JsonParser parser;
61    @NonNull
62    private final IJsonProblemHandler problemHandler;
63  
64    /**
65     * Construct a new Module-aware JSON parser using the default problem handler.
66     *
67     * @param parser
68     *          the JSON parser to parse with
69     * @see DefaultJsonProblemHandler
70     */
71    public MetaschemaJsonReader(
72        @NonNull JsonParser parser) {
73      this(parser, new DefaultJsonProblemHandler());
74    }
75  
76    /**
77     * Construct a new Module-aware JSON parser.
78     *
79     * @param parser
80     *          the JSON parser to parse with
81     * @param problemHandler
82     *          the problem handler implementation to use
83     */
84    public MetaschemaJsonReader(
85        @NonNull JsonParser parser,
86        @NonNull IJsonProblemHandler problemHandler) {
87      this.parser = parser;
88      this.problemHandler = problemHandler;
89    }
90  
91    @Override
92    public JsonParser getReader() {
93      return parser;
94    }
95  
96    @Override
97    public IJsonProblemHandler getProblemHandler() {
98      return problemHandler;
99    }
100 
101   /**
102    * Parses JSON into a bound object. This assembly must be a root assembly for
103    * which a call to {@link IAssemblyClassBinding#isRoot()} will return
104    * {@code true}.
105    * <p>
106    * This method expects the parser's current token to be:
107    * <ul>
108    * <li>{@code null} indicating that the parser has not yet parsed a JSON
109    * node;</li>
110    * <li>a {@link JsonToken#START_OBJECT} which represents the object wrapper
111    * containing the root field,</li>
112    * <li>a {@link JsonToken#FIELD_NAME} representing the root field to parse,
113    * or</li>
114    * <li>a peer field to the root field that will be handled by the
115    * {@link IJsonProblemHandler#handleUnknownProperty(IClassBinding, Object, String, IJsonParsingContext)}
116    * method.</li>
117    * </ul>
118    * <p>
119    * After parsing the current token will be:
120    * <ul>
121    * <li>the next token after the {@link JsonToken#END_OBJECT} corresponding to
122    * the initial {@link JsonToken#START_OBJECT} parsed by this method;</li>
123    * <li>the next token after the {@link JsonToken#END_OBJECT} for the root
124    * field's value; or</li>
125    * <li>the next token after all fields and associated values have been parsed
126    * looking for the root field. This next token will be the
127    * {@link JsonToken#END_OBJECT} for the object containing the fields. In this
128    * case the method will throw an {@link IOException} indicating the root was not
129    * found.</li>
130    * </ul>
131    *
132    * @param <T>
133    *          the Java type of the resulting bound instance
134    * @param targetDefinition
135    *          the definition describing the root element data to parse
136    * @return the bound object instance representing the JSON object
137    * @throws IOException
138    *           if an error occurred while parsing the JSON
139    */
140   @SuppressWarnings({
141       "PMD.CyclomaticComplexity", "PMD.NPathComplexity" // acceptable
142   })
143   @Nullable
144   public <T> T read(@NonNull IAssemblyClassBinding targetDefinition) throws IOException {
145     if (!targetDefinition.isRoot()) {
146       throw new UnsupportedOperationException(
147           String.format("The assembly '%s' is not a root assembly.", targetDefinition.getBoundClass().getName()));
148     }
149 
150     boolean objectWrapper = false;
151     if (parser.currentToken() == null) {
152       parser.nextToken();
153     }
154 
155     if (JsonToken.START_OBJECT.equals(parser.currentToken())) {
156       // advance past the start object to the field name
157       JsonUtil.assertAndAdvance(parser, JsonToken.START_OBJECT);
158       objectWrapper = true;
159     }
160 
161     String rootFieldName = targetDefinition.getRootJsonName();
162     JsonToken token;
163     Object instance = null;
164     while (!(JsonToken.END_OBJECT.equals(token = parser.currentToken()) || token == null)) {
165       if (!JsonToken.FIELD_NAME.equals(token)) {
166         throw new IOException(String.format("Expected FIELD_NAME token, found '%s'", token.toString()));
167       }
168 
169       String fieldName = parser.currentName();
170       if (fieldName.equals(rootFieldName)) {
171         // process the object value, bound to the requested class
172         JsonUtil.assertAndAdvance(parser, JsonToken.FIELD_NAME);
173 
174         // Make a temporary data type handler for the top-level definition
175         IDataTypeHandler dataTypeHandler = IDataTypeHandler.newDataTypeHandler(targetDefinition);
176 
177         // read the top-level definition
178         instance = dataTypeHandler.readItem(null, this);
179 
180         // stop now, since we found the root field
181         break;
182       }
183 
184       if (!getProblemHandler().handleUnknownProperty(targetDefinition, instance, fieldName, this)) {
185         LOGGER.warn("Skipping unhandled top-level JSON field '{}'.", fieldName);
186         JsonUtil.skipNextValue(parser);
187       }
188     }
189 
190     if (instance == null) {
191       throw new IOException(String.format("Failed to find root field '%s'.", rootFieldName));
192     }
193 
194     if (objectWrapper) {
195       // advance past the end object
196       JsonUtil.assertAndAdvance(parser, JsonToken.END_OBJECT);
197     }
198 
199     return ObjectUtils.asType(instance);
200   }
201 
202   /**
203    * Read the data associated with the {@code instance} and apply it to the
204    * provided {@code parentObject}.
205    * <p>
206    * Consumes the field if the field's name matches. If it matches, then
207    * {@code true} is returned after parsing the value. Otherwise, {@code false} is
208    * returned to indicate the property was not parsed.
209    *
210    * @param targetInstance
211    *          the instance to parse data for
212    * @param parentObject
213    *          the Java object that data parsed by this method will be stored in
214    * @return {@code true} if the instance was parsed, or {@code false} if the data
215    *         did not contain information for this instance
216    * @throws IOException
217    *           if an error occurred while parsing the data
218    */
219   protected boolean readInstance(
220       @NonNull IBoundNamedInstance targetInstance,
221       @NonNull Object parentObject) throws IOException {
222     // the parser's current token should be the JSON field name
223     JsonUtil.assertCurrent(parser, JsonToken.FIELD_NAME);
224 
225     String propertyName = parser.currentName();
226     if (LOGGER.isTraceEnabled()) {
227       LOGGER.trace("reading property {}", propertyName);
228     }
229 
230     boolean handled = targetInstance.getJsonName().equals(propertyName);
231     if (handled) {
232       // advance past the field name
233       parser.nextToken();
234 
235       Object value = readInstanceValue(targetInstance, parentObject);
236 
237       if (value != null) {
238         targetInstance.setValue(parentObject, value);
239       }
240     }
241 
242     // the current token will be either the next instance field name or the end of
243     // the parent object
244     JsonUtil.assertCurrent(parser, JsonToken.FIELD_NAME, JsonToken.END_OBJECT);
245     return handled;
246   }
247 
248   /**
249    * Read the data associated with the {@code instance}.
250    *
251    * @param instance
252    *          the instance that describes the syntax of the data to read
253    * @param parentObject
254    *          the Java object that data parsed by this method will be stored in
255    * @return the parsed value(s)
256    * @throws IOException
257    *           if an error occurred while parsing the input
258    */
259   protected Object readInstanceValue(
260       @NonNull IBoundNamedInstance instance,
261       @NonNull Object parentObject) throws IOException {
262     Object value;
263     if (instance instanceof IBoundNamedModelInstance) {
264 
265       // Deal with the collection or value type
266       IModelPropertyInfo info = ((IBoundNamedModelInstance) instance).getPropertyInfo();
267       IPropertyCollector collector = info.newPropertyCollector();
268 
269       // let the property info parse the value
270       info.readValues(collector, parentObject, this);
271 
272       // get the underlying value
273       value = collector.getValue();
274     } else if (instance instanceof IBoundFlagInstance) {
275       // just read the value directly
276       value = ((IBoundFlagInstance) instance).getDefinition().getJavaTypeAdapter().parse(parser);
277     } else if (instance instanceof IBoundFieldValueInstance) {
278       // just read the value directly
279       value = ((IBoundFieldValueInstance) instance).getJavaTypeAdapter().parse(parser);
280     } else {
281       throw new UnsupportedOperationException(
282           String.format("Unsupported instance type: %s", instance.getClass().getName()));
283     }
284     return value;
285   }
286 
287   // @SuppressFBWarnings(value = "UC_USELESS_CONDITION", justification = "false
288   // positive")
289   @SuppressWarnings({
290       "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity" // acceptable
291   })
292   @Override
293   public void readDefinitionValue(
294       IClassBinding targetDefinition,
295       Object targetObject,
296       Map<String, ? extends IBoundNamedInstance> instances) throws IOException {
297     IBoundFlagInstance valueKeyFlag = null;
298     if (targetDefinition instanceof IFieldClassBinding) {
299       IFieldClassBinding targetFieldDefinition = (IFieldClassBinding) targetDefinition;
300       valueKeyFlag = targetFieldDefinition.getJsonValueKeyFlagInstance();
301     }
302 
303     // make a copy, since we use the remaining values to initialize default values
304     Map<String, ? extends IBoundNamedInstance> remainingInstances = new HashMap<>(instances); // NOPMD not concurrent
305 
306     // handle each property
307     while (!JsonToken.END_OBJECT.equals(parser.currentToken())) {
308       boolean handled = false;
309       String propertyName = parser.getCurrentName();
310       assert propertyName != null;
311 
312       if (JsonToken.FIELD_NAME.equals(parser.currentToken())) {
313         // found a matching property
314         IBoundNamedInstance property = remainingInstances.get(propertyName);
315         if (property != null) {
316           handled = readInstance(property, targetObject);
317           remainingInstances.remove(propertyName);
318         }
319       } else {
320         throw new IOException(
321             String.format("Unexpected token: " + JsonUtil.toString(parser)));
322       }
323 
324       if (!handled) {
325         if (valueKeyFlag != null) {
326           // Handle JSON value key flag case
327           IFieldClassBinding targetFieldDefinition = (IFieldClassBinding) targetDefinition;
328           valueKeyFlag.setValue(targetObject,
329               valueKeyFlag.getDefinition().getJavaTypeAdapter().parse(propertyName));
330 
331           // advance past the FIELD_NAME to get the value
332           JsonUtil.assertAndAdvance(parser, JsonToken.FIELD_NAME);
333 
334           IBoundFieldValueInstance fieldValue = targetFieldDefinition.getFieldValueInstance();
335           fieldValue.setValue(
336               targetObject,
337               fieldValue.getJavaTypeAdapter().parse(parser));
338           valueKeyFlag = null; // NOPMD used as boolean check to avoid value key check
339         } else if (!getProblemHandler().handleUnknownProperty(
340             targetDefinition,
341             targetObject,
342             propertyName,
343             this)) {
344           // handle unrecognized property case
345           if (LOGGER.isWarnEnabled()) {
346             LOGGER.warn("Unrecognized property named '{}' at '{}'", propertyName,
347                 JsonUtil.toString(ObjectUtils.notNull(parser.getCurrentLocation())));
348           }
349           JsonUtil.assertAndAdvance(parser, JsonToken.FIELD_NAME);
350           JsonUtil.skipNextValue(parser);
351         }
352       }
353     }
354 
355     if (!remainingInstances.isEmpty()) {
356       getProblemHandler().handleMissingInstances(
357           targetDefinition,
358           targetObject,
359           ObjectUtils.notNull(remainingInstances.values()));
360     }
361   }
362 }