| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.cocoon.components.source.impl; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.FilterInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.MalformedURLException; |
| import java.sql.Blob; |
| import java.sql.Clob; |
| import java.sql.Connection; |
| import java.sql.ResultSet; |
| import java.sql.SQLException; |
| import java.sql.Statement; |
| import java.sql.Types; |
| import java.util.Iterator; |
| |
| import org.apache.avalon.excalibur.datasource.DataSourceComponent; |
| import org.apache.avalon.framework.logger.AbstractLogEnabled; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.avalon.framework.service.ServiceSelector; |
| import org.apache.avalon.framework.service.Serviceable; |
| import org.apache.excalibur.source.Source; |
| import org.apache.excalibur.source.SourceException; |
| import org.apache.excalibur.source.SourceNotFoundException; |
| import org.apache.excalibur.source.SourceValidity; |
| |
| import org.apache.cocoon.CascadingIOException; |
| |
| /** |
| * A <code>Source</code> that takes its content in a single JDBC column. Any |
| * kind of column can be used (clob, blob, varchar, etc), but "Blob" means |
| * that the whole content is contained in a single column. |
| * <p>The URL syntax is "blob:/datasource/table/column[cond]", where : |
| * <ul> |
| * <li>"datasource" is the jdbc datasource to use (defined in cocoon.xonf) |
| * <li>"table" is the database table, |
| * <li>"column" is (you can guess, now :) the column in "table", |
| * <li>"cond" is the boolean condition used to select a particular record in |
| * the table. |
| * </ul> |
| * <p>For example, "<code>blob:/personel/people/photo[userid='foo']</code>" |
| * will fetch the first column returned by the statement "<code>SELECT photo |
| * from people where userid='foo'</code>" in the datasource "<code>personel</code>" |
| * |
| * @author <a href="mailto:sylvain@apache.org">Sylvain Wallez</a> |
| * @author <a href="mailto:stephan@apache.org">Stephan Michels</a> |
| * @version CVS $Id$ |
| */ |
| public class BlobSource extends AbstractLogEnabled |
| implements Source, Serviceable { |
| |
| private static final String URL_PREFIX = "blob:/"; |
| private static final int URL_PREFIX_LEN = URL_PREFIX.length(); |
| |
| /** The ServiceManager instance */ |
| private ServiceManager manager; |
| |
| /** The system ID for this source */ |
| private String systemId; |
| |
| private String datasourceName; |
| |
| private String tableName; |
| |
| private String columnName; |
| |
| private String condition; |
| |
| /** |
| * Create a file source from a 'blob:' url and a component manager. |
| * <p>The url is of the form "blob:/datasource/table/column[condition] |
| */ |
| public BlobSource(String url) throws MalformedURLException { |
| this.systemId = url; |
| |
| // Parse the url |
| int start = URL_PREFIX_LEN; |
| |
| // Datasource |
| int end = url.indexOf('/', start); |
| if (end == -1) { |
| throw new MalformedURLException("Malformed blob source (cannot find datasource) : " + url); |
| } |
| |
| this.datasourceName = url.substring(start, end); |
| |
| // Table |
| start = end + 1; |
| end = url.indexOf('/', start); |
| if (end == -1) { |
| throw new MalformedURLException("Malformed blob source (cannot find table name) : " + url); |
| } |
| |
| this.tableName = url.substring(start, end); |
| |
| // Column |
| start = end + 1; |
| end = url.indexOf('[', start); |
| if (end == -1) { |
| this.columnName = url.substring(start); |
| } else { |
| this.columnName = url.substring(start, end); |
| |
| // Condition |
| start = end + 1; |
| end = url.length() - 1; |
| if (url.charAt(end) != ']') { |
| throw new MalformedURLException("Malformed url for a blob source (cannot find condition) : " + url); |
| } else { |
| this.condition = url.substring(start, end); |
| } |
| } |
| } |
| |
| /** |
| * Set the current <code>ServiceManager</code> instance used by this |
| * <code>Serviceable</code>. |
| */ |
| public void service(ServiceManager manager) { |
| this.manager = manager; |
| } |
| |
| /** |
| * Return the protocol |
| */ |
| public String getScheme() { |
| return URL_PREFIX; |
| } |
| |
| /** |
| * Return the unique identifer for this source |
| */ |
| public String getURI() { |
| return this.systemId; |
| } |
| |
| /** |
| * Get the input stream for this source. |
| */ |
| public InputStream getInputStream() throws IOException, SourceException { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Opening stream for datasource " + this.datasourceName + |
| ", table " + this.tableName + ", column " + this.columnName + |
| (this.condition == null ? ", no condition" : ", condition " + this.condition) |
| ); |
| } |
| |
| Connection conn = null; |
| Statement stmt = null; |
| try { |
| conn = getConnection(); |
| stmt = conn.createStatement(); |
| |
| StringBuffer selectBuf = new StringBuffer("SELECT ").append(this.columnName). |
| append(" FROM ").append(this.tableName); |
| |
| if (this.condition != null) { |
| selectBuf.append(" WHERE ").append(this.condition); |
| } |
| |
| String select = selectBuf.toString(); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Executing statement " + select); |
| } |
| |
| ResultSet rs = stmt.executeQuery(select); |
| if (!rs.next()) { |
| throw new SourceNotFoundException("Source not found."); |
| } |
| |
| int colType = rs.getMetaData().getColumnType(1); |
| switch(colType) { |
| case Types.BLOB : |
| Blob blob = rs.getBlob(1); |
| if (blob != null) { |
| return new JDBCInputStream(blob.getBinaryStream(), conn); |
| } |
| break; |
| |
| case Types.CLOB : |
| Clob clob = rs.getClob(1); |
| if (clob != null) { |
| return new JDBCInputStream(clob.getAsciiStream(), conn); |
| } |
| break; |
| |
| default : |
| String value = rs.getString(1); |
| if (value != null) { |
| return new ByteArrayInputStream(value.getBytes()); |
| } |
| } |
| |
| return new ByteArrayInputStream(new byte[0]); |
| } catch (SQLException e) { |
| String msg = "Cannot retrieve content from " + this.systemId; |
| getLogger().error(msg, e); |
| // IOException would be more adequate, but SourceException is cascaded... |
| throw new SourceException(msg, e); |
| } finally { |
| try { |
| if (stmt != null) { |
| stmt.close(); |
| } |
| } catch (SQLException e) { /* ignored */ } |
| try { |
| if (conn != null) { |
| conn.close(); |
| } |
| } catch (SQLException e) { /* ignored */ } |
| } |
| } |
| |
| /** |
| * Get the Validity object. This can either wrap the last modification |
| * date or the expires information or... |
| * If it is currently not possible to calculate such an information |
| * <code>null</code> is returned. |
| */ |
| public SourceValidity getValidity() { |
| return null; |
| } |
| |
| /** |
| * Refresh the content of this object after the underlying data |
| * content has changed. |
| */ |
| public void refresh() { |
| } |
| |
| /** |
| * |
| * @see org.apache.excalibur.source.Source#exists() |
| */ |
| public boolean exists() { |
| // FIXME |
| return true; |
| } |
| |
| /** |
| * The mime-type of the content described by this object. |
| * If the source is not able to determine the mime-type by itself |
| * this can be <code>null</code>. |
| */ |
| public String getMimeType() { |
| return null; |
| } |
| |
| /** |
| * Return the content length of the content or -1 if the length is |
| * unknown |
| */ |
| public long getContentLength() { |
| return -1; |
| } |
| |
| /** |
| * Get the last modification date. |
| * @return The last modification in milliseconds since January 1, 1970 GMT |
| * or 0 if it is unknown |
| */ |
| public long getLastModified() { |
| return 0; |
| } |
| |
| /** |
| * Get the value of a parameter. |
| * Using this it is possible to get custom information provided by the |
| * source implementation, like an expires date, HTTP headers etc. |
| */ |
| public String getParameter(String name) { |
| return null; |
| } |
| |
| /** |
| * Get the value of a parameter. |
| * Using this it is possible to get custom information provided by the |
| * source implementation, like an expires date, HTTP headers etc. |
| */ |
| public long getParameterAsLong(String name) { |
| return 0; |
| } |
| |
| /** |
| * Get parameter names |
| * Using this it is possible to get custom information provided by the |
| * source implementation, like an expires date, HTTP headers etc. |
| */ |
| public Iterator getParameterNames() { |
| return new EmptyIterator(); |
| } |
| |
| static class EmptyIterator implements Iterator { |
| public boolean hasNext() { return false; } |
| public Object next() { return null; } |
| public void remove() {} |
| } |
| |
| private Connection getConnection() throws SourceException { |
| |
| ServiceSelector selector = null; |
| DataSourceComponent datasource = null; |
| |
| try { |
| try { |
| selector = (ServiceSelector)this.manager.lookup(DataSourceComponent.ROLE + "Selector"); |
| |
| datasource = (DataSourceComponent)selector.select(this.datasourceName); |
| |
| } catch (Exception e) { |
| String msg = "Cannot get datasource '" + this.datasourceName + "'"; |
| getLogger().error(msg); |
| throw new SourceException(msg, e); |
| } |
| |
| try { |
| return datasource.getConnection(); |
| } catch (Exception e) { |
| String msg = "Cannot get connection for datasource '" + this .datasourceName + "'"; |
| getLogger().error(msg); |
| throw new SourceException(msg, e); |
| } |
| |
| } finally { |
| if (datasource != null) { |
| selector.release(datasource); |
| } |
| } |
| } |
| |
| /** |
| * An OutputStream that will close the connection that created it on |
| * close. |
| */ |
| private class JDBCInputStream extends FilterInputStream { |
| |
| private Connection cnx; |
| |
| private void closeCnx() throws IOException { |
| if (this.cnx != null) { |
| try { |
| cnx.close(); |
| } catch (Exception e) { |
| String msg = "Error closing the connection for " + BlobSource.this.getURI(); |
| BlobSource.this.getLogger().warn(msg, e); |
| throw new CascadingIOException(msg + " : " + e.getMessage(), e); |
| } finally { |
| cnx = null; |
| } |
| } |
| } |
| |
| public JDBCInputStream(InputStream stream, Connection cnx) { |
| super(stream); |
| this.cnx = cnx; |
| } |
| |
| public int read() throws IOException { |
| try { |
| int result = in.read(); |
| if (result == -1) { |
| closeCnx(); |
| } |
| return result; |
| } catch(IOException e) { |
| closeCnx(); |
| throw e; |
| } |
| } |
| |
| public int read(byte[] b) throws IOException { |
| try { |
| int result = in.read(b); |
| if (result == -1) { |
| closeCnx(); |
| } |
| return result; |
| } catch(IOException e) { |
| closeCnx(); |
| throw e; |
| } |
| } |
| |
| public int read(byte[] b, int off, int len) throws IOException { |
| try { |
| int result = in.read(b, off, len); |
| if (result == -1) { |
| closeCnx(); |
| } |
| return result; |
| } catch(IOException e) { |
| closeCnx(); |
| throw e; |
| } |
| } |
| |
| public void close() throws IOException { |
| super.close(); |
| closeCnx(); |
| } |
| } |
| } |