blob: e0cae67490420ca8ca38a28376b9feaa00cb945d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011-2014 Torkild U. Resheim.
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Torkild U. Resheim - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.internal.docs.epub.core;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.eclipse.mylyn.docs.epub.core.EPUB;
/**
* Various EPUB file related utilities.
*
* @author Torkild U. Resheim
*/
public class EPUBFileUtil {
static final int BUFFERSIZE = 2048;
/**
* Copies the contents of <i>source</i> to the new <i>destination</i> file.
*
* @param source
* the source file
* @param destination
* the destination file
* @throws IOException
*/
public static void copy(File source, File destination) throws IOException {
destination.getParentFile().mkdirs();
FileInputStream from = null;
FileOutputStream to = null;
try {
from = new FileInputStream(source);
to = new FileOutputStream(destination);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = from.read(buffer)) != -1) {
to.write(buffer, 0, bytesRead);
}
} finally {
if (from != null) {
try {
from.close();
} catch (IOException e) {
}
}
if (to != null) {
try {
to.close();
} catch (IOException e) {
}
}
}
}
/**
* Attempts to figure out the MIME-type for the file.
*
* @param file
* the file to determine MIME-type for
* @return the MIME-type or <code>null</code>
*/
public static String getMimeType(File file) {
String name = file.getName().toLowerCase();
// These are not (correctly) detected by mechanism below
if (name.endsWith("xhtml")) { //$NON-NLS-1$
return "application/xhtml+xml"; //$NON-NLS-1$
}
if (name.endsWith(".otf")) { //$NON-NLS-1$
return "font/opentype"; //$NON-NLS-1$
}
if (name.endsWith(".ttf")) { //$NON-NLS-1$
return "font/truetype"; //$NON-NLS-1$
}
if (name.endsWith(".svg")) { //$NON-NLS-1$
return "image/svg+xml"; //$NON-NLS-1$
}
if (name.endsWith(".css")) { //$NON-NLS-1$
return "text/css"; //$NON-NLS-1$
}
if (name.endsWith(".epub")) { //$NON-NLS-1$
return EPUB.MIMETYPE_EPUB;
}
// Use URLConnection or content type detection
try {
String mimeType_name = URLConnection.guessContentTypeFromName(file.getName());
InputStream is = new BufferedInputStream(new FileInputStream(file));
String mimeType_content = URLConnection.guessContentTypeFromStream(is);
is.close();
// Handle situations where we have file name that indicates we have
// plain HTML, but the contents say XML. Hence we are probably
// looking at XHTML (see bug 360701).
if (mimeType_name != null && mimeType_content != null) {
if (mimeType_name.equals("text/html") && mimeType_content.equals("application/xml")) { //$NON-NLS-1$ //$NON-NLS-2$
return "application/xhtml+xml"; //$NON-NLS-1$
}
}
// We trust name over content
return mimeType_name == null ? mimeType_content : mimeType_name;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* Creates a path segment list.
*
* @param root
* the root folder
* @param file
* the destination file
* @param segments
* @see #getRelativePath(File, File)
*/
private static void getPathSegments(File root, File file, ArrayList<String> segments) {
if (root.equals(file) || file == null) {
return;
}
segments.add(0, file.getName());
getPathSegments(root, file.getParentFile(), segments);
}
/**
* Determines the <i>root</i> relative path of <i>file</i> in a platform independent manner. The returned string is
* a path starting from but excluding <i>root</i> using the '/' character as a directory separator. If the
* <i>file</i> argument is a folder a trailing directory separator is added. if the <i>root</i> argument is a file,
* it's parent folder will be used.
* <p>
* Note that if <i>file</i> is <b>not relative</b> to root, it's absolute path will be returned.
* </p>
*
* @param root
* the root directory or file
* @param file
* the root contained file or directory
* @return the platform independent, relative path or an absolute path
*/
public static String getRelativePath(File root, File file) {
ArrayList<String> segments = new ArrayList<String>();
if (root.isFile()) {
root = root.getParentFile();
}
getPathSegments(root, file, segments);
StringBuilder path = new StringBuilder();
for (int p = 0; p < segments.size(); p++) {
if (p > 0) {
path.append('/');
}
path.append(segments.get(p));
}
if (file.isDirectory()) {
path.append('/');
}
return path.toString();
}
/**
* Unpacks the given <i>epubfile</i> to the <i>destination</i> directory. This method will also validate the first
* item contained in the EPUB (see {@link #writeEPUBHeader(ZipOutputStream)}).
* <p>
* If the destination folder does not already exist it will be created. Additionally the modification time stamp of
* this folder will be set to the same as the originating EPUB file.
* </p>
* TODO: Actually validate the mimetype file
*
* @param epubfile
* the EPUB file
* @param destination
* the destination folder
* @throws FileNotFoundException
* when EPUB file does not exist
* @throws IOException
* if the operation was unsuccessful
*/
public static void unzip(File epubfile, File destination) throws IOException {
if (!destination.exists()) {
if (!destination.mkdirs()) {
throw new IOException("Could not create directory for EPUB contents"); //$NON-NLS-1$
}
}
ZipInputStream in = new ZipInputStream(new FileInputStream(epubfile));
byte[] buf = new byte[BUFFERSIZE];
ZipEntry entry = null;
while ((entry = in.getNextEntry()) != null) {
// for each entry to be extracted
String entryName = entry.getName();
File newFile = new File(destination.getAbsolutePath() + File.separator + entryName);
if (entry.isDirectory()) {
newFile.mkdirs();
if (entry.getTime() > 0) {
newFile.setLastModified(entry.getTime());
}
continue;
} else {
newFile.getParentFile().mkdirs();
}
int n;
FileOutputStream fileoutputstream = new FileOutputStream(newFile);
while ((n = in.read(buf, 0, BUFFERSIZE)) > -1) {
fileoutputstream.write(buf, 0, n);
}
fileoutputstream.close();
in.closeEntry();
// Update the file modification time
if (entry.getTime() > 0) {
newFile.setLastModified(entry.getTime());
}
} // iterate over contents
in.close();
destination.setLastModified(epubfile.lastModified());
}
/**
* A correctly formatted EPUB file must contain an uncompressed entry named <b>mimetype</b> that is placed at the
* beginning. The contents of this file must be the ASCII-encoded string <b>application/epub+zip</b>. This method
* will create this file.
*
* @param zos
* the zip output stream to write to.
* @throws IOException
*/
public static void writeEPUBHeader(ZipOutputStream zos) throws IOException {
byte[] bytes = EPUB.MIMETYPE_EPUB.getBytes("ASCII"); //$NON-NLS-1$
ZipEntry mimetype = new ZipEntry("mimetype"); //$NON-NLS-1$
mimetype.setMethod(ZipOutputStream.STORED);
mimetype.setSize(bytes.length);
mimetype.setCompressedSize(bytes.length);
CRC32 crc = new CRC32();
crc.update(bytes);
mimetype.setCrc(crc.getValue());
zos.putNextEntry(mimetype);
zos.write(bytes);
zos.closeEntry();
}
/**
* Recursively compresses contents of the given folder into a zip-file. If a file already exists in the given
* location an exception will be thrown.
*
* @param destination
* the destination file
* @param folder
* the source folder
* @param uncompressed
* a list of files to keep uncompressed
* @throws ZipException
* @throws IOException
*/
public static void zip(File destination, File folder) throws ZipException, IOException {
if (destination.exists()) {
throw new IOException("A file already exists at " + destination.getAbsolutePath()); //$NON-NLS-1$
}
ZipOutputStream out = new ZipOutputStream(new FileOutputStream(destination));
writeEPUBHeader(out);
zip(folder, folder, out);
out.close();
}
/**
* Adds a folder recursively to the output stream.
*
* @param folder
* the root folder
* @param out
* the output stream
* @throws IOException
*/
private static void zip(File root, File folder, ZipOutputStream out) throws IOException {
// Files first in order to make sure "metadata" is placed first in the
// zip file. We need that in order to support EPUB properly.
File[] files = folder.listFiles(new java.io.FileFilter() {
public boolean accept(File pathname) {
return !pathname.isDirectory();
}
});
byte[] tmpBuf = new byte[BUFFERSIZE];
for (File file : files) {
FileInputStream in = new FileInputStream(file.getAbsolutePath());
ZipEntry zipEntry = new ZipEntry(getRelativePath(root, file));
out.putNextEntry(zipEntry);
int len;
while ((len = in.read(tmpBuf)) > 0) {
out.write(tmpBuf, 0, len);
}
out.closeEntry();
in.close();
}
File[] dirs = folder.listFiles(new java.io.FileFilter() {
public boolean accept(File pathname) {
return pathname.isDirectory();
}
});
for (File dir : dirs) {
out.putNextEntry(new ZipEntry(getRelativePath(root, dir)));
zip(root, dir, out);
}
}
}