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.model.info;
28  
29  import com.fasterxml.jackson.core.JsonGenerator;
30  import com.fasterxml.jackson.core.JsonParser;
31  import com.fasterxml.jackson.core.JsonToken;
32  
33  import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
34  import gov.nist.secauto.metaschema.core.model.IFieldDefinition;
35  import gov.nist.secauto.metaschema.core.model.util.JsonUtil;
36  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
37  import gov.nist.secauto.metaschema.databind.io.BindingException;
38  import gov.nist.secauto.metaschema.databind.io.json.IJsonParsingContext;
39  import gov.nist.secauto.metaschema.databind.io.json.IJsonWritingContext;
40  import gov.nist.secauto.metaschema.databind.io.xml.IXmlParsingContext;
41  import gov.nist.secauto.metaschema.databind.io.xml.IXmlWritingContext;
42  import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding;
43  import gov.nist.secauto.metaschema.databind.model.IBoundFieldValueInstance;
44  import gov.nist.secauto.metaschema.databind.model.IBoundFlagInstance;
45  import gov.nist.secauto.metaschema.databind.model.IBoundNamedInstance;
46  import gov.nist.secauto.metaschema.databind.model.IBoundNamedModelInstance;
47  import gov.nist.secauto.metaschema.databind.model.IClassBinding;
48  import gov.nist.secauto.metaschema.databind.model.IFieldClassBinding;
49  
50  import java.io.IOException;
51  import java.util.Collection;
52  import java.util.Map;
53  import java.util.function.Function;
54  import java.util.stream.Collectors;
55  import java.util.stream.Stream;
56  
57  import javax.xml.namespace.QName;
58  import javax.xml.stream.XMLStreamException;
59  import javax.xml.stream.events.StartElement;
60  
61  import edu.umd.cs.findbugs.annotations.NonNull;
62  import edu.umd.cs.findbugs.annotations.Nullable;
63  import nl.talsmasoftware.lazy4j.Lazy;
64  
65  class ClassDataTypeHandler implements IDataTypeHandler {
66    private final boolean jsonKeyRequired;
67    @NonNull
68    private final IClassBinding classBinding;
69  
70    @NonNull
71    private final Lazy<Map<String, ? extends IBoundNamedInstance>> propertyMap;
72  
73    /**
74     * Generates a mapping of property names to associated Module instances.
75     * <p>
76     * If {@code requiresJsonKey} is {@code true} then the instance used as the JSON
77     * key is not included in the mapping.
78     * <p>
79     * If the {@code targetDefinition} is an instance of {@link IFieldDefinition}
80     * and a JSON value key property is configured, then the value key flag and
81     * value are also ommitted from the mapping. Otherwise, the value is included in
82     * the mapping.
83     *
84     * @param targetDefinition
85     *          the Module bound definition to generate the instance map for
86     * @param requiresJsonKey
87     *          if {@code true} then the instance used as the JSON key is not
88     *          included in the mapping, or {@code false} otherwise
89     * @return a mapping of JSON property to related Module instance
90     */
91    @NonNull
92    private static Map<String, ? extends IBoundNamedInstance> getInstancesToParse(
93        @NonNull IClassBinding targetDefinition,
94        boolean requiresJsonKey) {
95      Collection<? extends IBoundFlagInstance> flags = targetDefinition.getFlagInstances();
96      int flagCount = flags.size() - (requiresJsonKey ? 1 : 0);
97  
98      @SuppressWarnings("resource") Stream<? extends IBoundNamedInstance> instanceStream;
99      if (targetDefinition instanceof IAssemblyClassBinding) {
100       // use all child instances
101       instanceStream = ((IAssemblyClassBinding) targetDefinition).getModelInstances().stream();
102     } else if (targetDefinition instanceof IFieldClassBinding) {
103       IFieldClassBinding targetFieldDefinition = (IFieldClassBinding) targetDefinition;
104 
105       IBoundFlagInstance jsonValueKeyFlag = targetFieldDefinition.getJsonValueKeyFlagInstance();
106       if (jsonValueKeyFlag == null && flagCount > 0) {
107         // the field value is handled as named field
108         IBoundFieldValueInstance fieldValue = targetFieldDefinition.getFieldValueInstance();
109         instanceStream = Stream.of(fieldValue);
110       } else {
111         // only the value, with no flags or a JSON value key flag
112         instanceStream = Stream.empty();
113       }
114     } else {
115       throw new UnsupportedOperationException(
116           String.format("Unsupported class binding type: %s", targetDefinition.getClass().getName()));
117     }
118 
119     if (requiresJsonKey) {
120       IBoundFlagInstance jsonKey = targetDefinition.getJsonKeyFlagInstance();
121       assert jsonKey != null;
122       instanceStream = Stream.concat(
123           flags.stream().filter((flag) -> !jsonKey.equals(flag)),
124           instanceStream);
125     } else {
126       instanceStream = Stream.concat(
127           flags.stream(),
128           instanceStream);
129     }
130     return ObjectUtils.notNull(instanceStream.collect(
131         Collectors.toUnmodifiableMap(
132             IBoundNamedInstance::getJsonName,
133             Function.identity())));
134   }
135 
136   public ClassDataTypeHandler(
137       @Nullable IBoundNamedModelInstance targetInstance,
138       @NonNull IClassBinding classBinding) {
139     this.classBinding = classBinding;
140 
141     this.jsonKeyRequired = targetInstance != null && targetInstance.getPropertyInfo().isJsonKeyRequired();
142     this.propertyMap = ObjectUtils.notNull(Lazy.lazy(() -> getInstancesToParse(
143         classBinding,
144         this.jsonKeyRequired)));
145   }
146 
147   @Override
148   public IDataTypeAdapter<?> getJavaTypeAdapter() {
149     // this is always null
150     return null;
151   }
152 
153   @Override
154   @NonNull
155   public IClassBinding getClassBinding() {
156     return classBinding;
157   }
158 
159   @Override
160   public boolean isUnwrappedValueAllowedInXml() {
161     // classes are always wrapped
162     return false;
163   }
164 
165   @Override
166   public boolean isJsonKeyRequired() {
167     return jsonKeyRequired;
168   }
169 
170   @NonNull
171   protected Map<String, ? extends IBoundNamedInstance> getJsonInstanceMap() {
172     return ObjectUtils.notNull(propertyMap.get());
173   }
174 
175   @SuppressWarnings("resource") // not owned
176   private boolean readItemJsonKey(
177       @NonNull Object targetObject,
178       IJsonParsingContext context) throws IOException {
179     JsonParser parser = context.getReader(); // NOPMD - intentional
180     IClassBinding definition = getClassBinding();
181 
182     IBoundFlagInstance jsonKey = definition.getJsonKeyFlagInstance();
183     if (jsonKey == null) {
184       throw new IOException(String.format("JSON key not defined for object '%s'%s",
185           definition.toCoordinates(), JsonUtil.generateLocationMessage(parser)));
186     }
187 
188     // the field will be the JSON key
189     String key = ObjectUtils.notNull(parser.getCurrentName());
190 
191     Object value = jsonKey.getDefinition().getJavaTypeAdapter().parse(key);
192     jsonKey.setValue(targetObject, value.toString());
193 
194     // advance past the FIELD_NAME
195     JsonUtil.assertAndAdvance(parser, JsonToken.FIELD_NAME);
196 
197     boolean keyObjectWrapper = JsonToken.START_OBJECT.equals(parser.currentToken());
198     if (keyObjectWrapper) {
199       JsonUtil.assertAndAdvance(parser, JsonToken.START_OBJECT);
200     }
201 
202     return keyObjectWrapper;
203   }
204 
205   @SuppressWarnings({
206       "resource", // not owned
207       "PMD.NPathComplexity", "PMD.CyclomaticComplexity" // ok
208   })
209   @Override
210   public <T> T readItem(Object parentObject, IJsonParsingContext context)
211       throws IOException {
212     JsonParser parser = context.getReader(); // NOPMD - intentional
213     boolean objectWrapper = JsonToken.START_OBJECT.equals(parser.currentToken());
214     if (objectWrapper) {
215       JsonUtil.assertAndAdvance(parser, JsonToken.START_OBJECT);
216     }
217 
218     Object targetObject;
219     try {
220       targetObject = classBinding.newInstance();
221       classBinding.callBeforeDeserialize(targetObject, parentObject);
222     } catch (BindingException ex) {
223       throw new IOException(ex);
224     }
225 
226     IClassBinding definition = getClassBinding();
227     boolean readJsonKey = isJsonKeyRequired();
228     boolean keyObjectWrapper = false;
229     if (readJsonKey) {
230       keyObjectWrapper = readItemJsonKey(targetObject, context);
231     }
232 
233     if (keyObjectWrapper || JsonToken.FIELD_NAME.equals(parser.currentToken())) {
234       context.readDefinitionValue(definition, targetObject, getJsonInstanceMap());
235     } else if (parser.currentToken().isScalarValue()) {
236       // this is just a value
237       IFieldClassBinding fieldDefinition = (IFieldClassBinding) definition;
238       Object fieldValue = fieldDefinition.getJavaTypeAdapter().parse(parser);
239       fieldDefinition.getFieldValueInstance().setValue(targetObject, fieldValue);
240     }
241 
242     try {
243       classBinding.callAfterDeserialize(targetObject, parentObject);
244     } catch (BindingException ex) {
245       throw new IOException(ex);
246     }
247 
248     if (keyObjectWrapper) {
249       // advance past the END_OBJECT for the JSON key
250       JsonUtil.assertAndAdvance(parser, JsonToken.END_OBJECT);
251     }
252 
253     if (objectWrapper) {
254       JsonUtil.assertAndAdvance(parser, JsonToken.END_OBJECT);
255     }
256     return ObjectUtils.asType(targetObject);
257   }
258 
259   @Override
260   public Object readItem(Object parentInstance, StartElement start, IXmlParsingContext context)
261       throws IOException, XMLStreamException {
262     return context.readDefinitionValue(getClassBinding(), parentInstance, start);
263   }
264 
265   @Override
266   public void writeItem(Object item, QName currentParentName, IXmlWritingContext context)
267       throws IOException, XMLStreamException {
268     context.writeDefinitionValue(classBinding, item, currentParentName);
269   }
270 
271   @SuppressWarnings("resource") // not owned
272   @Override
273   public void writeItem(Object targetObject, IJsonWritingContext context) throws IOException {
274     JsonGenerator writer = context.getWriter();
275 
276     writer.writeStartObject();
277 
278     IClassBinding definition = getClassBinding();
279     boolean writeJsonKey = isJsonKeyRequired();
280     if (writeJsonKey) {
281       IBoundFlagInstance jsonKey = definition.getJsonKeyFlagInstance();
282       assert jsonKey != null;
283 
284       // the field will be the JSON key
285       Object flagValue = jsonKey.getValue(targetObject);
286       String key = jsonKey.getValueAsString(flagValue);
287       if (key == null) {
288         throw new IOException(new NullPointerException("Null key value"));
289       }
290       writer.writeFieldName(key);
291 
292       // next the value will be a start object
293       writer.writeStartObject();
294     }
295 
296     context.writeDefinitionValue(
297         classBinding,
298         targetObject,
299         getJsonInstanceMap());
300 
301     if (writeJsonKey) {
302       writer.writeEndObject();
303     }
304 
305     writer.writeEndObject();
306   }
307 
308   @Override
309   public Object copyItem(@NonNull Object fromItem, Object parentInstance) throws BindingException {
310     return classBinding.copyBoundObject(fromItem, parentInstance);
311   }
312 
313 }