package net.sf.openrocket.file;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Set;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import net.sf.openrocket.appearance.Appearance;
import net.sf.openrocket.appearance.Decal;
import net.sf.openrocket.appearance.DecalImage;
import net.sf.openrocket.document.OpenRocketDocument;
import net.sf.openrocket.document.StorageOptions;
import net.sf.openrocket.document.StorageOptions.FileType;
import net.sf.openrocket.file.openrocket.OpenRocketSaver;
import net.sf.openrocket.file.rocksim.export.RocksimSaver;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.util.MathUtil;
public class GeneralRocketSaver {
/**
* Interface which can be implemented by the caller to receive progress information.
*
*/
public interface SavingProgress {
/**
* Inform the callback of the current progress.
* It is guaranteed that the value will be an integer between 0 and 100 representing
* percent complete. The SavingProgress object might not be notified the through
* setProgress when the save is complete. When called with the value 100, the saving process
* may not be complete, do not use this as an indication of completion.
*
* @param progress int value between 0 and 100 representing percent complete.
*/
public void setProgress(int progress);
}
/**
* Save the document to the specified file using the default storage options.
*
* @param dest the destination file.
* @param document the document to save.
* @throws IOException in case of an I/O error.
*/
public final void save(File dest, OpenRocketDocument document) throws IOException {
save(dest, document, document.getDefaultStorageOptions());
}
/**
* Save the document to the specified file using the given storage options.
*
* @param dest the destination file.
* @param document the document to save.
* @param options the storage options.
* @throws IOException in case of an I/O error.
*/
public final void save(File dest, OpenRocketDocument document, StorageOptions options) throws IOException {
save(dest, document, options, null);
}
/**
* Save the document to a file with default StorageOptions and a SavingProgress callback object.
*
* @param dest the destination stream.
* @param doc the document to save.
* @param progress a SavingProgress object used to provide progress information
* @throws IOException in case of an I/O error.
*/
public final void save(File dest, OpenRocketDocument doc, SavingProgress progress) throws IOException {
save(dest, doc, doc.getDefaultStorageOptions(), progress);
}
/**
* Save the document to a file with the given StorageOptions and a SavingProgress callback object.
*
* @param dest the destination stream.
* @param doc the document to save.
* @param options the storage options.
* @param progress a SavingProgress object used to provide progress information
* @throws IOException in case of an I/O error.
*/
public final void save(File dest, OpenRocketDocument doc, StorageOptions opts, SavingProgress progress) throws IOException {
// This method is the core operational method. It saves the document into a new (hopefully unique)
// file, then if the save is successful, it will copy the file over the old one.
// Write to a temporary file in the same directory as the specified file.
File temporaryNewFile = File.createTempFile("ORSave", ".tmp", dest.getParentFile());
OutputStream s = new BufferedOutputStream(new FileOutputStream(temporaryNewFile));
if (progress != null) {
long estimatedSize = this.estimateFileSize(doc, opts);
s = new ProgressOutputStream(s, estimatedSize, progress);
}
try {
save(dest.getName(), s, doc, opts);
} finally {
s.close();
}
// Move the temporary new file over the specified file.
boolean destExists = dest.exists();
File oldBackupFile = new File(dest.getParentFile(), dest.getName() + "-bak");
if (destExists) {
dest.renameTo(oldBackupFile);
}
// since we created the temporary new file in the same directory as the dest file,
// it is on the same filesystem, so File.renameTo will work just fine.
boolean success = temporaryNewFile.renameTo(dest);
if (success) {
if (destExists) {
oldBackupFile.delete();
}
}
}
/**
* Provide an estimate of the file size when saving the document with the
* specified options. This is used as an indication to the user and when estimating
* file save progress.
*
* @param doc the document.
* @param options the save options, compression must be taken into account.
* @return the estimated number of bytes the storage would take.
*/
public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) {
if (options.getFileType() == StorageOptions.FileType.ROCKSIM) {
return new RocksimSaver().estimateFileSize(doc, options);
} else {
return new OpenRocketSaver().estimateFileSize(doc, options);
}
}
private void save(String fileName, OutputStream output, OpenRocketDocument document, StorageOptions options) throws IOException {
// For now, we don't save decal inforamtion in ROCKSIM files, so don't do anything
// which follows.
// TODO - add support for decals in ROCKSIM files?
if (options.getFileType() == FileType.ROCKSIM) {
saveInternal(output, document, options);
output.close();
return;
}
Set<DecalImage> usedDecals = new TreeSet<DecalImage>();
// Look for all decals used in the rocket.
for (RocketComponent c : document.getRocket()) {
if (c.getAppearance() == null) {
continue;
}
Appearance ap = c.getAppearance();
if (ap.getTexture() == null) {
continue;
}
Decal decal = ap.getTexture();
usedDecals.add(decal.getImage());
}
saveAllPartsZipFile(output, document, options, usedDecals);
}
public void saveAllPartsZipFile(OutputStream output, OpenRocketDocument document, StorageOptions options, Set<DecalImage> decals) throws IOException {
// Open a zip stream to write to.
ZipOutputStream zos = new ZipOutputStream(output);
zos.setLevel(9);
// big try block to close the zos.
try {
ZipEntry mainFile = new ZipEntry("rocket.ork");
zos.putNextEntry(mainFile);
saveInternal(zos, document, options);
zos.closeEntry();
// Now we write out all the decal images files.
for (DecalImage image : decals) {
String name = image.getName();
ZipEntry decal = new ZipEntry(name);
zos.putNextEntry(decal);
InputStream is = image.getBytes();
int bytesRead = 0;
byte[] buffer = new byte[2048];
while ((bytesRead = is.read(buffer)) > 0) {
zos.write(buffer, 0, bytesRead);
}
zos.closeEntry();
}
zos.flush();
} finally {
zos.close();
}
}
// package scope for testing.
private void saveInternal(OutputStream output, OpenRocketDocument document, StorageOptions options)
throws IOException {
if (options.getFileType() == StorageOptions.FileType.ROCKSIM) {
new RocksimSaver().save(output, document, options);
} else {
new OpenRocketSaver().save(output, document, options);
}
}
private static class ProgressOutputStream extends FilterOutputStream {
private long estimatedSize;
private long bytesWritten = 0;
private SavingProgress progressCallback;
ProgressOutputStream(OutputStream ostream, long estimatedSize, SavingProgress progressCallback) {
super(ostream);
this.estimatedSize = estimatedSize;
this.progressCallback = progressCallback;
}
@Override
public void write(int b) throws IOException {
super.write(b);
bytesWritten++;
updateProgress();
}
@Override
public void write(byte[] b) throws IOException {
super.write(b);
bytesWritten += b.length;
updateProgress();
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
super.write(b, off, len);
bytesWritten += len;
updateProgress();
}
private void updateProgress() {
if (progressCallback != null) {
int p = 50;
if (estimatedSize > 0) {
p = (int) Math.floor(bytesWritten * 100.0 / estimatedSize);
p = MathUtil.clamp(p, 0, 100);
}
progressCallback.setProgress(p);
}
}
}
}