One of the things I see asked very frequently on the Users mailing list is how to return PDF's and other BLOB fields from a database (or from a file system). People either have trouble figuring out how to do it and require help, or have trouble making it work.
Please find attached source for a new Action called BLOBAction that I submit to you all for comments and, perhaps eventually, inclusion in Struts as a built-in Action like ForwardAction and the like. Consider this proof-of-concept code, quality-wise... You can read my rather verbose comments on this, but in short... Right now it works by accepting a bunch of parameters submitted with the request. This I feel is NOT the right approach. I was hoping that Struts would read in unknown attributes for <action> mappings from struts-config.xml, but that does not seem to be the case. I believe most, if not all, of these parameters should become attributes of the action mappings themselves, but this is one of the things I'm looking for feedback on. I am using this code (essentially this code... this is more generic though) in a production app here at work, so I know it works (aside from my tests here on this particular code which of course work). I'm sure it's not ready as-is, but I don't think it's too bad either. I look forward to any comments you may have (first and foremost: is this even worth it? I think it is, but I may be wrong on even that basic point!) -- Frank W. Zammetti Founder and Chief Software Architect Omnytex Technologies http://www.omnytex.com
/* * ==================================================================== * * The Apache Software License, Version 1.1 * * Copyright (c) 1999-2003 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowlegement: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowlegement may appear in the software itself, * if and wherever such third-party acknowlegements normally appear. * * 4. The names "The Jakarta Project", "Struts", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact [EMAIL PROTECTED] * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Group. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package org.apache.struts.action; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.sql.Blob; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import javax.naming.Context; import javax.naming.InitialContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletOutputStream; import javax.sql.DataSource; /** * A BLOBAction can be used generically to return a BLOB, i.e., an image or a * PDF or anything else. It can use a database or the file system as the * source of the BLOB. Note that this version, which is basically a more * generic proof-of-concept of an Action I use in a productiona app already, * gets all its parameters from request. This, I think, is *NOT* how it * should work, at least not exclusively. I believe most, if not all, of the * parameters used here should actually be attributes of the ActionMapping. * Ideally, if the parameters are found in request them they would override * what's in the mapping, but having them JUST be request parameters I don't * think is the right answer. I was hoping Struts would grab any attributes * it found for mappings whether it recognized them or not, but that doesn't * seem to be the case, hence I can't fully and properly implement this without * input from others. So, for now, everything is request parameters. * * The following parameters are accepted (remember, most or maybe even all of * these I think should be optional attributes of <action>... * * contentType .. This is the content type of the returned response. Values * like "image/gif", "image/jpg" or "application/pdf" are typical. * * dataSource .......... Either the value "database" or "file". Determines * whether the BLOB is served from a database or the file * system (probably the local file system, but could be a * mapped drive or remotely mounted volume too.) * dbConnectionSource .. Only applicable when dataSource="database". Either * the value "create" or "jndi". Use "create" when you * want the database connection to be created with each * invocation of this Action. Use "jndi" to get the * connection from JNDI (probably a connection pool). * dbDriverClass ....... Only applicable when dataSource="database". This is * the JDBC driver class to use to access the database. * dbURL ............... Only applicable when dataSource="database". This is * the connection string used to connect to the database. * This is vendor-specific! * dbUsername .......... Only applicable when dataSource="database". Username * used to connect to the database. * dbPassword .......... Only applicable when dataSource="database". Password * for the user specified by username, used to access the * database. * dbTable ............. Only applicable when dataSource="database". This is * the name of the table in the database that the BLOB is * coming from. * dbField ............. Only applicable when dataSource="database". This is * the field in the table specified by dbTable that the * BLOB is coming from. * dbQuery ............. Only applicable when dataSource="database". This is * the SQL in the WHERE clause used to retrieve the BLOB * from the database. * flFullPath .......... Only applicable when dataSource="file". This is a * fully-qualified filename, including path. Note that * this is NOT relative to the container, which might be * a security concern (although I view it as a powerful * capability... I think it's definitely a security * concern when all these parameters are passed on the * URL, but it shouldn't be an issue when they are * included in the <action> mappings). * * Sample usages: * * >> Serve from a database: * Assume you have an app server running on the local machine on port 8181, * and a test app installed called "blobtest" using an extension of .bt. * Further assume you have an action mapping in struts-config.xml pointing to * this action under the name BLOBServer.bt. Further still, assume * you have an Oracle database on a remote machine called * "dbsrvr" on listening on port 1521 with a schema named "schm1". Even * further assume you have a user "theusr" set up in the database with a * password of "mypw123". Lastly, assume you have a table named "logos" with * two fields, "logo" and "logoid" and you want to return the image stored in * the BLOB field "logo" where logoid=153. You are NOT using JNDI to get a * connection to the database, you want to create a connection on each * invocation of this Action. All that assumed, invoke this * action with the following URL to test it (i.e., as the src of an <img> * tag perhaps)... * * http://localhost:8181/toa/BLOBServer.bt?dataSource=database& * dbConnectionSource=create&contentType=image/gif&dbTable=logos&dbField=logo& * dbQuery=logoid=153&dbDriverClass=oracle.jdbc.driver.OracleDriver& * dbURL=jdbc:oracle:thin:@cdp03d:1521:utoa&dbUsername=utoa&dbPassword=utoatest * * * >> Serve from the file system: * Assume you have an image named test.gif in the directory c:\temp (on a * Windows system obviously)... * * http://localhost:8181/toa/BLOBServer.toa?dataSource=file& * flFullPath=c:\temp\test.gif * * * @author <a href="mailto:[EMAIL PROTECTED]">Frank W. Zammetti</a> * @version .1 * @date September 17, 2004 */ public class BLOBAction extends Action { /** * This usual execute method * * @param mapping Action mapping * @param form ActionForm * @param request HTTP Request as passed in to execute * @param response HTTP Response as passed in to execute * @return null No return, response is rendered fully from here * @throws Exception Exception */ public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // Get the parameter telling us the source of our BLOB as well as what // the content type should be String dataSource = (String)request.getParameter("dataSource"); String contentType = (String)request.getParameter("contentType"); // Determine whether the BLOB is coming from a database or the file // system and call the appropriate method, which returns an array of // bytes that we'll then render to the output stream byte[] bytes = null; if (dataSource.equalsIgnoreCase("database")) { bytes = serveFromDatabase(request); } if (dataSource.equalsIgnoreCase("file")) { bytes = serveFromFileSystem(request); } // Get ready to output ServletOutputStream out = response.getOutputStream(); // Set content type and output to stream response.setContentType(contentType); ByteArrayOutputStream ba = new ByteArrayOutputStream(); ba.write(bytes, 0, bytes.length); ba.writeTo(out); out.flush(); // Not forwarding anywhere return null; } // End process() /** * This method is called to return the BLOB as a byte array from a database * * @param request HTTP Request as passed in to execute * @return byte[] Byte array of the BLOB data * @throws Exception Exception */ private byte[] serveFromDatabase(HttpServletRequest request) throws Exception { // Get the parameters from request specific to a database source String dbTable = (String)request.getParameter("dbTable"); String dbField = (String)request.getParameter("dbField"); String dbQuery = (String)request.getParameter("dbQuery"); // Set up for the query String sql = "select " + dbField + " from " + dbTable + " where " + dbQuery; Connection conn = getDBConnection(request); // Do the query and get results Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet rs = stmt.executeQuery(sql); Blob blob = null; while (rs.next()) { blob = rs.getBlob(dbField); } // Clean up after ourselves rs.close(); conn.close(); // Return our BLOB return blob.getBytes(1, (int)(blob.length())); } // End serveFromDatabase /** * This method will return a Connection object to a database when a * database is the BLOB data source * * @param request HTTP Request as passed in to execute * @return Connection Database connection * @throws Exception Exception */ private Connection getDBConnection(HttpServletRequest request) throws Exception { // Determine whether we're creating a connection manually or getting one // from a JNDI lookup String dbConnSource = (String)request.getParameter("dbConnectionSource"); // Create the connection, or get it from JNDI Connection conn = null; if (dbConnSource.equalsIgnoreCase("create")) { // We're creating the database connection here, so get the required // parameters from request and do it String dbDriverClass = (String)request.getParameter("dbDriverClass"); String dbURL = (String)request.getParameter("dbURL"); String dbUsername = (String)request.getParameter("dbUsername"); String dbPassword = (String)request.getParameter("dbPassword"); Class.forName(dbDriverClass); conn = DriverManager.getConnection(dbURL, dbUsername, dbPassword); } else if (dbConnSource.equalsIgnoreCase("jndi")) { // We're getting the database connection from JNDI, easy enough! The // only extra thing we need is the JNDI context to use.. I found that // under WebSphere you have to look up in InitialContext, while under // Tomcat it's under java:comp/env. I very much assume I either don't // know something about JNDI or am doing something wrong, so I look // forward to anyone that can give me the right answer! For now though, // this code seems to do the trick. String jndiContext = (String)request.getParameter("jndiContext"); String jndiIdentifier = (String)request.getParameter("jndiIdentifier"); InitialContext jndiCntx = new InitialContext(); Context ctx = null; if (jndiContext == null || jndiContext.equalsIgnoreCase("")) { ctx = jndiCntx; } else { ctx = (Context)jndiCntx.lookup(jndiContext); } DataSource ds = (DataSource)ctx.lookup(jndiIdentifier); conn = ds.getConnection(); } // End dbConnSource if return conn; } // End getDBConnection() /** * This method is called to return the BLOB as a byte array from the * file system * * @param request HTTP Request as passed in to execute * @return byte[] Byte array of the BLOB data * @throws Exception Exception */ private byte[] serveFromFileSystem(HttpServletRequest request) throws Exception { // Get the parameters from request specific to a database source String flFullPath = (String)request.getParameter("flFullPath"); // Read in the file to a byte array File f = new File(flFullPath); FileInputStream istr = new FileInputStream(f); BufferedInputStream bstr = new BufferedInputStream(istr); int size = (int)f.length(); byte[] data = new byte[size]; bstr.read(data, 0, size); bstr.close(); // Return the BLOB data return data; } // End serveFromDatabase } // End class
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]