1package com.xtremelabs.robolectric.shadows;
2
3import android.database.sqlite.SQLiteCursor;
4import com.xtremelabs.robolectric.internal.Implementation;
5import com.xtremelabs.robolectric.internal.Implements;
6
7import java.sql.Clob;
8import java.sql.Connection;
9import java.sql.ResultSet;
10import java.sql.ResultSetMetaData;
11import java.sql.SQLException;
12import java.sql.Statement;
13import java.util.HashMap;
14import java.util.Map;
15
16/**
17 * Simulates an Android Cursor object, by wrapping a JDBC ResultSet.
18 */
19@Implements(SQLiteCursor.class)
20public 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     */
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
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
57    public int getColumnIndex(String columnName) {
58    	return getColIndex(columnName);
59    }
60
61    @Implementation
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
72    public final boolean moveToLast() {
73        return super.moveToLast();
74    }
75
76    @Implementation
77    @Override
78    public final boolean moveToFirst() {
79        return super.moveToFirst();
80    }
81
82    @Implementation
83    @Override
84    public boolean moveToNext() {
85        return super.moveToNext();
86    }
87
88    @Implementation
89    @Override
90    public boolean moveToPrevious() {
91        return super.moveToPrevious();
92    }
93
94    @Implementation
95    @Override
96    public boolean moveToPosition(int pos) {
97    	return super.moveToPosition(pos);
98    }
99
100    @Implementation
101    public byte[] getBlob(int columnIndex) {
102    	checkPosition();
103        return (byte[]) this.currentRow.get(getColumnNames()[columnIndex]);
104    }
105
106    @Implementation
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
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
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
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
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
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
162    private void checkPosition() {
163        if (-1 == currentRowNumber || getCount() == currentRowNumber) {
164            throw new IndexOutOfBoundsException(currentRowNumber + " " + getCount());
165        }
166    }
167
168    @Implementation
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
185    public boolean isClosed() {
186        return (resultSet == null);
187    }
188
189    @Implementation
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     */
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     */
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     */
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    }
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
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