/* -*- c-basic-offset: 2; indent-tabs-mode: nil; -*- */ /* * FreeDots -- MusicXML to braille music transcription * * Copyright 2008-2010 Mario Lang All Rights Reserved. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3, as * published by the Free Software Foundation. * * This code 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 (a copy is included in the LICENSE.txt file that * accompanied this code). * * You should have received a copy of the GNU General Public License * along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * This file is maintained by Mario Lang <mlang@delysid.org>. */ package freedots.musicxml; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.StringReader; import java.math.BigInteger; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.Map; import java.util.HashMap; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.TransformerFactory; import javax.xml.transform.Transformer; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import org.w3c.dom.DocumentType; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * A MusicXML document in score-partwise format. */ public final class Score { private Document document; private static XPathFactory xPathFactory = XPathFactory.newInstance(); private static DocumentBuilder documentBuilder; static { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); try { documentBuilder = documentBuilderFactory.newDocumentBuilder(); documentBuilder.setEntityResolver(new MusicXMLEntityResolver()); } catch (ParserConfigurationException e) { e.printStackTrace(); } } /* --- Header fields --- */ private Element workNumber, workTitle; private Element movementNumber, movementTitle; private Element composer, lyricist; private Element rights; private Element encoding; private List<Part> parts; public Score( final InputStream inputStream, final String extension ) throws ParserConfigurationException, IOException, SAXException, XPathExpressionException { parse(inputStream, extension); } /** Construct a score object from a URL. * @param filenameOrURL is either a local filename or a URL. */ public Score(final String filenameOrURL) throws ParserConfigurationException, IOException, SAXException, XPathExpressionException { File file = new File(filenameOrURL); InputStream inputStream = null; String extension = null; int dot = filenameOrURL.lastIndexOf('.'); if (dot != -1) extension = filenameOrURL.substring(dot + 1); if (file.exists()) { /* Local file */ inputStream = new FileInputStream(file); } else { URL url = new URL(filenameOrURL); inputStream = url.openConnection().getInputStream(); } parse(inputStream, extension); } private void parse(InputStream inputStream, String extension) throws ParserConfigurationException, IOException, SAXException, XPathExpressionException { Map<String,InputSource> Files = new HashMap<String,InputSource>(); String zipEntryName = null; if ("mxl".equalsIgnoreCase(extension)) { ZipInputStream zipInputStream = new ZipInputStream(inputStream); ZipEntry zipEntry = null; while ((zipEntry = zipInputStream.getNextEntry()) != null) { InputSource currentInputSource=getInputSourceFromZipInputStream(zipInputStream); Files.put(zipEntry.getName(),currentInputSource); if ("META-INF/container.xml".equals(zipEntry.getName())) { Document container = documentBuilder.parse(currentInputSource); XPath xpath = xPathFactory.newXPath(); zipEntryName = (String) xpath.evaluate("container/rootfiles/rootfile/@full-path", container, XPathConstants.STRING); } else if (zipEntry.getName().equals(zipEntryName)) document = documentBuilder.parse(currentInputSource); zipInputStream.closeEntry(); } if (document==null && !(zipEntryName==null)) { document = documentBuilder.parse(Files.get(zipEntryName)); } } else { document = documentBuilder.parse(inputStream); } document.getDocumentElement().normalize(); Element root = document.getDocumentElement(); assert root.getTagName().equals("score-partwise"); parts = new ArrayList<Part>(); Element partList = null; for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE) { Element scoreElement = (Element)node; if ("work".equals(scoreElement.getTagName())) { for (Node workNode = scoreElement.getFirstChild(); workNode != null; workNode = workNode.getNextSibling()) { if (workNode.getNodeType() == Node.ELEMENT_NODE) { Element workElement = (Element)workNode; if ("work-number".equals(workElement.getTagName())) { workNumber = workElement; } else if ("work-title".equals(workElement.getTagName())) { workTitle = workElement; } } } } else if ("movement-title".equals(scoreElement.getTagName())) { movementTitle = scoreElement; } else if ("movement-number".equals(scoreElement.getTagName())) { movementNumber = scoreElement; } else if ("identification".equals(scoreElement.getTagName())) { for (Node subNode = scoreElement.getFirstChild(); subNode != null; subNode = subNode.getNextSibling()) { if (subNode.getNodeType() == Node.ELEMENT_NODE) { Element identificationElement = (Element)subNode; if ("creator".equals(identificationElement.getTagName())) { Element creator = identificationElement; if (creator.getAttribute("type").equals("composer")) { composer = creator; } else if (creator.getAttribute("type").equals("lyricist")) { lyricist = creator; } } else if ("encoding".equals(identificationElement.getTagName())) { encoding = identificationElement; } } } } else if ("part-list".equals(scoreElement.getTagName())) { partList = scoreElement; } else if ("part".equals(scoreElement.getTagName())) { Element part = scoreElement; String idValue = part.getAttribute("id"); Element scorePart = null; for (Node partlistNode = partList.getFirstChild(); partlistNode != null; partlistNode = partlistNode.getNextSibling()) { if (partlistNode.getNodeType() == Node.ELEMENT_NODE && "score-part".equals(partlistNode.getNodeName())) { Element sp = (Element)partlistNode; if (idValue.equals(sp.getAttribute("id"))) { scorePart = sp; } } } if (scorePart != null) parts.add(new Part(part, scorePart, this)); else throw new RuntimeException("No <score-part> for part " + idValue); } } } } /** Demarshal this score object back to XML. * @param outputStream will be used to serialize the XML to. */ public void save(OutputStream outputStream) { assert document != null; DocumentType docType = document.getDoctype(); DOMSource domSource = new DOMSource(document); StreamResult resultStream = new StreamResult(outputStream); TransformerFactory transformerFactory = TransformerFactory.newInstance(); try { Transformer transformer = transformerFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, docType.getPublicId()); transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, docType.getSystemId()); try { transformer.transform(domSource, resultStream); } catch (javax.xml.transform.TransformerException e) { e.printStackTrace(); } } catch (javax.xml.transform.TransformerConfigurationException e) { e.printStackTrace(); } } /** Gets the content of the work-number element. * @return the work-number of this work or {@code null} if it was not * specified. */ public String getWorkNumber() { return workNumber != null ? workNumber.getTextContent() : null; } /** Gets the content of the work-title element. * @return the title of this work or {@code null} if it was not specified. */ public String getWorkTitle() { return workTitle != null ? workTitle.getTextContent() : null; } /** Get the content of the movement-number element. */ public String getMovementNumber() { return movementNumber != null ? movementNumber.getTextContent() : null; } /** Get the content of the movement-title element. */ public String getMovementTitle() { return movementTitle != null ? movementTitle.getTextContent() : null; } /** Get the composer (if set) of this score. */ public String getComposer() { return composer != null ? composer.getTextContent() : null; } /** Get the lyricist (if set) of this score. */ public String getLyricist() { return lyricist != null ? lyricist.getTextContent() : null; } private InputSource getInputSourceFromZipInputStream( ZipInputStream zipInputStream ) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(zipInputStream)); StringBuilder stringBuilder = new StringBuilder(); String string = null; while ((string = reader.readLine()) != null) stringBuilder.append(string + "\n"); return new InputSource(new StringReader(stringBuilder.toString())); } /** * Calculate the least common multiple of all divisions elements in the score. */ public int getDivisions() { XPath xPath = xPathFactory.newXPath(); try { String xPathExpression = "//attributes/divisions/text()"; NodeList nodeList = (NodeList) xPath.evaluate(xPathExpression, document, XPathConstants.NODESET); final int count = nodeList.getLength(); BigInteger result = BigInteger.ONE; for (int index = 0; index < count; index++) { Node node = nodeList.item(index); BigInteger divisions = new BigInteger(new Integer(Math.round(Float.parseFloat(node.getNodeValue()))).toString()); result = result.multiply(divisions).divide(result.gcd(divisions)); } return result.intValue(); } catch (XPathExpressionException e) { return 0; } } public List<Part> getParts() { return parts; } /** Indicates if the encoding supports a particular MusicXML element. * This is recommended for elements like beam, stem, and accidental, * where the absence of an element is ambiguous if you do not know if the * encoding supports that element. */ public boolean encodingSupports(String elementName) { if (encoding != null) { for (Node node = encoding.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals("supports")) { Element supports = (Element)node; if (supports.getAttribute("element").equals(elementName)) { return supports.getAttribute("type").equals(YES); } } } } return true; } /** Indicates if the encoding supports a particular MusicXML attribute * of a certain element with a given value. * This lets applications communicate, for example, that all system and/or * page breaks are contained in the MusicXML document. * @param element is the tagName of the {@link org.w3c.dom.Element} * @param attribute is the name of the attribute which is queried * @param value is true if support and false if absence is queried */ public boolean encodingSupports(String element, String attribute, boolean value) { if (encoding != null) { for (Node node = encoding.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals("supports")) { Element supports = (Element)node; if (supports.getAttribute("element").equals(element) && supports.getAttribute("attribute").equals(attribute) && supports.getAttribute("value").equals(YES) == value) { return supports.getAttribute("type").equals(YES); } } } } return true; } static final String YES = "yes"; /* --- W3C DOM convenience access utilities --- */ static Text getTextNode(Element element, String childTagName) { NodeList nodeList = element.getElementsByTagName(childTagName); if (nodeList.getLength() >= 1) { nodeList = nodeList.item(nodeList.getLength()-1).getChildNodes(); for (int index = 0; index < nodeList.getLength(); index++) { Node node = nodeList.item(index); if (node.getNodeType() == Node.TEXT_NODE) return (Text)node; } } return null; } }