/* * Copyright (c) 2008,2009 Declarative Engineering LLC. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Declarative Engineering LLC * verson 1 which accompanies this distribution, and is available at * http://declarativeengineering.com/legal/DE_Developer_License_v1.txt */ package com.foundation.web; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Date; import java.io.*; import com.common.debug.Debug; import com.common.io.StreamSupport; import com.foundation.web.interfaces.*; /** * Encapsulates a file to be returned to a client as a response to a web request. * The static methods provide assistance in setting up the file content properly and attaching it to the response object. */ public class FileContent implements IContent { /** The file whose content is being accessed. */ private File file = null; /** The starting position for the download (inclusive). */ private long start = 0; /** The ending position for the download (inclusive). */ private long end; /** The channel used to retrieve content from the file. This may be null if the channel is closed, or if the contents of the file were provided as a byte array. */ private FileChannel channel = null; /** The contents of the file if provided. This allows a caching system to be used while still providing mime information on the file. If null then the channel should be used. */ private byte[] fileContents = null; /** The offset into the file contents of the next byte to be retrieved. */ private int fileContentsOffset = 0; /** The directive indicating to the client whether the response should be cached, or null if the default behavior should be used (depends on the MimeType or if provided, the cacheLength attribute). */ private String cacheDirective = null; /** The date indicating to the client when any cache should become invalid, or null if the default behavior should be used (no date). */ private Date expiresDirective = null; /** The number of seconds the client is allowed to cache the content, null if the mime type's default cache controls should be used, or one of the CACHE_LENGTH_xxx identifiers indicating how caching should be handled. */ private Integer cacheLength = null; /** Whether the content should be downloaded, or null if the default behavior should occur based on the mime type. */ private Boolean isDownloaded = null; /** The custom download name, or if null the file name will be used. */ private String downloadName = null; /** The extension used to identify the mime type. If null, then the file reference will be used. */ private String extension = null; /** * Sets the content using a resource contained in the application's base directory. * @param IResponse The IResponse whose content is being set. * @param resource The resource to use as the content of the IResponse. */ public static void setContentResource(IResponse response, String resource) { setContentResource(response, resource, response.getRequest(), null, null, false); }//setContentResource()// /** * Sets the content using a resource contained in the application's base directory. * @param IResponse The IResponse whose content is being set. * @param resource The resource to use as the content of the IResponse. * @param clientCacheDate The date the client supplied for the IResponse it has cached. If non-null, this can be used to send a 304 Not Modified message back to the client if the resource is unchanged. */ public static void setContentResource(IResponse response, String resource, Date clientCacheDate) { setContentResource(response, resource, response.getRequest(), clientCacheDate, null, false); }//setContentResource()// /** * @param resource The resource to use as the content of the IResponse. * @param IResponse The IResponse whose content is being set. * @param request The request whose refering URL is used to create a relative path for the resource. If null, then the resource will be relative to the web app's base directory. */ public static void setContentResource(IResponse response, String resource, IRequest request) { setContentResource(response, resource, request, null, null, false); }//setContentResource()// /** * @param resource The resource to use as the content of the IResponse. * @param IResponse The IResponse whose content is being set. * @param request The request whose refering URL is used to create a relative path for the resource. If null, then the resource will be relative to the web app's base directory. * @param isDownload Whether the resource should be downloaded rather than opened in the browser. If this is null then the default action will be taken based on the mime settings. */ public static void setContentResource(IResponse response, String resource, IRequest request, Boolean isDownload) { setContentResource(response, resource, request, null, isDownload, false); }//setContentResource()// /** * Sets the content using a resource contained in the application's base directory. * @param IResponse The IResponse whose content is being set. * @param resource The resource to use as the content of the IResponse. * @param request The request whose refering URL is used to create a relative path for the resource. If null, then the resource will be relative to the web app's base directory. * @param clientCacheDate The date the client supplied for the IResponse it has cached. If non-null, this can be used to send a 304 Not Modified message back to the client if the resource is unchanged. * @param isDownload Whether the resource should be downloaded rather than opened in the browser. If this is null then the default action will be taken based on the mime settings. */ public static void setContentResource(IResponse response, String resource, IRequest request, Date clientCacheDate, Boolean isDownload) { setContentResource(response, resource, request, clientCacheDate, isDownload, false); }//setContentResource()// /** * Sets the content using a resource contained in the application's base directory. * @param IResponse The IResponse whose content is being set. * @param resource The resource to use as the content of the IResponse. * @param request The request whose refering URL is used to create a relative path for the resource. If null, then the resource will be relative to the web app's base directory. * @param clientCacheDate The date the client supplied for the IResponse it has cached. If non-null, this can be used to send a 304 Not Modified message back to the client if the resource is unchanged. * @param isDownload Whether the resource should be downloaded rather than opened in the browser. If this is null then the default action will be taken based on the mime settings. * @param infinateCache Whether the client should cache the resource indefinately. This is useful for overriding the default caching limits for things like images that will never change. */ public static void setContentResource(IResponse response, String resource, IRequest request, Date clientCacheDate, Boolean isDownload, boolean infinateCache) { File file = null; IWebApplication application = response.getApplication(); boolean success = false; file = new File(application.getBaseDirectory(), resource); if(file.exists()) { success = setContentResource(application, response, request, clientCacheDate, isDownload, infinateCache, file); }//if// if(!success && application.getExternalBaseDirectory() != null) { file = new File(application.getExternalBaseDirectory(), resource); if(file.exists()) { success = setContentResource(application, response, request, clientCacheDate, isDownload, infinateCache, file); }//if// }//if// if(!success && application.getCacheBaseDirectory() != null) { file = new File(application.getCacheBaseDirectory(), resource); if(file.exists()) { success = setContentResource(application, response, request, clientCacheDate, isDownload, infinateCache, file); }//if// }//if// if(!success) { response.setError(IResponse.ERROR_TYPE_RESOURCE_NOT_FOUND); }//if// }//setContentResource()// /** * Sets the content using a resource contained in the application's base directory. * @param IResponse The IResponse whose content is being set. * @param resource The resource to use as the content of the IResponse. * @param request The request whose refering URL is used to create a relative path for the resource. If null, then the resource will be relative to the web app's base directory. * @param clientCacheDate The date the client supplied for the IResponse it has cached. If non-null, this can be used to send a 304 Not Modified message back to the client if the resource is unchanged. * @param isDownload Whether the resource should be downloaded rather than opened in the browser. If this is null then the default action will be taken based on the mime settings. * @param infinateCache Whether the client should cache the resource indefinately. This is useful for overriding the default caching limits for things like images that will never change. */ public static void setContentResource(IResponse response, File file, IRequest request, Date clientCacheDate, Boolean isDownload, boolean infinateCache) { IWebApplication application = response.getApplication(); boolean success = false; if(file.exists()) { success = setContentResource(application, response, request, clientCacheDate, isDownload, infinateCache, file); }//if// if(!success) { response.setError(IResponse.ERROR_TYPE_RESOURCE_NOT_FOUND); }//if// }//setContentResource()// /** * Performs the actual work of setting the content resource, allowing for multiple file locations to be used by the caller. */ private static boolean setContentResource(IWebApplication application, IResponse response, IRequest request, Date clientCacheDate, Boolean isDownload, boolean infinateCache, File file) { boolean result = false; String canonicalPath = null; ICachedResource cache = null; if(application.isCachingResources()) { try { canonicalPath = file.getCanonicalPath(); cache = application.getCachedResource(canonicalPath); }//try// catch(Throwable e) {} }//if// if(cache != null) { if((clientCacheDate != null) && (!new Date(file.lastModified() & 0xFFFFFFFFFFFF0000L).after(clientCacheDate))) { //Send a 304 Not Modified message to the client.// response.setError(IResponse.ERROR_TYPE_RESOURCE_NOT_MODIFIED); }//if// else { //boolean includeRange = request.getUnmodifiedSinceDate() != null && request.getUnmodifiedSinceDate().getTime() >= cache.getLastModified(); boolean includeRange = request.getRange() != null; result = true; response.setContent(new FileContent(cache.getFile(), cache.getContents(), includeRange ? request.getLowerRange() : null, includeRange ? request.getUpperRange() : null)); }//else// }//if// else if(file.exists() && file.canRead() && file.isFile()) { result = true; try { FileContent fileContent = null; if((clientCacheDate != null) && (!new Date(file.lastModified() & 0xFFFFFFFFFFFF0000L).after(clientCacheDate))) { //Send a 304 Not Modified message to the client.// response.setError(IResponse.ERROR_TYPE_RESOURCE_NOT_MODIFIED); }//if// else if(canonicalPath != null && file.length() < application.getMaxCachedResourceSize()) { boolean includeRange; //TODO: Should read the bytes with a read lock & obtain the update TS at the same time for accuracy. byte[] bytes = StreamSupport.readBytes(file); application.setCachedResource(canonicalPath, file, bytes); //includeRange = request.getUnmodifiedSinceDate() != null && (request.getUnmodifiedSinceDate().getTime() & 0xFFFFFFFFFFFF0000L) >= (file.lastModified() & 0xFFFFFFFFFFFF0000L); includeRange = request.getRange() != null; response.setContent(fileContent = new FileContent(file, bytes, includeRange ? request.getLowerRange() : null, includeRange ? request.getUpperRange() : null)); }//else if// else { //TODO: It would be nice to be able to validate as the download occurs that the file hasn't been updated. //boolean includeRange = request.getUnmodifiedSinceDate() != null && (request.getUnmodifiedSinceDate().getTime() & 0xFFFFFFFFFFFF0000L) >= (file.lastModified() & 0xFFFFFFFFFFFF0000L); boolean includeRange = request.getRange() != null; FileContent content = new FileContent(file, includeRange ? request.getLowerRange() : null, includeRange ? request.getUpperRange() : null); if(isDownload != null) { content.setIsDownloaded(isDownload); //content.setCacheLength(new Integer(MimeType.CACHE_LENGTH_NEVER_CACHE)); }//if// response.setContent(fileContent = content); }//else// if(infinateCache && fileContent != null) { fileContent.setCacheLength(IResponse.INFINATE_CACHE_LENGTH); //6 months in seconds.// fileContent.setExpiresDirective(new Date(new Date().getTime() + (IResponse.INFINATE_CACHE_LENGTH.intValue() * 1000))); }//if// }//try// catch(Throwable e) { Debug.log(e); //Display an error.// response.setError(IResponse.ERROR_TYPE_RESOURCE_NOT_FOUND); }//catch// }//if// return result; }//setContentResource()// /** * FileContent constructor. * @param file The file whose contents will be streamed to the client. */ public FileContent(File file) throws FileNotFoundException, IOException { this(file, null, null); }//FileContent()// /** * FileContent constructor. * @param file The file being represented. * @param bytes The contents of the file. Use this method if the contents are already cached in memory. */ public FileContent(File file, byte[] bytes) { this(file, bytes, null, null); }//FileContent()// /** * FileContent constructor. * @param bytes The contents of the file. Use this method if the contents are already cached in memory. * @param name The file name including extension (used to identify the mime type, and used as the download name). */ public FileContent(byte[] bytes, String name) { this(bytes, name, null, null); }//FileContent()// /** * FileContent constructor. * @param file The file whose contents will be streamed to the client. */ public FileContent(File file, Long start, Long end) throws FileNotFoundException, IOException { this.file = file; this.channel = new FileInputStream(file).getChannel(); if(start != null) { this.start = start.longValue(); this.channel.position(start.longValue()); }//if// this.end = end != null ? end.longValue() : file.length() - 1; // Debug.log("Sending file " + file.getPath() + " starting at " + this.channel.position() + "; ending at " + this.end); }//FileContent()// /** * FileContent constructor. * @param file The file being represented. * @param bytes The contents of the file. Use this method if the contents are already cached in memory. */ public FileContent(File file, byte[] bytes, Long start, Long end) { this.file = file; this.fileContents = bytes; if(start != null) { this.start = start.longValue(); fileContentsOffset = start.intValue(); }//if// this.end = end != null ? end.longValue() : bytes.length - 1; // Debug.log("Sending cached file " + file.getPath() + " starting at " + this.fileContentsOffset + "; ending at " + this.end); }//FileContent()// /** * FileContent constructor. * @param bytes The contents of the file. Use this method if the contents are already cached in memory. * @param name The file name including extension (used to identify the mime type, and used as the download name). */ public FileContent(byte[] bytes, String name, Long start, Long end) { this.fileContents = bytes; this.extension = name.substring(name.lastIndexOf('.') + 1); this.downloadName = name; if(start != null) { this.start = start.longValue(); fileContentsOffset = start.intValue(); }//if// this.end = end != null ? end.longValue() : bytes.length - 1; // Debug.log("Sending cached (2) file " + this.downloadName + " starting at " + this.fileContentsOffset + "; ending at " + this.end); }//FileContent()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#release() */ public void release() { if(channel != null) { try { channel.close(); }//try// catch(Throwable e) {} channel = null; }//if// }//release()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#get(java.nio.ByteBuffer) */ public int get(ByteBuffer buffer) { int result = -1; if(fileContents != null) { if(fileContentsOffset < fileContents.length) { int length = (int) Math.min(end + 1, Math.min(buffer.remaining(), fileContents.length - fileContentsOffset)); // Debug.log("Sending " + length + " cached bytes starting at position " + fileContentsOffset + " of " + (file != null ? file.getPath() : downloadName)); buffer.put(fileContents, fileContentsOffset, length); fileContentsOffset += length; result = length; }//if// }//if// else if(channel != null) { boolean closeChannel = false; try { buffer.limit((int) Math.min(buffer.capacity(), end + 1 - channel.position() + buffer.position())); // long debugPosition = channel.position(); if(buffer.limit() == 0 || (result = channel.read(buffer)) == -1) { closeChannel = true; }//if// // Debug.log("Sending " + result + " file bytes (expected: " + buffer.limit() + ") starting at position " + debugPosition + " of " + file.getPath()); }//try// catch(IOException e) { closeChannel = true; Debug.log(file != null ? "Failed to read the file: " + file.getAbsolutePath() : "Failed while dealing with a byte buffer.", e); }//catch// if(closeChannel) { try { channel.close(); }//try// catch(Throwable e) {} channel = null; // Debug.log("File download complete: " + file.getPath()); }//if// }//else if// return result; }//get()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#getSize() */ public long getSize() { //Note: Can't use the file size when using a cached resource since it may differ from the cached value.// return (fileContents != null ? fileContents.length : file.length()) - start; }//getSize()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#getStart() */ public long getStart() { return start; }//getStart()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#getEnd() */ public long getEnd() { return end; }//getEnd()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#reset() */ public void reset() { if(channel != null) { try { channel.close(); }//try// catch(Throwable e) {} }//if// try { channel = new FileInputStream(file).getChannel(); }//try// catch(Throwable e) { Debug.log(e); }//catch// }//reset()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#getLastModifiedDate() */ public Date getLastModifiedDate() { return file != null ? new Date(file.lastModified()) : new Date(); }//getLastModifiedDate()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#getMimeType(com.foundation.web.interfaces.IMimeTypeProvider) */ public IMimeType getMimeType(IMimeTypeProvider provider) { return extension == null ? provider.getMimeType(file) : provider.getMimeType(extension); }//getMimeType()// /** * Sets the file's extension. Useful if only the file's contents are provided. * @param extension The file extension identifying the mime type. Should not include the separator. */ public void setExtension(String extension) { this.extension = extension; }//getMimeType()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#getCacheDirective() */ public String getCacheDirective() { return cacheDirective; }//getCacheDirective()// /** * Sets the custom cache directive to be used when sending the response to the client. * @param cacheDirective The directive indicating to the client whether the response should be cached, or null if the default behavior should be used (depends on the MimeType or if provided, the cacheLength attribute). */ public void setCacheDirective(String cacheDirective) { this.cacheDirective = cacheDirective; }//getCacheDirective()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#getExpiresDirective() */ public Date getExpiresDirective() { return expiresDirective; }//getExpiresDirective()// /** * Sets the default expires date directive to be used when sending the response to the client. * @param expiresDirective The date indicating to the client when any cache should become invalid, or null if the default behavior should be used (no date). */ public void setExpiresDirective(Date expiresDirective) { this.expiresDirective = expiresDirective; }//setExpiresDirective()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#getCacheLength() */ public Integer getCacheLength() { return cacheLength; }//getCacheLength()// /** * Sets the cache length in terms of seconds, or one of the CACHE_LENGTH_xxx identifiers defined by IContent. *
Note: This is an easier way of controlling caching than a custom CacheDirective. The custom cache directive will override this if provided.
* @param cacheLength The number of seconds the client is allowed to cache the content, null if the mime type's default cache controls should be used, or one of the CACHE_LENGTH_xxx identifiers indicating how caching should be handled. */ public void setCacheLength(Integer cacheLength) { this.cacheLength = cacheLength; }//setCacheLength()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#getIsDownloaded() */ public Boolean getIsDownloaded() { return isDownloaded; }//getIsDownloaded()// /** * Gets whether the content should be downloaded versus displayed in the browser. * @param isDownloaded Whether the content should be downloaded, or null if the default behavior should occur based on the mime type. */ public void setIsDownloaded(Boolean isDownloaded) { this.isDownloaded = isDownloaded; }//setIsDownloaded()// /* (non-Javadoc) * @see com.foundation.web.interfaces.IContent#getDownloadName() */ public String getDownloadName() { return downloadName == null ? file.getName() : downloadName; }//getDownloadName()// /** * Sets the download name used when downloading the content (versus displaying it in the browser). * @param downloadName The custom download name, or if null the file name will be used. */ public void setDownloadName(String downloadName) { this.downloadName = downloadName; }//setDownloadName()// }//FileContent//