package com.xenoage.zong.io.musicxml.in; import static com.xenoage.utils.PlatformUtils.platformUtils; import java.io.IOException; import java.util.List; import com.xenoage.utils.annotations.NonNull; import com.xenoage.utils.collections.CollectionUtils; import com.xenoage.utils.exceptions.InvalidFormatException; import com.xenoage.utils.io.BufferedInputStream; import com.xenoage.utils.io.InputStream; import com.xenoage.utils.io.ZipReader; import com.xenoage.utils.xml.XmlReader; import com.xenoage.zong.io.musicxml.FileType; import com.xenoage.zong.io.musicxml.link.LinkAttributes; import com.xenoage.zong.io.musicxml.opus.Opus; import com.xenoage.zong.io.musicxml.opus.OpusItem; import com.xenoage.zong.io.musicxml.opus.Score; /** * This class reads a compressed MusicXML file. * * Therefore, it is extracted in the main memory. Then, * the list of files (opus) or single files can be loaded. * When the class instance is cleaned up, the zip content * in the memory is cleaned up, too. * * This class can also handle nested compressed MusicXML files, * as long as they contain a single score and not an opus. * * @author Andreas Wenger */ public class CompressedFileInput { private OpusItem rootItem; private ZipReader zip; /** * Creates a {@link CompressedFileInput} instance for the given * compressed MusicXML data. */ public CompressedFileInput(InputStream inputStream) throws IOException { //load zip contents zip = platformUtils().createZipReader(inputStream); if (zip == null) { throw new IOException("Zip reader is unsupported"); } //parse META-INF/container.xml String rootfilePath = null; try { InputStream containerStream = zip.openFile("META-INF/container.xml"); XmlReader containerReader = platformUtils().createXmlReader(containerStream); rootfilePath = readRootFilePath(containerReader); containerStream.close(); } catch (Exception ex) { throw new IOException( "Compressed MusicXML file has no (well-formed) META-INF/container.xml", ex); } //load root file try { BufferedInputStream rootStream = new BufferedInputStream(zip.openFile(rootfilePath)); rootStream.mark(); FileType type = FileTypeReader.getFileType(rootStream); rootStream.reset(); rootStream.unmark(); if (type == null) throw new IllegalStateException("Unknown root file type"); switch (type) { case Compressed: throw new IllegalStateException("Root file may (currently) not be compressed"); case XMLOpus: rootItem = new OpusFileInput().readOpusFile(rootStream); break; case XMLScorePartwise: case XMLScoreTimewise: rootItem = new Score(new LinkAttributes(rootfilePath), null); } rootStream.close(); } catch (IOException ex) { throw new IOException("Could not load root file", ex); } } @NonNull private String readRootFilePath(XmlReader containerReader) throws IOException { XmlReader r = containerReader; r.openNextChildElement(); //root element while (r.openNextChildElement()) { if (r.getElementName().equals("rootfiles")) { //rootfiles element while (r.openNextChildElement()) { if (r.getElementName().equals("rootfile")) { //rootfile element String fullPath = r.getAttribute("full-path"); if (fullPath == null) throw new IOException("full-path of rootfile not found"); return fullPath; } r.closeElement(); } } r.closeElement(); } throw new IOException("rootfile not found"); } /** * Gets the item which is the main document in this compressed * MusicXML file: Either a {@link Score} or an {@link Opus}. */ public OpusItem getRootItem() { return rootItem; } /** * Returns true, if the file does not only contain a single score but * a whole opus. */ public boolean isOpus() { return (rootItem instanceof Opus); } /** * Gets a (flattened) list of all filenames in this opus. If this file * contains no opus but a single score, the filename of the single score * is returned. */ public List<String> getScoreFilenames() throws IOException { List<String> ret = CollectionUtils.alist(); if (isOpus()) { getScoreFilenames( //new OpusFileInput().resolveOpusLinks((Opus) rootItem, zip, null), ret); //TODO (Opus) rootItem, ret); } else { ret.add(((Score) rootItem).getLink().getHref()); } return ret; } /** * Loads and returns the {@link com.xenoage.zong.core.Score} at the given path. */ public com.xenoage.zong.core.Score loadScore(String path) throws InvalidFormatException, IOException { BufferedInputStream bis = new BufferedInputStream(zip.openFile(path)); //XML or compressed? bis.mark(); FileType fileType = FileTypeReader.getFileType(bis); bis.reset(); bis.unmark(); if (fileType == null) throw new InvalidFormatException("Score has invalid format: " + path); com.xenoage.zong.core.Score ret = null; switch (fileType) { case Compressed: ret = loadCompressedScore(path); break; case XMLScorePartwise: ret = new MusicXmlScoreFileInput().read(bis, path); break; case XMLScoreTimewise: throw new IllegalStateException("score-timewise is currently not implemented"); default: throw new InvalidFormatException("Score has invalid format: " + path); } bis.close(); return ret; } private com.xenoage.zong.core.Score loadCompressedScore(String path) throws IOException { CompressedFileInput zipScore = new CompressedFileInput(zip.openFile(path)); com.xenoage.zong.core.Score ret = zipScore.loadScore(((Score) zipScore.getRootItem()).getLink().getHref()); zipScore.close(); return ret; } private void getScoreFilenames(Opus resolvedOpus, List<String> acc) { for (OpusItem item : resolvedOpus.getItems()) { if (item instanceof Score) acc.add(((Score) item).getLink().getHref()); else if (item instanceof Opus) getScoreFilenames((Opus) item, acc); } } public void close() { zip.close(); } }