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.xml;
028
029import gov.nist.secauto.metaschema.databind.io.json.DefaultJsonProblemHandler;
030import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding;
031import gov.nist.secauto.metaschema.databind.model.IBoundAssemblyInstance;
032import gov.nist.secauto.metaschema.databind.model.IBoundFieldInstance;
033import gov.nist.secauto.metaschema.databind.model.IBoundFieldValueInstance;
034import gov.nist.secauto.metaschema.databind.model.IBoundFlagInstance;
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.codehaus.stax2.XMLStreamWriter2;
042
043import java.io.IOException;
044
045import javax.xml.namespace.NamespaceContext;
046import javax.xml.namespace.QName;
047import javax.xml.stream.XMLStreamException;
048
049import edu.umd.cs.findbugs.annotations.NonNull;
050
051public class MetaschemaXmlWriter implements IXmlWritingContext {
052  @NonNull
053  private final XMLStreamWriter2 writer;
054
055  /**
056   * Construct a new Module-aware JSON writer.
057   *
058   * @param writer
059   *          the XML stream writer to write with
060   * @see DefaultJsonProblemHandler
061   */
062  public MetaschemaXmlWriter(
063      @NonNull XMLStreamWriter2 writer) {
064    this.writer = writer;
065  }
066
067  @Override
068  public XMLStreamWriter2 getWriter() {
069    return writer;
070  }
071
072  /**
073   * Writes data in a bound object to XML. 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 XMLStreamException
082   *           if an error occurred while parsing into XML
083   * @throws IOException
084   *           if an error occurred while writing the output
085   */
086  public void write(
087      @NonNull IAssemblyClassBinding targetDefinition,
088      @NonNull Object targetObject) throws XMLStreamException, IOException {
089    writer.writeStartDocument("UTF-8", "1.0");
090
091    QName rootQName = targetDefinition.getRootXmlQName();
092
093    NamespaceContext nsContext = writer.getNamespaceContext();
094    String prefix = nsContext.getPrefix(rootQName.getNamespaceURI());
095    if (prefix == null) {
096      prefix = "";
097    }
098
099    writer.writeStartElement(prefix, rootQName.getLocalPart(), rootQName.getNamespaceURI());
100
101    writeDefinitionValue(targetDefinition, targetObject, rootQName);
102
103    writer.writeEndElement();
104  }
105
106  @Override
107  public void writeDefinitionValue(
108      @NonNull IClassBinding targetDefinition,
109      @NonNull Object targetObject,
110      @NonNull QName parentName) throws IOException {
111    // write flags
112    for (IBoundFlagInstance flag : targetDefinition.getFlagInstances()) {
113      assert flag != null;
114      writeFlagInstance(flag, targetObject);
115    }
116
117    if (targetDefinition instanceof IAssemblyClassBinding) {
118      for (IBoundNamedModelInstance modelProperty : ((IAssemblyClassBinding) targetDefinition).getModelInstances()) {
119        assert modelProperty != null;
120        writeModelInstanceValues(modelProperty, targetObject, parentName);
121      }
122    } else if (targetDefinition instanceof IFieldClassBinding) {
123      IBoundFieldValueInstance fieldValueInstance = ((IFieldClassBinding) targetDefinition).getFieldValueInstance();
124
125      Object value = fieldValueInstance.getValue(targetObject);
126      if (value != null) {
127        try {
128          fieldValueInstance.getJavaTypeAdapter().writeXmlValue(value, parentName, writer);
129        } catch (XMLStreamException ex) {
130          throw new IOException(ex);
131        }
132      }
133    } else {
134      throw new UnsupportedOperationException(
135          String.format("Unsupported class binding type: %s", targetDefinition.getClass().getName()));
136    }
137  }
138
139  /**
140   * Write the data described by the provided {@code targetInstance} as an XML
141   * attribute.
142   *
143   * @param targetInstance
144   *          the model instance that describes the syntax of the data to write
145   * @param parentObject
146   *          the Java object that data written by this method is stored in
147   * @throws IOException
148   *           if an error occurred while writing the XML
149   */
150  protected void writeFlagInstance(
151      @NonNull IBoundFlagInstance targetInstance,
152      @NonNull Object parentObject)
153      throws IOException {
154    Object objectValue = targetInstance.getValue(parentObject);
155    String value
156        = objectValue == null ? null : targetInstance.getDefinition().getJavaTypeAdapter().asString(objectValue);
157
158    if (value != null) {
159      QName name = targetInstance.getXmlQName();
160      try {
161        if (name.getNamespaceURI().isEmpty()) {
162          writer.writeAttribute(name.getLocalPart(), value);
163        } else {
164          writer.writeAttribute(name.getNamespaceURI(), name.getLocalPart(), value);
165        }
166      } catch (XMLStreamException ex) {
167        throw new IOException(ex);
168      }
169    }
170  }
171
172  /**
173   * Write the data described by the provided {@code targetInstance} as an XML
174   * element.
175   *
176   * @param targetInstance
177   *          the model instance that describes the syntax of the data to write
178   * @param parentObject
179   *          the Java object that data written by this method is stored in
180   * @param parentName
181   *          the qualified name of the XML data's parent element
182   * @return {@code true} id the value was written or {@code false} otherwise
183   * @throws IOException
184   *           if an error occurred while writing the XML
185   */
186  protected boolean writeModelInstanceValues(
187      @NonNull IBoundNamedModelInstance targetInstance,
188      @NonNull Object parentObject,
189      @NonNull QName parentName)
190      throws IOException {
191    Object value = targetInstance.getValue(parentObject);
192    if (value == null) {
193      return false; // NOPMD - intentional
194    }
195
196    IModelPropertyInfo propertyInfo = targetInstance.getPropertyInfo();
197    if (targetInstance.getMinOccurs() > 0 || propertyInfo.getItemCount(value) > 0) {
198      // only write a property if the wrapper is required or if it has contents
199      QName currentStart = parentName;
200
201      try {
202        QName groupQName = targetInstance.getXmlGroupAsQName();
203        if (groupQName != null) {
204          // write the grouping element
205          writer.writeStartElement(groupQName.getNamespaceURI(), groupQName.getLocalPart());
206          currentStart = groupQName;
207        }
208
209        // There are one or more named values based on cardinality
210        propertyInfo.writeValues(value, currentStart, this);
211
212        if (groupQName != null) {
213          writer.writeEndElement();
214        }
215      } catch (XMLStreamException ex) {
216        throw new IOException(ex);
217      }
218    }
219    return true;
220  }
221
222  @Override
223  public void writeInstanceValue(
224      @NonNull IBoundNamedModelInstance targetInstance,
225      @NonNull Object targetObject,
226      @NonNull QName parentName) throws IOException {
227    // figure out how to parse the item
228    IDataTypeHandler handler = targetInstance.getDataTypeHandler();
229
230    // figure out if we need to write the wrapper or not
231    boolean writeWrapper = targetInstance instanceof IBoundAssemblyInstance
232        || ((IBoundFieldInstance) targetInstance).isInXmlWrapped()
233        || !handler.isUnwrappedValueAllowedInXml();
234
235    try {
236      QName currentParentName;
237      if (writeWrapper) {
238        currentParentName = targetInstance.getXmlQName();
239        writer.writeStartElement(currentParentName.getNamespaceURI(), currentParentName.getLocalPart());
240      } else {
241        currentParentName = parentName;
242      }
243
244      // write the value
245      handler.writeItem(targetObject, currentParentName, this);
246
247      if (writeWrapper) {
248        writer.writeEndElement();
249      }
250    } catch (XMLStreamException ex) {
251      throw new IOException(ex);
252    }
253  }
254}