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.components.source.impl;
018:
019: import java.io.ByteArrayInputStream;
020: import java.io.FilterInputStream;
021: import java.io.IOException;
022: import java.io.InputStream;
023: import java.net.MalformedURLException;
024: import java.sql.Blob;
025: import java.sql.Clob;
026: import java.sql.Connection;
027: import java.sql.ResultSet;
028: import java.sql.SQLException;
029: import java.sql.Statement;
030: import java.sql.Types;
031: import java.util.Iterator;
032:
033: import org.apache.avalon.excalibur.datasource.DataSourceComponent;
034: import org.apache.avalon.framework.logger.AbstractLogEnabled;
035: import org.apache.avalon.framework.service.ServiceManager;
036: import org.apache.avalon.framework.service.ServiceSelector;
037: import org.apache.avalon.framework.service.Serviceable;
038: import org.apache.excalibur.source.Source;
039: import org.apache.excalibur.source.SourceException;
040: import org.apache.excalibur.source.SourceNotFoundException;
041: import org.apache.excalibur.source.SourceValidity;
042:
043: import org.apache.cocoon.CascadingIOException;
044:
045: /**
046: * A <code>Source</code> that takes its content in a single JDBC column. Any
047: * kind of column can be used (clob, blob, varchar, etc), but "Blob" means
048: * that the whole content is contained in a single column.
049: * <p>The URL syntax is "blob:/datasource/table/column[cond]", where :
050: * <ul>
051: * <li>"datasource" is the jdbc datasource to use (defined in cocoon.xonf)
052: * <li>"table" is the database table,
053: * <li>"column" is (you can guess, now :) the column in "table",
054: * <li>"cond" is the boolean condition used to select a particular record in
055: * the table.
056: * </ul>
057: * <p>For example, "<code>blob:/personel/people/photo[userid='foo']</code>"
058: * will fetch the first column returned by the statement "<code>SELECT photo
059: * from people where userid='foo'</code>" in the datasource "<code>personel</code>"
060: *
061: * @author <a href="mailto:sylvain@apache.org">Sylvain Wallez</a>
062: * @author <a href="mailto:stephan@apache.org">Stephan Michels</a>
063: * @version CVS $Id: BlobSource.java 452425 2006-10-03 11:18:47Z vgritsenko $
064: */
065: public class BlobSource extends AbstractLogEnabled implements Source,
066: Serviceable {
067:
068: private static final String URL_PREFIX = "blob:/";
069: private static final int URL_PREFIX_LEN = URL_PREFIX.length();
070:
071: /** The ServiceManager instance */
072: private ServiceManager manager;
073:
074: /** The system ID for this source */
075: private String systemId;
076:
077: private String datasourceName;
078:
079: private String tableName;
080:
081: private String columnName;
082:
083: private String condition;
084:
085: /**
086: * Create a file source from a 'blob:' url and a component manager.
087: * <p>The url is of the form "blob:/datasource/table/column[condition]
088: */
089: public BlobSource(String url) throws MalformedURLException {
090: this .systemId = url;
091:
092: // Parse the url
093: int start = URL_PREFIX_LEN;
094:
095: // Datasource
096: int end = url.indexOf('/', start);
097: if (end == -1) {
098: throw new MalformedURLException(
099: "Malformed blob source (cannot find datasource) : "
100: + url);
101: }
102:
103: this .datasourceName = url.substring(start, end);
104:
105: // Table
106: start = end + 1;
107: end = url.indexOf('/', start);
108: if (end == -1) {
109: throw new MalformedURLException(
110: "Malformed blob source (cannot find table name) : "
111: + url);
112: }
113:
114: this .tableName = url.substring(start, end);
115:
116: // Column
117: start = end + 1;
118: end = url.indexOf('[', start);
119: if (end == -1) {
120: this .columnName = url.substring(start);
121: } else {
122: this .columnName = url.substring(start, end);
123:
124: // Condition
125: start = end + 1;
126: end = url.length() - 1;
127: if (url.charAt(end) != ']') {
128: throw new MalformedURLException(
129: "Malformed url for a blob source (cannot find condition) : "
130: + url);
131: } else {
132: this .condition = url.substring(start, end);
133: }
134: }
135: }
136:
137: /**
138: * Set the current <code>ServiceManager</code> instance used by this
139: * <code>Serviceable</code>.
140: */
141: public void service(ServiceManager manager) {
142: this .manager = manager;
143: }
144:
145: /**
146: * Return the protocol
147: */
148: public String getScheme() {
149: return URL_PREFIX;
150: }
151:
152: /**
153: * Return the unique identifer for this source
154: */
155: public String getURI() {
156: return this .systemId;
157: }
158:
159: /**
160: * Get the input stream for this source.
161: */
162: public InputStream getInputStream() throws IOException,
163: SourceException {
164: if (getLogger().isDebugEnabled()) {
165: getLogger()
166: .debug(
167: "Opening stream for datasource "
168: + this .datasourceName
169: + ", table "
170: + this .tableName
171: + ", column "
172: + this .columnName
173: + (this .condition == null ? ", no condition"
174: : ", condition "
175: + this .condition));
176: }
177:
178: Connection conn = null;
179: Statement stmt = null;
180: try {
181: conn = getConnection();
182: stmt = conn.createStatement();
183:
184: StringBuffer selectBuf = new StringBuffer("SELECT ")
185: .append(this .columnName).append(" FROM ").append(
186: this .tableName);
187:
188: if (this .condition != null) {
189: selectBuf.append(" WHERE ").append(this .condition);
190: }
191:
192: String select = selectBuf.toString();
193: if (getLogger().isDebugEnabled()) {
194: getLogger().debug("Executing statement " + select);
195: }
196:
197: ResultSet rs = stmt.executeQuery(select);
198: if (!rs.next()) {
199: throw new SourceNotFoundException("Source not found.");
200: }
201:
202: int colType = rs.getMetaData().getColumnType(1);
203: switch (colType) {
204: case Types.BLOB:
205: Blob blob = rs.getBlob(1);
206: if (blob != null) {
207: return new JDBCInputStream(blob.getBinaryStream(),
208: conn);
209: }
210: break;
211:
212: case Types.CLOB:
213: Clob clob = rs.getClob(1);
214: if (clob != null) {
215: return new JDBCInputStream(clob.getAsciiStream(),
216: conn);
217: }
218: break;
219:
220: default:
221: String value = rs.getString(1);
222: if (value != null) {
223: return new ByteArrayInputStream(value.getBytes());
224: }
225: }
226:
227: return new ByteArrayInputStream(new byte[0]);
228: } catch (SQLException e) {
229: String msg = "Cannot retrieve content from "
230: + this .systemId;
231: getLogger().error(msg, e);
232: // IOException would be more adequate, but SourceException is cascaded...
233: throw new SourceException(msg, e);
234: } finally {
235: try {
236: if (stmt != null) {
237: stmt.close();
238: }
239: } catch (SQLException e) { /* ignored */
240: }
241: try {
242: if (conn != null) {
243: conn.close();
244: }
245: } catch (SQLException e) { /* ignored */
246: }
247: }
248: }
249:
250: /**
251: * Get the Validity object. This can either wrap the last modification
252: * date or the expires information or...
253: * If it is currently not possible to calculate such an information
254: * <code>null</code> is returned.
255: */
256: public SourceValidity getValidity() {
257: return null;
258: }
259:
260: /**
261: * Refresh the content of this object after the underlying data
262: * content has changed.
263: */
264: public void refresh() {
265: }
266:
267: /**
268: *
269: * @see org.apache.excalibur.source.Source#exists()
270: */
271: public boolean exists() {
272: // FIXME
273: return true;
274: }
275:
276: /**
277: * The mime-type of the content described by this object.
278: * If the source is not able to determine the mime-type by itself
279: * this can be <code>null</code>.
280: */
281: public String getMimeType() {
282: return null;
283: }
284:
285: /**
286: * Return the content length of the content or -1 if the length is
287: * unknown
288: */
289: public long getContentLength() {
290: return -1;
291: }
292:
293: /**
294: * Get the last modification date.
295: * @return The last modification in milliseconds since January 1, 1970 GMT
296: * or 0 if it is unknown
297: */
298: public long getLastModified() {
299: return 0;
300: }
301:
302: /**
303: * Get the value of a parameter.
304: * Using this it is possible to get custom information provided by the
305: * source implementation, like an expires date, HTTP headers etc.
306: */
307: public String getParameter(String name) {
308: return null;
309: }
310:
311: /**
312: * Get the value of a parameter.
313: * Using this it is possible to get custom information provided by the
314: * source implementation, like an expires date, HTTP headers etc.
315: */
316: public long getParameterAsLong(String name) {
317: return 0;
318: }
319:
320: /**
321: * Get parameter names
322: * Using this it is possible to get custom information provided by the
323: * source implementation, like an expires date, HTTP headers etc.
324: */
325: public Iterator getParameterNames() {
326: return new EmptyIterator();
327: }
328:
329: static class EmptyIterator implements Iterator {
330: public boolean hasNext() {
331: return false;
332: }
333:
334: public Object next() {
335: return null;
336: }
337:
338: public void remove() {
339: }
340: }
341:
342: private Connection getConnection() throws SourceException {
343:
344: ServiceSelector selector = null;
345: DataSourceComponent datasource = null;
346:
347: try {
348: try {
349: selector = (ServiceSelector) this .manager
350: .lookup(DataSourceComponent.ROLE + "Selector");
351:
352: datasource = (DataSourceComponent) selector
353: .select(this .datasourceName);
354:
355: } catch (Exception e) {
356: String msg = "Cannot get datasource '"
357: + this .datasourceName + "'";
358: getLogger().error(msg);
359: throw new SourceException(msg, e);
360: }
361:
362: try {
363: return datasource.getConnection();
364: } catch (Exception e) {
365: String msg = "Cannot get connection for datasource '"
366: + this .datasourceName + "'";
367: getLogger().error(msg);
368: throw new SourceException(msg, e);
369: }
370:
371: } finally {
372: if (datasource != null) {
373: selector.release(datasource);
374: }
375: }
376: }
377:
378: /**
379: * An OutputStream that will close the connection that created it on
380: * close.
381: */
382: private class JDBCInputStream extends FilterInputStream {
383:
384: private Connection cnx;
385:
386: private void closeCnx() throws IOException {
387: if (this .cnx != null) {
388: try {
389: cnx.close();
390: } catch (Exception e) {
391: String msg = "Error closing the connection for "
392: + BlobSource.this .getURI();
393: BlobSource.this .getLogger().warn(msg, e);
394: throw new CascadingIOException(msg + " : "
395: + e.getMessage(), e);
396: } finally {
397: cnx = null;
398: }
399: }
400: }
401:
402: public JDBCInputStream(InputStream stream, Connection cnx) {
403: super (stream);
404: this .cnx = cnx;
405: }
406:
407: public int read() throws IOException {
408: try {
409: int result = in.read();
410: if (result == -1) {
411: closeCnx();
412: }
413: return result;
414: } catch (IOException e) {
415: closeCnx();
416: throw e;
417: }
418: }
419:
420: public int read(byte[] b) throws IOException {
421: try {
422: int result = in.read(b);
423: if (result == -1) {
424: closeCnx();
425: }
426: return result;
427: } catch (IOException e) {
428: closeCnx();
429: throw e;
430: }
431: }
432:
433: public int read(byte[] b, int off, int len) throws IOException {
434: try {
435: int result = in.read(b, off, len);
436: if (result == -1) {
437: closeCnx();
438: }
439: return result;
440: } catch (IOException e) {
441: closeCnx();
442: throw e;
443: }
444: }
445:
446: public void close() throws IOException {
447: super.close();
448: closeCnx();
449: }
450: }
451: }
|