/******************************************************************************* * Copyright (c) MOBAC developers * * 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, see <http://www.gnu.org/licenses/>. ******************************************************************************/ /* ********************************************* * Copyright: Andreas Sander * * * ********************************************* */ package mobac.program.atlascreators.impl.rmp; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import java.util.ArrayList; import mobac.program.atlascreators.impl.rmp.interfaces.RmpFileEntry; import mobac.program.atlascreators.impl.rmp.rmpfile.RmpIni; import mobac.utilities.Utilities; import mobac.utilities.stream.CountingOutputStream; import mobac.utilities.stream.RandomAccessFileOutputStream; import org.apache.commons.io.output.NullOutputStream; import org.apache.log4j.Logger; /** * Class that writes files in RMP archive format * */ public class RmpWriter { /** * Max file size: 2147483647 bytes = 2047,99 MiB */ public static final long MAX_FILE_SIZE = 0xffffffffl; private static final Logger log = Logger.getLogger(RmpWriter.class); private final ArrayList<EntryInfo> entries = new ArrayList<EntryInfo>(); private final File rmpFile; private final RandomAccessFile rmpOutputFile; private int projectedEntryCount; private ChecksumOutputStream entryOut; /** * @param imageName * @param layerCount * projected number of layers that will be written to this rmp file * @param rmpFile * @throws IOException * @throws InterruptedException */ public RmpWriter(String imageName, int layerCount, File rmpFile) throws IOException, InterruptedException { this.rmpFile = rmpFile; // We only use one A00 entry per map/layer - therefore we can // pre-calculate the number of entries: // RmpIni + (TLM & A00) per layer + Bmp2Bit + Bmp4bit this.projectedEntryCount = (3 + (2 * layerCount)); if (rmpFile.exists()) Utilities.deleteFile(rmpFile); log.debug("Writing data to " + rmpFile.getAbsolutePath()); rmpOutputFile = new RandomAccessFile(rmpFile, "rw"); // Calculate offset to the directory end int directoryEndOffset = projectedEntryCount * 24 + 10; rmpOutputFile.seek(directoryEndOffset); entryOut = new ChecksumOutputStream(new RandomAccessFileOutputStream(rmpOutputFile)); /* --- Write the directory-end marker --- */ RmpTools.writeFixedString(entryOut, "MAGELLAN", 30); RmpIni rmpIni = new RmpIni(imageName, layerCount); /* --- Create packer and fill it with content --- */ writeFileEntry(rmpIni); } public void writeFileEntry(RmpFileEntry entry) throws IOException, InterruptedException { EntryInfo info = new EntryInfo(); info.name = entry.getFileName(); info.extendsion = entry.getFileExtension(); info.offset = rmpOutputFile.getFilePointer(); entry.writeFileContent(entryOut); info.length = rmpOutputFile.getFilePointer() - info.offset; if ((info.length % 2) != 0) entryOut.write(0); entries.add(info); if (rmpOutputFile.getFilePointer() > MAX_FILE_SIZE) throwRmpTooLarge(); log.debug("Written data of entry " + entry + " bytes=" + info.length); } public void prepareFileEntry(RmpFileEntry entry) throws IOException, InterruptedException { EntryInfo info = new EntryInfo(); info.name = entry.getFileName(); info.extendsion = entry.getFileExtension(); long pos = rmpOutputFile.getFilePointer(); info.offset = pos; CountingOutputStream cout = new CountingOutputStream(new NullOutputStream()); entry.writeFileContent(cout); info.length = cout.getBytesWritten(); long newPos = pos + info.length; if ((info.length % 2) != 0) newPos++; if (newPos > MAX_FILE_SIZE) throwRmpTooLarge(); rmpOutputFile.seek(newPos); entries.add(info); log.debug("Prepared data of entry " + entry + " bytes=" + info.length); } public void writePreparedFileEntry(RmpFileEntry entry) throws IOException, InterruptedException { long pos = rmpOutputFile.getFilePointer(); EntryInfo info = new EntryInfo(); info.name = entry.getFileName(); info.extendsion = entry.getFileExtension(); int index = entries.indexOf(info); if (index < 0) throw new RuntimeException("Index for entry not found"); info = entries.get(index); rmpOutputFile.seek(info.offset); entry.writeFileContent(entryOut); if (rmpOutputFile.getFilePointer() > MAX_FILE_SIZE) throwRmpTooLarge(); long newLength = rmpOutputFile.getFilePointer() - info.offset; if (newLength != info.length) throw new RuntimeException("Length of entry has changed!"); if ((newLength % 2) != 0) entryOut.write(0); // restore old file position rmpOutputFile.seek(pos); } private void throwRmpTooLarge() throws IOException { throw new IOException("RMP file size exeeds 2GiB! The RMP file format does not support that."); } /** * Writes the directory of the archive into the rmp file * * @throws IOException * Error accessing disk */ public void writeDirectory() throws IOException { if (projectedEntryCount != entries.size()) throw new RuntimeException("Entry count does not correspond " + "to the projected layer count: \nProjected: " + projectedEntryCount + "\nPresent:" + entries.size()); // Finalize the list of written entries RmpTools.writeFixedString(entryOut, "MAGELLAN", 8); entryOut.writeChecksum(); log.debug("Finished writing entries, updating directory"); /* --- Create file --- */ rmpOutputFile.seek(0); OutputStream out = new RandomAccessFileOutputStream(rmpOutputFile); ChecksumOutputStream cout = new ChecksumOutputStream(out); /* --- Write header with number of files --- */ RmpTools.writeValue(cout, entries.size(), 4); RmpTools.writeValue(cout, entries.size(), 4); /* --- Write the directory --- */ log.debug("Writing directory: " + entries.size() + " entries"); for (EntryInfo entryInfo : entries) { log.trace("Entry: " + entryInfo); /* --- Write directory entry --- */ RmpTools.writeFixedString(cout, entryInfo.name, 9); RmpTools.writeFixedString(cout, entryInfo.extendsion, 7); RmpTools.writeValue(cout, entryInfo.offset, 4); RmpTools.writeValue(cout, entryInfo.length, 4); } /* --- Write the header checksum (2 bytes) --- */ cout.writeChecksum(); } public void close() { try { rmpOutputFile.close(); } catch (Exception e) { log.error("", e); } } public void delete() throws IOException { close(); Utilities.deleteFile(rmpFile); } private static class EntryInfo { String name; String extendsion; long offset; long length; @Override public String toString() { return "\"" + name + "." + extendsion + "\" offset=" + offset + " length=" + length; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((extendsion == null) ? 0 : extendsion.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; EntryInfo other = (EntryInfo) obj; if (extendsion == null) { if (other.extendsion != null) return false; } else if (!extendsion.equals(other.extendsion)) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } } }