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.JsonGenerator;
030
031import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding;
032import gov.nist.secauto.metaschema.databind.model.IBoundFieldValueInstance;
033import gov.nist.secauto.metaschema.databind.model.IBoundFlagInstance;
034import gov.nist.secauto.metaschema.databind.model.IBoundNamedInstance;
035import gov.nist.secauto.metaschema.databind.model.IBoundNamedModelInstance;
036import gov.nist.secauto.metaschema.databind.model.IClassBinding;
037import gov.nist.secauto.metaschema.databind.model.IFieldClassBinding;
038import gov.nist.secauto.metaschema.databind.model.info.IDataTypeHandler;
039import gov.nist.secauto.metaschema.databind.model.info.IModelPropertyInfo;
040
041import org.apache.logging.log4j.LogManager;
042import org.apache.logging.log4j.Logger;
043
044import java.io.IOException;
045import java.util.Map;
046
047import edu.umd.cs.findbugs.annotations.NonNull;
048
049public class MetaschemaJsonWriter implements IJsonWritingContext {
050  private static final Logger LOGGER = LogManager.getLogger(MetaschemaJsonWriter.class);
051
052  @NonNull
053  private final JsonGenerator writer;
054
055  /**
056   * Construct a new Module-aware JSON writer.
057   *
058   * @param generator
059   *          the JSON generator to write with
060   * @see DefaultJsonProblemHandler
061   */
062  public MetaschemaJsonWriter(
063      @NonNull JsonGenerator generator) {
064    this.writer = generator;
065  }
066
067  @Override
068  public JsonGenerator getWriter() {
069    return writer;
070  }
071
072  /**
073   * Writes data in a bound object to JSON. This assembly must be a root assembly
074   * for which a call to {@link IAssemblyClassBinding#isRoot()} will return
075   * {@code true}.
076   *
077   * @param targetDefinition
078   *          the definition describing the root element data to write
079   * @param targetObject
080   *          the bound object
081   * @throws IOException
082   *           if an error occurred while reading the JSON
083   */
084  public void write(
085      @NonNull IAssemblyClassBinding targetDefinition,
086      @NonNull Object targetObject) throws IOException {
087    if (!targetDefinition.isRoot()) {
088      throw new UnsupportedOperationException(
089          String.format("The assembly '%s' is not a root assembly.", targetDefinition.getBoundClass().getName()));
090    }
091    // first write the initial START_OBJECT
092    writer.writeStartObject();
093
094    writer.writeFieldName(targetDefinition.getRootJsonName());
095
096    // Make a temporary data type handler for the root class
097    IDataTypeHandler dataTypeHandler = IDataTypeHandler.newDataTypeHandler(targetDefinition);
098    dataTypeHandler.writeItem(targetObject, this);
099
100    // end of root object
101    writer.writeEndObject();
102  }
103
104  @Override
105  public void writeDefinitionValue(
106      IClassBinding targetDefinition,
107      Object targetObject,
108      Map<String, ? extends IBoundNamedInstance> instances) throws IOException {
109    for (IBoundNamedInstance property : instances.values()) {
110      assert property != null;
111      writeInstance(property, targetObject);
112    }
113
114    if (targetDefinition instanceof IFieldClassBinding) {
115      IFieldClassBinding fieldDefinition = (IFieldClassBinding) targetDefinition;
116      IBoundFieldValueInstance fieldValueInstance = fieldDefinition.getFieldValueInstance();
117      Object fieldValue = fieldValueInstance.getValue(targetObject);
118      if (fieldValue != null) {
119        String valueKeyName;
120        IBoundFlagInstance jsonValueKey = fieldDefinition.getJsonValueKeyFlagInstance();
121        if (jsonValueKey != null) {
122          valueKeyName = jsonValueKey.getValueAsString(jsonValueKey.getValue(targetObject));
123        } else {
124          valueKeyName = fieldValueInstance.getJsonValueKeyName();
125        }
126        writer.writeFieldName(valueKeyName);
127        fieldValueInstance.getJavaTypeAdapter().writeJsonValue(fieldValue, writer);
128      }
129    }
130  }
131
132  /**
133   * Write the instance data contained in the {@code parentObject} based on the
134   * structure described by the {@code targetInstance}.
135   *
136   * @param targetInstance
137   *          the instance to write data for
138   * @param parentObject
139   *          the Java object containing the instance data to write
140   * @throws IOException
141   *           if an error occurred while writing the data
142   */
143  protected void writeInstance(
144      @NonNull IBoundNamedInstance targetInstance,
145      @NonNull Object parentObject)
146      throws IOException {
147    if (targetInstance instanceof IBoundFlagInstance) {
148      writeFlagInstanceValue((IBoundFlagInstance) targetInstance, parentObject);
149    } else if (targetInstance instanceof IBoundNamedModelInstance) {
150      writeModelInstanceValues((IBoundNamedModelInstance) targetInstance, parentObject);
151    } else if (targetInstance instanceof IBoundFieldValueInstance) {
152      writeFieldValueInstanceValue((IBoundFieldValueInstance) targetInstance, parentObject);
153    } else {
154      throw new UnsupportedOperationException(
155          String.format("Unsupported class binding type: %s", targetInstance.getClass().getName()));
156    }
157  }
158
159  /**
160   * Write the instance data contained in the {@code parentObject} based on the
161   * structure described by the {@code targetInstance}.
162   *
163   * @param targetInstance
164   *          the instance to write data for
165   * @param parentObject
166   *          the Java object containing the instance data to write
167   * @throws IOException
168   *           if an error occurred while writing the data
169   */
170  protected void writeFlagInstanceValue(
171      @NonNull IBoundFlagInstance targetInstance,
172      @NonNull Object parentObject) throws IOException {
173    Object value = targetInstance.getValue(parentObject);
174    if (value != null) {
175      // write the field name
176      writer.writeFieldName(targetInstance.getJsonName());
177
178      // write the value
179      targetInstance.getDefinition().getJavaTypeAdapter().writeJsonValue(value, writer);
180    }
181  }
182
183  /**
184   * Write the instance data contained in the {@code parentObject} based on the
185   * structure described by the {@code targetInstance}.
186   *
187   * @param targetInstance
188   *          the instance to write data for
189   * @param parentObject
190   *          the Java object containing the instance data to write
191   * @throws IOException
192   *           if an error occurred while writing the data
193   */
194  protected void writeModelInstanceValues(
195      @NonNull IBoundNamedModelInstance targetInstance,
196      @NonNull Object parentObject) throws IOException {
197    IModelPropertyInfo propertyInfo = targetInstance.getPropertyInfo();
198    if (propertyInfo.isValueSet(parentObject)) {
199      // write the field name
200      writer.writeFieldName(targetInstance.getJsonName());
201
202      // dispatch to the property info implementation to address cardinality
203      propertyInfo.writeValues(parentObject, this);
204    }
205  }
206
207  /**
208   * Write the instance data contained in the {@code parentObject} based on the
209   * structure described by the {@code targetInstance}.
210   *
211   * @param targetInstance
212   *          the instance to write data for
213   * @param parentObject
214   *          the Java object containing the instance data to write
215   * @throws IOException
216   *           if an error occurred while writing the data
217   */
218  protected void writeFieldValueInstanceValue(
219      @NonNull IBoundFieldValueInstance targetInstance,
220      @NonNull Object parentObject) throws IOException {
221    Object value = targetInstance.getValue(parentObject);
222    if (value != null) {
223      // There are two modes:
224      // 1) use of a JSON value key, or
225      // 2) a simple value named "value"
226      IBoundFlagInstance jsonValueKey = targetInstance.getParentClassBinding().getJsonValueKeyFlagInstance();
227
228      String valueKeyName;
229      if (jsonValueKey != null) {
230        // this is the JSON value key case
231        valueKeyName = jsonValueKey.getValue(parentObject).toString();
232      } else {
233        valueKeyName = targetInstance.getJsonValueKeyName();
234      }
235      writer.writeFieldName(valueKeyName);
236      LOGGER.info("FIELD: {}", valueKeyName);
237      targetInstance.getJavaTypeAdapter().writeJsonValue(value, writer);
238    }
239  }
240}