package org.crosswire.data; import java.sql.Clob; import java.sql.Blob; import java.sql.Timestamp; import java.sql.PreparedStatement; import java.sql.Statement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.Connection; import java.sql.SQLException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Vector; import java.util.List; import java.util.Set; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.text.SimpleDateFormat; import java.io.Serializable; import java.io.InputStream; import java.io.ByteArrayOutputStream; import java.math.BigDecimal; import java.math.BigInteger; import org.crosswire.utils.HTTPUtils; import java.lang.reflect.ParameterizedType; import org.apache.log4j.Logger; public class DataObject implements Cloneable, Serializable { public static int DETAIL_HEADERONLY = 1; public static int DETAIL_BRIEF = 2; public static int DETAIL_COMPLETE = 3; public static int DETAIL_EXTRA = 4; public static int parseDetail(String detail, int defaultDetail) { int detailLevel = defaultDetail; if ("header".equals(detail)) detailLevel = org.crosswire.data.DataObject.DETAIL_HEADERONLY; else if ("brief".equals(detail)) detailLevel = org.crosswire.data.DataObject.DETAIL_BRIEF; else if ("complete".equals(detail)) detailLevel = org.crosswire.data.DataObject.DETAIL_COMPLETE; else if ("extra".equals(detail)) detailLevel = org.crosswire.data.DataObject.DETAIL_EXTRA; // for backward compat else if ("headeronly".equals(detail)) detailLevel = org.crosswire.data.DataObject.DETAIL_HEADERONLY; return detailLevel; } // mask of all DETAIL bits public static int DETAIL_MASK = 7; public static int FORMAT_FULL = 8; public static int FORMAT_MOBILE = 16; // mask of all FORMAT bits public static int FORMAT_MASK = 24; private boolean includeNulls = false; private Map values = new HashMap(); public Map getData() { return values; } private static boolean DEBUG = true; private static Logger logger = Logger.getLogger(DataObject.class); /* Nice idea someday, but not today * public class DataSet extends Vector { String sql = null; PreparedStatement preparedSQL = null; List params = null; int limit = 2145999999; int lastPage; String keys[] = null; DataSet(String sql, String keys[]) { this(sql, keys, -1); } DataSet(String sql, String keys[], int limit) { super(); this.sql = sql; this.keys = keys; if (limit != -1) this.limit = limit; } DataSet(PreparedStatement preparedSQL, List params, String keys[]) { this(preparedSQL, params, keys, -1); } DataSet(PreparedStatement preparedSQL, List params, String keys[], int limit) { super(); this.preparedSQL = preparedSQL; this.params = params; this.keys = keys; if (limit != -1) this.limit = limit; } @Override public synchronized Object get(int index) { int requestedPage = index / limit; if (requestedPage != lastPage) selectPage(requestedPage); return super.get(index % limit); } private void selectPage(int requestedPage) { } } */ public DataObject() { } public DataObject(DataObject o) { values = o.values; } public void copyFrom(DataObject o) { try { values = new HashMap<>(o.values); } catch (Exception e) { logger.error("Error copying from another object", e); } } public Object clone() { DataObject o = null; try { o = (DataObject)super.clone(); o.values = new HashMap<>(this.values); } catch (Exception e) { logger.error("Error cloning ourself", e); } return o; } // can be overriden to provide table name public String getTableName() { return null; } // can be overriden to provide key column names public List getKeyFields() { return new ArrayList(values.keySet()); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof DataObject)) return super.equals(o); for (String k : getKeyFields()) { if (getValue(k) == null) { if (((DataObject)o).getValue(k) != null) return false; } else if (!getValue(k).equals(((DataObject)o).getValue(k))) return false; } return true; } @Override public int hashCode() { int hashCode = 0; for (String k : getKeyFields()) { Object v = getValue(k); hashCode = (int)(((hashCode / 2) + (v != null ? v.hashCode() / 4 : "NULL".hashCode() / 4)) * 1.25); } return hashCode; } public static void setDebug(boolean val) { DEBUG = val; } public void setIncludeNulls(boolean val) { includeNulls = val; } public boolean isIncludeNulls() { return includeNulls; } public float getFloatValue(String key) { return ((Float) getValue(key)).floatValue(); } public int getIntValue(String key) { int retVal = 0; Object o = getValue(key); if (o instanceof Integer) { retVal = ((Integer)o).intValue(); } else if (o instanceof Long) { retVal = ((Long)o).intValue(); } else if (o instanceof Double) { retVal = ((Double)o).intValue(); } else if (o instanceof BigInteger) { retVal = ((BigInteger)o).intValue(); } else if (o instanceof BigDecimal) { retVal = ((BigDecimal)o).intValue(); } else if (o instanceof String) { retVal = Integer.valueOf((String)o); } return retVal; } public Long getLongValue(String key) { Long retVal = null; Object o = getValue(key); if (o instanceof Integer) { retVal = ((Integer)o).longValue(); } else if (o instanceof Long) { retVal = ((Long)o).longValue(); } else if (o instanceof Double) { retVal = ((Double)o).longValue(); } else if (o instanceof BigInteger) { retVal = ((BigInteger)o).longValue(); } else if (o instanceof BigDecimal) { retVal = ((BigDecimal)o).longValue(); } else if (o instanceof String) { retVal = Long.valueOf((String)o); } return retVal; } public Double getDoubleValue(String key) { Double retVal = null; Object o = getValue(key); if (o instanceof Integer) { retVal = ((Integer)o).doubleValue(); } else if (o instanceof Long) { retVal = ((Long)o).doubleValue(); } else if (o instanceof Double) { retVal = ((Double)o).doubleValue(); } else if (o instanceof BigInteger) { retVal = ((BigInteger)o).doubleValue(); } else if (o instanceof BigDecimal) { retVal = ((BigDecimal)o).doubleValue(); } else if (o instanceof String) { retVal = Double.valueOf((String)o); } return retVal; } public BigDecimal getBigDecimalValue(String key) { BigDecimal retVal = null; Object o = getValue(key); if (o instanceof Integer) { retVal = BigDecimal.valueOf((Integer)o); } else if (o instanceof Long) { retVal = BigDecimal.valueOf((Long)o); } else if (o instanceof Double) { retVal = BigDecimal.valueOf((Double)o); } else if (o instanceof BigInteger) { retVal = BigDecimal.valueOf(((BigInteger)o).longValue()); } else if (o instanceof BigDecimal) { retVal = ((BigDecimal)o); } else if (o instanceof String) { retVal = BigDecimal.valueOf(Double.valueOf((String)o)); } return retVal; } public boolean getBoolValue(String key) { boolean retVal = false; Object o = getValue(key); if (o instanceof Boolean) { retVal = ((Boolean)o).booleanValue(); } else if (o instanceof Integer) { retVal = ((Integer)o).intValue() != 0; } else if (o instanceof Long) { retVal = ((Long)o).intValue() != 0; } else if (o instanceof String) { retVal = "true".equals((String)o); } return retVal; } public String getStringValue(String key) { Object retVal = getValue(key); if ((retVal == null) || ("null".equals(retVal))) { retVal = ""; } if (retVal instanceof Clob) { Clob c = (Clob)retVal; try { retVal = c.getSubString(1L, (int) c.length()); } catch (SQLException e) { e.printStackTrace(); } } else if (retVal instanceof java.sql.Date) { retVal = df.format(new java.util.Date(((java.sql.Date) retVal).getTime())); } else if (retVal instanceof java.sql.Timestamp) { retVal = df.format(new java.util.Date(((java.sql.Timestamp) retVal).getTime())); } return retVal.toString(); } public Timestamp getTSValue(String key) { Timestamp retVal = (Timestamp) getValue(key); return retVal; } private static SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); public static void setDateFormat(String format) { df.applyPattern(format); } public java.util.Date getDateValue(String key) { java.util.Date retVal = null; SimpleDateFormat formatter = new SimpleDateFormat(); Object o = getValue(key); if (o instanceof String) { try { retVal = formatter.parse(o.toString()); } catch (Exception e) { logger.error("Error getting DATE value for field ["+key+"]", e); } } else if (o instanceof java.sql.Date) { retVal = new java.util.Date(((java.sql.Date) o).getTime()); } else if (o instanceof java.sql.Timestamp) { retVal = new java.util.Date(((java.sql.Timestamp) o).getTime()); } return retVal; } public byte[] getBlob(String key) { byte[] retVal = null; Object o = getValue(key); if (o instanceof byte[]) { retVal = (byte[])o; } return retVal; } public void setIntValue(String key, int val) { setValue(key, new Integer(val)); } public void setBlob(String key, byte[] val) { setValue(key, val); } public void setBlob(String key, InputStream val) { setValue(key, getBytes(val)); } public void setBoolValue(String key, boolean val) { setValue(key, new Boolean(val)); } public void setFloatValue(String key, float val) { setValue(key, new Float(val)); } public void setDateValue(String key, java.util.Date d) { if (d != null) { setValue(key, new java.sql.Date(d.getTime())); } else setValue(key, null); } public void setTSValue(String key, java.util.Date d) { if (d != null) { // round to the second because some databases don't keep such precision setValue(key, new java.sql.Timestamp(((long)d.getTime()/1000)*1000)); } else setValue(key, null); } public void setValue(String key, Object val) { if (val != null) { values.put(key, val); } else values.remove(key); } public void setValue(String key, String val, int maxLen) { if ((val != null) && (val.length() > maxLen)) val = val.substring(0, maxLen); values.put(key, val); } public void addChild(E child) { } public void fillFromRow(ResultSet rs) throws Exception { ResultSetMetaData meta = rs.getMetaData(); int colCount = meta.getColumnCount(); String [] columnNames = new String[colCount]; for (int i = 0; i < colCount; i++) { columnNames[i] = meta.getColumnName(i + 1); } fillFromRow(rs, columnNames); } public void fillFromRow(ResultSet rs, String columnNames[]) throws Exception { for (int i = 0; i < columnNames.length; i++) { Object o = rs.getObject(i + 1); if (o != null || includeNulls) { if (o == null) { values.put(columnNames[i], null); } else if (o instanceof Blob) { values.put(columnNames[i], ((Blob)o).getBytes(1, (int)((Blob)o).length())); ((Blob)o).free(); } else { values.put(columnNames[i], o); } //logger.debug("Filling Column: " + columnNames[i] + " with: " +o); } else { // values.remove(columnNames[i]); } } } public List getDataSet(String sql) { return getDataSet(sql, null); } public List getDataSet(String sql, String keys[]) { return getDataSet(sql, keys, null); } public List getDataSet(String sql, String keys[], Vector existingRows) { Vector retVal = new Vector(); getDataSet(sql, keys, existingRows, retVal); return retVal; } public List getDataSet(String sql, String keys[], List existingRows) { return getDataSet(sql, keys, existingRows, null); } public List getDataSet(String sql, String keys[], List existingRows, List retVal) { if (retVal == null) retVal = new ArrayList(); long timer = System.currentTimeMillis(); logger.debug("DataObject.getDataSet start (0)"); Connection c = null; Statement stmt = null; ResultSet rs = null; SQLException resultException = null; try { c = getDBConnection(); logger.debug("DataObject.getDataSet gotConnection ("+(System.currentTimeMillis() - timer) + "ms)"); timer = System.currentTimeMillis(); if (c == null) { logger.fatal("Couldn't get DB connection"); return retVal; } try { List params = new ArrayList(); sql = processMacros(sql, params); if (params.size() > 0) { PreparedStatement s = prepareSQL(c, sql, params); // retVal = new DataSet(s, params, keys); logger.debug("DataObject.getDataSet prepared statement ("+(System.currentTimeMillis() - timer) + "ms)"); timer = System.currentTimeMillis(); rs = s.executeQuery(); stmt = s; } else { logger.debug("Executing unprepared SQL: " + sql); // retVal = new DataSet(sql, keys); Statement s = stmt = c.createStatement(); rs = s.executeQuery(sql); } logger.debug("DataObject.getDataSet finished execution ("+(System.currentTimeMillis() - timer) + "ms)"); timer = System.currentTimeMillis(); Map pks = new HashMap<>(); // if we've been given a dataset to include... if (existingRows != null) { for (Object o : existingRows) { E dObj = (E)o; if (keys != null) { String keyValue = ""; for (String key : keys) { keyValue += (dObj.getValue(key) != null ? dObj.getValue(key).toString() : "[null]") + "+"; } pks.put(keyValue, dObj); } retVal.add(dObj); } } ResultSetMetaData meta = rs.getMetaData(); int colCount = meta.getColumnCount(); String [] columnNames = new String[colCount]; for (int i = 0; i < colCount; i++) { columnNames[i] = meta.getColumnName(i + 1); } // put our new data object in the return vector while (rs.next()) { E master = null; E dataObj = null; // if (!"java.lang.Object".equals(this.getClass().getGenericSuperclass().getTypeName())) { if (!"class java.lang.Object".equals(this.getClass().getGenericSuperclass().toString())) { try { if (((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0] instanceof Class) { dataObj = (E) ((Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]).newInstance(); } else dataObj = (E) this.getClass().newInstance(); } catch (Exception e) { e.printStackTrace(); } } if (dataObj == null) dataObj = (E)new DataObject(); dataObj.fillFromRow(rs, columnNames); if (keys != null) { String keyValue = ""; for (String key : keys) { keyValue += (dataObj.getValue(key) != null ? dataObj.getValue(key).toString() : "[null]") + "+"; } master = pks.get(keyValue); if (master == null) { pks.put(keyValue, dataObj); dataObj.addChild(dataObj); // we add ourself as the master and also the first child because these rows are composite objects } else master.addChild(dataObj); } if (master == null) retVal.add(dataObj); } logger.debug("DataObject.getDataSet finished creating DataObjects ("+(System.currentTimeMillis() - timer) + "ms)"); timer = System.currentTimeMillis(); } catch (Exception e1) { logger.error("Error preparing SQL Statement", e1); } } catch (Exception e) { logger.error("Error accessing database", e); throw new RuntimeException(e.getCause()); } finally { try { if (stmt != null) { stmt.close(); } if (c != null) { c.close(); } } catch (Exception e) { logger.error("Error closing connections to database", e); } } logger.debug("rs.size() = " + retVal.size()); logger.debug("DataObject.getDataSet end ("+(System.currentTimeMillis() - timer) + "ms)"); timer = System.currentTimeMillis(); return retVal; } public Object getValue(String colName) { return values.get(colName); } public int executeSQL(String testSQL) { return executeSQL(testSQL, null); } public int executeSQL(String testSQL, E altVals) { return executeSQL(testSQL, altVals, null); } public int executeSQL(String testSQL, E altVals, String autoColName) { Connection c = null; PreparedStatement stmt = null; SQLException resultException = null; int r = -1; try { c = getDBConnection(); if (c == null) { logger.fatal("couldn't get DB connection"); return -1; } try { List params = new ArrayList(); testSQL = processMacros(testSQL, params, altVals); stmt = prepareSQL(c, testSQL, params, true); r = stmt.executeUpdate(); logger.debug("Update Executed. Retrieving Generated Keys."); if (autoColName != null) { try { ResultSet rs = stmt.getGeneratedKeys(); if (rs.next()) { setValue(autoColName, rs.getString(1)); } } catch (Exception e) { logger.error("Error retrieving generated keys", e); } } } catch (Exception e1) { logger.error("Error sending SQL to database", e1); } } catch (Exception e) { logger.error("Error accessing Database", e); } finally { try { if (stmt != null) { stmt.close(); } if (c != null) { c.close(); } } catch (Exception e) { logger.error("Error closing connections to database", e); } } return r; } public static interface DBConnector { public Connection getDBConnection(String poolName); } private static class CTXConnector implements DBConnector { public Connection getDBConnection(String poolName) { javax.sql.DataSource ds = null; Connection connRSFind = null; try { javax.naming.Context initContext = new javax.naming.InitialContext(); if (initContext == null) { throw new Exception("Boom - No Context"); } javax.naming.Context envContext = (javax.naming.Context)initContext.lookup("java:/comp/env"); if (envContext == null) { throw new Exception("Boom - No Context"); } ds = (javax.sql.DataSource) envContext.lookup(poolName); try { connRSFind = ds.getConnection(); // connRSFind = ((javax.sql.PooledConnection)connRSFind).getConnection(); } catch (java.sql.SQLException e) { logger.fatal("Failed to obtain new data source connection", e); throw new RuntimeException(e.getCause()); } } catch (Exception e) { logger.fatal("Could Not get data source", e); throw new RuntimeException(e.getCause()); } return connRSFind; } }; private static DBConnector dbConnector = new CTXConnector(); // There are two mechanism to configure database connection. // If you are using 1 connection and running in a webapp, then // the default setup will work for you and will look for a "storeDB" jndi // entry defining your storage database JDBC connection in you META-INF/context.xml // // If you're not configuring your JDBC connection with JNDI, then... you can extend // DBConnector and supply it to the system using this static method: setDBConnector // // If you are not using a single database connection, see getDBConnection method // public static void setDBConnector(DBConnector c) { dbConnector = (c != null) ? c : new CTXConnector(); } public static Connection getDefaultDBConnection() { return getDefaultDBConnection("storeDB"); } public static Connection getDefaultDBConnection(String poolName) { return dbConnector.getDBConnection(poolName); } // If you need to support multiple DB connections for your application, then // you can extend DataObject, once for each database connection, and override // this method: getDBConnection to supply the correct connection. Then // for each of your actual DataObjects, instead of extending DataObject directly, // extend your database-specific DataObject extension public Connection getDBConnection() { return DataObject.getDefaultDBConnection("storeDB"); } private PreparedStatement prepareSQL(Connection c, String inString, List params) throws Exception { return prepareSQL(c, inString, params, false); } private PreparedStatement prepareSQL(Connection c, String inString, List params, boolean update) throws Exception { logger.debug("Preparing SQL: " + inString); PreparedStatement stmt = (update) ? c.prepareStatement(inString, PreparedStatement.RETURN_GENERATED_KEYS) : c.prepareStatement(inString); for (int j = 0; j < params.size(); j++) { Object val = params.get(j); try { if (val instanceof String) { stmt.setString(j + 1, (String) val); } } catch (java.sql.DataTruncation dt) { logger.warn("Data truncated!", dt); } /* if (val == null) { stmt.setNull(j + 1); } else */ if (val instanceof Integer) { stmt.setInt(j + 1, ((Integer) val).intValue()); } else if (val instanceof Boolean) { stmt.setBoolean(j + 1, ((Boolean) val).booleanValue()); } else if (val instanceof java.sql.Timestamp) { stmt.setTimestamp(j + 1, ((java.sql.Timestamp) val)); } else if (val instanceof java.sql.Date) { stmt.setDate(j + 1, ((java.sql.Date) val)); } else if (val instanceof byte[]) { stmt.setBytes(j + 1, (byte[]) val); } else { stmt.setObject(j + 1, val); } logger.debug("Param " + j + ": " + val); } return stmt; } private String processMacros(String inString, List params) throws Exception { return processMacros(inString, params, null); } private String processMacros(String inString, List params, E altValues) throws Exception { int i = 1; for (int offset = inString.indexOf('{', i); offset > -1; offset = inString .indexOf('{', i)) { int end = inString.indexOf('}', offset); if (end > -1) { String paramName = inString.substring(offset + 1, end); Object paramVal = null; paramVal = (paramName.startsWith("+")) ? altValues.getValue(paramName.substring(1)) : getValue(paramName); logger.debug("Adding Param (from {" + paramName + "}: " + paramVal); if (paramVal instanceof Integer) { inString = inString.substring(0, offset) + Integer.toString((Integer)paramVal) + inString.substring(end + 1); } else { params.add(paramVal); inString = inString.substring(0, offset) + "?" + inString.substring(end + 1); } } i = offset + 1; } return inString; } public Set getModifiedFields(E orig) { Set retVal = new HashSet(); java.util.HashSet keys = new HashSet(values.keySet()); keys.addAll(orig.getData().keySet()); for (String key : keys) { Object n = values.get(key); Object o = orig.getValue(key); if ((n == null && o != null) || (n != null && !n.equals("null") && !n.equals(o))) { retVal.add(key); } } return retVal; } public int save(E orig) { return save(orig, getTableName(), getKeyFields()); } /** * @deprecated */ public int save(E orig, String tableName, String[] keyCols) { return save(orig, tableName, Arrays.asList(keyCols)); } public int save(E orig, String tableName, List keyCols) { StringBuffer updates = new StringBuffer(); Set updateColumns = getModifiedFields(orig); boolean first = true; for (String key : updateColumns) { updates.append(((first) ? "" : ",") + key + "={" + key + "}"); first = false; } if (!first) { first = true; String sql = "UPDATE " + tableName + " SET " + updates.toString() + " WHERE "; for (String kc : keyCols) { sql += ((first) ? "" : " AND ") + kc + "={+" + kc + "}"; first = false; } return executeSQL(sql, orig); } return 0; } /** * @deprecated */ public E saveNew(String tableName, String[] keys) { return saveNew(tableName, keys == null ? null : Arrays.asList(keys)); } public E saveNew() { return saveNew(getTableName(), getKeyFields()); } public E saveNew(String tableName, List keys) { String autoColName = keys != null && !keys.isEmpty() ? keys.get(0) : null; saveNew(tableName, autoColName); return lookupThis(tableName, keys); } public int saveNew(String tableName) { return saveNew(tableName, (String)null); } // override this to supply any augmentation to the insert statement, e.g., // { COLUMNID_NEEDING_LOOKUP : SELECT COLUMNID FROM OTHER_TABLE WHERE KEYCOLUMNID={KEYCOLUMNID} } public Map getInsertAugments() { return Collections.emptyMap(); } public int saveNew(String tableName, String autoColName) { StringBuffer cols = new StringBuffer(); StringBuffer vals = new StringBuffer(); int count = 0; for (String key: values.keySet()) { Object n = values.get(key); if ((!n.equals("null"))) { cols.append(((count == 0) ? "" : ",") + key); vals.append(((count == 0) ? "" : ",") + "{" + key + "}"); count++; } } Map insertAugments = getInsertAugments(); for (String key: insertAugments.keySet()) { cols.append(((count == 0) ? "" : ",") + key); vals.append(((count == 0) ? "" : ",") + "(" + insertAugments.get(key) + ")"); } if (count > 0) { String sql = "INSERT INTO " + tableName + " (" + cols.toString() + ") VALUES ("; sql += vals; sql += ")"; return executeSQL(sql, null, autoColName); } return 0; } public E lookupThis() { return lookupThis(getTableName(), getKeyFields()); } public E lookupThis(String tableName) { return lookupThis(tableName, getKeyFields()); } /** * @deprecated */ public E lookupThis(String tableName, String keys[]) { return lookupThis(tableName, Arrays.asList(keys)); } public E lookupThis(String tableName, List keys) { StringBuffer where = new StringBuffer(); if (keys == null) keys = new ArrayList(values.keySet()); boolean first = true; for (String key : keys) { Object n = values.get(key); where.append(((first) ? "" : " AND ") + key + "={" + key + "}"); first = false; } String sql = "SELECT * FROM " + tableName + " WHERE " + where.toString(); List results = getDataSet(sql, null, null, null); if (results.size() > 1) { logger.error("Serious Data Error, this object is not unique. Object: "+this.toString()+"; SQL: " + sql, new Throwable()); } else if (results.size() == 0) { logger.error("Serious Data Error, couldn't lookup object by all fields. Object: "+this.toString()+"; SQL: " + sql + "; keys: " + keys, new Throwable()); } return results.size() > 0 ? results.get(0) : null; } public String toString() { StringBuffer retVal = new StringBuffer("<" + this.getClass().getName() + ">\n"); for (String key : values.keySet()) { Object n = values.get(key); retVal.append("\t<"+key+">"+HTTPUtils.canonize(n.toString())+"\n"); } retVal.append(""); return retVal.toString(); } static public byte[] getBytes(InputStream is) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] data = new byte[8192]; int count; try { while ((count = is.read(data, 0, data.length)) != -1) { buffer.write(data, 0, count); } buffer.flush(); } catch (Exception e) {} return buffer.toByteArray(); } }