001: package org.gomba;
002:
003: import java.io.IOException;
004: import java.io.InputStream;
005: import java.io.OutputStream;
006: import java.io.Reader;
007: import java.io.Writer;
008: import java.sql.Blob;
009: import java.sql.Clob;
010: import java.sql.Connection;
011: import java.sql.DatabaseMetaData;
012: import java.sql.ResultSet;
013: import java.sql.ResultSetMetaData;
014: import java.sql.SQLException;
015: import java.sql.Types;
016: import java.util.Iterator;
017: import java.util.List;
018:
019: import javax.servlet.ServletConfig;
020: import javax.servlet.ServletException;
021: import javax.servlet.http.HttpServletRequest;
022: import javax.servlet.http.HttpServletResponse;
023: import javax.sql.DataSource;
024:
025: /**
026: * Write a LOB to a JDBC data store. the SQL in the <code>query</code>
027: * init-param should be a SELECT that selects a LOB field. Currently LOBs are
028: * overwritten but not cleared, so previous data in the LOB that exceeds the
029: * length of the new data will not be overwritten. This servlet inherits the
030: * init-params of {@link org.gomba.AbstractServlet}, plus:
031: * <dl>
032: * <dt>http-method</dt>
033: * <dd>The value can be POST or PUT. (Required)</dd>
034: * <dt>column</dt>
035: * <dd>The result set column containing the BLOB or CLOB value. This init-param
036: * is required only if the result set contains more than one column. (Optional)
037: * </dd>
038: * <dt>update-query</dt>
039: * <dd>An SQL statement that writes the BLOB or CLOB to the db. The statement
040: * must contain one parameter of BLOB or CLOB type using the
041: * <code>${blob.myColumn}</code> syntax. This is required only if the JDBC
042: * driver does not support LOB in-place modification. (Optional)</dd>
043: * </dl>
044: *
045: * Note about HTTP method usage. The POST method is normally used for creation
046: * (INSERT in SQL) operations. The PUT method is normally used for update
047: * (UPDATE in SQL) operations.
048: *
049: * @author Flavio Tordini
050: * @version $Id: LOBUpdateServlet.java,v 1.8 2005/07/21 09:12:22 flaviotordini Exp $
051: */
052: public class LOBUpdateServlet extends SingleQueryServlet {
053:
054: /**
055: * The result set column name to render. May be null.
056: */
057: private String columnName;
058:
059: /**
060: * <code>true</code> if this servlet supports the POST HTTP method.
061: */
062: private boolean supportPost;
063:
064: /**
065: * <code>true</code> if this servlet supports the PUT HTTP method.
066: */
067: private boolean supportPut;
068:
069: /**
070: * The parsed update query definition, if any.
071: */
072: private QueryDefinition updateQueryDefinition;
073:
074: /**
075: * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
076: */
077: public void init(ServletConfig config) throws ServletException {
078: super .init(config);
079:
080: // optional UPDATE query
081: String updateQuery = config.getInitParameter("update-query");
082:
083: // check db/driver capabilities
084: try {
085: if (updateQuery == null
086: && checkLocatorsUpdateCopy(getDataSource())) {
087: throw new ServletException(
088: "The database does not allow in-place LOB modification. "
089: + "Updates made to a LOB are made on a copy and not directly to the LOB. "
090: + "The 'update-query' init-param must be specified.");
091: }
092: } catch (SQLException sqle) {
093: throw new ServletException(
094: "Error checking if locators update copies.", sqle);
095: }
096:
097: // parse the update query definition
098: if (updateQuery != null) {
099: try {
100: this .updateQueryDefinition = new QueryDefinition(
101: updateQuery);
102: } catch (Exception e) {
103: throw new ServletException(
104: "Error parsing update query definition.", e);
105: }
106: }
107:
108: // name of the column to write the LOB to
109: this .columnName = config.getInitParameter("column");
110:
111: // supported HTTP method
112: String httpMethod = config.getInitParameter("http-method");
113: if (httpMethod == null) {
114: throw new ServletException(
115: "Missing init-param: http-method");
116: }
117: if (httpMethod.equals("POST")) {
118: this .supportPost = true;
119: } else if (httpMethod.equals("PUT")) {
120: this .supportPut = true;
121: } else {
122: throw new ServletException("Unsupported HTTP method: "
123: + httpMethod);
124: }
125: }
126:
127: /**
128: * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest,
129: * javax.servlet.http.HttpServletResponse)
130: */
131: protected void doPost(HttpServletRequest request,
132: HttpServletResponse response) throws ServletException,
133: IOException {
134: if (this .supportPost) {
135: processRequest(request, response, false);
136: } else {
137: response
138: .sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
139: }
140: }
141:
142: /**
143: * @see javax.servlet.http.HttpServlet#doPut(javax.servlet.http.HttpServletRequest,
144: * javax.servlet.http.HttpServletResponse)
145: */
146: protected void doPut(HttpServletRequest request,
147: HttpServletResponse response) throws ServletException,
148: IOException {
149: if (this .supportPut) {
150: processRequest(request, response, false);
151: } else {
152: response
153: .sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
154: }
155: }
156:
157: /**
158: * Get a reference to the LOB object and write the content of the request
159: * body to it.
160: *
161: * @see org.gomba.AbstractServlet#doInput(java.sql.ResultSet,
162: * javax.servlet.http.HttpServletRequest, org.gomba.ParameterResolver)
163: */
164: protected void doInput(ResultSet resultSet,
165: HttpServletRequest request,
166: ParameterResolver parameterResolver, Connection connection)
167: throws Exception {
168:
169: if (resultSet == null) {
170: throw new Exception("Resultset is null.");
171: }
172:
173: ResultSetMetaData rsmd = resultSet.getMetaData();
174: if (rsmd.getColumnCount() != 1 && this .columnName == null) {
175: throw new Exception(
176: "The resultset contains more than one column. "
177: + "You must set the 'column' init-param.");
178: }
179:
180: // find out the column index
181: final int columnIndex;
182: if (this .columnName != null) {
183: columnIndex = DatumServlet.getColumnIndex(rsmd,
184: this .columnName);
185: } else {
186: columnIndex = 1;
187: }
188:
189: final int columnType = rsmd.getColumnType(columnIndex);
190:
191: // write!
192: switch (columnType) {
193:
194: case Types.BLOB:
195: case Types.LONGVARBINARY:
196:
197: if (this .updateQueryDefinition == null) {
198:
199: // in-place modification
200: // Get a reference to the blob
201: Blob blob = resultSet.getBlob(columnIndex);
202: if (blob == null) {
203: throw new Exception("BLOB value is null.");
204: }
205:
206: // clear the BLOB. If we don't do this, previous data in the
207: // BLOB that exceeds the length of the new data will not be
208: // overwritten.
209: // FIXME Not supported by MySQL
210: // blob.truncate(0);
211:
212: // copy the request body to the BLOB
213: writeBytes(request.getInputStream(), blob
214: .setBinaryStream(1));
215:
216: } else {
217:
218: // use the update query (this is the most common situation:
219: // MySQL, Oracle)
220: Query updateQuery = new Query(
221: this .updateQueryDefinition, parameterResolver);
222:
223: // The blob object should be already in the parameters via the
224: // BlobColumnDomain
225: Blob blob = getBlob(updateQuery
226: .getStatementParameters());
227: if (blob == null) {
228: throw new Exception(
229: "Unable to get reference to a BLOB. "
230: + "Maybe the BLOB field is null on the db "
231: + "or the wrong column name has been specified.");
232: }
233:
234: // clear the BLOB. If we don't do this, previous data in the
235: // BLOB that exceeds the length of the new data will not be
236: // overwritten.
237: // FIXME Not supported by MySQL
238: // blob.truncate(0);
239:
240: // write the request body to the blob
241: writeBytes(request.getInputStream(), blob
242: .setBinaryStream(1));
243: // perform the update
244: updateQuery.execute(connection);
245:
246: }
247: break;
248:
249: case Types.CLOB:
250: case Types.LONGVARCHAR:
251:
252: if (this .updateQueryDefinition == null) {
253:
254: // in-place modification
255: // Get a reference to the CLOB
256: Clob clob = resultSet.getClob(columnIndex);
257: if (clob == null) {
258: throw new Exception("CLOB value is null.");
259: }
260:
261: // clear the CLOB. If we don't do this, previous data in the
262: // CLOB that exceeds the length of the new data will not be
263: // overwritten.
264: // FIXME Not supported by MySQL
265: // blob.truncate(0);
266:
267: // copy the request body to the CLOB
268: writeCharacters(request.getReader(), clob
269: .setCharacterStream(1));
270:
271: } else {
272:
273: // use the update query (this is the most common situation:
274: // MySQL, Oracle)
275: Query updateQuery = new Query(
276: this .updateQueryDefinition, parameterResolver);
277:
278: // The blob object should be already in the parameters via the
279: // BlobColumnDomain
280: Clob clob = getClob(updateQuery
281: .getStatementParameters());
282: if (clob == null) {
283: throw new Exception(
284: "Unable to get reference to a CLOB. "
285: + "Maybe the CLOB field is null on the db "
286: + "or the wrong column name has been specified.");
287: }
288:
289: // clear the CLOB. If we don't do this, previous data in the
290: // CLOB that exceeds the length of the new data will not be
291: // overwritten.
292: // FIXME Not supported by MySQL
293: // clob.truncate(0);
294:
295: // write the request body to the blob
296: writeCharacters(request.getReader(), clob
297: .setCharacterStream(1));
298: // perform the update
299: updateQuery.execute(connection);
300:
301: }
302: break;
303:
304: default:
305: throw new ServletException("Invalid SQL data type: "
306: + columnType);
307: }
308: }
309:
310: /**
311: * Write characters from a Reader to a Writer.
312: */
313: private static void writeCharacters(Reader reader, Writer writer)
314: throws IOException {
315: try {
316: char[] buffer = new char[4096];
317: int length;
318: while ((length = reader.read(buffer)) >= 0) {
319: writer.write(buffer, 0, length);
320: }
321: } finally {
322: writer.close();
323: writer.close();
324: }
325: }
326:
327: /**
328: * Write bytes from an InputStream to an OutputStream.
329: */
330: private static void writeBytes(InputStream is, OutputStream os)
331: throws IOException {
332: try {
333: byte[] buffer = new byte[4096];
334: int length;
335: while ((length = is.read(buffer)) >= 0) {
336: os.write(buffer, 0, length);
337: }
338: } finally {
339: is.close();
340: os.close();
341: }
342: }
343:
344: /**
345: * Check if the JDBC driver supports in-place modification of LOB locators.
346: */
347: private static boolean checkLocatorsUpdateCopy(DataSource dataSource)
348: throws SQLException {
349: Connection connection = dataSource.getConnection();
350: try {
351: DatabaseMetaData metaData = connection.getMetaData();
352: return metaData.locatorsUpdateCopy();
353: } finally {
354: if (connection != null) {
355: connection.close();
356: }
357: }
358: }
359:
360: /**
361: * Iterate thorugh the statement parameters and find a Blob. TODO make sure
362: * there is only one Blob
363: */
364: private static Blob getBlob(List parameters) throws Exception {
365: for (Iterator i = parameters.iterator(); i.hasNext();) {
366: Object parameter = i.next();
367: if (parameter instanceof Blob) {
368: return (Blob) parameter;
369: }
370: }
371: return null;
372: }
373:
374: /**
375: * Iterate thorugh the statement parameters and find a Clob. TODO make sure
376: * there is only one Clob
377: */
378: private static Clob getClob(List parameters) throws Exception {
379: for (Iterator i = parameters.iterator(); i.hasNext();) {
380: Object parameter = i.next();
381: if (parameter instanceof Clob) {
382: return (Clob) parameter;
383: }
384: }
385: return null;
386: }
387:
388: }
|