001: package org.gomba;
002:
003: import java.io.IOException;
004: import java.sql.Connection;
005: import java.sql.ResultSet;
006: import java.util.Iterator;
007: import java.util.Map;
008:
009: import javax.servlet.RequestDispatcher;
010: import javax.servlet.ServletConfig;
011: import javax.servlet.ServletException;
012: import javax.servlet.http.HttpServletRequest;
013: import javax.servlet.http.HttpServletResponse;
014:
015: /**
016: * Base class for servlets that perform a SELECT and render the results to the
017: * HTTP response body.
018: *
019: * </p>
020: * Init params:
021: * <dl>
022: * <dt>query</dt>
023: * <dd>The SQL query to execute. May contain ${} parameters. This init-param
024: * also accepts a path to a dynamic resource (a JSP) when dynamic SQL generation
025: * is needed. The path must begin with a "/" and is interpreted as relative to
026: * the current context root. (Required)</dd>
027: * <dt>skip</dt>
028: * <dd>The number of records to skip. May contain ${} parameters. (Optional)
029: * </dd>
030: * <dt>max</dt>
031: * <dd>The maximum number of records to load. May contain ${} parameters.
032: * (Optional)</dd>
033: * <dt>nodata-http-status</dt>
034: * <dd>The HTTP status code in case of empty resultset. If the code is 200 (OK)
035: * then the subclassing servlet will output its default value. Defaults to 200
036: * (OK). A useful code is 404 (Not found). (Optional)</dd>
037: * <dt>nodata-default-resource</dt>
038: * <dd>Path to a resource to serve in case of empty resultset. The path must
039: * begin with a "/" and is interpreted as relative to the current context root.
040: * When this init-param is not specified, the subclassing servlet default output
041: * is used. (Optional)</dd>
042: * </dl>
043: * </p>
044: *
045: * @author Flavio Tordini
046: * @version $Id: SingleQueryServlet.java,v 1.5 2005/10/19 13:48:16 flaviotordini Exp $
047: */
048: public abstract class SingleQueryServlet extends AbstractServlet {
049:
050: private final static String INIT_PARAM_QUERY = "query";
051:
052: private final static String INIT_PARAM_SKIP = "skip";
053:
054: private final static String INIT_PARAM_MAX = "max";
055:
056: private final static String INIT_PARAM_NO_DATA_HTTP_STATUS = "nodata-http-status";
057:
058: private final static String INIT_PARAM_NO_DATA_DEFAULT_RESOURCE = "nodata-default-resource";
059:
060: /**
061: * The parsed query definition. It is null when the query is dynamic, i.e. a
062: * dynamic resource (a JSP) is used to generate the SQL.
063: */
064: private QueryDefinition queryDefinition;
065:
066: /**
067: * The path of a resource that dynamically generates a SQL query.
068: */
069: private String queryResource;
070:
071: /**
072: * Skip and max expressions. These are only set when a dynamic query is
073: * used.
074: */
075: private Expression skip, max;
076:
077: /**
078: * The HTTP status code in case of empty resultset.
079: */
080: private int noDataHttpStatusCode = HttpServletResponse.SC_OK;
081:
082: /**
083: * Path to a resource to serve in case of empty resultset. If null the
084: * doDefaultOutput method is invoked.
085: */
086: private String noDataDefaultResource;
087:
088: /**
089: * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
090: */
091: public void init(ServletConfig config) throws ServletException {
092: super .init(config);
093:
094: // parse the query definition
095: try {
096: String query = config.getInitParameter(INIT_PARAM_QUERY);
097: String skip = config.getInitParameter(INIT_PARAM_SKIP);
098: String max = config.getInitParameter(INIT_PARAM_MAX);
099: if (!query.startsWith("/")) {
100: this .queryDefinition = new QueryDefinition(query, skip,
101: max);
102: } else {
103: this .queryResource = query;
104: if (skip != null) {
105: this .skip = new Expression(skip);
106: }
107: if (max != null) {
108: this .max = new Expression(max);
109: }
110: }
111: } catch (Exception e) {
112: throw new ServletException(
113: "Error parsing query definition.", e);
114: }
115:
116: // the HTTP status code to send in case of empty resultset.
117: try {
118: String noDataHttpStatusCodeString = config
119: .getInitParameter(INIT_PARAM_NO_DATA_HTTP_STATUS);
120: if (noDataHttpStatusCodeString != null) {
121: this .noDataHttpStatusCode = Integer
122: .parseInt(noDataHttpStatusCodeString);
123: }
124: } catch (NumberFormatException e) {
125: throw new ServletException("Error parsing "
126: + INIT_PARAM_NO_DATA_HTTP_STATUS, e);
127: }
128:
129: // default resource in case of empty resultset.
130: this .noDataDefaultResource = config
131: .getInitParameter(INIT_PARAM_NO_DATA_DEFAULT_RESOURCE);
132: }
133:
134: /**
135: * Get the QueryDefinition, it can be a fixed QueryDefinition created at
136: * init-time. Or a dynamic one created by evaluating a JSP.
137: */
138: private QueryDefinition getQueryDefinition(
139: HttpServletRequest request, HttpServletResponse response)
140: throws ServletException, IOException {
141: QueryDefinition requestQueryDefinition;
142: if (this .queryDefinition == null) {
143: // dynamic query
144: String sql = getDynamicQuery(this .queryResource, request,
145: response);
146: try {
147: requestQueryDefinition = new QueryDefinition(sql,
148: this .skip, this .max);
149: } catch (Exception e) {
150: throw new ServletException(
151: "Error parsing query definition.", e);
152: }
153: } else {
154: // fixed query
155: requestQueryDefinition = this .queryDefinition;
156: }
157: return requestQueryDefinition;
158: }
159:
160: /**
161: * The real work is done here.
162: *
163: * @param request
164: * The HTTP request
165: * @param response
166: * The HTTP response
167: * @param renderBody
168: * Wheter to render the response body or not.
169: */
170: protected final void processRequest(HttpServletRequest request,
171: HttpServletResponse response, boolean renderBody)
172: throws ServletException, IOException {
173:
174: // get current time for benchmarking purposes
175: final long startTime = System.currentTimeMillis();
176:
177: // create the parameter resolver that will help us throughout this
178: // request
179: final ParameterResolver parameterResolver = new ParameterResolver(
180: request);
181:
182: // get the query definition
183: QueryDefinition requestQueryDefinition = getQueryDefinition(
184: request, response);
185:
186: // build the Query
187: final Query query = getQuery(requestQueryDefinition,
188: parameterResolver);
189:
190: // find out if this request is part of a transaction
191: Transaction transaction = getTransaction(parameterResolver);
192:
193: Query.QueryResult queryResult = null;
194: Connection connection = null;
195:
196: // surround everything in this try/finally to be able to free JDBC
197: // resources even in case of exceptions
198: try {
199:
200: try {
201:
202: if (transaction == null) {
203: connection = getDataSource().getConnection();
204: } else {
205: if (isDebugMode()) {
206: log("Request is part of transaction: "
207: + transaction.getUri());
208: }
209: connection = transaction.getConnection();
210: }
211:
212: // execute the query
213: queryResult = query.execute(connection);
214:
215: // queryResult may be null, if the query is an update.
216: if (queryResult != null) {
217:
218: // Make sure the resultset cursor is positioned on a row. If
219: // resultset is empty or after the last row, stop processing
220: // this request.
221: if (!maybeMoveCursor(queryResult.getResultSet())) {
222: // the resultset is empty!
223:
224: // if status is not 200 set the HTTP status and stop
225: if (this .noDataHttpStatusCode != HttpServletResponse.SC_OK) {
226: response
227: .sendError(this .noDataHttpStatusCode);
228: return;
229: }
230:
231: // set the response headers right away
232: // if the response headers include an expression using
233: // the 'column' domain (which requires a resultset
234: // available to the ParameterResolver) an exception will
235: // be thrown. The exception is caught and logged if in
236: // debug mode.
237: Map responseHeaders = getResponseHeaders();
238: if (responseHeaders != null) {
239: try {
240: for (Iterator i = responseHeaders
241: .entrySet().iterator(); i
242: .hasNext();) {
243: Map.Entry mapEntry = (Map.Entry) i
244: .next();
245:
246: String headerName = (String) mapEntry
247: .getKey();
248: Object headerValue;
249: try {
250: headerValue = ((Expression) mapEntry
251: .getValue())
252: .replaceParameters(parameterResolver);
253: } catch (ParameterResolver.UnavailableResultSetException urse) {
254: if (isDebugMode()) {
255: log(
256: "Cannot set response header: "
257: + headerName,
258: urse);
259: }
260: continue;
261: }
262: setResponseHeader(response,
263: headerName, headerValue);
264:
265: }
266: } catch (Exception e) {
267: throw new ServletException(
268: "Error setting response headers.",
269: e);
270: }
271: }
272:
273: if (this .noDataDefaultResource == null) {
274: // default output
275: try {
276: // subclasses will implement this!
277: doDefaultOutput(response);
278: } catch (Exception e) {
279: throw new ServletException(
280: "Error rendering default output.",
281: e);
282: }
283: } else {
284: // default resource
285: serveDefaultResource(
286: this .noDataDefaultResource,
287: request, response);
288: }
289:
290: // stop processing this request.
291: return;
292: }
293:
294: // set reference to the result set in order to resolve
295: // 'column' domain parameters
296: parameterResolver.setResultSet(queryResult
297: .getResultSet());
298:
299: }
300:
301: } catch (Exception e) {
302: // log the SQL for debugging
303: log("Error executing query: " + query, e);
304: // but don't expose the SQL nor the exception on the web for
305: // security reasons. We don't want users to be able to see our
306: // SQL nor the JDBC driver exception messages which usually
307: // contain table names and such.
308: response.sendError(
309: HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
310: "Error executing query. See logs for details.");
311: return;
312: }
313:
314: try {
315: ResultSet resultSet = null;
316: if (queryResult != null) {
317: resultSet = queryResult.getResultSet();
318: }
319: // subclasses will implement this!
320: doInput(resultSet, request, parameterResolver,
321: connection);
322: } catch (Exception e) {
323: throw new ServletException(
324: "Error processing request body.", e);
325: }
326:
327: // set the response headers
328: setResponseHeaders(response, parameterResolver);
329:
330: // set the HTTP status code
331: if (getHttpStatusCode() != HttpServletResponse.SC_OK) {
332: response.setStatus(getHttpStatusCode());
333: }
334:
335: // optionally write to the response body
336: if (renderBody) {
337: if (queryResult == null) {
338: throw new ServletException(
339: "Resultset is null. The query didn't return a resultset.");
340: }
341: try {
342: // subclasses will implement this!
343: doOutput(queryResult.getResultSet(), response,
344: parameterResolver);
345: } catch (Exception e) {
346: throw new ServletException(
347: "Error rendering results.", e);
348: }
349: }
350:
351: } finally {
352: // *always* free the JDBC resources!
353: try {
354: if (queryResult != null) {
355: try {
356: queryResult.close();
357: } catch (Exception e) {
358: throw new ServletException(
359: "Error freeing JDBC resources.", e);
360: }
361: }
362:
363: } finally {
364: // close the JDBC connection if this request is not part of a
365: // transaction
366: if (transaction == null && connection != null) {
367: try {
368: connection.close();
369: } catch (Exception e) {
370: throw new ServletException(
371: "Error closing JDBC connection.", e);
372: }
373: }
374: }
375:
376: // processing time
377: if (isDebugMode()) {
378: log(getProfilingMessage(request, startTime));
379: }
380: }
381:
382: }
383:
384: /**
385: * Override this method in order to process data from the request body. The
386: * contract for subclasses is not to close the ResultSet and not to call
387: * ResultSet.next().
388: *
389: * @param resultSet
390: * The resultset, may be null.
391: * @param request
392: * The HTTP request to read from
393: * @param parameterResolver
394: * The object used to resolve parameters.
395: */
396: protected void doInput(ResultSet resultSet,
397: HttpServletRequest request,
398: ParameterResolver parameterResolver, Connection connection)
399: throws Exception {
400: // dummy
401: }
402:
403: /**
404: * Render the content of the resultset in the response body. The contract
405: * for subclasses is not to close the ResultSet and expect it to be
406: * positioned on the first row to render (This means ResultSet.next() should
407: * be called <strong>after </strong> the first row has been rendered.
408: *
409: * @param resultSet
410: * The resultset to render
411: * @param response
412: * The HTTP response to write to
413: * @param parameterResolver
414: * The object used to resolve parameters.
415: */
416: protected void doOutput(ResultSet resultSet,
417: HttpServletResponse response,
418: ParameterResolver parameterResolver) throws Exception {
419: // dummy
420: }
421:
422: /**
423: * Render a default value when the resultset is empty (0 rows).
424: *
425: * @param response
426: * The HTTP response to write to
427: */
428: protected void doDefaultOutput(HttpServletResponse response)
429: throws Exception {
430: // dummy
431: }
432:
433: /**
434: * Serve a default resource.
435: *
436: * @param defaultResource
437: * Path to the resource
438: * @param request
439: * The HTTP request
440: * @param response
441: * The HTTP response
442: */
443: private final void serveDefaultResource(String defaultResource,
444: HttpServletRequest request, HttpServletResponse response)
445: throws ServletException, IOException {
446: // get the dispatcher
447: RequestDispatcher dispatcher = getServletContext()
448: .getRequestDispatcher(defaultResource);
449: if (dispatcher == null) {
450: throw new ServletException(
451: "Cannot get a RequestDispatcher for path: "
452: + defaultResource);
453: }
454: dispatcher.forward(request, response);
455: }
456: }
|