/*
* DefaultHomeOutputStream.java 13 Oct 2008
*
* Sweet Home 3D, Copyright (c) 2008 Emmanuel PUYBARET / eTeks <info@eteks.com>
*
* 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, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.eteks.sweethome3d.io;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import com.eteks.sweethome3d.model.Content;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.tools.ResourceURLContent;
import com.eteks.sweethome3d.tools.TemporaryURLContent;
import com.eteks.sweethome3d.tools.URLContent;
/**
* An <code>OutputStream</code> filter that writes a home in a stream
* at .sh3d file format.
* @see DefaultHomeInputStream
*/
public class DefaultHomeOutputStream extends FilterOutputStream {
private int compressionLevel;
private boolean includeOnlyTemporaryContent;
private List<Content> contents = new ArrayList<Content>();
private Map<URL, List<String>> zipUrlEntriesCache = new HashMap<URL, List<String>>();
/**
* Creates a stream that will serialize a home and all the contents it references
* in an uncompressed zip stream.
*/
public DefaultHomeOutputStream(OutputStream out) throws IOException {
this(out, 0, false);
}
/**
* Creates a stream that will serialize a home in a zip stream.
* @param compressionLevel 0-9
* @param includeOnlyTemporaryContent if <code>true</code>, only content instances of
* <code>TemporaryURLContent</code> class referenced by the saved home
* will be written. If <code>false</code>, all the content instances
* referenced by the saved home will be written in the zip stream.
*/
public DefaultHomeOutputStream(OutputStream out,
int compressionLevel,
boolean includeOnlyTemporaryContent) throws IOException {
super(out);
this.compressionLevel = compressionLevel;
this.includeOnlyTemporaryContent = includeOnlyTemporaryContent;
}
/**
* Throws an <code>InterruptedRecorderException</code> exception
* if current thread is interrupted. The interrupted status of the current thread
* is cleared when an exception is thrown.
*/
private static void checkCurrentThreadIsntInterrupted() throws InterruptedIOException {
if (Thread.interrupted()) {
throw new InterruptedIOException();
}
}
/**
* Writes home in a zipped stream followed by <code>Content</code> objects
* it points to.
*/
public void writeHome(Home home) throws IOException {
// Create a zip output on out stream
ZipOutputStream zipOut = new ZipOutputStream(this.out);
zipOut.setLevel(this.compressionLevel);
checkCurrentThreadIsntInterrupted();
// Write home in first entry in a file "Home"
zipOut.putNextEntry(new ZipEntry("Home"));
// Use an ObjectOutputStream that keeps track of Content objects
ObjectOutputStream objectOut = new HomeObjectOutputStream(zipOut);
objectOut.writeObject(home);
objectOut.flush();
zipOut.closeEntry();
// Write Content objects in files "0" to "n"
for (int i = 0, n = contents.size(); i < n; i++) {
Content content = contents.get(i);
String entryNameOrDirectory = String.valueOf(i);
if (content instanceof ResourceURLContent) {
writeResourceZipEntries(zipOut, entryNameOrDirectory, (ResourceURLContent)content);
} else if (content instanceof URLContent
&& ((URLContent)content).isJAREntry()) {
URLContent urlContent = (URLContent)content;
// If content comes from a home stream
if (urlContent instanceof HomeURLContent) {
writeHomeZipEntries(zipOut, entryNameOrDirectory, (HomeURLContent)urlContent);
} else {
writeZipEntries(zipOut, entryNameOrDirectory, urlContent);
}
} else {
writeZipEntry(zipOut, entryNameOrDirectory, content);
}
}
// Finish zip writing
zipOut.finish();
}
/**
* Writes in <code>zipOut</code> stream one or more entries matching the content
* <code>urlContent</code> coming from a resource file.
*/
private void writeResourceZipEntries(ZipOutputStream zipOut,
String entryNameOrDirectory,
ResourceURLContent urlContent) throws IOException {
if (urlContent.isMultiPartResource()) {
if (urlContent.isJAREntry()) {
URL zipUrl = urlContent.getJAREntryURL();
String entryName = urlContent.getJAREntryName();
int lastSlashIndex = entryName.lastIndexOf('/');
String entryDirectory = entryName.substring(0, lastSlashIndex + 1);
// Write in home stream each zipped stream entry that is stored in the same directory
for (String zipEntryName : getZipUrlEntries(zipUrl)) {
if (zipEntryName.startsWith(entryDirectory)) {
Content siblingContent = new URLContent(new URL("jar:" + zipUrl + "!/"
+ URLEncoder.encode(zipEntryName, "UTF-8").replace("+", "%20")));
writeZipEntry(zipOut, entryNameOrDirectory + zipEntryName.substring(lastSlashIndex), siblingContent);
}
}
} else {
// This should be the case only when resource isn't in a JAR file during development
File contentFile = new File(urlContent.getURL().getFile());
File parentFile = new File(contentFile.getParent());
File [] siblingFiles = parentFile.listFiles();
// Write in home stream each file that is stored in the same directory
for (File siblingFile : siblingFiles) {
if (!siblingFile.isDirectory()) {
writeZipEntry(zipOut, entryNameOrDirectory + "/" + siblingFile.getName(),
new URLContent(siblingFile.toURI().toURL()));
}
}
}
} else {
writeZipEntry(zipOut, entryNameOrDirectory, urlContent);
}
}
/**
* Returns the list of entries contained in <code>zipUrl</code>.
*/
private List<String> getZipUrlEntries(URL zipUrl) throws IOException {
List<String> zipUrlEntries = this.zipUrlEntriesCache.get(zipUrl);
if (zipUrlEntries == null) {
zipUrlEntries = new ArrayList<String>();
this.zipUrlEntriesCache.put(zipUrl, zipUrlEntries);
ZipInputStream zipIn = null;
try {
// Search all entries of zip url
zipIn = new ZipInputStream(zipUrl.openStream());
for (ZipEntry entry; (entry = zipIn.getNextEntry()) != null; ) {
zipUrlEntries.add(entry.getName());
}
} finally {
if (zipIn != null) {
zipIn.close();
}
}
}
return zipUrlEntries;
}
/**
* Writes in <code>zipOut</code> stream one or more entries matching the content
* <code>urlContent</code> coming from a home file.
*/
private void writeHomeZipEntries(ZipOutputStream zipOut,
String entryNameOrDirectory,
HomeURLContent urlContent) throws IOException {
String entryName = urlContent.getJAREntryName();
int slashIndex = entryName.indexOf('/');
// If content comes from a directory of a home file
if (slashIndex > 0) {
URL zipUrl = urlContent.getJAREntryURL();
String entryDirectory = entryName.substring(0, slashIndex + 1);
// Write in home stream each zipped stream entry that is stored in the same directory
for (String zipEntryName : getZipUrlEntries(zipUrl)) {
if (zipEntryName.startsWith(entryDirectory)) {
Content siblingContent = new URLContent(new URL("jar:" + zipUrl + "!/"
+ URLEncoder.encode(zipEntryName, "UTF-8").replace("+", "%20")));
writeZipEntry(zipOut, entryNameOrDirectory + zipEntryName.substring(slashIndex), siblingContent);
}
}
} else {
writeZipEntry(zipOut, entryNameOrDirectory, urlContent);
}
}
/**
* Writes in <code>zipOut</code> stream all the sibling files of the zipped
* <code>urlContent</code>.
*/
private void writeZipEntries(ZipOutputStream zipOut,
String directory,
URLContent urlContent) throws IOException {
ZipInputStream zipIn = null;
try {
// Open zipped stream that contains urlContent
zipIn = new ZipInputStream(urlContent.getJAREntryURL().openStream());
// Write each zipped stream entry in home stream
for (ZipEntry entry; (entry = zipIn.getNextEntry()) != null; ) {
String zipEntryName = entry.getName();
Content siblingContent = new URLContent(new URL("jar:" + urlContent.getJAREntryURL() + "!/"
+ URLEncoder.encode(zipEntryName, "UTF-8").replace("+", "%20")));
writeZipEntry(zipOut, directory + "/" + zipEntryName, siblingContent);
}
} finally {
if (zipIn != null) {
zipIn.close();
}
}
}
/**
* Writes in <code>zipOut</code> stream a new entry named <code>entryName</code> that
* contains a given <code>content</code>.
*/
private void writeZipEntry(ZipOutputStream zipOut, String entryName, Content content) throws IOException {
checkCurrentThreadIsntInterrupted();
byte [] buffer = new byte [8192];
InputStream contentIn = null;
try {
zipOut.putNextEntry(new ZipEntry(entryName));
contentIn = content.openStream();
int size;
while ((size = contentIn.read(buffer)) != -1) {
zipOut.write(buffer, 0, size);
}
zipOut.closeEntry();
} finally {
if (contentIn != null) {
contentIn.close();
}
}
}
/**
* <code>ObjectOutputStream</code> that replaces <code>Content</code> objects
* by temporary <code>URLContent</code> objects and stores them in a list.
*/
private class HomeObjectOutputStream extends ObjectOutputStream {
public HomeObjectOutputStream(OutputStream out) throws IOException {
super(out);
enableReplaceObject(true);
}
@Override
protected Object replaceObject(Object obj) throws IOException {
if (obj instanceof TemporaryURLContent
|| obj instanceof HomeURLContent
|| (!includeOnlyTemporaryContent && obj instanceof Content)) {
// Add obj to Content objects list
contents.add((Content)obj);
String subEntryName = "";
if (obj instanceof URLContent) {
URLContent urlContent = (URLContent)obj;
// If content comes from a zipped content
if (urlContent.isJAREntry()) {
String entryName = urlContent.getJAREntryName();
if (urlContent instanceof HomeURLContent) {
int slashIndex = entryName.indexOf('/');
// If content comes from a directory of a home file
if (slashIndex > 0) {
// Retrieve entry name in zipped stream without the directory
subEntryName = entryName.substring(slashIndex);
}
} else if (urlContent instanceof ResourceURLContent) {
ResourceURLContent resourceUrlContent = (ResourceURLContent)urlContent;
if (resourceUrlContent.isMultiPartResource()) {
// If content is a resource coming from a JAR file, retrieve its file name
subEntryName = entryName.substring(entryName.lastIndexOf('/'));
}
} else {
// Retrieve entry name in zipped stream
subEntryName = "/" + entryName;
}
} else if (urlContent instanceof ResourceURLContent) {
ResourceURLContent resourceUrlContent = (ResourceURLContent)urlContent;
// If content is a resource coming from a directory (this should be the case
// only when resource isn't in a JAR file during development), retrieve its file name
if (resourceUrlContent.isMultiPartResource()) {
subEntryName = "/" + new File(resourceUrlContent.getURL().getFile()).getName();
}
}
}
// Return a temporary URL that points to content object
return new URLContent(new URL("jar:file:temp!/" + (contents.size() - 1) + subEntryName));
} else {
return obj;
}
}
}
}