FileContent.java

/*
 * FileContent
 */
package gov.usgs.earthquake.product;

import gov.usgs.util.StreamUtils;

import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URISyntaxException;

import java.util.Date;
import java.util.Map;
import java.util.HashMap;
import java.util.regex.Pattern;

import javax.activation.MimetypesFileTypeMap;

/**
 * Content stored in a file.
 */
public class FileContent extends AbstractContent {

	/** Used to look up file types. */
	private static MimetypesFileTypeMap SYSTEM_MIME_TYPES = new MimetypesFileTypeMap();

	/** Explicit list of file extensions with standard mime types. */
	private static Map<String, String> MIME_TYPES = new HashMap<String, String>();
	static {
		MIME_TYPES.put("atom", "application/atom+xml");
		MIME_TYPES.put("css", "text/css");
		MIME_TYPES.put("gif", "image/gif");
		MIME_TYPES.put("gz", "application/gzip");
		MIME_TYPES.put("html", "text/html");
		MIME_TYPES.put("jpg", "image/jpeg");
		MIME_TYPES.put("js", "text/javascript");
		MIME_TYPES.put("json", "application/json");
		MIME_TYPES.put("kml", "application/vnd.google-earth.kml+xml");
		MIME_TYPES.put("kmz", "application/vnd.google-earth.kmz");
		MIME_TYPES.put("pdf", "application/pdf");
		MIME_TYPES.put("png", "image/png");
		MIME_TYPES.put("ps", "application/postscript");
		MIME_TYPES.put("txt", "text/plain");
		MIME_TYPES.put("xml", "application/xml");
		MIME_TYPES.put("zip", "application/zip");
	}

	/** The actual content. */
	private File content;

	/**
	 * Construct a new FileContent that does not use a nested path. same as new
	 * FileContent(file, file.getParentFile());
	 *
	 * @param file
	 *            the source of content.
	 */
	public FileContent(final File file) {
		this.content = file;
		this.setLastModified(new Date(file.lastModified()));
		this.setLength(file.length());
		this.setContentType(getMimeType(file));
	}

	/**
	 * Construct a new FileContent from a URLContent for legacy products
	 *
	 * @param urlc
	 *            the source of content.
	 * @throws URISyntaxException if error in URI
	 */
	public FileContent(final URLContent urlc) throws URISyntaxException {
		super(urlc);
		this.content = new File(urlc.getURL().toURI());
	}

	/**
	 * Convert a Content to a file backed content.
	 *
	 * The file written is new File(baseDirectory, content.getPath()).
	 *
	 * @param content
	 *            the content that will be converted to a file.
	 * @param toWrite
	 *            the file where content is written.
	 * @throws IOException
	 *            if IO error occurs
	 */
	public FileContent(final Content content, final File toWrite)
			throws IOException {
		super(content);

		// this file content object will use the newly created file
		this.content = toWrite;

		// make sure the parent directory exists
		File parent = toWrite.getCanonicalFile().getParentFile();
		if (!parent.isDirectory()) {
			parent.mkdirs();
		}

		// save handle to stream to force it closed
		OutputStream out = null;
		InputStream in = null;
		try {
			in = content.getInputStream();
			out = StreamUtils.getOutputStream(toWrite);
			// write the file
			StreamUtils.transferStream(in, out);
		} finally {
			// force the stream closed
			StreamUtils.closeStream(in);
			StreamUtils.closeStream(out);
		}

		// update modification date in filesystem
		toWrite.setLastModified(content.getLastModified().getTime());

		// verify file length
		Long length = getLength();
		if (length > 0 && !length.equals(toWrite.length())) {
			throw new IOException("Written file length ("
					+ toWrite.length()
					+ ") does not match non-zero content length (" + length
					+ ")");
		}

		// length may still be <= 0 if content was input stream.
		setLength(toWrite.length());
	}

	/**
	 * @return an InputStream for the wrapped content.
	 */
	public InputStream getInputStream() throws IOException {
		return StreamUtils.getInputStream(content);
	}

	/**
	 * @return the wrapped file.
	 */
	public File getFile() {
		return content;
	}

	/**
	 * Search a directory for files. This is equivalent to
	 * getDirectoryContents(directory, directory).
	 *
	 * @param directory
	 *            the directory to search.
	 * @return a map of relative paths to FileContent objects.
	 * @throws IOException if IO error occurs
	 */
	public static Map<String, FileContent> getDirectoryContents(
			final File directory) throws IOException {
		File absDirectory = directory.getCanonicalFile();
		return getDirectoryContents(absDirectory, absDirectory);
	}

	/**
	 * Search a directory for files. The path to files relative to baseDirectory
	 * is used as a key in the returned map.
	 *
	 * @param directory
	 *            the directory to search.
	 * @param baseDirectory
	 *            the directory used to compute relative paths.
	 * @return a map of relative paths to FileContent objects.
	 * @throws IOException if IO error occurs
	 */
	public static Map<String, FileContent> getDirectoryContents(
			final File directory, final File baseDirectory) throws IOException {
		Map<String, FileContent> contents = new HashMap<String, FileContent>();

		// compute the base path once, and escape the pattern being matched
		String basePath = Pattern.quote(baseDirectory.getCanonicalPath()
				+ File.separator);

		File[] files = directory.listFiles();
		for (File file : files) {
			if (file.isDirectory()) {
				// recurse into sub directory
				contents.putAll(getDirectoryContents(file.getCanonicalFile(),
						baseDirectory.getCanonicalFile()));
			} else {
				String path = file.getCanonicalPath().replaceAll(basePath, "");
				contents.put(path, new FileContent(file));
			}
		}

		return contents;
	}

	/**
	 * This implementation calls defaultGetMimeType, and exists so subclasses
	 * can override.
	 *
	 * @param file
	 *            file to check.
	 * @return corresponding mime type.
	 */
	public String getMimeType(final File file) {
		return defaultGetMimeType(file);
	}

	/**
	 * Check a local list of mime types, and fall back to MimetypeFileTypesMap
	 * if not specified.
	 *
	 * @param file
	 *            file to check.
	 * @return corresponding mime type.
	 */
	protected static String defaultGetMimeType(final File file) {
		String name = file.getName();
		int index = name.lastIndexOf('.');
		if (index != -1) {
			String extension = name.substring(index + 1);
			if (MIME_TYPES.containsKey(extension)) {
				return MIME_TYPES.get(extension);
			}
		}
		return SYSTEM_MIME_TYPES.getContentType(file);
	}

	/**
	 * Free any resources associated with this content.
	 */
	public void close() {
		// nothing to free
	}

}