blob: f428aad71229ff3d10c1b6debfef6e687f3ff925 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 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
17package android.database;
18
19import org.apache.commons.codec.binary.Hex;
20
21import android.content.ContentValues;
22import android.content.Context;
Fred Quintana89437372009-05-15 15:10:40 -070023import android.content.OperationApplicationException;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080024import android.database.sqlite.SQLiteAbortException;
25import android.database.sqlite.SQLiteConstraintException;
26import android.database.sqlite.SQLiteDatabase;
27import android.database.sqlite.SQLiteDatabaseCorruptException;
28import android.database.sqlite.SQLiteDiskIOException;
29import android.database.sqlite.SQLiteException;
30import android.database.sqlite.SQLiteFullException;
31import android.database.sqlite.SQLiteProgram;
32import android.database.sqlite.SQLiteStatement;
33import android.os.Parcel;
Bjorn Bringerta006b4722010-04-14 14:43:26 +010034import android.os.ParcelFileDescriptor;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080035import android.text.TextUtils;
36import android.util.Config;
37import android.util.Log;
38
39import java.io.FileNotFoundException;
40import java.io.PrintStream;
41import java.text.Collator;
42import java.util.HashMap;
43import java.util.Map;
44
45/**
46 * Static utility methods for dealing with databases and {@link Cursor}s.
47 */
48public class DatabaseUtils {
49 private static final String TAG = "DatabaseUtils";
50
51 private static final boolean DEBUG = false;
52 private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
53
54 private static final String[] countProjection = new String[]{"count(*)"};
55
Vasu Norice38b982010-07-22 13:57:13 -070056 /** One of the values returned by {@link #getSqlStatementType(String)}. */
57 public static final int STATEMENT_SELECT = 1;
58 /** One of the values returned by {@link #getSqlStatementType(String)}. */
59 public static final int STATEMENT_UPDATE = 2;
60 /** One of the values returned by {@link #getSqlStatementType(String)}. */
61 public static final int STATEMENT_ATTACH = 3;
62 /** One of the values returned by {@link #getSqlStatementType(String)}. */
63 public static final int STATEMENT_BEGIN = 4;
64 /** One of the values returned by {@link #getSqlStatementType(String)}. */
65 public static final int STATEMENT_COMMIT = 5;
66 /** One of the values returned by {@link #getSqlStatementType(String)}. */
67 public static final int STATEMENT_ABORT = 6;
68 /** One of the values returned by {@link #getSqlStatementType(String)}. */
Vasu Nori4e874ed2010-09-15 18:40:49 -070069 public static final int STATEMENT_PRAGMA = 7;
70 /** One of the values returned by {@link #getSqlStatementType(String)}. */
71 public static final int STATEMENT_DDL = 8;
72 /** One of the values returned by {@link #getSqlStatementType(String)}. */
73 public static final int STATEMENT_UNPREPARED = 9;
74 /** One of the values returned by {@link #getSqlStatementType(String)}. */
75 public static final int STATEMENT_OTHER = 99;
Vasu Norice38b982010-07-22 13:57:13 -070076
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080077 /**
78 * Special function for writing an exception result at the header of
79 * a parcel, to be used when returning an exception from a transaction.
80 * exception will be re-thrown by the function in another process
81 * @param reply Parcel to write to
82 * @param e The Exception to be written.
83 * @see Parcel#writeNoException
84 * @see Parcel#writeException
85 */
86 public static final void writeExceptionToParcel(Parcel reply, Exception e) {
87 int code = 0;
88 boolean logException = true;
89 if (e instanceof FileNotFoundException) {
90 code = 1;
91 logException = false;
92 } else if (e instanceof IllegalArgumentException) {
93 code = 2;
94 } else if (e instanceof UnsupportedOperationException) {
95 code = 3;
96 } else if (e instanceof SQLiteAbortException) {
97 code = 4;
98 } else if (e instanceof SQLiteConstraintException) {
99 code = 5;
100 } else if (e instanceof SQLiteDatabaseCorruptException) {
101 code = 6;
102 } else if (e instanceof SQLiteFullException) {
103 code = 7;
104 } else if (e instanceof SQLiteDiskIOException) {
105 code = 8;
106 } else if (e instanceof SQLiteException) {
107 code = 9;
Fred Quintana89437372009-05-15 15:10:40 -0700108 } else if (e instanceof OperationApplicationException) {
109 code = 10;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800110 } else {
111 reply.writeException(e);
112 Log.e(TAG, "Writing exception to parcel", e);
113 return;
114 }
115 reply.writeInt(code);
116 reply.writeString(e.getMessage());
117
118 if (logException) {
119 Log.e(TAG, "Writing exception to parcel", e);
120 }
121 }
122
123 /**
124 * Special function for reading an exception result from the header of
125 * a parcel, to be used after receiving the result of a transaction. This
126 * will throw the exception for you if it had been written to the Parcel,
127 * otherwise return and let you read the normal result data from the Parcel.
128 * @param reply Parcel to read from
129 * @see Parcel#writeNoException
130 * @see Parcel#readException
131 */
132 public static final void readExceptionFromParcel(Parcel reply) {
Brad Fitzpatrick5b747192010-07-12 11:05:38 -0700133 int code = reply.readExceptionCode();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800134 if (code == 0) return;
135 String msg = reply.readString();
136 DatabaseUtils.readExceptionFromParcel(reply, msg, code);
137 }
138
139 public static void readExceptionWithFileNotFoundExceptionFromParcel(
140 Parcel reply) throws FileNotFoundException {
Brad Fitzpatrick5b747192010-07-12 11:05:38 -0700141 int code = reply.readExceptionCode();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800142 if (code == 0) return;
143 String msg = reply.readString();
144 if (code == 1) {
145 throw new FileNotFoundException(msg);
146 } else {
147 DatabaseUtils.readExceptionFromParcel(reply, msg, code);
148 }
149 }
150
Fred Quintana89437372009-05-15 15:10:40 -0700151 public static void readExceptionWithOperationApplicationExceptionFromParcel(
152 Parcel reply) throws OperationApplicationException {
Brad Fitzpatrick5b747192010-07-12 11:05:38 -0700153 int code = reply.readExceptionCode();
Fred Quintana89437372009-05-15 15:10:40 -0700154 if (code == 0) return;
155 String msg = reply.readString();
156 if (code == 10) {
157 throw new OperationApplicationException(msg);
158 } else {
159 DatabaseUtils.readExceptionFromParcel(reply, msg, code);
160 }
161 }
162
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800163 private static final void readExceptionFromParcel(Parcel reply, String msg, int code) {
164 switch (code) {
165 case 2:
166 throw new IllegalArgumentException(msg);
167 case 3:
168 throw new UnsupportedOperationException(msg);
169 case 4:
170 throw new SQLiteAbortException(msg);
171 case 5:
172 throw new SQLiteConstraintException(msg);
173 case 6:
174 throw new SQLiteDatabaseCorruptException(msg);
175 case 7:
176 throw new SQLiteFullException(msg);
177 case 8:
178 throw new SQLiteDiskIOException(msg);
179 case 9:
180 throw new SQLiteException(msg);
181 default:
182 reply.readException(code, msg);
183 }
184 }
185
186 /**
187 * Binds the given Object to the given SQLiteProgram using the proper
188 * typing. For example, bind numbers as longs/doubles, and everything else
189 * as a string by call toString() on it.
190 *
191 * @param prog the program to bind the object to
192 * @param index the 1-based index to bind at
193 * @param value the value to bind
194 */
195 public static void bindObjectToProgram(SQLiteProgram prog, int index,
196 Object value) {
197 if (value == null) {
198 prog.bindNull(index);
199 } else if (value instanceof Double || value instanceof Float) {
200 prog.bindDouble(index, ((Number)value).doubleValue());
201 } else if (value instanceof Number) {
202 prog.bindLong(index, ((Number)value).longValue());
203 } else if (value instanceof Boolean) {
204 Boolean bool = (Boolean)value;
205 if (bool) {
206 prog.bindLong(index, 1);
207 } else {
208 prog.bindLong(index, 0);
209 }
210 } else if (value instanceof byte[]){
211 prog.bindBlob(index, (byte[]) value);
212 } else {
213 prog.bindString(index, value.toString());
214 }
215 }
216
217 /**
Vasu Nori8b0dd7d2010-05-18 11:54:31 -0700218 * Returns data type of the given object's value.
219 *<p>
220 * Returned values are
221 * <ul>
222 * <li>{@link Cursor#FIELD_TYPE_NULL}</li>
223 * <li>{@link Cursor#FIELD_TYPE_INTEGER}</li>
224 * <li>{@link Cursor#FIELD_TYPE_FLOAT}</li>
225 * <li>{@link Cursor#FIELD_TYPE_STRING}</li>
226 * <li>{@link Cursor#FIELD_TYPE_BLOB}</li>
227 *</ul>
228 *</p>
229 *
230 * @param obj the object whose value type is to be returned
231 * @return object value type
232 * @hide
233 */
234 public static int getTypeOfObject(Object obj) {
235 if (obj == null) {
236 return Cursor.FIELD_TYPE_NULL;
237 } else if (obj instanceof byte[]) {
238 return Cursor.FIELD_TYPE_BLOB;
239 } else if (obj instanceof Float || obj instanceof Double) {
240 return Cursor.FIELD_TYPE_FLOAT;
241 } else if (obj instanceof Long || obj instanceof Integer) {
242 return Cursor.FIELD_TYPE_INTEGER;
243 } else {
244 return Cursor.FIELD_TYPE_STRING;
245 }
246 }
247
248 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800249 * Appends an SQL string to the given StringBuilder, including the opening
250 * and closing single quotes. Any single quotes internal to sqlString will
251 * be escaped.
252 *
253 * This method is deprecated because we want to encourage everyone
254 * to use the "?" binding form. However, when implementing a
255 * ContentProvider, one may want to add WHERE clauses that were
256 * not provided by the caller. Since "?" is a positional form,
257 * using it in this case could break the caller because the
258 * indexes would be shifted to accomodate the ContentProvider's
259 * internal bindings. In that case, it may be necessary to
260 * construct a WHERE clause manually. This method is useful for
261 * those cases.
262 *
263 * @param sb the StringBuilder that the SQL string will be appended to
264 * @param sqlString the raw string to be appended, which may contain single
265 * quotes
266 */
267 public static void appendEscapedSQLString(StringBuilder sb, String sqlString) {
268 sb.append('\'');
269 if (sqlString.indexOf('\'') != -1) {
270 int length = sqlString.length();
271 for (int i = 0; i < length; i++) {
272 char c = sqlString.charAt(i);
273 if (c == '\'') {
274 sb.append('\'');
275 }
276 sb.append(c);
277 }
278 } else
279 sb.append(sqlString);
280 sb.append('\'');
281 }
Fred Quintana22f71142009-03-24 20:10:17 -0700282
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800283 /**
284 * SQL-escape a string.
285 */
286 public static String sqlEscapeString(String value) {
287 StringBuilder escaper = new StringBuilder();
288
289 DatabaseUtils.appendEscapedSQLString(escaper, value);
290
291 return escaper.toString();
292 }
293
294 /**
295 * Appends an Object to an SQL string with the proper escaping, etc.
296 */
297 public static final void appendValueToSql(StringBuilder sql, Object value) {
298 if (value == null) {
299 sql.append("NULL");
300 } else if (value instanceof Boolean) {
301 Boolean bool = (Boolean)value;
302 if (bool) {
303 sql.append('1');
304 } else {
305 sql.append('0');
306 }
307 } else {
308 appendEscapedSQLString(sql, value.toString());
309 }
310 }
Fred Quintana22f71142009-03-24 20:10:17 -0700311
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800312 /**
313 * Concatenates two SQL WHERE clauses, handling empty or null values.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800314 */
315 public static String concatenateWhere(String a, String b) {
316 if (TextUtils.isEmpty(a)) {
317 return b;
318 }
319 if (TextUtils.isEmpty(b)) {
320 return a;
321 }
Fred Quintana22f71142009-03-24 20:10:17 -0700322
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800323 return "(" + a + ") AND (" + b + ")";
324 }
Fred Quintana22f71142009-03-24 20:10:17 -0700325
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800326 /**
Fred Quintana22f71142009-03-24 20:10:17 -0700327 * return the collation key
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800328 * @param name
329 * @return the collation key
330 */
331 public static String getCollationKey(String name) {
332 byte [] arr = getCollationKeyInBytes(name);
333 try {
334 return new String(arr, 0, getKeyLen(arr), "ISO8859_1");
335 } catch (Exception ex) {
336 return "";
337 }
338 }
Fred Quintana22f71142009-03-24 20:10:17 -0700339
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800340 /**
341 * return the collation key in hex format
342 * @param name
343 * @return the collation key in hex format
344 */
345 public static String getHexCollationKey(String name) {
346 byte [] arr = getCollationKeyInBytes(name);
347 char[] keys = Hex.encodeHex(arr);
348 return new String(keys, 0, getKeyLen(arr) * 2);
349 }
Fred Quintana22f71142009-03-24 20:10:17 -0700350
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800351 private static int getKeyLen(byte[] arr) {
352 if (arr[arr.length - 1] != 0) {
353 return arr.length;
354 } else {
355 // remove zero "termination"
356 return arr.length-1;
357 }
358 }
Fred Quintana22f71142009-03-24 20:10:17 -0700359
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800360 private static byte[] getCollationKeyInBytes(String name) {
361 if (mColl == null) {
362 mColl = Collator.getInstance();
363 mColl.setStrength(Collator.PRIMARY);
364 }
Fred Quintana22f71142009-03-24 20:10:17 -0700365 return mColl.getCollationKey(name).toByteArray();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800366 }
Fred Quintana22f71142009-03-24 20:10:17 -0700367
368 private static Collator mColl = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800369 /**
370 * Prints the contents of a Cursor to System.out. The position is restored
371 * after printing.
372 *
373 * @param cursor the cursor to print
374 */
375 public static void dumpCursor(Cursor cursor) {
376 dumpCursor(cursor, System.out);
377 }
378
379 /**
380 * Prints the contents of a Cursor to a PrintSteam. The position is restored
381 * after printing.
382 *
383 * @param cursor the cursor to print
384 * @param stream the stream to print to
385 */
386 public static void dumpCursor(Cursor cursor, PrintStream stream) {
387 stream.println(">>>>> Dumping cursor " + cursor);
388 if (cursor != null) {
389 int startPos = cursor.getPosition();
390
391 cursor.moveToPosition(-1);
392 while (cursor.moveToNext()) {
393 dumpCurrentRow(cursor, stream);
394 }
395 cursor.moveToPosition(startPos);
396 }
397 stream.println("<<<<<");
398 }
399
400 /**
401 * Prints the contents of a Cursor to a StringBuilder. The position
402 * is restored after printing.
403 *
404 * @param cursor the cursor to print
405 * @param sb the StringBuilder to print to
406 */
407 public static void dumpCursor(Cursor cursor, StringBuilder sb) {
408 sb.append(">>>>> Dumping cursor " + cursor + "\n");
409 if (cursor != null) {
410 int startPos = cursor.getPosition();
411
412 cursor.moveToPosition(-1);
413 while (cursor.moveToNext()) {
414 dumpCurrentRow(cursor, sb);
415 }
416 cursor.moveToPosition(startPos);
417 }
418 sb.append("<<<<<\n");
419 }
420
421 /**
422 * Prints the contents of a Cursor to a String. The position is restored
423 * after printing.
424 *
425 * @param cursor the cursor to print
426 * @return a String that contains the dumped cursor
427 */
428 public static String dumpCursorToString(Cursor cursor) {
429 StringBuilder sb = new StringBuilder();
430 dumpCursor(cursor, sb);
431 return sb.toString();
432 }
433
434 /**
435 * Prints the contents of a Cursor's current row to System.out.
436 *
437 * @param cursor the cursor to print from
438 */
439 public static void dumpCurrentRow(Cursor cursor) {
440 dumpCurrentRow(cursor, System.out);
441 }
442
443 /**
444 * Prints the contents of a Cursor's current row to a PrintSteam.
445 *
446 * @param cursor the cursor to print
447 * @param stream the stream to print to
448 */
449 public static void dumpCurrentRow(Cursor cursor, PrintStream stream) {
450 String[] cols = cursor.getColumnNames();
451 stream.println("" + cursor.getPosition() + " {");
452 int length = cols.length;
453 for (int i = 0; i< length; i++) {
454 String value;
455 try {
456 value = cursor.getString(i);
457 } catch (SQLiteException e) {
458 // assume that if the getString threw this exception then the column is not
459 // representable by a string, e.g. it is a BLOB.
460 value = "<unprintable>";
461 }
462 stream.println(" " + cols[i] + '=' + value);
463 }
464 stream.println("}");
465 }
466
467 /**
468 * Prints the contents of a Cursor's current row to a StringBuilder.
469 *
470 * @param cursor the cursor to print
471 * @param sb the StringBuilder to print to
472 */
473 public static void dumpCurrentRow(Cursor cursor, StringBuilder sb) {
474 String[] cols = cursor.getColumnNames();
475 sb.append("" + cursor.getPosition() + " {\n");
476 int length = cols.length;
477 for (int i = 0; i < length; i++) {
478 String value;
479 try {
480 value = cursor.getString(i);
481 } catch (SQLiteException e) {
482 // assume that if the getString threw this exception then the column is not
483 // representable by a string, e.g. it is a BLOB.
484 value = "<unprintable>";
485 }
486 sb.append(" " + cols[i] + '=' + value + "\n");
487 }
488 sb.append("}\n");
489 }
490
491 /**
492 * Dump the contents of a Cursor's current row to a String.
493 *
494 * @param cursor the cursor to print
495 * @return a String that contains the dumped cursor row
496 */
497 public static String dumpCurrentRowToString(Cursor cursor) {
498 StringBuilder sb = new StringBuilder();
499 dumpCurrentRow(cursor, sb);
500 return sb.toString();
501 }
502
503 /**
504 * Reads a String out of a field in a Cursor and writes it to a Map.
505 *
506 * @param cursor The cursor to read from
507 * @param field The TEXT field to read
508 * @param values The {@link ContentValues} to put the value into, with the field as the key
509 */
510 public static void cursorStringToContentValues(Cursor cursor, String field,
511 ContentValues values) {
512 cursorStringToContentValues(cursor, field, values, field);
513 }
514
515 /**
516 * Reads a String out of a field in a Cursor and writes it to an InsertHelper.
517 *
518 * @param cursor The cursor to read from
519 * @param field The TEXT field to read
520 * @param inserter The InsertHelper to bind into
521 * @param index the index of the bind entry in the InsertHelper
522 */
523 public static void cursorStringToInsertHelper(Cursor cursor, String field,
524 InsertHelper inserter, int index) {
525 inserter.bind(index, cursor.getString(cursor.getColumnIndexOrThrow(field)));
526 }
527
528 /**
529 * Reads a String out of a field in a Cursor and writes it to a Map.
530 *
531 * @param cursor The cursor to read from
532 * @param field The TEXT field to read
533 * @param values The {@link ContentValues} to put the value into, with the field as the key
534 * @param key The key to store the value with in the map
535 */
536 public static void cursorStringToContentValues(Cursor cursor, String field,
537 ContentValues values, String key) {
538 values.put(key, cursor.getString(cursor.getColumnIndexOrThrow(field)));
539 }
540
541 /**
542 * Reads an Integer out of a field in a Cursor and writes it to a Map.
543 *
544 * @param cursor The cursor to read from
545 * @param field The INTEGER field to read
546 * @param values The {@link ContentValues} to put the value into, with the field as the key
547 */
548 public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values) {
549 cursorIntToContentValues(cursor, field, values, field);
550 }
551
552 /**
553 * Reads a Integer out of a field in a Cursor and writes it to a Map.
554 *
555 * @param cursor The cursor to read from
556 * @param field The INTEGER field to read
557 * @param values The {@link ContentValues} to put the value into, with the field as the key
558 * @param key The key to store the value with in the map
559 */
560 public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values,
561 String key) {
562 int colIndex = cursor.getColumnIndex(field);
563 if (!cursor.isNull(colIndex)) {
564 values.put(key, cursor.getInt(colIndex));
565 } else {
566 values.put(key, (Integer) null);
567 }
568 }
569
570 /**
571 * Reads a Long out of a field in a Cursor and writes it to a Map.
572 *
573 * @param cursor The cursor to read from
574 * @param field The INTEGER field to read
575 * @param values The {@link ContentValues} to put the value into, with the field as the key
576 */
577 public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values)
578 {
579 cursorLongToContentValues(cursor, field, values, field);
580 }
581
582 /**
583 * Reads a Long out of a field in a Cursor and writes it to a Map.
584 *
585 * @param cursor The cursor to read from
586 * @param field The INTEGER field to read
587 * @param values The {@link ContentValues} to put the value into
588 * @param key The key to store the value with in the map
589 */
590 public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values,
591 String key) {
592 int colIndex = cursor.getColumnIndex(field);
593 if (!cursor.isNull(colIndex)) {
594 Long value = Long.valueOf(cursor.getLong(colIndex));
595 values.put(key, value);
596 } else {
597 values.put(key, (Long) null);
598 }
599 }
600
601 /**
602 * Reads a Double out of a field in a Cursor and writes it to a Map.
603 *
604 * @param cursor The cursor to read from
605 * @param field The REAL field to read
606 * @param values The {@link ContentValues} to put the value into
607 */
608 public static void cursorDoubleToCursorValues(Cursor cursor, String field, ContentValues values)
609 {
610 cursorDoubleToContentValues(cursor, field, values, field);
611 }
612
613 /**
614 * Reads a Double out of a field in a Cursor and writes it to a Map.
615 *
616 * @param cursor The cursor to read from
617 * @param field The REAL field to read
618 * @param values The {@link ContentValues} to put the value into
619 * @param key The key to store the value with in the map
620 */
621 public static void cursorDoubleToContentValues(Cursor cursor, String field,
622 ContentValues values, String key) {
623 int colIndex = cursor.getColumnIndex(field);
624 if (!cursor.isNull(colIndex)) {
625 values.put(key, cursor.getDouble(colIndex));
626 } else {
627 values.put(key, (Double) null);
628 }
629 }
630
631 /**
632 * Read the entire contents of a cursor row and store them in a ContentValues.
633 *
634 * @param cursor the cursor to read from.
635 * @param values the {@link ContentValues} to put the row into.
636 */
637 public static void cursorRowToContentValues(Cursor cursor, ContentValues values) {
638 AbstractWindowedCursor awc =
639 (cursor instanceof AbstractWindowedCursor) ? (AbstractWindowedCursor) cursor : null;
640
641 String[] columns = cursor.getColumnNames();
642 int length = columns.length;
643 for (int i = 0; i < length; i++) {
644 if (awc != null && awc.isBlob(i)) {
645 values.put(columns[i], cursor.getBlob(i));
646 } else {
647 values.put(columns[i], cursor.getString(i));
648 }
649 }
650 }
651
652 /**
653 * Query the table for the number of rows in the table.
654 * @param db the database the table is in
655 * @param table the name of the table to query
656 * @return the number of rows in the table
657 */
658 public static long queryNumEntries(SQLiteDatabase db, String table) {
Christian Mehlmauere7731f02010-06-16 22:56:07 +0200659 return queryNumEntries(db, table, null, null);
660 }
661
662 /**
663 * Query the table for the number of rows in the table.
664 * @param db the database the table is in
665 * @param table the name of the table to query
666 * @param selection A filter declaring which rows to return,
667 * formatted as an SQL WHERE clause (excluding the WHERE itself).
668 * Passing null will count all rows for the given table
669 * @return the number of rows in the table filtered by the selection
670 */
671 public static long queryNumEntries(SQLiteDatabase db, String table, String selection) {
672 return queryNumEntries(db, table, selection, null);
673 }
674
675 /**
676 * Query the table for the number of rows in the table.
677 * @param db the database the table is in
678 * @param table the name of the table to query
679 * @param selection A filter declaring which rows to return,
680 * formatted as an SQL WHERE clause (excluding the WHERE itself).
681 * Passing null will count all rows for the given table
682 * @param selectionArgs You may include ?s in selection,
683 * which will be replaced by the values from selectionArgs,
684 * in order that they appear in the selection.
685 * The values will be bound as Strings.
686 * @return the number of rows in the table filtered by the selection
687 */
688 public static long queryNumEntries(SQLiteDatabase db, String table, String selection,
689 String[] selectionArgs) {
690 String s = (!TextUtils.isEmpty(selection)) ? " where " + selection : "";
691 return longForQuery(db, "select count(*) from " + table + s,
692 selectionArgs);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800693 }
694
695 /**
696 * Utility method to run the query on the db and return the value in the
697 * first column of the first row.
698 */
699 public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
700 SQLiteStatement prog = db.compileStatement(query);
701 try {
702 return longForQuery(prog, selectionArgs);
703 } finally {
704 prog.close();
705 }
706 }
707
708 /**
709 * Utility method to run the pre-compiled query and return the value in the
710 * first column of the first row.
711 */
712 public static long longForQuery(SQLiteStatement prog, String[] selectionArgs) {
Vasu Nori0732f792010-07-29 17:24:12 -0700713 prog.bindAllArgsAsStrings(selectionArgs);
714 return prog.simpleQueryForLong();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800715 }
716
717 /**
718 * Utility method to run the query on the db and return the value in the
719 * first column of the first row.
720 */
721 public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
722 SQLiteStatement prog = db.compileStatement(query);
723 try {
724 return stringForQuery(prog, selectionArgs);
725 } finally {
726 prog.close();
727 }
728 }
729
730 /**
731 * Utility method to run the pre-compiled query and return the value in the
732 * first column of the first row.
733 */
734 public static String stringForQuery(SQLiteStatement prog, String[] selectionArgs) {
Vasu Nori0732f792010-07-29 17:24:12 -0700735 prog.bindAllArgsAsStrings(selectionArgs);
736 return prog.simpleQueryForString();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800737 }
738
739 /**
Bjorn Bringerta006b4722010-04-14 14:43:26 +0100740 * Utility method to run the query on the db and return the blob value in the
741 * first column of the first row.
742 *
743 * @return A read-only file descriptor for a copy of the blob value.
744 */
745 public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteDatabase db,
746 String query, String[] selectionArgs) {
747 SQLiteStatement prog = db.compileStatement(query);
748 try {
749 return blobFileDescriptorForQuery(prog, selectionArgs);
750 } finally {
751 prog.close();
752 }
753 }
754
755 /**
756 * Utility method to run the pre-compiled query and return the blob value in the
757 * first column of the first row.
758 *
759 * @return A read-only file descriptor for a copy of the blob value.
760 */
761 public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteStatement prog,
762 String[] selectionArgs) {
763 prog.bindAllArgsAsStrings(selectionArgs);
764 return prog.simpleQueryForBlobFileDescriptor();
765 }
766
767 /**
Fred Quintana2ec6c562009-12-09 16:00:31 -0800768 * Reads a String out of a column in a Cursor and writes it to a ContentValues.
769 * Adds nothing to the ContentValues if the column isn't present or if its value is null.
770 *
771 * @param cursor The cursor to read from
772 * @param column The column to read
773 * @param values The {@link ContentValues} to put the value into
774 */
775 public static void cursorStringToContentValuesIfPresent(Cursor cursor, ContentValues values,
776 String column) {
Dmitri Plotnikov9a9ce602010-07-26 15:52:07 -0700777 final int index = cursor.getColumnIndex(column);
778 if (index != -1 && !cursor.isNull(index)) {
Fred Quintana2ec6c562009-12-09 16:00:31 -0800779 values.put(column, cursor.getString(index));
780 }
781 }
782
783 /**
784 * Reads a Long out of a column in a Cursor and writes it to a ContentValues.
785 * Adds nothing to the ContentValues if the column isn't present or if its value is null.
786 *
787 * @param cursor The cursor to read from
788 * @param column The column to read
789 * @param values The {@link ContentValues} to put the value into
790 */
791 public static void cursorLongToContentValuesIfPresent(Cursor cursor, ContentValues values,
792 String column) {
Dmitri Plotnikov9a9ce602010-07-26 15:52:07 -0700793 final int index = cursor.getColumnIndex(column);
794 if (index != -1 && !cursor.isNull(index)) {
Fred Quintana2ec6c562009-12-09 16:00:31 -0800795 values.put(column, cursor.getLong(index));
796 }
797 }
798
799 /**
800 * Reads a Short out of a column in a Cursor and writes it to a ContentValues.
801 * Adds nothing to the ContentValues if the column isn't present or if its value is null.
802 *
803 * @param cursor The cursor to read from
804 * @param column The column to read
805 * @param values The {@link ContentValues} to put the value into
806 */
807 public static void cursorShortToContentValuesIfPresent(Cursor cursor, ContentValues values,
808 String column) {
Dmitri Plotnikov9a9ce602010-07-26 15:52:07 -0700809 final int index = cursor.getColumnIndex(column);
810 if (index != -1 && !cursor.isNull(index)) {
Fred Quintana2ec6c562009-12-09 16:00:31 -0800811 values.put(column, cursor.getShort(index));
812 }
813 }
814
815 /**
816 * Reads a Integer out of a column in a Cursor and writes it to a ContentValues.
817 * Adds nothing to the ContentValues if the column isn't present or if its value is null.
818 *
819 * @param cursor The cursor to read from
820 * @param column The column to read
821 * @param values The {@link ContentValues} to put the value into
822 */
823 public static void cursorIntToContentValuesIfPresent(Cursor cursor, ContentValues values,
824 String column) {
Dmitri Plotnikov9a9ce602010-07-26 15:52:07 -0700825 final int index = cursor.getColumnIndex(column);
826 if (index != -1 && !cursor.isNull(index)) {
Fred Quintana2ec6c562009-12-09 16:00:31 -0800827 values.put(column, cursor.getInt(index));
828 }
829 }
830
831 /**
832 * Reads a Float out of a column in a Cursor and writes it to a ContentValues.
833 * Adds nothing to the ContentValues if the column isn't present or if its value is null.
834 *
835 * @param cursor The cursor to read from
836 * @param column The column to read
837 * @param values The {@link ContentValues} to put the value into
838 */
839 public static void cursorFloatToContentValuesIfPresent(Cursor cursor, ContentValues values,
840 String column) {
Dmitri Plotnikov9a9ce602010-07-26 15:52:07 -0700841 final int index = cursor.getColumnIndex(column);
842 if (index != -1 && !cursor.isNull(index)) {
Fred Quintana2ec6c562009-12-09 16:00:31 -0800843 values.put(column, cursor.getFloat(index));
844 }
845 }
846
847 /**
848 * Reads a Double out of a column in a Cursor and writes it to a ContentValues.
849 * Adds nothing to the ContentValues if the column isn't present or if its value is null.
850 *
851 * @param cursor The cursor to read from
852 * @param column The column to read
853 * @param values The {@link ContentValues} to put the value into
854 */
855 public static void cursorDoubleToContentValuesIfPresent(Cursor cursor, ContentValues values,
856 String column) {
Dmitri Plotnikov9a9ce602010-07-26 15:52:07 -0700857 final int index = cursor.getColumnIndex(column);
858 if (index != -1 && !cursor.isNull(index)) {
Fred Quintana2ec6c562009-12-09 16:00:31 -0800859 values.put(column, cursor.getDouble(index));
860 }
861 }
862
863 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800864 * This class allows users to do multiple inserts into a table but
865 * compile the SQL insert statement only once, which may increase
866 * performance.
867 */
868 public static class InsertHelper {
869 private final SQLiteDatabase mDb;
870 private final String mTableName;
871 private HashMap<String, Integer> mColumns;
872 private String mInsertSQL = null;
873 private SQLiteStatement mInsertStatement = null;
874 private SQLiteStatement mReplaceStatement = null;
875 private SQLiteStatement mPreparedStatement = null;
876
877 /**
878 * {@hide}
879 *
880 * These are the columns returned by sqlite's "PRAGMA
881 * table_info(...)" command that we depend on.
882 */
883 public static final int TABLE_INFO_PRAGMA_COLUMNNAME_INDEX = 1;
884 public static final int TABLE_INFO_PRAGMA_DEFAULT_INDEX = 4;
885
886 /**
887 * @param db the SQLiteDatabase to insert into
888 * @param tableName the name of the table to insert into
889 */
890 public InsertHelper(SQLiteDatabase db, String tableName) {
891 mDb = db;
892 mTableName = tableName;
893 }
894
895 private void buildSQL() throws SQLException {
896 StringBuilder sb = new StringBuilder(128);
897 sb.append("INSERT INTO ");
898 sb.append(mTableName);
899 sb.append(" (");
900
901 StringBuilder sbv = new StringBuilder(128);
902 sbv.append("VALUES (");
903
904 int i = 1;
905 Cursor cur = null;
906 try {
907 cur = mDb.rawQuery("PRAGMA table_info(" + mTableName + ")", null);
908 mColumns = new HashMap<String, Integer>(cur.getCount());
909 while (cur.moveToNext()) {
910 String columnName = cur.getString(TABLE_INFO_PRAGMA_COLUMNNAME_INDEX);
911 String defaultValue = cur.getString(TABLE_INFO_PRAGMA_DEFAULT_INDEX);
912
913 mColumns.put(columnName, i);
914 sb.append("'");
915 sb.append(columnName);
916 sb.append("'");
917
918 if (defaultValue == null) {
919 sbv.append("?");
920 } else {
921 sbv.append("COALESCE(?, ");
922 sbv.append(defaultValue);
923 sbv.append(")");
924 }
925
926 sb.append(i == cur.getCount() ? ") " : ", ");
927 sbv.append(i == cur.getCount() ? ");" : ", ");
928 ++i;
929 }
930 } finally {
931 if (cur != null) cur.close();
932 }
933
934 sb.append(sbv);
935
936 mInsertSQL = sb.toString();
937 if (LOCAL_LOGV) Log.v(TAG, "insert statement is " + mInsertSQL);
938 }
939
940 private SQLiteStatement getStatement(boolean allowReplace) throws SQLException {
941 if (allowReplace) {
942 if (mReplaceStatement == null) {
943 if (mInsertSQL == null) buildSQL();
944 // chop "INSERT" off the front and prepend "INSERT OR REPLACE" instead.
945 String replaceSQL = "INSERT OR REPLACE" + mInsertSQL.substring(6);
946 mReplaceStatement = mDb.compileStatement(replaceSQL);
947 }
948 return mReplaceStatement;
949 } else {
950 if (mInsertStatement == null) {
951 if (mInsertSQL == null) buildSQL();
952 mInsertStatement = mDb.compileStatement(mInsertSQL);
953 }
954 return mInsertStatement;
955 }
956 }
957
958 /**
959 * Performs an insert, adding a new row with the given values.
960 *
961 * @param values the set of values with which to populate the
962 * new row
963 * @param allowReplace if true, the statement does "INSERT OR
964 * REPLACE" instead of "INSERT", silently deleting any
965 * previously existing rows that would cause a conflict
966 *
967 * @return the row ID of the newly inserted row, or -1 if an
968 * error occurred
969 */
970 private synchronized long insertInternal(ContentValues values, boolean allowReplace) {
971 try {
972 SQLiteStatement stmt = getStatement(allowReplace);
973 stmt.clearBindings();
974 if (LOCAL_LOGV) Log.v(TAG, "--- inserting in table " + mTableName);
975 for (Map.Entry<String, Object> e: values.valueSet()) {
976 final String key = e.getKey();
977 int i = getColumnIndex(key);
978 DatabaseUtils.bindObjectToProgram(stmt, i, e.getValue());
979 if (LOCAL_LOGV) {
980 Log.v(TAG, "binding " + e.getValue() + " to column " +
981 i + " (" + key + ")");
982 }
983 }
984 return stmt.executeInsert();
985 } catch (SQLException e) {
986 Log.e(TAG, "Error inserting " + values + " into table " + mTableName, e);
987 return -1;
988 }
989 }
990
991 /**
992 * Returns the index of the specified column. This is index is suitagble for use
993 * in calls to bind().
994 * @param key the column name
995 * @return the index of the column
996 */
997 public int getColumnIndex(String key) {
998 getStatement(false);
999 final Integer index = mColumns.get(key);
1000 if (index == null) {
1001 throw new IllegalArgumentException("column '" + key + "' is invalid");
1002 }
1003 return index;
1004 }
1005
1006 /**
1007 * Bind the value to an index. A prepareForInsert() or prepareForReplace()
1008 * without a matching execute() must have already have been called.
1009 * @param index the index of the slot to which to bind
1010 * @param value the value to bind
1011 */
1012 public void bind(int index, double value) {
1013 mPreparedStatement.bindDouble(index, value);
1014 }
1015
1016 /**
1017 * Bind the value to an index. A prepareForInsert() or prepareForReplace()
1018 * without a matching execute() must have already have been called.
1019 * @param index the index of the slot to which to bind
1020 * @param value the value to bind
1021 */
1022 public void bind(int index, float value) {
1023 mPreparedStatement.bindDouble(index, value);
1024 }
1025
1026 /**
1027 * Bind the value to an index. A prepareForInsert() or prepareForReplace()
1028 * without a matching execute() must have already have been called.
1029 * @param index the index of the slot to which to bind
1030 * @param value the value to bind
1031 */
1032 public void bind(int index, long value) {
1033 mPreparedStatement.bindLong(index, value);
1034 }
1035
1036 /**
1037 * Bind the value to an index. A prepareForInsert() or prepareForReplace()
1038 * without a matching execute() must have already have been called.
1039 * @param index the index of the slot to which to bind
1040 * @param value the value to bind
1041 */
1042 public void bind(int index, int value) {
1043 mPreparedStatement.bindLong(index, value);
1044 }
1045
1046 /**
1047 * Bind the value to an index. A prepareForInsert() or prepareForReplace()
1048 * without a matching execute() must have already have been called.
1049 * @param index the index of the slot to which to bind
1050 * @param value the value to bind
1051 */
1052 public void bind(int index, boolean value) {
1053 mPreparedStatement.bindLong(index, value ? 1 : 0);
1054 }
1055
1056 /**
1057 * Bind null to an index. A prepareForInsert() or prepareForReplace()
1058 * without a matching execute() must have already have been called.
1059 * @param index the index of the slot to which to bind
1060 */
1061 public void bindNull(int index) {
1062 mPreparedStatement.bindNull(index);
1063 }
1064
1065 /**
1066 * Bind the value to an index. A prepareForInsert() or prepareForReplace()
1067 * without a matching execute() must have already have been called.
1068 * @param index the index of the slot to which to bind
1069 * @param value the value to bind
1070 */
1071 public void bind(int index, byte[] value) {
1072 if (value == null) {
1073 mPreparedStatement.bindNull(index);
1074 } else {
1075 mPreparedStatement.bindBlob(index, value);
1076 }
1077 }
1078
1079 /**
1080 * Bind the value to an index. A prepareForInsert() or prepareForReplace()
1081 * without a matching execute() must have already have been called.
1082 * @param index the index of the slot to which to bind
1083 * @param value the value to bind
1084 */
1085 public void bind(int index, String value) {
1086 if (value == null) {
1087 mPreparedStatement.bindNull(index);
1088 } else {
1089 mPreparedStatement.bindString(index, value);
1090 }
1091 }
1092
1093 /**
1094 * Performs an insert, adding a new row with the given values.
1095 * If the table contains conflicting rows, an error is
1096 * returned.
1097 *
1098 * @param values the set of values with which to populate the
1099 * new row
1100 *
1101 * @return the row ID of the newly inserted row, or -1 if an
1102 * error occurred
1103 */
1104 public long insert(ContentValues values) {
1105 return insertInternal(values, false);
1106 }
1107
1108 /**
1109 * Execute the previously prepared insert or replace using the bound values
1110 * since the last call to prepareForInsert or prepareForReplace.
1111 *
1112 * <p>Note that calling bind() and then execute() is not thread-safe. The only thread-safe
1113 * way to use this class is to call insert() or replace().
1114 *
1115 * @return the row ID of the newly inserted row, or -1 if an
1116 * error occurred
1117 */
1118 public long execute() {
1119 if (mPreparedStatement == null) {
1120 throw new IllegalStateException("you must prepare this inserter before calling "
1121 + "execute");
1122 }
1123 try {
1124 if (LOCAL_LOGV) Log.v(TAG, "--- doing insert or replace in table " + mTableName);
1125 return mPreparedStatement.executeInsert();
1126 } catch (SQLException e) {
1127 Log.e(TAG, "Error executing InsertHelper with table " + mTableName, e);
1128 return -1;
1129 } finally {
1130 // you can only call this once per prepare
1131 mPreparedStatement = null;
1132 }
1133 }
1134
1135 /**
1136 * Prepare the InsertHelper for an insert. The pattern for this is:
1137 * <ul>
1138 * <li>prepareForInsert()
1139 * <li>bind(index, value);
1140 * <li>bind(index, value);
1141 * <li>...
1142 * <li>bind(index, value);
1143 * <li>execute();
1144 * </ul>
1145 */
1146 public void prepareForInsert() {
1147 mPreparedStatement = getStatement(false);
1148 mPreparedStatement.clearBindings();
1149 }
1150
1151 /**
1152 * Prepare the InsertHelper for a replace. The pattern for this is:
1153 * <ul>
1154 * <li>prepareForReplace()
1155 * <li>bind(index, value);
1156 * <li>bind(index, value);
1157 * <li>...
1158 * <li>bind(index, value);
1159 * <li>execute();
1160 * </ul>
1161 */
1162 public void prepareForReplace() {
1163 mPreparedStatement = getStatement(true);
1164 mPreparedStatement.clearBindings();
1165 }
1166
1167 /**
1168 * Performs an insert, adding a new row with the given values.
1169 * If the table contains conflicting rows, they are deleted
1170 * and replaced with the new row.
1171 *
1172 * @param values the set of values with which to populate the
1173 * new row
1174 *
1175 * @return the row ID of the newly inserted row, or -1 if an
1176 * error occurred
1177 */
1178 public long replace(ContentValues values) {
1179 return insertInternal(values, true);
1180 }
1181
1182 /**
1183 * Close this object and release any resources associated with
1184 * it. The behavior of calling <code>insert()</code> after
1185 * calling this method is undefined.
1186 */
1187 public void close() {
1188 if (mInsertStatement != null) {
1189 mInsertStatement.close();
1190 mInsertStatement = null;
1191 }
1192 if (mReplaceStatement != null) {
1193 mReplaceStatement.close();
1194 mReplaceStatement = null;
1195 }
1196 mInsertSQL = null;
1197 mColumns = null;
1198 }
1199 }
1200
1201 /**
1202 * Creates a db and populates it with the sql statements in sqlStatements.
1203 *
1204 * @param context the context to use to create the db
1205 * @param dbName the name of the db to create
1206 * @param dbVersion the version to set on the db
1207 * @param sqlStatements the statements to use to populate the db. This should be a single string
1208 * of the form returned by sqlite3's <tt>.dump</tt> command (statements separated by
1209 * semicolons)
1210 */
1211 static public void createDbFromSqlStatements(
1212 Context context, String dbName, int dbVersion, String sqlStatements) {
1213 SQLiteDatabase db = context.openOrCreateDatabase(dbName, 0, null);
1214 // TODO: this is not quite safe since it assumes that all semicolons at the end of a line
1215 // terminate statements. It is possible that a text field contains ;\n. We will have to fix
1216 // this if that turns out to be a problem.
1217 String[] statements = TextUtils.split(sqlStatements, ";\n");
1218 for (String statement : statements) {
1219 if (TextUtils.isEmpty(statement)) continue;
1220 db.execSQL(statement);
1221 }
1222 db.setVersion(dbVersion);
1223 db.close();
1224 }
Vasu Norice38b982010-07-22 13:57:13 -07001225
1226 /**
1227 * Returns one of the following which represent the type of the given SQL statement.
1228 * <ol>
1229 * <li>{@link #STATEMENT_SELECT}</li>
1230 * <li>{@link #STATEMENT_UPDATE}</li>
1231 * <li>{@link #STATEMENT_ATTACH}</li>
1232 * <li>{@link #STATEMENT_BEGIN}</li>
1233 * <li>{@link #STATEMENT_COMMIT}</li>
1234 * <li>{@link #STATEMENT_ABORT}</li>
1235 * <li>{@link #STATEMENT_OTHER}</li>
1236 * </ol>
1237 * @param sql the SQL statement whose type is returned by this method
1238 * @return one of the values listed above
1239 */
1240 public static int getSqlStatementType(String sql) {
1241 sql = sql.trim();
1242 if (sql.length() < 3) {
1243 return STATEMENT_OTHER;
1244 }
1245 String prefixSql = sql.substring(0, 3).toUpperCase();
1246 if (prefixSql.equals("SEL")) {
1247 return STATEMENT_SELECT;
1248 } else if (prefixSql.equals("INS") ||
1249 prefixSql.equals("UPD") ||
1250 prefixSql.equals("REP") ||
1251 prefixSql.equals("DEL")) {
1252 return STATEMENT_UPDATE;
1253 } else if (prefixSql.equals("ATT")) {
1254 return STATEMENT_ATTACH;
1255 } else if (prefixSql.equals("COM")) {
1256 return STATEMENT_COMMIT;
1257 } else if (prefixSql.equals("END")) {
1258 return STATEMENT_COMMIT;
1259 } else if (prefixSql.equals("ROL")) {
1260 return STATEMENT_ABORT;
1261 } else if (prefixSql.equals("BEG")) {
1262 return STATEMENT_BEGIN;
Vasu Nori4e874ed2010-09-15 18:40:49 -07001263 } else if (prefixSql.equals("PRA")) {
1264 return STATEMENT_PRAGMA;
1265 } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") ||
1266 prefixSql.equals("ALT")) {
1267 return STATEMENT_DDL;
1268 } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) {
1269 return STATEMENT_UNPREPARED;
Vasu Norice38b982010-07-22 13:57:13 -07001270 }
1271 return STATEMENT_OTHER;
1272 }
Jeff Hamiltonf0cfe342010-08-09 16:54:05 -05001273
1274 /**
1275 * Appends one set of selection args to another. This is useful when adding a selection
1276 * argument to a user provided set.
1277 */
1278 public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
1279 if (originalValues == null || originalValues.length == 0) {
1280 return newValues;
1281 }
1282 String[] result = new String[originalValues.length + newValues.length ];
1283 System.arraycopy(originalValues, 0, result, 0, originalValues.length);
1284 System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
1285 return result;
1286 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001287}