package no.thc.cayenne.workbench; import java.io.InputStream; import java.io.OutputStream; import java.sql.Blob; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.Fault; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.ObjectId; import org.apache.cayenne.Persistent; import org.apache.cayenne.access.DataDomain; import org.apache.cayenne.access.DataNode; import org.apache.cayenne.access.QueryLogger; import org.apache.cayenne.conf.Configuration; import org.apache.cayenne.dba.DbAdapter; import org.apache.cayenne.map.DbAttribute; import org.apache.cayenne.map.DbEntity; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.map.ObjAttribute; import org.apache.cayenne.map.ObjEntity; import org.apache.cayenne.util.MemoryBlob; /** * A read only java.sql.Blob implementation that also is a Fault. The Blob are * not fetched before it is needed. *
* The byte array constructor must be used to insert/update/write. All * set-methods will throw an unchecked exception. *
* The underlying Blob is unrefed after sent to the jdbc driver to keep the * memory usage low. An attempt to read from the Blob should then lead to a new * query. * * @since 3.0 * @author halset */ public class BlobFault extends Fault implements Blob { private ObjectContext ctxt; private ObjectId oid; private String name; private Blob blob; /** * For reading. The Fault-handling will fetch the Blob when it is needed. */ public BlobFault() { } /** * Construct a BlobFault with the given underlying Blob * * @param blob */ BlobFault(Blob blob) { this.blob = blob; } /** * For writing. Will construct a MemoryBlob that will be cleared out on * write to the database. This makes it possible to insert more data into * the database than do fit inside the vm heap memory. * * @param dataForWriting * a byte array with the data */ public BlobFault(byte[] dataForWriting) { this(new MemoryBlob(dataForWriting)); } public Object resolveFault(Persistent sourceObject, String relationshipName) { ctxt = sourceObject.getObjectContext(); oid = sourceObject.getObjectId(); name = relationshipName; return this; } /** * Return the underlaying Blob if any. Will not try to fetch the blob. The * returned Blob can be null or in a disconnected state. * * @return the underlying Blob or null */ Blob getUnderlyingBlob() { return blob; } /** * Clear the underlying Blob. Next attempt to access the Blob should lead to * a new fetch from the database. */ void clear() { blob = null; } /** * Return the underlying Blob. Will try to fetch the Blob if the underlying * Blob is null. The returned Blob can be in a disconnected state. * * @return * @throws SQLException */ private Blob getBlob() throws SQLException { // TODO: know when to refetch the Blob. Perhaps try .length()? if (blob != null) { return blob; } // check for needed values if ((ctxt == null) || (oid == null) || (name == null)) { throw new CayenneRuntimeException("Fault not properly injected"); } // handle temp oid if (oid.isTemporary()) { throw new CayenneRuntimeException( "Can not resolv blob fault for temp oid " + oid.toString()); } /* * Both SelectQuery and SQLTemplate can be used to query, but the * resulting blob will also be returned as a BlobFault. */ EntityResolver entityResolver = ctxt.getEntityResolver(); ObjEntity objEntity = entityResolver.getObjEntity(oid.getEntityName()); DbEntity dbEntity = objEntity.getDbEntity(); StringBuffer query = new StringBuffer(); query.append("SELECT "); ObjAttribute oat = (ObjAttribute) objEntity.getAttribute(name); query.append(oat.getDbAttributeName()); query.append(" FROM "); query.append(dbEntity.getFullyQualifiedName()); query.append(" WHERE "); List attributes = new ArrayList(); List values = new ArrayList(); for (Iterator it = oid.getIdSnapshot().entrySet().iterator(); it .hasNext();) { Map.Entry entry = (Map.Entry) it.next(); String attrName = (String) entry.getKey(); DbAttribute attr = (DbAttribute) dbEntity.getAttribute(attrName); attributes.add(attr); values.add(entry.getValue()); query.append(attr.getName()); query.append(" = ?"); if (it.hasNext()) { query.append(" AND "); } } // ask the DataNode for a Connection DataDomain domain = Configuration.getSharedConfiguration().getDomain(); // TODO: use the correct DataNode final DataNode node = (DataNode) domain.getDataNodes().iterator() .next(); final DbAdapter adapter = node.getAdapter(); if (QueryLogger.isLoggable()) { QueryLogger.logQuery(query.toString(), values); } Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = node.getDataSource().getConnection(); // at least PostgreSQL need auto commit off boolean autoCommit = conn.getAutoCommit(); QueryLogger.log("autoCommit before blob fetch: " + Boolean.toString(autoCommit)); conn.setAutoCommit(false); st = conn.prepareStatement(query.toString()); // set the prepared statement parameters for (int i = 0; i < values.size(); i++) { Object val = values.get(i); DbAttribute attr = (DbAttribute) attributes.get(i); adapter.bindParameter(st, val, i + 1, attr.getType(), attr .getScale()); } rs = st.executeQuery(); if (rs.next()) { // remember the Blob, but it will not work forever blob = rs.getBlob(1); } else { throw new CayenneRuntimeException( "query did not return any rows for oid " + oid.toString()); } // set back to old autoCommit value // TODO: should probably set this one back to true, but it destroys // the Blob - at least for PostgreSQL // conn.setAutoCommit(autoCommit); return blob; } catch (Exception e) { throw new CayenneRuntimeException(e); } finally { if (rs != null) { rs.close(); } if (st != null) { st.close(); } if (conn != null) { conn.close(); } } } public long length() throws SQLException { try { return getBlob().length(); } catch (SQLException e) { // probably a disconnected state - try to refetch clear(); return getBlob().length(); } } public byte[] getBytes(long pos, int length) throws SQLException { try { return getBlob().getBytes(pos, length); } catch (SQLException e) { // probably a disconnected state - try to refetch clear(); return getBlob().getBytes(pos, length); } } public InputStream getBinaryStream() throws SQLException { try { return getBlob().getBinaryStream(); } catch (SQLException e) { // probably a disconnected state - try to refetch clear(); return getBlob().getBinaryStream(); } } public long position(byte[] pattern, long start) throws SQLException { throw new UnsupportedOperationException("Read Only Blob"); } public long position(Blob pattern, long start) throws SQLException { throw new UnsupportedOperationException("Read Only Blob"); } public OutputStream setBinaryStream(long pos) throws SQLException { throw new UnsupportedOperationException("Read Only Blob"); } public int setBytes(long pos, byte[] bytes) throws SQLException { throw new UnsupportedOperationException("Read Only Blob"); } public int setBytes(long pos, byte[] bytes, int offset, int len) throws SQLException { throw new UnsupportedOperationException("Read Only Blob"); } public void truncate(long len) throws SQLException { throw new UnsupportedOperationException("Read Only Blob"); } }