/* * Copyright 1998-2014 University Corporation for Atmospheric Research/Unidata * * Portions of this software were developed by the Unidata Program at the * University Corporation for Atmospheric Research. * * Access and use of this software shall impose the following obligations * and understandings on the user. The user is granted the right, without * any fee or cost, to use, copy, modify, alter, enhance and distribute * this software, and any derivative works thereof, and its supporting * documentation for any purpose whatsoever, provided that this entire * notice appears in all copies of the software, derivative works and * supporting documentation. Further, UCAR requests that the user credit * UCAR/Unidata in any publications that result from the use of this * software or in any product that includes this software. The names UCAR * and/or Unidata, however, may not be used in any advertising or publicity * to endorse or promote any products or commercial entity unless specific * written permission is obtained from UCAR/Unidata. The user also * understands that UCAR/Unidata is not obligated to provide the user with * any support, consulting, training or assistance of any kind with regard * to the use, operation and performance of this software nor to provide * the user with any updates, revisions, new versions or "bug fixes." * * THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL, * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION * WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE. */ package thredds.server.views; import org.springframework.web.servlet.view.AbstractView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletOutputStream; import java.util.Map; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import thredds.servlet.Debug; import thredds.servlet.ServletUtil; import ucar.nc2.util.CancelTask; import ucar.nc2.util.IO; import ucar.nc2.util.cache.FileCacheIF; import ucar.nc2.util.cache.FileCacheable; import ucar.nc2.util.cache.FileFactory; import ucar.unidata.io.RandomAccessFile; /** * Render the response to a request for a local file including byte range requests. * * <p> * This view supports the following model elements: * <pre> * KEY OBJECT Required * === ====== ======== * "file" java.io.File yes * "contentType" java.lang.String no * "characterEncoding" java.lang.Stringb no * </pre> * * NOTE: If the content type is determined to be text, the character encoding * is assumed to be UTF-8 unless * * @author edavis * @since 4.0 */ public class FileView extends AbstractView { private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger( FileView.class ); /* private static final ucar.nc2.util.cache.FileFactory fileFactory = new FileFactory() { public FileCacheable open(String location, int buffer_size, CancelTask cancelTask, Object iospMessage) throws IOException { return RandomAccessFile.acquire(location); } }; private FileCacheIF fileCacheRaf; public void setFileCacheRaf( FileCacheIF fileCacheRaf) { this.fileCacheRaf = fileCacheRaf; } public void init() { if ( this.fileCacheRaf == null ) this.fileCacheRaf = ServletUtil.getFileCache(); if ( this.fileCacheRaf == null ) throw new IllegalStateException( "FileCacheRaf not configured."); } */ protected void renderMergedOutputModel( Map model, HttpServletRequest req, HttpServletResponse res ) throws Exception { if ( model == null || model.isEmpty() ) throw new IllegalArgumentException( "Model must not be null or empty." ); if ( ! model.containsKey( "file" ) ) throw new IllegalArgumentException( "Model must contain \"file\" key." ); Object o = model.get( "file" ); if ( ! ( o instanceof File ) ) throw new IllegalArgumentException( "Object mapped by \"file\" key must be a File." ); File file = (File) o; // Check that file exists and is not a directory. if ( ! file.isFile() ) { // ToDo Send error or throw exception to be handled by Spring exception handling stuff. // throw new IllegalArgumentException( "File must exist and not be a directory." ); res.sendError( HttpServletResponse.SC_BAD_REQUEST ); return; } // Check if content type is specified. String contentType = null; if ( model.containsKey( "contentType")) { o = model.get( "contentType"); if ( o instanceof String ) contentType = (String) o; } // Check if content type is specified. String characterEncoding = null; if ( model.containsKey( "characterEncoding")) { o = model.get( "characterEncoding"); if ( o instanceof String ) characterEncoding = (String) o; } // Set the type of the file String filename = file.getPath(); if ( null == contentType ) { if ( filename.endsWith( ".html" ) ) contentType = "text/html; charset=utf-8"; else if ( filename.endsWith( ".xml" ) ) contentType = "application/xml; charset=utf-8"; else if ( filename.endsWith( ".txt" ) || filename.endsWith( ".log" ) || filename.endsWith( ".out" ) ) contentType = "text/plain; charset=utf-8"; else if ( filename.indexOf( ".log." ) > 0 ) contentType = "text/plain; charset=utf-8"; else if ( filename.endsWith( ".nc" ) ) contentType = "application/x-netcdf"; else contentType = this.getServletContext().getMimeType( filename ); if ( contentType == null ) contentType = "application/octet-stream"; } // ToDo Do I need/want to do this? if ( characterEncoding == null ) { if ( ( ! contentType.contains( "charset=") ) && ( contentType.startsWith( "text/" ) || contentType.startsWith( "application/xml"))) { characterEncoding = "utf-8"; } } // Set content type and character encoding as given/determined. res.setContentType( contentType ); if ( characterEncoding != null ) res.setCharacterEncoding( characterEncoding ); // The rest of this is from John's thredds.servlet.ServletUtil.returnFile(...) // see if its a Range Request boolean isRangeRequest = false; long startPos = 0, endPos = Long.MAX_VALUE; String rangeRequest = req.getHeader( "Range" ); if ( rangeRequest != null ) { // bytes=12-34 or bytes=12- int pos = rangeRequest.indexOf( "=" ); if ( pos > 0 ) { int pos2 = rangeRequest.indexOf( "-" ); if ( pos2 > 0 ) { String startString = rangeRequest.substring( pos + 1, pos2 ); String endString = rangeRequest.substring( pos2 + 1 ); startPos = Long.parseLong( startString ); if ( endString.length() > 0 ) endPos = Long.parseLong( endString ) + 1; isRangeRequest = true; } } } // set content length long fileSize = file.length(); long contentLength = fileSize; if ( isRangeRequest ) { endPos = Math.min( endPos, fileSize ); contentLength = endPos - startPos; } res.setContentLength( (int) contentLength ); boolean debugRequest = Debug.isSet( "returnFile" ); if ( debugRequest ) log.debug( "renderMergedOutputModel(): filename = " + filename + " contentType = " + contentType + " contentLength = " + contentLength ); // indicate we allow Range Requests if ( ! isRangeRequest ) res.addHeader( "Accept-Ranges", "bytes" ); if ( req.getMethod().equals( "HEAD" ) ) { return; } try { if ( isRangeRequest ) { // set before content is sent res.addHeader( "Content-Range", "bytes " + startPos + "-" + ( endPos - 1 ) + "/" + fileSize ); res.setStatus( HttpServletResponse.SC_PARTIAL_CONTENT ); try (RandomAccessFile craf = RandomAccessFile.acquire( filename)) { IO.copyRafB( craf, startPos, contentLength, res.getOutputStream(), new byte[60000] ); return; } } // Return the file ServletOutputStream out = res.getOutputStream(); IO.copyFileB( file, out, 60000 ); res.flushBuffer(); out.close(); if ( debugRequest ) log.debug( "renderMergedOutputModel(): file response ok = " + filename ); } // @todo Split up this exception handling: those from file access vs those from dealing with response // File access: catch and res.sendError() // response: don't catch (let bubble up out of doGet() etc) catch ( FileNotFoundException e ) { log.error( "returnFile(): FileNotFoundException= " + filename ); if ( ! res.isCommitted() ) res.sendError( HttpServletResponse.SC_NOT_FOUND ); } catch ( java.net.SocketException e ) { log.info( "returnFile(): SocketException sending file: " + filename + " " + e.getMessage() ); } catch ( IOException e ) { String eName = e.getClass().getName(); // dont want compile time dependency on ClientAbortException if ( eName.equals( "org.apache.catalina.connector.ClientAbortException" ) ) { log.info( "returnFile(): ClientAbortException while sending file: " + filename + " " + e.getMessage() ); return; } log.error( "returnFile(): IOException (" + e.getClass().getName() + ") sending file ", e ); res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Problem sending file" ); } } }