001/* 002 * Portions of this software was developed by employees of the National Institute 003 * of Standards and Technology (NIST), an agency of the Federal Government and is 004 * being made available as a public service. Pursuant to title 17 United States 005 * Code Section 105, works of NIST employees are not subject to copyright 006 * protection in the United States. This software may be subject to foreign 007 * copyright. Permission in the United States and in foreign countries, to the 008 * extent that NIST may hold copyright, to use, copy, modify, create derivative 009 * works, and distribute this software and its documentation without fee is hereby 010 * granted on a non-exclusive basis, provided that this notice and disclaimer 011 * of warranty appears in all copies. 012 * 013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER 014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY 015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF 016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM 017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE 018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT 019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, 020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, 021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY, 022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR 023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT 024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER. 025 */ 026 027package gov.nist.secauto.metaschema.databind.io.json; 028 029import com.fasterxml.jackson.core.JsonParser; 030import com.fasterxml.jackson.core.JsonToken; 031 032import gov.nist.secauto.metaschema.core.model.util.JsonUtil; 033import gov.nist.secauto.metaschema.core.util.ObjectUtils; 034import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding; 035import gov.nist.secauto.metaschema.databind.model.IBoundFieldValueInstance; 036import gov.nist.secauto.metaschema.databind.model.IBoundFlagInstance; 037import gov.nist.secauto.metaschema.databind.model.IBoundNamedInstance; 038import gov.nist.secauto.metaschema.databind.model.IBoundNamedModelInstance; 039import gov.nist.secauto.metaschema.databind.model.IClassBinding; 040import gov.nist.secauto.metaschema.databind.model.IFieldClassBinding; 041import gov.nist.secauto.metaschema.databind.model.info.IDataTypeHandler; 042import gov.nist.secauto.metaschema.databind.model.info.IModelPropertyInfo; 043import gov.nist.secauto.metaschema.databind.model.info.IPropertyCollector; 044 045import org.apache.logging.log4j.LogManager; 046import org.apache.logging.log4j.Logger; 047 048import java.io.IOException; 049import java.util.HashMap; 050import java.util.Map; 051 052import edu.umd.cs.findbugs.annotations.NonNull; 053import edu.umd.cs.findbugs.annotations.Nullable; 054 055public class MetaschemaJsonReader 056 implements IJsonParsingContext { 057 private static final Logger LOGGER = LogManager.getLogger(MetaschemaJsonReader.class); 058 059 @NonNull 060 private final JsonParser parser; 061 @NonNull 062 private final IJsonProblemHandler problemHandler; 063 064 /** 065 * Construct a new Module-aware JSON parser using the default problem handler. 066 * 067 * @param parser 068 * the JSON parser to parse with 069 * @see DefaultJsonProblemHandler 070 */ 071 public MetaschemaJsonReader( 072 @NonNull JsonParser parser) { 073 this(parser, new DefaultJsonProblemHandler()); 074 } 075 076 /** 077 * Construct a new Module-aware JSON parser. 078 * 079 * @param parser 080 * the JSON parser to parse with 081 * @param problemHandler 082 * the problem handler implementation to use 083 */ 084 public MetaschemaJsonReader( 085 @NonNull JsonParser parser, 086 @NonNull IJsonProblemHandler problemHandler) { 087 this.parser = parser; 088 this.problemHandler = problemHandler; 089 } 090 091 @Override 092 public JsonParser getReader() { 093 return parser; 094 } 095 096 @Override 097 public IJsonProblemHandler getProblemHandler() { 098 return problemHandler; 099 } 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}