1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.database;
18 
19 import android.compat.annotation.UnsupportedAppUsage;
20 import android.os.Build;
21 
22 import java.util.ArrayList;
23 
24 /**
25  * A mutable cursor implementation backed by an array of {@code Object}s. Use
26  * {@link #newRow()} to add rows. Automatically expands internal capacity
27  * as needed.
28  */
29 @android.ravenwood.annotation.RavenwoodKeepWholeClass
30 public class MatrixCursor extends AbstractCursor {
31 
32     private final String[] columnNames;
33     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
34     private Object[] data;
35     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
36     private int rowCount = 0;
37     private final int columnCount;
38 
39     /**
40      * Constructs a new cursor with the given initial capacity.
41      *
42      * @param columnNames names of the columns, the ordering of which
43      *  determines column ordering elsewhere in this cursor
44      * @param initialCapacity in rows
45      */
MatrixCursor(String[] columnNames, int initialCapacity)46     public MatrixCursor(String[] columnNames, int initialCapacity) {
47         this.columnNames = columnNames;
48         this.columnCount = columnNames.length;
49 
50         if (initialCapacity < 1) {
51             initialCapacity = 1;
52         }
53 
54         this.data = new Object[columnCount * initialCapacity];
55     }
56 
57     /**
58      * Constructs a new cursor.
59      *
60      * @param columnNames names of the columns, the ordering of which
61      *  determines column ordering elsewhere in this cursor
62      */
MatrixCursor(String[] columnNames)63     public MatrixCursor(String[] columnNames) {
64         this(columnNames, 16);
65     }
66 
67     /**
68      * Gets value at the given column for the current row.
69      */
70     @UnsupportedAppUsage
get(int column)71     private Object get(int column) {
72         if (column < 0 || column >= columnCount) {
73             throw new CursorIndexOutOfBoundsException("Requested column: "
74                     + column + ", # of columns: " +  columnCount);
75         }
76         if (mPos < 0) {
77             throw new CursorIndexOutOfBoundsException("Before first row.");
78         }
79         if (mPos >= rowCount) {
80             throw new CursorIndexOutOfBoundsException("After last row.");
81         }
82         return data[mPos * columnCount + column];
83     }
84 
85     /**
86      * Adds a new row to the end and returns a builder for that row. Not safe
87      * for concurrent use.
88      *
89      * @return builder which can be used to set the column values for the new
90      *  row
91      */
newRow()92     public RowBuilder newRow() {
93         final int row = rowCount++;
94         final int endIndex = rowCount * columnCount;
95         ensureCapacity(endIndex);
96         return new RowBuilder(row);
97     }
98 
99     /**
100      * Adds a new row to the end with the given column values. Not safe
101      * for concurrent use.
102      *
103      * @throws IllegalArgumentException if {@code columnValues.length !=
104      *  columnNames.length}
105      * @param columnValues in the same order as the the column names specified
106      *  at cursor construction time
107      */
addRow(Object[] columnValues)108     public void addRow(Object[] columnValues) {
109         if (columnValues.length != columnCount) {
110             throw new IllegalArgumentException("columnNames.length = "
111                     + columnCount + ", columnValues.length = "
112                     + columnValues.length);
113         }
114 
115         int start = rowCount++ * columnCount;
116         ensureCapacity(start + columnCount);
117         System.arraycopy(columnValues, 0, data, start, columnCount);
118     }
119 
120     /**
121      * Adds a new row to the end with the given column values. Not safe
122      * for concurrent use.
123      *
124      * @throws IllegalArgumentException if {@code columnValues.size() !=
125      *  columnNames.length}
126      * @param columnValues in the same order as the the column names specified
127      *  at cursor construction time
128      */
addRow(Iterable<?> columnValues)129     public void addRow(Iterable<?> columnValues) {
130         int start = rowCount * columnCount;
131         int end = start + columnCount;
132         ensureCapacity(end);
133 
134         if (columnValues instanceof ArrayList<?>) {
135             addRow((ArrayList<?>) columnValues, start);
136             return;
137         }
138 
139         int current = start;
140         Object[] localData = data;
141         for (Object columnValue : columnValues) {
142             if (current == end) {
143                 // TODO: null out row?
144                 throw new IllegalArgumentException(
145                         "columnValues.size() > columnNames.length");
146             }
147             localData[current++] = columnValue;
148         }
149 
150         if (current != end) {
151             // TODO: null out row?
152             throw new IllegalArgumentException(
153                     "columnValues.size() < columnNames.length");
154         }
155 
156         // Increase row count here in case we encounter an exception.
157         rowCount++;
158     }
159 
160     /** Optimization for {@link ArrayList}. */
addRow(ArrayList<?> columnValues, int start)161     private void addRow(ArrayList<?> columnValues, int start) {
162         int size = columnValues.size();
163         if (size != columnCount) {
164             throw new IllegalArgumentException("columnNames.length = "
165                     + columnCount + ", columnValues.size() = " + size);
166         }
167 
168         rowCount++;
169         Object[] localData = data;
170         for (int i = 0; i < size; i++) {
171             localData[start + i] = columnValues.get(i);
172         }
173     }
174 
175     /** Ensures that this cursor has enough capacity. */
ensureCapacity(int size)176     private void ensureCapacity(int size) {
177         if (size > data.length) {
178             Object[] oldData = this.data;
179             int newSize = data.length * 2;
180             if (newSize < size) {
181                 newSize = size;
182             }
183             this.data = new Object[newSize];
184             System.arraycopy(oldData, 0, this.data, 0, oldData.length);
185         }
186     }
187 
188     /**
189      * Builds a row of values using either of these approaches:
190      * <ul>
191      * <li>Values can be added with explicit column ordering using
192      * {@link #add(Object)}, which starts from the left-most column and adds one
193      * column value at a time. This follows the same ordering as the column
194      * names specified at cursor construction time.
195      * <li>Column and value pairs can be offered for possible inclusion using
196      * {@link #add(String, Object)}. If the cursor includes the given column,
197      * the value will be set for that column, otherwise the value is ignored.
198      * This approach is useful when matching data to a custom projection.
199      * </ul>
200      * Undefined values are left as {@code null}.
201      */
202     public class RowBuilder {
203         private final int row;
204         private final int endIndex;
205 
206         private int index;
207 
RowBuilder(int row)208         RowBuilder(int row) {
209             this.row = row;
210             this.index = row * columnCount;
211             this.endIndex = index + columnCount;
212         }
213 
214         /**
215          * Sets the next column value in this row.
216          *
217          * @throws CursorIndexOutOfBoundsException if you try to add too many
218          *  values
219          * @return this builder to support chaining
220          */
add(Object columnValue)221         public RowBuilder add(Object columnValue) {
222             if (index == endIndex) {
223                 throw new CursorIndexOutOfBoundsException(
224                         "No more columns left.");
225             }
226 
227             data[index++] = columnValue;
228             return this;
229         }
230 
231         /**
232          * Offer value for possible inclusion if this cursor defines the given
233          * column. Columns not defined by the cursor are silently ignored.
234          *
235          * @return this builder to support chaining
236          */
add(String columnName, Object value)237         public RowBuilder add(String columnName, Object value) {
238             for (int i = 0; i < columnNames.length; i++) {
239                 if (columnName.equals(columnNames[i])) {
240                     data[(row * columnCount) + i] = value;
241                 }
242             }
243             return this;
244         }
245 
246         /** @hide */
add(int columnIndex, Object value)247         public final RowBuilder add(int columnIndex, Object value) {
248             data[(row * columnCount) + columnIndex] = value;
249             return this;
250         }
251     }
252 
253     // AbstractCursor implementation.
254 
255     @Override
getCount()256     public int getCount() {
257         return rowCount;
258     }
259 
260     @Override
getColumnNames()261     public String[] getColumnNames() {
262         return columnNames;
263     }
264 
265     @Override
getString(int column)266     public String getString(int column) {
267         Object value = get(column);
268         if (value == null) return null;
269         return value.toString();
270     }
271 
272     @Override
getShort(int column)273     public short getShort(int column) {
274         Object value = get(column);
275         if (value == null) return 0;
276         if (value instanceof Number) return ((Number) value).shortValue();
277         return Short.parseShort(value.toString());
278     }
279 
280     @Override
getInt(int column)281     public int getInt(int column) {
282         Object value = get(column);
283         if (value == null) return 0;
284         if (value instanceof Number) return ((Number) value).intValue();
285         return Integer.parseInt(value.toString());
286     }
287 
288     @Override
getLong(int column)289     public long getLong(int column) {
290         Object value = get(column);
291         if (value == null) return 0;
292         if (value instanceof Number) return ((Number) value).longValue();
293         return Long.parseLong(value.toString());
294     }
295 
296     @Override
getFloat(int column)297     public float getFloat(int column) {
298         Object value = get(column);
299         if (value == null) return 0.0f;
300         if (value instanceof Number) return ((Number) value).floatValue();
301         return Float.parseFloat(value.toString());
302     }
303 
304     @Override
getDouble(int column)305     public double getDouble(int column) {
306         Object value = get(column);
307         if (value == null) return 0.0d;
308         if (value instanceof Number) return ((Number) value).doubleValue();
309         return Double.parseDouble(value.toString());
310     }
311 
312     @Override
getBlob(int column)313     public byte[] getBlob(int column) {
314         Object value = get(column);
315         return (byte[]) value;
316     }
317 
318     @Override
getType(int column)319     public int getType(int column) {
320         return DatabaseUtils.getTypeOfObject(get(column));
321     }
322 
323     @Override
isNull(int column)324     public boolean isNull(int column) {
325         return get(column) == null;
326     }
327 }
328