// // OMEXMLServiceImpl.java // /* OME Bio-Formats package for reading and converting biological file formats. Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package loci.formats.services; import java.io.IOException; import java.util.Hashtable; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Templates; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import loci.common.services.AbstractService; import loci.common.services.ServiceException; import loci.common.xml.XMLTools; import loci.formats.FormatTools; import loci.formats.MetadataTools; import loci.formats.meta.IMetadata; import loci.formats.meta.MetadataConverter; import loci.formats.meta.MetadataRetrieve; import loci.formats.meta.MetadataStore; import loci.formats.ome.OMEXMLMetadata; import loci.formats.ome.OMEXMLMetadataImpl; import ome.xml.OMEXMLFactory; import ome.xml.model.BinData; import ome.xml.model.Channel; import ome.xml.model.Image; import ome.xml.model.MetadataOnly; import ome.xml.model.OME; import ome.xml.model.OMEModel; import ome.xml.model.OMEModelImpl; import ome.xml.model.OMEModelObject; import ome.xml.model.Pixels; import ome.xml.model.StructuredAnnotations; import ome.xml.model.XMLAnnotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * <dl><dt><b>Source code:</b></dt> * <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/services/OMEXMLServiceImpl.java">Trac</a>, * <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/services/OMEXMLServiceImpl.java;hb=HEAD">Gitweb</a></dd></dl> * * @author callan */ public class OMEXMLServiceImpl extends AbstractService implements OMEXMLService { public static final String NO_OME_XML_MSG = "ome-xml.jar is required to read OME-TIFF files. " + "Please download it from " + FormatTools.URL_BIO_FORMATS_LIBRARIES; /** Logger for this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(OMEXMLService.class); /** Reordering stylesheet. */ private static final Templates reorderXSLT = XMLTools.getStylesheet("/loci/formats/meta/reorder-2008-09.xsl", OMEXMLServiceImpl.class); /** Stylesheets for updating from previous schema releases. */ private static final Templates UPDATE_2003FC = XMLTools.getStylesheet("/loci/formats/meta/2003-FC-to-2008-09.xsl", OMEXMLServiceImpl.class); private static final Templates UPDATE_2006LO = XMLTools.getStylesheet("/loci/formats/meta/2006-LO-to-2008-09.xsl", OMEXMLServiceImpl.class); private static final Templates UPDATE_200706 = XMLTools.getStylesheet("/loci/formats/meta/2007-06-to-2008-09.xsl", OMEXMLServiceImpl.class); private static final Templates UPDATE_200802 = XMLTools.getStylesheet("/loci/formats/meta/2008-02-to-2008-09.xsl", OMEXMLServiceImpl.class); private static final Templates UPDATE_200809 = XMLTools.getStylesheet("/loci/formats/meta/2008-09-to-2009-09.xsl", OMEXMLServiceImpl.class); private static final Templates UPDATE_200909 = XMLTools.getStylesheet("/loci/formats/meta/2009-09-to-2010-04.xsl", OMEXMLServiceImpl.class); private static final Templates UPDATE_201004 = XMLTools.getStylesheet("/loci/formats/meta/2010-04-to-2010-06.xsl", OMEXMLServiceImpl.class); private static final Templates UPDATE_201006 = XMLTools.getStylesheet("/loci/formats/meta/2010-06-to-2011-06.xsl", OMEXMLServiceImpl.class); private static final String SCHEMA_PATH = "http://www.openmicroscopy.org/Schemas/OME/"; /** * Default constructor. */ public OMEXMLServiceImpl() { checkClassDependency(ome.xml.model.OMEModelObject.class); } /** @see OMEXMLService#getLatestVersion() */ public String getLatestVersion() { return OMEXMLFactory.LATEST_VERSION; } /** @see OMEXMLService#transformToLatestVersion(String) */ public String transformToLatestVersion(String xml) throws ServiceException { String version = getOMEXMLVersion(xml); if (version.equals(getLatestVersion())) return xml; LOGGER.debug("Attempting to update XML with version: {}", version); LOGGER.trace("Initial dump: {}", xml); String transformed = null; try { if (version.equals("2003-FC")) { xml = verifyOMENamespace(xml); LOGGER.debug("Running UPDATE_2003FC stylesheet."); transformed = XMLTools.transformXML(xml, UPDATE_2003FC); } else if (version.equals("2006-LO")) { xml = verifyOMENamespace(xml); LOGGER.debug("Running UPDATE_2006LO stylesheet."); transformed = XMLTools.transformXML(xml, UPDATE_2006LO); } else if (version.equals("2007-06")) { xml = verifyOMENamespace(xml); LOGGER.debug("Running UPDATE_200706 stylesheet."); transformed = XMLTools.transformXML(xml, UPDATE_200706); } else if (version.equals("2008-02")) { xml = verifyOMENamespace(xml); LOGGER.debug("Running UPDATE_200802 stylesheet."); transformed = XMLTools.transformXML(xml, UPDATE_200802); } else transformed = xml; LOGGER.debug("XML updated to at least 2008-09"); LOGGER.trace("At least 2008-09 dump: {}", transformed); if (!version.equals("2009-09") && !version.equals("2010-04") && !version.equals("2010-06")) { transformed = verifyOMENamespace(transformed); LOGGER.debug("Running UPDATE_200809 stylesheet."); transformed = XMLTools.transformXML(transformed, UPDATE_200809); } LOGGER.debug("XML updated to at least 2009-09"); LOGGER.trace("At least 2009-09 dump: {}", transformed); if (!version.equals("2010-04") && !version.equals("2010-06")) { transformed = verifyOMENamespace(transformed); LOGGER.debug("Running UPDATE_200909 stylesheet."); transformed = XMLTools.transformXML(transformed, UPDATE_200909); } else transformed = xml; LOGGER.debug("XML updated to at least 2010-04"); LOGGER.trace("At least 2010-04 dump: {}", transformed); if (!version.equals("2010-06")) { transformed = verifyOMENamespace(transformed); LOGGER.debug("Running UPDATE_201004 stylesheet."); transformed = XMLTools.transformXML(transformed, UPDATE_201004); } else transformed = xml; LOGGER.debug("XML updated to at least 2010-06"); transformed = verifyOMENamespace(transformed); LOGGER.debug("Running UPDATE_201006 stylesheet."); transformed = XMLTools.transformXML(transformed, UPDATE_201006); LOGGER.debug("XML updated to at least 2011-06"); // fix namespaces transformed = transformed.replaceAll("<ns.*?:", "<"); transformed = transformed.replaceAll("xmlns:ns.*?=", "xmlns:OME="); transformed = transformed.replaceAll("</ns.*?:", "</"); LOGGER.trace("Transformed XML dump: {}", transformed); return transformed; } catch (IOException e) { LOGGER.warn("Could not transform version " + version + " OME-XML."); } return null; } /** @see OMEXMLService#createOMEXMLMetadata() */ public OMEXMLMetadata createOMEXMLMetadata() throws ServiceException { return createOMEXMLMetadata(null); } /** @see OMEXMLService#createOMEXMLMetadata(java.lang.String) */ public OMEXMLMetadata createOMEXMLMetadata(String xml) throws ServiceException { return createOMEXMLMetadata(xml, null); } /** * @see OMEXMLService#createOMEXMLMetadata(java.lang.String, java.lang.String) */ public OMEXMLMetadata createOMEXMLMetadata(String xml, String version) throws ServiceException { if (xml != null) { xml = XMLTools.sanitizeXML(xml); } OMEModelObject ome = xml == null ? null : createRoot(transformToLatestVersion(xml)); OMEXMLMetadata meta = new OMEXMLMetadataImpl(); if (ome != null) meta.setRoot(ome); return meta; } /** @see OMEXMLService#createOMEXMLRoot(java.lang.String) */ public Object createOMEXMLRoot(String xml) throws ServiceException { return createRoot(transformToLatestVersion(xml)); } /** @see OMEXMLService#isOMEXMLMetadata(java.lang.Object) */ public boolean isOMEXMLMetadata(Object o) { return o instanceof OMEXMLMetadata; } /** @see OMEXMLService#isOMEXMLRoot(java.lang.Object) */ public boolean isOMEXMLRoot(Object o) { return o instanceof OMEModelObject; } /** * Constructs an OME root node. <b>NOTE:</b> This method is mostly here to * ensure type safety of return values as instances of service dependency * classes should not leak out of the interface. * @param xml String of XML to create the root node from. * @return An ome.xml.model.OMEModelObject subclass root node. * @throws IOException If there is an error reading from the string. * @throws SAXException If there is an error parsing the XML. * @throws ParserConfigurationException If there is an error preparing the * parsing infrastructure. */ private OMEModelObject createRoot(String xml) throws ServiceException { try { OMEModel model = new OMEModelImpl(); OME ome = new OME(XMLTools.parseDOM(xml).getDocumentElement(), model); model.resolveReferences(); return ome; } catch (Exception e) { throw new ServiceException(e); } } /** @see OMEXMLService#getOMEXMLVersion(java.lang.Object) */ public String getOMEXMLVersion(Object o) { if (o == null) return null; if (o instanceof OMEXMLMetadata || o instanceof OMEModelObject) { return OMEXMLFactory.LATEST_VERSION; } else if (o instanceof String) { String xml = (String) o; try { Element e = XMLTools.parseDOM(xml).getDocumentElement(); String namespace = e.getAttribute("xmlns"); if (namespace == null || namespace.equals("")) { namespace = e.getAttribute("xmlns:ome"); } return namespace.endsWith("ome.xsd") ? "2003-FC" : namespace.substring(namespace.lastIndexOf("/") + 1); } catch (ParserConfigurationException pce) { } catch (SAXException se) { } catch (IOException ioe) { } } return null; } /** @see OMEXMLService#getOMEMetadata(loci.formats.meta.MetadataRetrieve) */ public OMEXMLMetadata getOMEMetadata(MetadataRetrieve src) throws ServiceException { // check if the metadata is already an OME-XML metadata object if (src instanceof OMEXMLMetadata) return (OMEXMLMetadata) src; // populate a new OME-XML metadata object with metadata // converted from the non-OME-XML metadata object OMEXMLMetadata omexmlMeta = createOMEXMLMetadata(); convertMetadata(src, omexmlMeta); return omexmlMeta; } /** @see OMEXMLService#getOMEXML(loci.formats.meta.MetadataRetrieve) */ public String getOMEXML(MetadataRetrieve src) throws ServiceException { OMEXMLMetadata omexmlMeta = getOMEMetadata(src); String xml = omexmlMeta.dumpXML(); // make sure that the namespace has been set correctly // convert XML string to DOM Document doc = null; Exception exception = null; try { doc = XMLTools.parseDOM(xml); } catch (ParserConfigurationException exc) { exception = exc; } catch (SAXException exc) { exception = exc; } catch (IOException exc) { exception = exc; } if (exception != null) { LOGGER.info("Malformed OME-XML", exception); return null; } Element root = doc.getDocumentElement(); root.setAttribute("xmlns", SCHEMA_PATH + getLatestVersion()); // convert tweaked DOM back to XML string try { xml = XMLTools.getXML(doc); } catch (TransformerConfigurationException exc) { exception = exc; } catch (TransformerException exc) { exception = exc; } if (exception != null) { LOGGER.info("Internal XML conversion error", exception); return null; } return xml; } /** @see OMEXMLService#validateOMEXML(java.lang.String) */ public boolean validateOMEXML(String xml) { return validateOMEXML(xml, false); } /** @see OMEXMLService#validateOMEXML(java.lang.String, boolean) */ public boolean validateOMEXML(String xml, boolean pixelsHack) { // HACK: Inject a TiffData element beneath any childless Pixels elements. if (pixelsHack) { // convert XML string to DOM Document doc = null; Exception exception = null; try { doc = XMLTools.parseDOM(xml); } catch (ParserConfigurationException exc) { exception = exc; } catch (SAXException exc) { exception = exc; } catch (IOException exc) { exception = exc; } if (exception != null) { LOGGER.info("Malformed OME-XML", exception); return false; } // inject TiffData elements as needed NodeList list = doc.getElementsByTagName("Pixels"); for (int i=0; i<list.getLength(); i++) { Node node = list.item(i); NodeList children = node.getChildNodes(); boolean needsTiffData = true; for (int j=0; j<children.getLength(); j++) { Node child = children.item(j); String name = child.getLocalName(); if ("TiffData".equals(name) || "BinData".equals(name)) { needsTiffData = false; break; } } if (needsTiffData) { // inject TiffData element Node tiffData = doc.createElement("TiffData"); node.insertBefore(tiffData, node.getFirstChild()); } } // convert tweaked DOM back to XML string try { xml = XMLTools.getXML(doc); } catch (TransformerConfigurationException exc) { exception = exc; } catch (TransformerException exc) { exception = exc; } if (exception != null) { LOGGER.info("Internal XML conversion error", exception); return false; } } return XMLTools.validateXML(xml, "OME-XML"); } /** * @see OMEXMLService#populateOriginalMetadata(loci.formats.ome.OMEXMLMetadata, Hashtable) */ public void populateOriginalMetadata(OMEXMLMetadata omexmlMeta, Hashtable<String, Object> metadata) { ((OMEXMLMetadataImpl) omexmlMeta).resolveReferences(); OME root = (OME) omexmlMeta.getRoot(); StructuredAnnotations annotations = root.getStructuredAnnotations(); if (annotations == null) annotations = new StructuredAnnotations(); int annotationIndex = annotations.sizeOfXMLAnnotationList(); for (String key : metadata.keySet()) { OriginalMetadataAnnotation annotation = new OriginalMetadataAnnotation(); annotation.setID(MetadataTools.createLSID("Annotation", annotationIndex)); annotation.setKey(key); annotation.setValue(metadata.get(key).toString()); annotations.addXMLAnnotation(annotation); annotationIndex++; } root.setStructuredAnnotations(annotations); omexmlMeta.setRoot(root); } /** * @see OMEXMLService#populateOriginalMetadata(loci.formats.ome.OMEXMLMetadata, java.lang.String, java.lang.String) */ public void populateOriginalMetadata(OMEXMLMetadata omexmlMeta, String key, String value) { ((OMEXMLMetadataImpl) omexmlMeta).resolveReferences(); OME root = (OME) omexmlMeta.getRoot(); StructuredAnnotations annotations = root.getStructuredAnnotations(); if (annotations == null) annotations = new StructuredAnnotations(); int annotationIndex = annotations.sizeOfXMLAnnotationList(); OriginalMetadataAnnotation annotation = new OriginalMetadataAnnotation(); annotation.setID(MetadataTools.createLSID("Annotation", annotationIndex)); annotation.setKey(key); annotation.setValue(value); annotations.addXMLAnnotation(annotation); root.setStructuredAnnotations(annotations); omexmlMeta.setRoot(root); } /** * @see OMEXMLService#convertMetadata(java.lang.String, loci.formats.meta.MetadataStore) */ public void convertMetadata(String xml, MetadataStore dest) throws ServiceException { OMEModelObject ome = createRoot(transformToLatestVersion(xml)); String rootVersion = getOMEXMLVersion(ome); String storeVersion = getOMEXMLVersion(dest); if (rootVersion.equals(storeVersion)) { // metadata store is already an OME-XML metadata object of the // correct schema version; populate OME-XML string directly if (!(dest instanceof OMEXMLMetadata)) { throw new IllegalArgumentException( "Expecting OMEXMLMetadata instance."); } dest.setRoot(ome); } else { // metadata store is incompatible; create an OME-XML // metadata object and copy it into the destination IMetadata src = createOMEXMLMetadata(xml); convertMetadata(src, dest); } } /** * @see OMEXMLService#convertMetadata(loci.formats.meta.MetadataRetrieve, loci.formats.meta.MetadataStore) */ public void convertMetadata(MetadataRetrieve src, MetadataStore dest) { MetadataConverter.convertMetadata(src, dest); } /** @see OMEXMLService#removeBinData(OMEXMLMetadata) */ public void removeBinData(OMEXMLMetadata omexmlMeta) { ((OMEXMLMetadataImpl) omexmlMeta).resolveReferences(); OME root = (OME) omexmlMeta.getRoot(); List<Image> images = root.copyImageList(); for (Image img : images) { Pixels pix = img.getPixels(); List<BinData> binData = pix.copyBinDataList(); for (BinData bin : binData) { pix.removeBinData(bin); } } omexmlMeta.setRoot(root); } /** @see OMEXMLService#removeChannels(OMEXMLMetadata, int, int) */ public void removeChannels(OMEXMLMetadata omexmlMeta, int image, int sizeC) { ((OMEXMLMetadataImpl) omexmlMeta).resolveReferences(); OME root = (OME) omexmlMeta.getRoot(); Pixels img = root.getImage(image).getPixels(); List<Channel> channels = img.copyChannelList(); for (int c=0; c<channels.size(); c++) { Channel channel = channels.get(c); if (channel.getID() == null || c >= sizeC) { img.removeChannel(channel); } } omexmlMeta.setRoot(root); } /** @see OMEXMLService#addMetadataOnly(OMEXMLMetadata, int) */ public void addMetadataOnly(OMEXMLMetadata omexmlMeta, int image) { ((OMEXMLMetadataImpl) omexmlMeta).resolveReferences(); MetadataOnly meta = new MetadataOnly(); OME root = (OME) omexmlMeta.getRoot(); Pixels pix = root.getImage(image).getPixels(); pix.setMetadataOnly(meta); omexmlMeta.setRoot(root); } /** @see OMEXMLService#isEqual(OMEXMLMetadata, OMEXMLMetadata) */ public boolean isEqual(OMEXMLMetadata src1, OMEXMLMetadata src2) { ((OMEXMLMetadataImpl) src1).resolveReferences(); ((OMEXMLMetadataImpl) src2).resolveReferences(); OME omeRoot1 = (OME) src1.getRoot(); OME omeRoot2 = (OME) src2.getRoot(); DocumentBuilder builder = null; try { builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); } catch (ParserConfigurationException e) { return false; } Document doc1 = builder.newDocument(); Document doc2 = builder.newDocument(); Element root1 = omeRoot1.asXMLElement(doc1); Element root2 = omeRoot2.asXMLElement(doc2); return equals(root1, root2); } // -- Utility methods - casting -- /** @see OMEXMLService#asStore(loci.formats.meta.MetadataRetrieve) */ public MetadataStore asStore(MetadataRetrieve meta) { return meta instanceof MetadataStore ? (MetadataStore) meta : null; } /** @see OMEXMLService#asRetrieve(loci.formats.meta.MetadataStore) */ public MetadataRetrieve asRetrieve(MetadataStore meta) { return meta instanceof MetadataRetrieve ? (MetadataRetrieve) meta : null; } // -- Helper methods -- /** Ensures that an xmlns:ome element exists. */ private String verifyOMENamespace(String xml) { try { Document doc = XMLTools.parseDOM(xml); Element e = doc.getDocumentElement(); String omeNamespace = e.getAttribute("xmlns:ome"); if (omeNamespace == null || omeNamespace.equals("")) { e.setAttribute("xmlns:ome", e.getAttribute("xmlns")); } return XMLTools.getXML(doc); } catch (ParserConfigurationException pce) { } catch (TransformerConfigurationException tce) { } catch (TransformerException te) { } catch (SAXException se) { } catch (IOException ioe) { } return null; } /** Compares two Elements for equality. */ public boolean equals(Node e1, Node e2) { NodeList children1 = e1.getChildNodes(); NodeList children2 = e2.getChildNodes(); String localName1 = e1.getLocalName(); if (localName1 == null) { localName1 = ""; } String localName2 = e2.getLocalName(); if (localName2 == null) { localName2 = ""; } if (!localName1.equals(localName2)) { return false; } if (localName1.equals("StructuredAnnotations")) { // we don't care about StructuredAnnotations at all return true; } NamedNodeMap attributes1 = e1.getAttributes(); NamedNodeMap attributes2 = e2.getAttributes(); if (attributes1 == null || attributes2 == null) { if ((attributes1 == null && attributes2 != null) || (attributes1 != null && attributes2 == null)) { return false; } } else if (attributes1.getLength() != attributes2.getLength()) { return false; } else { // make sure that all of the attributes are equal, except for IDs int nAttributes = attributes1.getLength(); for (int i=0; i<nAttributes; i++) { Node n1 = attributes1.item(i); String localName = n1.getNodeName(); if (localName != null && !localName.equals("ID")) { Node n2 = attributes2.getNamedItem(localName); if (n2 == null) { return false; } if (!equals(n1, n2)) { return false; } } else if ("ID".equals(localName)) { if (localName1.endsWith("Settings")) { // this is a reference to a different node // the references are equal if the two referenced nodes are equal Node n2 = attributes2.getNamedItem(localName); Node realRoot1 = findRootNode(e1); Node realRoot2 = findRootNode(e2); String refName = localName1.replaceAll("Settings", ""); Node ref1 = findChildWithID(realRoot1, refName, n1.getNodeValue()); Node ref2 = findChildWithID(realRoot2, refName, n2.getNodeValue()); if (ref1 == null && ref2 == null) { return true; } else if ((ref1 == null && ref2 != null) || (ref1 != null && ref2 == null) || !equals(ref1, ref2)) { return false; } } } } } if (children1.getLength() != children2.getLength()) { return false; } Object node1 = e1.getNodeValue(); Object node2 = e2.getNodeValue(); if (node1 == null && node2 != null) { return false; } if (node1 != null && !node1.equals(node2) && !localName1.equals("")) { return false; } for (int i=0; i<children1.getLength(); i++) { if (!equals(children1.item(i), children2.item(i))) { return false; } } return true; } /** Return the absolute root node for the specified child node. */ private Node findRootNode(Node child) { if (child.getParentNode() != null) { return findRootNode(child.getParentNode()); } return child; } /** Return the child node with specified value for the "ID" attribute. */ private Node findChildWithID(Node root, String name, String id) { NamedNodeMap attributes = root.getAttributes(); if (attributes != null) { Node idNode = attributes.getNamedItem("ID"); if (idNode != null && id.equals(idNode.getNodeValue()) && name.equals(root.getNodeName())) { return root; } } NodeList children = root.getChildNodes(); for (int i=0; i<children.getLength(); i++) { Node result = findChildWithID(children.item(i), name, id); if (result != null) { return result; } } return null; } // -- Helper class -- class OriginalMetadataAnnotation extends XMLAnnotation { private static final String ORIGINAL_METADATA_NS = "openmicroscopy.org/OriginalMetadata"; private String key, value; // -- OriginalMetadataAnnotation methods -- public void setKey(String key) { this.key = key; } public void setValue(String value) { this.value = value; } // -- XMLAnnotation methods -- /* @see ome.xml.model.XMLAnnotation#asXMLElement(Document, Element) */ protected Element asXMLElement(Document document, Element element) { if (element == null) { element = document.createElementNS(XMLAnnotation.NAMESPACE, "XMLAnnotation"); } Element keyElement = document.createElementNS(ORIGINAL_METADATA_NS, "Key"); Element valueElement = document.createElementNS(ORIGINAL_METADATA_NS, "Value"); keyElement.setTextContent(key); valueElement.setTextContent(value); Element originalMetadata = document.createElementNS(ORIGINAL_METADATA_NS, "OriginalMetadata"); originalMetadata.appendChild(keyElement); originalMetadata.appendChild(valueElement); Element annotationValue = document.createElementNS(XMLAnnotation.NAMESPACE, "Value"); annotationValue.appendChild(originalMetadata); element.appendChild(annotationValue); return super.asXMLElement(document, element); } } }