001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.reading;
018:
019: import java.io.BufferedInputStream;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.sql.Connection;
023: import java.sql.PreparedStatement;
024: import java.sql.ResultSet;
025: import java.sql.SQLException;
026: import java.sql.Timestamp;
027: import java.util.Map;
028:
029: import org.apache.avalon.excalibur.datasource.DataSourceComponent;
030: import org.apache.avalon.framework.activity.Disposable;
031: import org.apache.avalon.framework.configuration.Configurable;
032: import org.apache.avalon.framework.configuration.Configuration;
033: import org.apache.avalon.framework.configuration.ConfigurationException;
034: import org.apache.avalon.framework.parameters.Parameters;
035: import org.apache.avalon.framework.service.ServiceException;
036: import org.apache.avalon.framework.service.ServiceManager;
037: import org.apache.avalon.framework.service.ServiceSelector;
038: import org.apache.cocoon.ProcessingException;
039: import org.apache.cocoon.ResourceNotFoundException;
040: import org.apache.cocoon.caching.CacheableProcessingComponent;
041: import org.apache.cocoon.environment.ObjectModelHelper;
042: import org.apache.cocoon.environment.Request;
043: import org.apache.cocoon.environment.Response;
044: import org.apache.cocoon.environment.SourceResolver;
045: import org.apache.excalibur.source.SourceValidity;
046: import org.apache.excalibur.source.impl.validity.NOPValidity;
047: import org.apache.excalibur.source.impl.validity.TimeStampValidity;
048: import org.xml.sax.SAXException;
049:
050: /**
051: * This Reader pulls a resource from a database. It is configured with
052: * the Connection to use, parameters specify the table and column
053: * to pull the image from, and source specifies the source key information.
054: *
055: * @author <a href="mailto:bloritsch@apache.org">Berin Loritsch</a>
056: * @version CVS $Id: DatabaseReader.java 452159 2006-10-02 18:20:45Z vgritsenko $
057: */
058: public class DatabaseReader extends ServiceableReader implements
059: Configurable, Disposable, CacheableProcessingComponent {
060:
061: private ServiceSelector datasourceSelector;
062: private DataSourceComponent datasource;
063:
064: private int typeColumn;
065: private boolean defaultCache = true;
066:
067: private Connection connection;
068: private PreparedStatement statement;
069: private ResultSet resultSet;
070: private InputStream resource; // because HSQL doesn't yet implement getBlob()
071: private String mimeType;
072: private long lastModified = System.currentTimeMillis();
073: private boolean doCommit;
074:
075: public void service(final ServiceManager manager)
076: throws ServiceException {
077: super .service(manager);
078: this .datasourceSelector = (ServiceSelector) manager
079: .lookup(DataSourceComponent.ROLE + "Selector");
080: }
081:
082: /**
083: * Configure the <code>Reader</code> so that we can use the same database
084: * for all instances.
085: */
086: public void configure(Configuration conf)
087: throws ConfigurationException {
088: this .defaultCache = conf.getChild("invalidate").getValue(
089: "never").equals("always");
090:
091: String datasourceName = conf.getChild("use-connection")
092: .getValue();
093: try {
094: this .datasource = (DataSourceComponent) datasourceSelector
095: .select(datasourceName);
096: } catch (ServiceException e) {
097: throw new ConfigurationException("Datasource '"
098: + datasourceName + "' is not available.", e);
099: }
100: }
101:
102: /**
103: * Set the <code>SourceResolver</code> the object model <code>Map</code>,
104: * the source and sitemap <code>Parameters</code> used to process the request.
105: */
106: public void setup(SourceResolver resolver, Map objectModel,
107: String src, Parameters par) throws ProcessingException,
108: SAXException, IOException {
109: super .setup(resolver, objectModel, src, par);
110:
111: try {
112: this .connection = datasource.getConnection();
113: if (this .connection.getAutoCommit()) {
114: this .connection.setAutoCommit(false);
115: }
116:
117: statement = connection.prepareStatement(getQuery());
118: statement.setString(1, this .source);
119:
120: resultSet = statement.executeQuery();
121: if (!resultSet.next()) {
122: throw new ResourceNotFoundException(
123: "There is no resource with that key");
124: }
125:
126: Response response = ObjectModelHelper
127: .getResponse(objectModel);
128: Request request = ObjectModelHelper.getRequest(objectModel);
129:
130: if (modifiedSince(resultSet, request, response)) {
131: this .resource = resultSet.getBinaryStream(1);
132: if (this .resource == null) {
133: throw new ResourceNotFoundException(
134: "There is no resource with that key");
135: }
136:
137: if (this .typeColumn != 0) {
138: this .mimeType = resultSet
139: .getString(this .typeColumn);
140: }
141: }
142:
143: this .doCommit = true;
144: } catch (Exception e) {
145: this .doCommit = false;
146: throw new ResourceNotFoundException(
147: "DatabaseReader error:", e);
148: }
149: }
150:
151: /**
152: * Generates the resource we need to retrieve verbatim from the
153: * database. Granted, this can be used for any resource from a
154: * database, so we may want to get rid of the bias toward images.
155: * This reader requires a number of parameters:
156: *
157: * <pre>
158: * <parameter name="table" value="database_table_name"/>
159: * <parameter name="image" value="database_resource_column_name"/>
160: * <parameter name="key" value="database_lookup_key_column_name"/>
161: * </pre>
162: *
163: * Please note that if any of those parameters are missing, this
164: * <code>Reader</code> cannot function. There are a number of other
165: * parameters that allow you to provide hints for the reader to
166: * optimize resource use:
167: *
168: * <pre>
169: * <parameter name="last-modified" value="database_timestamp_column_name"/>
170: * <parameter name="content-type" value="content_mime_type"/>
171: * <parameter name="type-column" value="database_content_mime_type_column"/>
172: * <parameter name="expires" value="number_of_millis_before_refresh"/>
173: * <parameter name="where" value="alternate_key = 'foo'"/>
174: * <parameter name="order-by" value="alternate_key DESC"/>
175: * </pre>
176: *
177: * Lastly, the <code>key</code> value is derived from the value of
178: * the <code>source</code> string.
179: */
180: public void generate() throws ProcessingException, SAXException,
181: IOException {
182: try {
183: Response response = ObjectModelHelper
184: .getResponse(objectModel);
185: serialize(response);
186: } catch (IOException e) {
187: getLogger().warn("Assuming client reset stream");
188: this .doCommit = false;
189: } catch (Exception e) {
190: this .doCommit = false;
191: throw new ResourceNotFoundException(
192: "DatabaseReader error:", e);
193: }
194: }
195:
196: /**
197: * This method builds the query string used for accessing the database.
198: * If the required parameters do not exist, then we cannot build a
199: * correct query.
200: */
201: protected String getQuery() throws ProcessingException {
202: String table = this .parameters.getParameter("table", null);
203: String column = this .parameters.getParameter("image", null);
204: String key = this .parameters.getParameter("key", null);
205: String where = this .parameters.getParameter("where", null);
206: String orderBy = this .parameters.getParameter("order-by", null);
207: String typeColumn = this .parameters.getParameter("type-column",
208: null);
209:
210: if (table == null || column == null || key == null) {
211: throw new ProcessingException(
212: "We are missing a required parameter. Please include 'table', 'image', and 'key'");
213: }
214:
215: String date = this .parameters.getParameter("last-modified",
216: null);
217: StringBuffer query = new StringBuffer("SELECT ");
218:
219: int columnNo = 1;
220: query.append(column);
221: columnNo++;
222:
223: if (date != null) {
224: query.append(", ").append(date);
225: columnNo++;
226: }
227:
228: if (null != orderBy) {
229: query.append(", ");
230:
231: if (orderBy.endsWith(" DESC")) {
232: query
233: .append(orderBy.substring(0,
234: orderBy.length() - 5));
235: } else {
236: query.append(orderBy);
237: }
238: columnNo++;
239: }
240:
241: if (null != typeColumn) {
242: query.append(", ").append(typeColumn);
243: this .typeColumn = columnNo;
244: }
245:
246: query.append(" FROM ").append(table);
247: query.append(" WHERE ").append(key).append(" = ?");
248:
249: if (null != where) {
250: query.append(" AND ").append(where);
251: }
252:
253: if (null != orderBy) {
254: query.append(" ORDER BY ").append(orderBy);
255: }
256:
257: return query.toString();
258: }
259:
260: /**
261: * Tests whether a resource has been modified or not. As Blobs and
262: * database columns usually do not have intrinsic dates on them (at
263: * least easily accessible), we have to have a database column that
264: * holds a date for the resource. Please note, that the database
265: * column <strong>must</strong> be a <code>Timestamp</code> column.
266: *
267: * In the absence of such a column this method <em>always</em>
268: * returns <code>true</code>. This is because databases are much
269: * more prone to change than filesystems, and don't have intrinsic
270: * timestamps on column updates.
271: */
272: protected boolean modifiedSince(ResultSet set, Request request,
273: Response response) throws SQLException {
274: String lastModified = this .parameters.getParameter(
275: "last-modified", null);
276: if (lastModified != null) {
277: Timestamp modified = set.getTimestamp(lastModified, null);
278: if (null != modified) {
279: this .lastModified = modified.getTime();
280: } else {
281: // assume it has never been modified
282: }
283:
284: response.setDateHeader("Last-Modified", this .lastModified);
285: return this .lastModified > request
286: .getDateHeader("if-modified-since");
287: }
288:
289: // if we have nothing to compare to, then we must assume it
290: // has been modified
291: return true;
292: }
293:
294: /**
295: * This method actually performs the serialization.
296: */
297: public void serialize(Response response) throws IOException,
298: SQLException {
299: if (this .resource == null) {
300: throw new SQLException("The Blob is empty!");
301: }
302:
303: InputStream is = new BufferedInputStream(this .resource);
304:
305: long expires = parameters.getParameterAsInteger("expires", -1);
306: if (expires > 0) {
307: response.setDateHeader("Expires", System
308: .currentTimeMillis()
309: + expires);
310: }
311:
312: response.setHeader("Accept-Ranges", "bytes");
313:
314: byte[] buffer = new byte[8192];
315: int length;
316: while ((length = is.read(buffer)) > -1) {
317: out.write(buffer, 0, length);
318: }
319: is.close();
320: out.flush();
321: }
322:
323: /**
324: * Generate the unique key.
325: * This key must be unique inside the space of this component.
326: *
327: * @return The generated key hashes the src
328: */
329: public java.io.Serializable getKey() {
330: return this .source;
331: }
332:
333: /**
334: * Generate the validity object.
335: *
336: * @return The generated validity object or <code>null</code> if the
337: * component is currently not cacheable.
338: */
339: public SourceValidity getValidity() {
340: if (this .lastModified > 0) {
341: return new TimeStampValidity(this .lastModified);
342: } else {
343: if (this .defaultCache) {
344: return NOPValidity.SHARED_INSTANCE;
345: } else {
346: return null;
347: }
348: }
349: }
350:
351: public void recycle() {
352: super .recycle();
353: this .resource = null;
354: this .lastModified = 0;
355: this .mimeType = null;
356: this .typeColumn = 0;
357:
358: try {
359: if (resultSet != null) {
360: resultSet.close();
361: }
362: } catch (SQLException e) { /* ignored */
363: }
364: resultSet = null;
365:
366: try {
367: if (statement != null) {
368: statement.close();
369: }
370: } catch (SQLException e) { /* ignored */
371: }
372: statement = null;
373:
374: if (this .connection != null) {
375: try {
376: if (this .doCommit) {
377: this .connection.commit();
378: } else {
379: this .connection.rollback();
380: }
381: } catch (SQLException e) {
382: getLogger().warn(
383: "Could not commit or rollback connection", e);
384: }
385:
386: try {
387: this .connection.close();
388: } catch (SQLException e) { /* ignored */
389: }
390: this .connection = null;
391: }
392: }
393:
394: public void dispose() {
395: recycle();
396: if (datasource != null) {
397: datasourceSelector.release(datasource);
398: datasource = null;
399: }
400: if (datasourceSelector != null) {
401: manager.release(datasourceSelector);
402: datasourceSelector = null;
403: }
404: manager = null;
405: }
406:
407: public String getMimeType() {
408: if (mimeType != null) {
409: return mimeType;
410: }
411:
412: return this .parameters.getParameter("content-type", super
413: .getMimeType());
414: }
415: }
|