001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.databind.io.xml; 007 008import org.codehaus.stax2.XMLStreamWriter2; 009import org.w3c.dom.Element; 010 011import java.io.IOException; 012import java.util.List; 013 014import javax.xml.namespace.NamespaceContext; 015import javax.xml.stream.XMLStreamException; 016 017import dev.metaschema.core.model.IAnyContent; 018import dev.metaschema.core.model.IAnyInstance; 019import dev.metaschema.core.model.IBoundObject; 020import dev.metaschema.core.qname.IEnhancedQName; 021import dev.metaschema.databind.io.json.DefaultJsonProblemHandler; 022import dev.metaschema.databind.model.IBoundDefinitionModel; 023import dev.metaschema.databind.model.IBoundDefinitionModelAssembly; 024import dev.metaschema.databind.model.IBoundDefinitionModelComplex; 025import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex; 026import dev.metaschema.databind.model.IBoundFieldValue; 027import dev.metaschema.databind.model.IBoundInstanceFlag; 028import dev.metaschema.databind.model.IBoundInstanceModel; 029import dev.metaschema.databind.model.IBoundInstanceModelAny; 030import dev.metaschema.databind.model.IBoundInstanceModelAssembly; 031import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup; 032import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex; 033import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar; 034import dev.metaschema.databind.model.IBoundInstanceModelGroupedAssembly; 035import dev.metaschema.databind.model.IBoundInstanceModelGroupedField; 036import dev.metaschema.databind.model.IBoundInstanceModelGroupedNamed; 037import dev.metaschema.databind.model.IBoundInstanceModelNamed; 038import dev.metaschema.databind.model.info.AbstractModelInstanceWriteHandler; 039import dev.metaschema.databind.model.info.IFeatureComplexItemValueHandler; 040import dev.metaschema.databind.model.info.IItemWriteHandler; 041import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo; 042import edu.umd.cs.findbugs.annotations.NonNull; 043 044/** 045 * Provides support for writing Metaschema-bound Java objects to XML format. 046 * <p> 047 * This class implements the {@link IXmlWritingContext} interface to serialize 048 * bound objects to XML using StAX's {@link XMLStreamWriter2}. It handles flags 049 * as attributes and fields/assemblies as child elements according to the 050 * Metaschema XML serialization rules. 051 * 052 * @see IXmlWritingContext 053 * @see XMLStreamWriter2 054 */ 055public class MetaschemaXmlWriter implements IXmlWritingContext { 056 @NonNull 057 private final XMLStreamWriter2 writer; 058 059 /** 060 * Construct a new Module-aware JSON writer. 061 * 062 * @param writer 063 * the XML stream writer to write with 064 * @see DefaultJsonProblemHandler 065 */ 066 public MetaschemaXmlWriter( 067 @NonNull XMLStreamWriter2 writer) { 068 this.writer = writer; 069 } 070 071 @Override 072 public XMLStreamWriter2 getWriter() { 073 return writer; 074 } 075 076 // ===================================== 077 // Entry point for top-level-definitions 078 // ===================================== 079 080 @Override 081 public void write( 082 @NonNull IBoundDefinitionModelComplex definition, 083 @NonNull IBoundObject item) throws IOException { 084 085 IEnhancedQName qname = definition.getQName(); 086 087 definition.writeItem(item, new ItemWriter(qname)); 088 } 089 090 @Override 091 public void writeRoot( 092 @NonNull IBoundDefinitionModelAssembly definition, 093 @NonNull IBoundObject item) throws IOException { 094 IEnhancedQName rootEQName = definition.getRootQName(); 095 if (rootEQName == null) { 096 throw new IllegalArgumentException( 097 String.format("The assembly definition '%s' does not have a root QName.", 098 definition.getQName())); 099 } 100 101 definition.writeItem(item, new ItemWriter(rootEQName)); 102 } 103 104 // ================ 105 // Instance writers 106 // ================ 107 108 private <T> void writeModelInstance( 109 @NonNull IBoundInstanceModel<T> instance, 110 @NonNull Object parentItem, 111 @NonNull ItemWriter itemWriter) throws IOException { 112 Object value = instance.getValue(parentItem); 113 if (value == null) { 114 return; 115 } 116 117 // this if is not strictly needed, since isEmpty will return false on a null 118 // value 119 // checking null here potentially avoids the expensive operation of 120 // instantiating 121 IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo(); 122 if (!collectionInfo.isEmpty(value)) { 123 IEnhancedQName currentQName = itemWriter.getObjectQName(); 124 IEnhancedQName groupAsEQName = instance.getEffectiveXmlGroupAsQName(); 125 try { 126 if (groupAsEQName != null) { 127 // write the grouping element 128 writer.writeStartElement(groupAsEQName.getNamespace(), groupAsEQName.getLocalName()); 129 currentQName = groupAsEQName; 130 } 131 132 collectionInfo.writeItems( 133 new ModelInstanceWriteHandler<>(instance, new ItemWriter(currentQName)), 134 value); 135 136 if (groupAsEQName != null) { 137 writer.writeEndElement(); 138 } 139 } catch (XMLStreamException ex) { 140 throw new IOException(ex); 141 } 142 } 143 } 144 145 private static class ModelInstanceWriteHandler<ITEM> 146 extends AbstractModelInstanceWriteHandler<ITEM> { 147 @NonNull 148 private final ItemWriter itemWriter; 149 150 public ModelInstanceWriteHandler( 151 @NonNull IBoundInstanceModel<ITEM> instance, 152 @NonNull ItemWriter itemWriter) { 153 super(instance); 154 this.itemWriter = itemWriter; 155 } 156 157 @Override 158 public void writeItem(ITEM item) throws IOException { 159 IBoundInstanceModel<ITEM> instance = getInstance(); 160 instance.writeItem(item, itemWriter); 161 } 162 } 163 164 private class ItemWriter 165 extends AbstractItemWriter { 166 167 public ItemWriter(@NonNull IEnhancedQName qname) { 168 super(qname); 169 } 170 171 private <T extends IBoundInstanceModelNamed<IBoundObject> & IFeatureComplexItemValueHandler> void writeFlags( 172 @NonNull IBoundObject parentItem, 173 @NonNull T instance) throws IOException { 174 writeFlags(parentItem, instance.getDefinition()); 175 } 176 177 private <T extends IBoundInstanceModelGroupedNamed & IFeatureComplexItemValueHandler> void writeFlags( 178 @NonNull IBoundObject parentItem, 179 @NonNull T instance) throws IOException { 180 writeFlags(parentItem, instance.getDefinition()); 181 } 182 183 private void writeFlags( 184 @NonNull IBoundObject parentItem, 185 @NonNull IBoundDefinitionModel<?> definition) throws IOException { 186 for (IBoundInstanceFlag flag : definition.getFlagInstances()) { 187 assert flag != null; 188 189 Object value = flag.getValue(parentItem); 190 if (value != null) { 191 writeItemFlag(value, flag); 192 } 193 } 194 } 195 196 private <T extends IBoundInstanceModelAssembly & IFeatureComplexItemValueHandler> void writeAssemblyModel( 197 @NonNull IBoundObject parentItem, 198 @NonNull T instance) throws IOException { 199 writeAssemblyModel(parentItem, instance.getDefinition()); 200 } 201 202 private <T extends IBoundInstanceModelGroupedAssembly & IFeatureComplexItemValueHandler> void writeAssemblyModel( 203 @NonNull IBoundObject parentItem, 204 @NonNull T instance) throws IOException { 205 writeAssemblyModel(parentItem, instance.getDefinition()); 206 } 207 208 private void writeAssemblyModel( 209 @NonNull IBoundObject parentItem, 210 @NonNull IBoundDefinitionModelAssembly definition) throws IOException { 211 for (IBoundInstanceModel<?> modelInstance : definition.getModelInstances()) { 212 assert modelInstance != null; 213 writeModelInstance(modelInstance, parentItem, this); 214 } 215 216 // Write any content if present 217 IAnyInstance anyInstance = definition.getModelContainer().getAnyInstance(); 218 if (anyInstance instanceof IBoundInstanceModelAny) { 219 IBoundInstanceModelAny boundAny = (IBoundInstanceModelAny) anyInstance; 220 IAnyContent anyContent = boundAny.getAnyContent(parentItem); 221 if (anyContent instanceof XmlAnyContent) { 222 XmlAnyContent xmlAnyContent = (XmlAnyContent) anyContent; 223 if (!xmlAnyContent.isEmpty()) { 224 try { 225 List<Element> elements = xmlAnyContent.getElements(); 226 for (Element element : elements) { 227 XmlDomUtil.elementToStax(element, writer); 228 } 229 } catch (XMLStreamException ex) { 230 throw new IOException(ex); 231 } 232 } 233 } 234 } 235 } 236 237 private void writeFieldValue( 238 @NonNull IBoundObject parentItem, 239 @NonNull IBoundInstanceModelFieldComplex instance) throws IOException { 240 writeFieldValue(parentItem, instance.getDefinition()); 241 } 242 243 private void writeFieldValue( 244 @NonNull IBoundObject parentItem, 245 @NonNull IBoundInstanceModelGroupedField instance) throws IOException { 246 writeFieldValue(parentItem, instance.getDefinition()); 247 } 248 249 private void writeFieldValue( 250 @NonNull IBoundObject parentItem, 251 @NonNull IBoundDefinitionModelFieldComplex definition) throws IOException { 252 definition.getFieldValue().writeItem(parentItem, this); 253 } 254 255 private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModelNamed<IBoundObject>> void writeModelObject( 256 @NonNull T instance, 257 @NonNull IBoundObject parentItem, 258 @NonNull ObjectWriter<T> propertyWriter) throws IOException { 259 try { 260 IEnhancedQName wrapperQName = instance.getQName(); 261 writer.writeStartElement(wrapperQName.getNamespace(), wrapperQName.getLocalName()); 262 263 propertyWriter.accept(parentItem, instance); 264 265 writer.writeEndElement(); 266 } catch (XMLStreamException ex) { 267 throw new IOException(ex); 268 } 269 } 270 271 private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModelGroupedNamed> void writeGroupedModelObject( 272 @NonNull T instance, 273 @NonNull IBoundObject parentItem, 274 @NonNull ObjectWriter<T> propertyWriter) throws IOException { 275 try { 276 IEnhancedQName wrapperQName = instance.getQName(); 277 writer.writeStartElement(wrapperQName.getNamespace(), wrapperQName.getLocalName()); 278 279 propertyWriter.accept(parentItem, instance); 280 281 writer.writeEndElement(); 282 } catch (XMLStreamException ex) { 283 throw new IOException(ex); 284 } 285 } 286 287 private <T extends IFeatureComplexItemValueHandler & IBoundDefinitionModelComplex> void writeDefinitionObject( 288 @NonNull T definition, 289 @NonNull IBoundObject parentItem, 290 @NonNull ObjectWriter<T> propertyWriter) throws IOException { 291 292 try { 293 IEnhancedQName qname = getObjectQName(); 294 NamespaceContext nsContext = writer.getNamespaceContext(); 295 String prefix = nsContext.getPrefix(qname.getNamespace()); 296 if (prefix == null) { 297 prefix = ""; 298 } 299 300 writer.writeStartElement(prefix, qname.getLocalName(), qname.getNamespace()); 301 302 propertyWriter.accept(parentItem, definition); 303 304 writer.writeEndElement(); 305 } catch (XMLStreamException ex) { 306 throw new IOException(ex); 307 } 308 } 309 310 @Override 311 public void writeItemFlag(Object item, IBoundInstanceFlag instance) throws IOException { 312 String itemString; 313 try { 314 itemString = instance.getJavaTypeAdapter().asString(item); 315 } catch (IllegalArgumentException ex) { 316 throw new IOException(ex); 317 } 318 IEnhancedQName name = instance.getQName(); 319 try { 320 if (name.getNamespace().isEmpty()) { 321 writer.writeAttribute(name.getLocalName(), itemString); 322 } else { 323 writer.writeAttribute(name.getNamespace(), name.getLocalName(), itemString); 324 } 325 } catch (XMLStreamException ex) { 326 throw new IOException(ex); 327 } 328 } 329 330 @Override 331 public void writeItemField(Object item, IBoundInstanceModelFieldScalar instance) throws IOException { 332 try { 333 if (instance.isEffectiveValueWrappedInXml()) { 334 IEnhancedQName wrapperQName = instance.getQName(); 335 writer.writeStartElement(wrapperQName.getNamespace(), wrapperQName.getLocalName()); 336 instance.getJavaTypeAdapter().writeXmlValue(item, wrapperQName, writer); 337 writer.writeEndElement(); 338 } else { 339 instance.getJavaTypeAdapter().writeXmlValue(item, getObjectQName(), writer); 340 } 341 } catch (XMLStreamException ex) { 342 throw new IOException(ex); 343 } 344 } 345 346 @Override 347 public void writeItemField(IBoundObject item, IBoundInstanceModelFieldComplex instance) throws IOException { 348 ItemWriter itemWriter = new ItemWriter(instance.getQName()); 349 writeModelObject( 350 instance, 351 item, 352 ((ObjectWriter<IBoundInstanceModelFieldComplex>) this::writeFlags) 353 .andThen(itemWriter::writeFieldValue)); 354 } 355 356 @Override 357 public void writeItemField(IBoundObject item, IBoundInstanceModelGroupedField instance) throws IOException { 358 ItemWriter itemWriter = new ItemWriter(instance.getQName()); 359 writeGroupedModelObject( 360 instance, 361 item, 362 ((ObjectWriter<IBoundInstanceModelGroupedField>) this::writeFlags) 363 .andThen(itemWriter::writeFieldValue)); 364 } 365 366 @Override 367 public void writeItemField(IBoundObject item, IBoundDefinitionModelFieldComplex definition) throws IOException { 368 ItemWriter itemWriter = new ItemWriter(definition.getQName()); 369 writeDefinitionObject( 370 definition, 371 item, 372 ((ObjectWriter<IBoundDefinitionModelFieldComplex>) this::writeFlags) 373 .andThen(itemWriter::writeFieldValue)); 374 } 375 376 @Override 377 public void writeItemFieldValue(Object parentItem, IBoundFieldValue fieldValue) throws IOException { 378 Object item = fieldValue.getValue(parentItem); 379 if (item != null) { 380 fieldValue.getJavaTypeAdapter().writeXmlValue(item, getObjectQName(), writer); 381 } 382 } 383 384 @Override 385 public void writeItemAssembly(IBoundObject item, IBoundInstanceModelAssembly instance) throws IOException { 386 ItemWriter itemWriter = new ItemWriter(instance.getQName()); 387 writeModelObject( 388 instance, 389 item, 390 ((ObjectWriter<IBoundInstanceModelAssembly>) this::writeFlags) 391 .andThen(itemWriter::writeAssemblyModel)); 392 } 393 394 @Override 395 public void writeItemAssembly(IBoundObject item, IBoundInstanceModelGroupedAssembly instance) throws IOException { 396 ItemWriter itemWriter = new ItemWriter(instance.getQName()); 397 writeGroupedModelObject( 398 instance, 399 item, 400 ((ObjectWriter<IBoundInstanceModelGroupedAssembly>) this::writeFlags) 401 .andThen(itemWriter::writeAssemblyModel)); 402 } 403 404 @Override 405 public void writeItemAssembly(IBoundObject item, IBoundDefinitionModelAssembly definition) throws IOException { 406 // this is a special case where we are writing a top-level, potentially root, 407 // element. Need to take the object qname passed in 408 writeDefinitionObject( 409 definition, 410 item, 411 ((ObjectWriter<IBoundDefinitionModelAssembly>) this::writeFlags) 412 .andThen(this::writeAssemblyModel)); 413 } 414 415 @Override 416 public void writeChoiceGroupItem(IBoundObject item, IBoundInstanceModelChoiceGroup instance) throws IOException { 417 IBoundInstanceModelGroupedNamed actualInstance = instance.getItemInstance(item); 418 assert actualInstance != null; 419 actualInstance.writeItem(item, this); 420 } 421 } 422 423 private abstract static class AbstractItemWriter implements IItemWriteHandler { 424 @NonNull 425 private final IEnhancedQName objectQName; 426 427 protected AbstractItemWriter(@NonNull IEnhancedQName qname) { 428 this.objectQName = qname; 429 } 430 431 /** 432 * Get the qualified name of the item's parent. 433 * 434 * @return the qualified name 435 */ 436 @NonNull 437 protected IEnhancedQName getObjectQName() { 438 return objectQName; 439 } 440 } 441}