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