1 package com.xtremelabs.robolectric.shadows; 2 3 import android.database.sqlite.SQLiteCursor; 4 import com.xtremelabs.robolectric.internal.Implementation; 5 import com.xtremelabs.robolectric.internal.Implements; 6 7 import java.sql.Clob; 8 import java.sql.Connection; 9 import java.sql.ResultSet; 10 import java.sql.ResultSetMetaData; 11 import java.sql.SQLException; 12 import java.sql.Statement; 13 import java.util.HashMap; 14 import java.util.Map; 15 16 /** 17 * Simulates an Android Cursor object, by wrapping a JDBC ResultSet. 18 */ 19 @Implements(SQLiteCursor.class) 20 public class ShadowSQLiteCursor extends ShadowAbstractCursor { 21 22 private ResultSet resultSet; 23 24 25 /** 26 * Stores the column names so they are retrievable after the resultSet has closed 27 */ cacheColumnNames(ResultSet rs)28 private void cacheColumnNames(ResultSet rs) { 29 try { 30 ResultSetMetaData metaData = rs.getMetaData(); 31 int columnCount = metaData.getColumnCount(); 32 columnNameArray = new String[columnCount]; 33 for (int columnIndex = 1; columnIndex <= columnCount; columnIndex++) { 34 String cName = metaData.getColumnName(columnIndex).toLowerCase(); 35 this.columnNames.put(cName, columnIndex-1); 36 this.columnNameArray[columnIndex-1]=cName; 37 } 38 } catch (SQLException e) { 39 throw new RuntimeException("SQL exception in cacheColumnNames", e); 40 } 41 } 42 43 44 45 getColIndex(String columnName)46 private Integer getColIndex(String columnName) { 47 if (columnName == null) { 48 return -1; 49 } 50 51 Integer i = this.columnNames.get(columnName.toLowerCase()); 52 if (i==null) return -1; 53 return i; 54 } 55 56 @Implementation getColumnIndex(String columnName)57 public int getColumnIndex(String columnName) { 58 return getColIndex(columnName); 59 } 60 61 @Implementation getColumnIndexOrThrow(String columnName)62 public int getColumnIndexOrThrow(String columnName) { 63 Integer columnIndex = getColIndex(columnName); 64 if (columnIndex == -1) { 65 throw new IllegalArgumentException("Column index does not exist"); 66 } 67 return columnIndex; 68 } 69 70 @Implementation 71 @Override moveToLast()72 public final boolean moveToLast() { 73 return super.moveToLast(); 74 } 75 76 @Implementation 77 @Override moveToFirst()78 public final boolean moveToFirst() { 79 return super.moveToFirst(); 80 } 81 82 @Implementation 83 @Override moveToNext()84 public boolean moveToNext() { 85 return super.moveToNext(); 86 } 87 88 @Implementation 89 @Override moveToPrevious()90 public boolean moveToPrevious() { 91 return super.moveToPrevious(); 92 } 93 94 @Implementation 95 @Override moveToPosition(int pos)96 public boolean moveToPosition(int pos) { 97 return super.moveToPosition(pos); 98 } 99 100 @Implementation getBlob(int columnIndex)101 public byte[] getBlob(int columnIndex) { 102 checkPosition(); 103 return (byte[]) this.currentRow.get(getColumnNames()[columnIndex]); 104 } 105 106 @Implementation getString(int columnIndex)107 public String getString(int columnIndex) { 108 checkPosition(); 109 Object value = this.currentRow.get(getColumnNames()[columnIndex]); 110 if (value instanceof Clob) { 111 try { 112 return ((Clob) value).getSubString(1, (int)((Clob) value).length()); 113 } catch (SQLException x) { 114 throw new RuntimeException(x); 115 } 116 } else { 117 return (String)value; 118 } 119 } 120 121 @Implementation getShort(int columnIndex)122 public short getShort(int columnIndex) { 123 checkPosition(); 124 Object o =this.currentRow.get(getColumnNames()[columnIndex]); 125 if (o==null) return 0; 126 return new Short(o.toString()); 127 } 128 129 @Implementation getInt(int columnIndex)130 public int getInt(int columnIndex) { 131 checkPosition(); 132 Object o =this.currentRow.get(getColumnNames()[columnIndex]); 133 if (o==null) return 0; 134 return new Integer(o.toString()); 135 } 136 137 @Implementation getLong(int columnIndex)138 public long getLong(int columnIndex) { 139 checkPosition(); 140 Object o =this.currentRow.get(getColumnNames()[columnIndex]); 141 if (o==null) return 0; 142 return new Long(o.toString()); 143 } 144 145 @Implementation getFloat(int columnIndex)146 public float getFloat(int columnIndex) { 147 checkPosition(); 148 Object o =this.currentRow.get(getColumnNames()[columnIndex]); 149 if (o==null) return 0; 150 return new Float(o.toString()); 151 152 } 153 154 @Implementation getDouble(int columnIndex)155 public double getDouble(int columnIndex) { 156 checkPosition(); 157 Object o =this.currentRow.get(getColumnNames()[columnIndex]); 158 if (o==null) return 0; 159 return new Double(o.toString()); 160 } 161 checkPosition()162 private void checkPosition() { 163 if (-1 == currentRowNumber || getCount() == currentRowNumber) { 164 throw new IndexOutOfBoundsException(currentRowNumber + " " + getCount()); 165 } 166 } 167 168 @Implementation close()169 public void close() { 170 if (resultSet == null) { 171 return; 172 } 173 174 try { 175 resultSet.close(); 176 resultSet = null; 177 rows = null; 178 currentRow = null; 179 } catch (SQLException e) { 180 throw new RuntimeException("SQL exception in close", e); 181 } 182 } 183 184 @Implementation isClosed()185 public boolean isClosed() { 186 return (resultSet == null); 187 } 188 189 @Implementation isNull(int columnIndex)190 public boolean isNull(int columnIndex) { 191 Object o = this.currentRow.get(getColumnNames()[columnIndex]); 192 return o == null; 193 } 194 195 /** 196 * Allows test cases access to the underlying JDBC ResultSet, for use in 197 * assertions. 198 * 199 * @return the result set 200 */ getResultSet()201 public ResultSet getResultSet() { 202 return resultSet; 203 } 204 205 /** 206 * Allows test cases access to the underlying JDBC ResultSetMetaData, for use in 207 * assertions. Available even if cl 208 * 209 * @return the result set 210 */ getResultSetMetaData()211 public ResultSet getResultSetMetaData() { 212 return resultSet; 213 } 214 215 /** 216 * loads a row's values 217 * @param rs 218 * @return 219 * @throws SQLException 220 */ fillRowValues(ResultSet rs)221 private Map<String,Object> fillRowValues(ResultSet rs) throws SQLException { 222 Map<String,Object> row = new HashMap<String,Object>(); 223 for (String s : getColumnNames()) { 224 row.put(s, rs.getObject(s)); 225 } 226 return row; 227 } fillRows(String sql, Connection connection)228 private void fillRows(String sql, Connection connection) throws SQLException { 229 //ResultSets in SQLite\Android are only TYPE_FORWARD_ONLY. Android caches results in the WindowedCursor to allow moveToPrevious() to function. 230 //Robolectric will have to cache the results too. In the rows map. 231 Statement statement = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); 232 ResultSet rs = statement.executeQuery(sql); 233 int count = 0; 234 if (rs.next()) { 235 do { 236 Map<String,Object> row = fillRowValues(rs); 237 rows.put(count, row); 238 count++; 239 } while (rs.next()); 240 } else { 241 rs.close(); 242 } 243 244 rowCount = count; 245 246 } 247 setResultSet(ResultSet result, String sql)248 public void setResultSet(ResultSet result, String sql) { 249 this.resultSet = result; 250 rowCount = 0; 251 252 //Cache all rows. Caching rows should be thought of as a simple replacement for ShadowCursorWindow 253 if (resultSet != null) { 254 cacheColumnNames(resultSet); 255 try { 256 fillRows(sql, result.getStatement().getConnection()); 257 } catch (SQLException e) { 258 throw new RuntimeException("SQL exception in setResultSet", e); 259 } 260 } 261 } 262 } 263