//------------------------------------------------------------------------------------------------// // // // M a r s h a l l i n g // // // //------------------------------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // Copyright © Herve Bitteur and others 2000-2016. All rights reserved. // This software is released under the GNU Lesser General Public License v3. // Go to https://github.com/Audiveris/proxymusic/issues to report bugs or suggestions. //------------------------------------------------------------------------------------------------// // </editor-fold> package org.audiveris.proxymusic.util; import org.audiveris.proxymusic.Encoding; import org.audiveris.proxymusic.Identification; import org.audiveris.proxymusic.ObjectFactory; import org.audiveris.proxymusic.ScorePartwise; import org.audiveris.proxymusic.opus.Opus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; import java.io.BufferedWriter; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.String; // Don't remove this line! import java.util.Arrays; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeConstants; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.NamespaceContext; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import javax.xml.stream.util.StreamReaderDelegate; /** * Class {@code Marshalling} gathers static methods to marshal and to un-marshal {@link * ScorePartwise} or {@link Opus} java objects to/from an output/input stream in UTF8 * encoding and using MusicXML format. * <p> * Several tricks are used to work around namespaces during marshalling and un-marshalling since * MusicXML does not support them * (MusicXML uses prefixed attribute names such as <i>xlink:href</i>, although it never binds the * xlink prefix to its proper namespace URI). * <p> * No access to a DTD (local or remote) is made during the un-marshalling which ignores DTDs. * <p> * The method {@link #getContext(Class)} is publicly visible so as to allow an asynchronous * elaboration of the JAXB context, which can be an expensive operation because of the large number * of Java classes in the ScorePartwise hierarchy. * * @author Hervé Bitteur */ public abstract class Marshalling { //~ Static fields/initializers ----------------------------------------------------------------- private static final Logger logger = LoggerFactory.getLogger( Marshalling.class); /** JAXB contexts. */ private static final Map<Class, JAXBContext> jaxbContextMap = new HashMap<Class, JAXBContext>(); /** The XML document statement. */ private static final String XML_LINE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; /** The official URI for XLink namespace. */ private static final String XLINK_NAMESPACE_URI = "http://www.w3.org/1999/xlink"; /** The collection of all supported attributes with xlink: prefix. */ private static final List<String> XLINK_ATTRIBUTES = Arrays.asList( "href", "type", "role", "title", "show", "actuate"); /** The DOCTYPE statement for PARTWISE. */ private static final String PARTWISE_DOCTYPE_LINE = "<!DOCTYPE score-partwise PUBLIC \"-//Recordare//DTD MusicXML " + ProgramId.VERSION + " Partwise//EN\" \"http://www.musicxml.org/dtds/partwise.dtd\">"; /** The DOCTYPE statement for OPUS. */ private static final String OPUS_DOCTYPE_LINE = "<!DOCTYPE opus PUBLIC \"-//Recordare//DTD MusicXML " + ProgramId.VERSION + " Opus//EN\" \"http://www.musicxml.org/dtds/opus.dtd\">"; //~ Constructors ------------------------------------------------------------------------------- /** * This class is not meant to be instantiated. */ private Marshalling () { } //~ Methods ------------------------------------------------------------------------------------ //----------------// // getJaxbContext // //----------------// /** * Get access to (and elaborate if not yet done) the needed JAXB context. * This method can be called any time. * * @param classe the desired class * @return the ready to use JAXB context * @exception JAXBException if anything goes wrong */ public static JAXBContext getContext (Class classe) throws JAXBException { // Lazy creation JAXBContext context = jaxbContextMap.get(classe); if (context == null) { synchronized (jaxbContextMap) { context = jaxbContextMap.get(classe); // It may have just been created! if (context == null) { context = JAXBContext.newInstance(classe); jaxbContextMap.put(classe, context); } } } return context; } //---------// // marshal // //---------// /** * Marshal the provided <b>ScorePartwise</b> instance to an OutputStream. * * @param scorePartwise the root scorePartwise element * @param os the output stream (not closed by this method) * @param injectSignature false if ProxyMusic encoder must not be referenced * @param indentation formatting indentation value, null for no formatting. * When formatting, a comment line is inserted before parts and measures * @throws MarshallingException global exception (use getCause() for original exception) */ public static void marshal (final ScorePartwise scorePartwise, final OutputStream os, final boolean injectSignature, final Integer indentation) throws MarshallingException { try { // Inject version & signature annotate(scorePartwise, injectSignature); Marshaller marshaller = getContext(ScorePartwise.class).createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); Writer out = new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); out.write(XML_LINE); out.write("\n"); out.write(PARTWISE_DOCTYPE_LINE); XMLOutputFactory outputFactory = XMLOutputFactory.newFactory(); XMLStreamWriter writer = outputFactory.createXMLStreamWriter(out); // Use our custom XmlStreamWriter for name-space, formatting and comment line writer = new MyStreamWriter(writer, indentation); // Marshalling marshaller.marshal(scorePartwise, writer); writer.flush(); } catch (Exception ex) { throw new MarshallingException(ex); } } //---------// // marshal // //---------// /** * Marshal the provided <b>Opus</b> instance to an OutputStream. * * @param opus the root opus element * @param os the output stream (not closed by this method) * @throws MarshallingException global exception (use getCause() for original exception) */ public static void marshal (final Opus opus, final OutputStream os) throws MarshallingException { try { Marshaller marshaller = getContext(Opus.class).createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); Writer out = new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); out.write(XML_LINE); out.write("\n"); out.write(OPUS_DOCTYPE_LINE); XMLOutputFactory outputFactory = XMLOutputFactory.newFactory(); XMLStreamWriter writer = outputFactory.createXMLStreamWriter(out); // Our custom XmlStreamWriter for name-space, formatting and comment line writer = new MyStreamWriter(writer, 2); // Marshalling org.audiveris.proxymusic.opus.ObjectFactory opusFactory = new org.audiveris.proxymusic.opus.ObjectFactory(); JAXBElement<Opus> elem = opusFactory.createOpus(opus); marshaller.marshal(elem, writer); writer.flush(); } catch (Exception ex) { throw new MarshallingException(ex); } } //---------// // marshal // //---------// /** * Marshal the provided <b>ScorePartwise</b> instance directly to a <b>DOM node</b>. * * @param scorePartwise the root element * @param node the DOM node where elements must be added * @param injectSignature false if ProxyMusic encoder must not be referenced * @throws MarshallingException global exception (use getCause() for original exception) */ public static void marshal (final ScorePartwise scorePartwise, final Node node, final boolean injectSignature) throws MarshallingException { try { annotate(scorePartwise, injectSignature); Marshaller marshaller = getContext(ScorePartwise.class).createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); marshaller.marshal(scorePartwise, node); } catch (Exception ex) { throw new MarshallingException(ex); } } //-----------// // unmarshal // //-----------// /** * Un-marshal a <b>ScorePartwise</b> instance or an <b>Opus</b> instance from an InputStream. * <p> * Nota: The URLs of MusicXML DTD are specifically ignored by this method. * * @param is the input stream * @return the root element (either Opus or ScorePartwise object) * @throws UnmarshallingException global exception (use getCause() for original exception) */ public static Object unmarshal (final InputStream is) throws UnmarshallingException { try { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); // Do not try to resolve DTDs (especially on the network!) inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); // Make the input reader non namespace aware // (attributes xlink:href and the like will be manually handled on the fly) inputFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, false); // OK XMLStreamReader xsr = inputFactory.createXMLStreamReader(is); // Use our specific stream reader xsr = new MyStreamReader(xsr); XMLEventReader reader = inputFactory.createXMLEventReader(xsr); while (reader.hasNext()) { // Peek root element, to decide between ScorePartwise or Opus un-marshalling XMLEvent event = reader.peek(); if (event.isStartElement()) { StartElement rootStart = event.asStartElement(); QName qName = rootStart.getName(); String name = qName.getLocalPart(); if (name.equals("opus")) { Unmarshaller um = getContext(Opus.class).createUnmarshaller(); return um.unmarshal(reader, Opus.class).getValue(); } else if (name.equals("score-partwise")) { Unmarshaller um = getContext(ScorePartwise.class).createUnmarshaller(); return um.unmarshal(reader, ScorePartwise.class).getValue(); } else { reader.next(); } } else { reader.next(); } } return null; } catch (Exception ex) { throw new UnmarshallingException(ex); } } //----------// // annotate // //----------// /** * Annotate the scorePartwise tree with information about MusicXML version and, if * so desired, with signature composed of ProxyMusic version and date of marshalling. * * @param scorePartwise the tree to annotate * @param injectSignature if true, ProxyMusic information is added */ private static void annotate (final ScorePartwise scorePartwise, final boolean injectSignature) throws DatatypeConfigurationException { // Predefined factory for all proxymusic elements ObjectFactory factory = new ObjectFactory(); // Inject version scorePartwise.setVersion(ProgramId.VERSION); // Inject signature if so desired if (injectSignature) { // Identification Identification identification = scorePartwise.getIdentification(); if (identification == null) { identification = factory.createIdentification(); scorePartwise.setIdentification(identification); } // Encoding Encoding encoding = identification.getEncoding(); if (encoding == null) { encoding = factory.createEncoding(); identification.setEncoding(encoding); } // [Encoding]/Software (only if ProxyMusic is not already listed there) List<JAXBElement<?>> list = encoding.getEncodingDateOrEncoderOrSoftware(); final String programName = ProgramId.NAME + " "; for (Iterator<JAXBElement<?>> it = list.iterator(); it.hasNext();) { JAXBElement<?> element = it.next(); if (element.getName().getLocalPart().equals("software")) { Object obj = element.getValue(); if (obj instanceof String && ((String) obj).startsWith(programName)) { // Remove it it.remove(); break; } } } list.add( factory.createEncodingSoftware( ProgramId.NAME + " " + ProgramId.VERSION + "." + ProgramId.REVISION)); // [Encoding]/EncodingDate (overwrite any existing date) for (Iterator<JAXBElement<?>> it = list.iterator(); it.hasNext();) { if (it.next().getName().getLocalPart().equals("encoding-date")) { it.remove(); break; } } // Use date without time information (patch by lasconic) // Output: 2012-05-03 // instead of: 2012-05-03T16:17:51.250+02:00 XMLGregorianCalendar gc = DatatypeFactory.newInstance() .newXMLGregorianCalendar( new GregorianCalendar()); gc.setTimezone(DatatypeConstants.FIELD_UNDEFINED); gc.setTime( DatatypeConstants.FIELD_UNDEFINED, DatatypeConstants.FIELD_UNDEFINED, DatatypeConstants.FIELD_UNDEFINED); list.add(factory.createEncodingEncodingDate(gc)); } } //~ Inner Classes ------------------------------------------------------------------------------ /** Global exception for formatting. */ public static class FormattingException extends Exception { //~ Constructors --------------------------------------------------------------------------- public FormattingException (Throwable cause) { super(cause); } } /** Global exception for marshalling. */ public static class MarshallingException extends Exception { //~ Constructors --------------------------------------------------------------------------- public MarshallingException (Throwable cause) { super(cause); } } /** Global exception for un-marshalling. */ public static class UnmarshallingException extends Exception { //~ Constructors --------------------------------------------------------------------------- public UnmarshallingException (Throwable cause) { super(cause); } } //--------------------// // MyNamespaceContext // //--------------------// /** * Class to avoid any namespace binding during marshal operation. */ private static class MyNamespaceContext implements NamespaceContext { //~ Instance fields ------------------------------------------------------------------------ private String defaultNS = ""; //~ Methods -------------------------------------------------------------------------------- public String getNamespaceURI (String prefix) { if ("".equals(prefix)) { return defaultNS; } return null; } public String getPrefix (String namespaceURI) { // Trick for xlink:... if (XLINK_NAMESPACE_URI.equals(namespaceURI)) { return "xlink"; } return ""; } public Iterator getPrefixes (String namespaceURI) { return null; } public void setDefaultNS (String ns) { defaultNS = ns; } } //----------------// // MyStreamReader // //----------------// /** * Class to resolve any xlink:xxx attribute on the fly during unmarshal operation. */ private static class MyStreamReader extends StreamReaderDelegate { //~ Constructors --------------------------------------------------------------------------- public MyStreamReader (XMLStreamReader reader) { super(reader); } //~ Methods -------------------------------------------------------------------------------- @Override public QName getAttributeName (int index) { String prefix = getAttributePrefix(index); if ("xlink".equals(prefix)) { QName qName = super.getAttributeName(index); String local = qName.getLocalPart(); if (XLINK_ATTRIBUTES.contains(local)) { return new QName(XLINK_NAMESPACE_URI, local, "xlink"); } } return super.getAttributeName(index); } } //----------------// // MyStreamWriter // //----------------// /** * Class {@code MyStreamWriter} removes the namespaces from the marshal operation. * <p> * It is a wrapper for an XMLStreamWriter that intercepts and removes the relevant namespace * info. It does so by treating all namespace declarations as default namespaces. * <p> * The "xlink:" prefix is preserved for relevant attributes (opus). * <p> * It also performs formatting on the fly, if a non-null indentation value was provided. * <p> * Detected empty elements are written as one (empty) element rather than start + end tags. * <p> * It also inserts a comment line just before a part or measure element begins. * * @author Blaise Doughan (namespace handling) * @see <a href="http://stackoverflow.com/a/5722013">Blaise article</a> * @author Hervé Bitteur (other features) */ private static class MyStreamWriter extends StreamWriterDelegate { //~ Instance fields ------------------------------------------------------------------------ /** Special name context. */ private final MyNamespaceContext nc = new MyNamespaceContext(); /** Indentation string, if any. */ private final String INDENT; /** Current level of indentation. */ private int level; /** Are we closing element(s)?. */ private boolean closing; /** Pending element if any. (meant to handle empty elements) */ private PendingElement pending; //~ Constructors --------------------------------------------------------------------------- /** * Creates a new {@code MyXMLStreamWriter} object. * * @param writer the real XML stream writer * @param indentValue desired indentation value, if any (null, 0 or n) * * @throws XMLStreamException for any processing error */ public MyStreamWriter (XMLStreamWriter writer, Integer indentValue) throws XMLStreamException { super(writer); writer.setNamespaceContext(nc); INDENT = getIndentString(indentValue); } //~ Methods -------------------------------------------------------------------------------- @Override public void setNamespaceContext (NamespaceContext context) throws XMLStreamException { // void (we keep using our own NamespaceContext) } @Override public void writeAttribute (String localName, String value) throws XMLStreamException { checkPending(); super.writeAttribute(localName, value); } @Override public void writeAttribute (String namespaceURI, String localName, String value) throws XMLStreamException { checkPending(); super.writeAttribute(namespaceURI, localName, value); } @Override public void writeAttribute (String prefix, String namespaceURI, String localName, String value) throws XMLStreamException { checkPending(); super.writeAttribute(prefix, namespaceURI, localName, value); } @Override public void writeCData (String data) throws XMLStreamException { checkPending(); super.writeCData(data); } @Override public void writeCharacters (String text) throws XMLStreamException { checkPending(); super.writeCharacters(text); } @Override public void writeCharacters (char[] text, int start, int len) throws XMLStreamException { checkPending(); super.writeCharacters(text, start, len); } @Override public void writeComment (String data) throws XMLStreamException { checkPending(); indentComment(); super.writeComment(data); } @Override public void writeDTD (String dtd) throws XMLStreamException { checkPending(); super.writeDTD(dtd); } @Override public void writeDefaultNamespace (String namespaceURI) throws XMLStreamException { checkPending(); super.writeDefaultNamespace(namespaceURI); } @Override public void writeEmptyElement (String localName) throws XMLStreamException { checkPending(); super.writeEmptyElement(localName); } @Override public void writeEmptyElement (String namespaceURI, String localName) throws XMLStreamException { checkPending(); super.writeEmptyElement(namespaceURI, localName); } @Override public void writeEmptyElement (String prefix, String localName, String namespaceURI) throws XMLStreamException { checkPending(); super.writeEmptyElement(prefix, localName, namespaceURI); } @Override public void writeEndElement () throws XMLStreamException { if (pending != null) { // The end is immediately following the start, with nothing in between: // So, write an empty element, instead of start + end pending.writeEmpty(); indentEnd(); pending = null; } else { indentEnd(); super.writeEndElement(); } } @Override public void writeEntityRef (String name) throws XMLStreamException { checkPending(); super.writeEntityRef(name); } @Override public void writeNamespace (String prefix, String namespaceURI) throws XMLStreamException { // void (we don't output this information) } @Override public void writeProcessingInstruction (String target) throws XMLStreamException { checkPending(); super.writeProcessingInstruction(target); } @Override public void writeProcessingInstruction (String target, String data) throws XMLStreamException { checkPending(); super.writeProcessingInstruction(target, data); } @Override public void writeStartElement (String localName) throws XMLStreamException { checkPending(); indentStart(localName); // We don't write the element start now, but save it as pending pending = new PendingElement(localName); } @Override public void writeStartElement (String namespaceURI, String localName) throws XMLStreamException { checkPending(); indentStart(localName); // We don't write the element start now, but save it as pending pending = new PendingElement2(namespaceURI, localName); } @Override public void writeStartElement (String prefix, String localName, String namespaceURI) throws XMLStreamException { checkPending(); indentStart(localName); // We don't write the element start now, but save it as pending pending = new PendingElement3(prefix, localName, namespaceURI); } //--------------// // checkPending // //--------------// private void checkPending () throws XMLStreamException { if (pending != null) { pending.writeStart(); pending = null; } } //----------// // doIndent // //----------// /** * Insert a new line, followed by proper level of indentation. * * @throws XMLStreamException */ private void doIndent () throws XMLStreamException { super.writeCharacters("\n"); for (int i = 0; i < level; i++) { super.writeCharacters(INDENT); } } //-----------------// // getIndentString // //-----------------// /** * Build proper indentation string. * * @param value desired indentation value * @return the indentation string to use: null for no indentation at all, empty string for * LF only, non-empty string for LF and concrete indentation */ private String getIndentString (Integer value) { if (value == null) { return null; } StringBuilder sb = new StringBuilder(); for (int i = 0; i < value; i++) { sb.append(" "); } return sb.toString(); } //---------------// // indentComment // //---------------// /** * Indentation before comment. Always indent. * * @throws XMLStreamException */ private void indentComment () throws XMLStreamException { if (INDENT != null) { doIndent(); } } //-----------// // indentEnd // //-----------// /** * Indentation before end tag. Indent except on first close. * * @throws XMLStreamException */ private void indentEnd () throws XMLStreamException { if (INDENT != null) { level--; if (closing) { doIndent(); } closing = true; } } //-------------// // indentStart // //-------------// /** * Indentation before start tag. Always indent. * * @param localName the local tag name * @throws XMLStreamException */ private void indentStart (String localName) throws XMLStreamException { if (INDENT != null) { // Insert visible comment lines only for measures and parts if (localName.equals("measure")) { doIndent(); super.writeComment("======================================================="); } else if (localName.equals("part")) { doIndent(); super.writeComment("= = = = = = = = = = = = = = = = = = = = = = = = = = = = ="); } doIndent(); level++; closing = false; } } //~ Inner Classes -------------------------------------------------------------------------- /** * Class meant to save a starting element with its parameters. */ private class PendingElement { //~ Instance fields -------------------------------------------------------------------- final String localName; //~ Constructors ----------------------------------------------------------------------- public PendingElement (String localName) { this.localName = localName; } //~ Methods ---------------------------------------------------------------------------- /** Write an empty element. */ public void writeEmpty () throws XMLStreamException { getParent().writeEmptyElement(localName); } /** Write just the element start. */ public void writeStart () throws XMLStreamException { getParent().writeStartElement(localName); } } private class PendingElement2 extends PendingElement { //~ Instance fields -------------------------------------------------------------------- final String namespaceURI; //~ Constructors ----------------------------------------------------------------------- public PendingElement2 (String namespaceURI, String localName) { super(localName); this.namespaceURI = namespaceURI; } //~ Methods ---------------------------------------------------------------------------- @Override public void writeEmpty () throws XMLStreamException { getParent().writeEmptyElement(namespaceURI, localName); } @Override public void writeStart () throws XMLStreamException { getParent().writeStartElement(namespaceURI, localName); } } private class PendingElement3 extends PendingElement2 { //~ Instance fields -------------------------------------------------------------------- final String prefix; //~ Constructors ----------------------------------------------------------------------- public PendingElement3 (String prefix, String localName, String namespaceURI) { super(namespaceURI, localName); this.prefix = prefix; } //~ Methods ---------------------------------------------------------------------------- @Override public void writeEmpty () throws XMLStreamException { getParent().writeEmptyElement(prefix, localName, namespaceURI); } @Override public void writeStart () throws XMLStreamException { getParent().writeStartElement(prefix, localName, namespaceURI); } } } } // //--------------// // // prettyFormat // // //--------------// // /** // * Properly format an input XML string. // * (not used actually, since formatting is done in custom stream writer) // * // * @param input the XML input string // * @param indent indentation value // * @return the formatted XML output string // * @throws FormattingException global exception. Use getCause() for original exception // * @see <a // * href="http://stackoverflow.com/questions/139076/how-to-pretty-print-xml-from-java">Author // * article</a> // */ // @Deprecated // public static String prettyFormat (String input, // int indent) // throws FormattingException // { // try { // Source xmlInput = new StreamSource(new StringReader(input)); // StringWriter stringWriter = new StringWriter(); // StreamResult xmlOutput = new StreamResult(stringWriter); // TransformerFactory transformerFactory = TransformerFactory.newInstance(); // transformerFactory.setAttribute("indent-number", indent); // // Transformer transformer = transformerFactory.newTransformer(); // transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); // transformer.setOutputProperty(OutputKeys.INDENT, "yes"); // transformer.transform(xmlInput, xmlOutput); // // return xmlOutput.getWriter().toString(); // } catch (Exception ex) { // throw new FormattingException(ex); // } // }