Java tutorial
/* * Copyright (C) 2014 University of Washington * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package org.path.common.android.utilities; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.opendatakit.aggregate.odktables.rest.ConflictType; import org.opendatakit.aggregate.odktables.rest.ElementDataType; import org.opendatakit.aggregate.odktables.rest.ElementType; import org.opendatakit.aggregate.odktables.rest.KeyValueStoreConstants; import org.opendatakit.aggregate.odktables.rest.SavepointTypeManipulator; import org.opendatakit.aggregate.odktables.rest.SyncState; import org.opendatakit.aggregate.odktables.rest.TableConstants; import org.opendatakit.aggregate.odktables.rest.entity.Column; import org.path.common.android.data.ColorRule; import org.path.common.android.data.ColumnDefinition; import org.path.common.android.data.KeyValueStoreEntry; import org.path.common.android.data.TableDefinitionEntry; import org.path.common.android.data.UserTable; import org.path.common.android.database.DatabaseConstants; import org.path.common.android.provider.ColumnDefinitionsColumns; import org.path.common.android.provider.DataTableColumns; import org.path.common.android.provider.InstanceColumns; import org.path.common.android.provider.KeyValueStoreColumns; import org.path.common.android.provider.TableDefinitionsColumns; import org.path.common.android.utilities.StaticStateManipulator.IStaticFieldManipulator; import android.annotation.SuppressLint; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; public class ODKDatabaseUtils { private static final String t = "ODKDatabaseUtils"; /** * values that can be returned from getTableHealth() */ public static final int TABLE_HEALTH_IS_CLEAN = 0; public static final int TABLE_HEALTH_HAS_CONFLICTS = 1; public static final int TABLE_HEALTH_HAS_CHECKPOINTS = 2; public static final int TABLE_HEALTH_HAS_CHECKPOINTS_AND_CONFLICTS = 3; public static final String DEFAULT_LOCALE = "default"; public static final String DEFAULT_CREATOR = "anonymous"; /* * These are the columns that are present in any row in the database. Each row * should have these in addition to the user-defined columns. If you add a * column here you have to be sure to also add it in the create table * statement, which can't be programmatically created easily. */ private static final List<String> ADMIN_COLUMNS; /** * These are the columns that should be exported */ private static final List<String> EXPORT_COLUMNS; static { ArrayList<String> adminColumns = new ArrayList<String>(); adminColumns.add(DataTableColumns.ID); adminColumns.add(DataTableColumns.ROW_ETAG); adminColumns.add(DataTableColumns.SYNC_STATE); // not exportable adminColumns.add(DataTableColumns.CONFLICT_TYPE); // not exportable adminColumns.add(DataTableColumns.FILTER_TYPE); adminColumns.add(DataTableColumns.FILTER_VALUE); adminColumns.add(DataTableColumns.FORM_ID); adminColumns.add(DataTableColumns.LOCALE); adminColumns.add(DataTableColumns.SAVEPOINT_TYPE); adminColumns.add(DataTableColumns.SAVEPOINT_TIMESTAMP); adminColumns.add(DataTableColumns.SAVEPOINT_CREATOR); Collections.sort(adminColumns); ADMIN_COLUMNS = Collections.unmodifiableList(adminColumns); ArrayList<String> exportColumns = new ArrayList<String>(); exportColumns.add(DataTableColumns.ID); exportColumns.add(DataTableColumns.ROW_ETAG); exportColumns.add(DataTableColumns.FILTER_TYPE); exportColumns.add(DataTableColumns.FILTER_VALUE); exportColumns.add(DataTableColumns.FORM_ID); exportColumns.add(DataTableColumns.LOCALE); exportColumns.add(DataTableColumns.SAVEPOINT_TYPE); exportColumns.add(DataTableColumns.SAVEPOINT_TIMESTAMP); exportColumns.add(DataTableColumns.SAVEPOINT_CREATOR); Collections.sort(exportColumns); EXPORT_COLUMNS = Collections.unmodifiableList(exportColumns); } private static ODKDatabaseUtils databaseUtil = new ODKDatabaseUtils(); static { // register a state-reset manipulator for 'databaseUtil' field. StaticStateManipulator.get().register(50, new IStaticFieldManipulator() { @Override public void reset() { databaseUtil = new ODKDatabaseUtils(); } }); } public static ODKDatabaseUtils get() { return databaseUtil; } /** * For mocking -- supply a mocked object. * * @param util */ public static void set(ODKDatabaseUtils util) { databaseUtil = util; } protected ODKDatabaseUtils() { } /** * Return an unmodifiable list of the admin columns that must be present in * every database table. * * @return */ public List<String> getAdminColumns() { return ADMIN_COLUMNS; } /** * Return an unmodifiable list of the admin columns that should be exported to * a CSV file. This list excludes the SYNC_STATE and CONFLICT_TYPE columns. * * @return */ public List<String> getExportColumns() { return EXPORT_COLUMNS; } /** * Perform a raw query with bind parameters. * * @param db * @param sql * @param selectionArgs * @return */ public Cursor rawQuery(SQLiteDatabase db, String sql, String[] selectionArgs) { Cursor c = db.rawQuery(sql, selectionArgs); return c; } /** * Perform a query with the given parameters. * * @param db * @param distinct * true if each returned row should be distinct (collapse duplicates) * @param table * @param columns * @param selection * @param selectionArgs * @param groupBy * @param having * @param orderBy * @param limit * @return */ public Cursor query(SQLiteDatabase db, boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { Cursor c = db.query(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); return c; } /** * Get a {@link UserTable} for this table based on the given where clause. All * columns from the table are returned. * <p> * SELECT * FROM table WHERE whereClause GROUP BY groupBy[]s HAVING * havingClause ORDER BY orderbyElement orderByDirection * <p> * If any of the clause parts are omitted (null), then the appropriate * simplified SQL statement is constructed. * * @param db * @param appName * @param tableId * @param columnDefns * @param whereClause * the whereClause for the selection, beginning with "WHERE". Must * include "?" instead of actual values, which are instead passed in * the selectionArgs. * @param selectionArgs * an array of string values for bind parameters * @param groupBy * an array of elementKeys * @param having * @param orderByElementKey * elementKey to order the results by * @param orderByDirection * either "ASC" or "DESC" * @return */ public UserTable rawSqlQuery(SQLiteDatabase db, String appName, String tableId, ArrayList<ColumnDefinition> columnDefns, String whereClause, String[] selectionArgs, String[] groupBy, String having, String orderByElementKey, String orderByDirection) { Cursor c = null; try { StringBuilder s = new StringBuilder(); s.append("SELECT * FROM \"").append(tableId).append("\" "); if (whereClause != null && whereClause.length() != 0) { s.append(" WHERE ").append(whereClause); } if (groupBy != null && groupBy.length != 0) { s.append(" GROUP BY "); boolean first = true; for (String elementKey : groupBy) { if (!first) { s.append(", "); } first = false; s.append(elementKey); } if (having != null && having.length() != 0) { s.append(" HAVING ").append(having); } } if (orderByElementKey != null && orderByElementKey.length() != 0) { s.append(" ORDER BY ").append(orderByElementKey); if (orderByDirection != null && orderByDirection.length() != 0) { s.append(" ").append(orderByDirection); } else { s.append(" ASC"); } } String sqlQuery = s.toString(); c = db.rawQuery(sqlQuery, selectionArgs); UserTable table = new UserTable(c, appName, tableId, columnDefns, whereClause, selectionArgs, groupBy, having, orderByElementKey, orderByDirection); return table; } finally { if (c != null && !c.isClosed()) { c.close(); } } } /** * Return the row(s) for the given tableId and rowId. If the row has * checkpoints or conflicts, the returned UserTable will have more than one * Row returned. Otherwise, it will contain a single row. * * @param db * @param appName * @param tableId * @param orderedDefns * @param rowId * @return */ public UserTable getDataInExistingDBTableWithId(SQLiteDatabase db, String appName, String tableId, ArrayList<ColumnDefinition> orderedDefns, String rowId) { UserTable table = rawSqlQuery(db, appName, tableId, orderedDefns, DataTableColumns.ID + "=?", new String[] { rowId }, null, null, DataTableColumns.SAVEPOINT_TIMESTAMP, "DESC"); return table; } /** * Return all the columns in the given table, including any metadata columns. * This does a direct query against the database and is suitable for accessing * non-managed tables. It does not access any metadata and therefore will not * report non-unit-of-retention (grouping) columns. * * @param db * @param tableId * @return */ public String[] getAllColumnNames(SQLiteDatabase db, String tableId) { Cursor cursor = db.rawQuery("SELECT * FROM " + tableId + " LIMIT 1", null); String[] colNames = cursor.getColumnNames(); return colNames; } /** * Retrieve the list of user-defined columns for a tableId using the metadata * for that table. Returns the unit-of-retention and non-unit-of-retention * (grouping) columns. * * @param db * @param tableId * @return */ public ArrayList<Column> getUserDefinedColumns(SQLiteDatabase db, String tableId) { ArrayList<Column> userDefinedColumns = new ArrayList<Column>(); String selection = ColumnDefinitionsColumns.TABLE_ID + "=?"; String[] selectionArgs = { tableId }; //@formatter:off String[] cols = { ColumnDefinitionsColumns.ELEMENT_KEY, ColumnDefinitionsColumns.ELEMENT_NAME, ColumnDefinitionsColumns.ELEMENT_TYPE, ColumnDefinitionsColumns.LIST_CHILD_ELEMENT_KEYS }; //@formatter:on Cursor c = null; try { c = db.query(DatabaseConstants.COLUMN_DEFINITIONS_TABLE_NAME, cols, selection, selectionArgs, null, null, ColumnDefinitionsColumns.ELEMENT_KEY + " ASC"); int elemKeyIndex = c.getColumnIndexOrThrow(ColumnDefinitionsColumns.ELEMENT_KEY); int elemNameIndex = c.getColumnIndexOrThrow(ColumnDefinitionsColumns.ELEMENT_NAME); int elemTypeIndex = c.getColumnIndexOrThrow(ColumnDefinitionsColumns.ELEMENT_TYPE); int listChildrenIndex = c.getColumnIndexOrThrow(ColumnDefinitionsColumns.LIST_CHILD_ELEMENT_KEYS); c.moveToFirst(); while (!c.isAfterLast()) { String elementKey = getIndexAsString(c, elemKeyIndex); String elementName = getIndexAsString(c, elemNameIndex); String elementType = getIndexAsString(c, elemTypeIndex); String listOfChildren = getIndexAsString(c, listChildrenIndex); userDefinedColumns.add(new Column(elementKey, elementName, elementType, listOfChildren)); c.moveToNext(); } } finally { if (c != null && !c.isClosed()) { c.close(); } } return userDefinedColumns; } /** * Verifies that the tableId exists in the database. * * @param db * @param tableId * @return true if table is listed in table definitions. */ public boolean hasTableId(SQLiteDatabase db, String tableId) { Cursor c = null; try { //@formatter:off c = db.query(DatabaseConstants.TABLE_DEFS_TABLE_NAME, null, TableDefinitionsColumns.TABLE_ID + "=?", new String[] { tableId }, null, null, null); //@formatter:on if (c.moveToFirst()) { // we know about the table... // tableId is the database table name... return true; } } finally { if (c != null && !c.isClosed()) { c.close(); } } return false; } /** * Return the health of a data table. The health can be one of * <ul> * <li>TABLE_HEALTH_IS_CLEAN = 0</li> * <li>TABLE_HEALTH_HAS_CONFLICTS = 1</li> * <li>TABLE_HEALTH_HAS_CHECKPOINTS = 2</li> * <li>TABLE_HEALTH_HAS_CHECKPOINTS_AND_CONFLICTS = 3</li> * <ul> * * @param db * @param tableId * @return */ public int getTableHealth(SQLiteDatabase db, String tableId) { StringBuilder b = new StringBuilder(); b.append("SELECT SUM(case when _savepoint_type is null then 1 else 0 end) as checkpoints,") .append("SUM(case when _conflict_type is not null then 1 else 0 end) as conflicts from \"") .append(tableId).append("\""); Cursor c = null; try { c = db.rawQuery(b.toString(), null); int idxCheckpoints = c.getColumnIndex("checkpoints"); int idxConflicts = c.getColumnIndex("conflicts"); c.moveToFirst(); Integer checkpoints = ODKDatabaseUtils.get().getIndexAsType(c, Integer.class, idxCheckpoints); Integer conflicts = ODKDatabaseUtils.get().getIndexAsType(c, Integer.class, idxConflicts); c.close(); int outcome = TABLE_HEALTH_IS_CLEAN; if (checkpoints != null && checkpoints != 0) { outcome += TABLE_HEALTH_HAS_CHECKPOINTS; } if (conflicts != null && conflicts != 0) { outcome += TABLE_HEALTH_HAS_CONFLICTS; } return outcome; } finally { if (c != null && !c.isClosed()) { c.close(); } } } /** * Return all the tableIds in the database. * * @param db * @return an ArrayList<String> of tableIds */ public ArrayList<String> getAllTableIds(SQLiteDatabase db) { ArrayList<String> tableIds = new ArrayList<String>(); Cursor c = null; try { c = db.query(DatabaseConstants.TABLE_DEFS_TABLE_NAME, new String[] { TableDefinitionsColumns.TABLE_ID }, null, null, null, null, TableDefinitionsColumns.TABLE_ID + " ASC"); if (c.moveToFirst()) { int idxId = c.getColumnIndex(TableDefinitionsColumns.TABLE_ID); do { String tableId = c.getString(idxId); if (tableId == null || tableId.length() == 0) { c.close(); throw new IllegalStateException("getAllTableIds: Unexpected tableId found!"); } tableIds.add(tableId); } while (c.moveToNext()); } } finally { if (c != null && !c.isClosed()) { c.close(); } } return tableIds; } /** * Drop the given tableId and remove all the files (both configuration and * data attachments) associated with that table. * * @param db * @param appName * @param tableId */ public void deleteDBTableAndAllData(SQLiteDatabase db, final String appName, final String tableId) { SyncETagsUtils seu = new SyncETagsUtils(); boolean dbWithinTransaction = db.inTransaction(); try { String whereClause = TableDefinitionsColumns.TABLE_ID + " = ?"; String[] whereArgs = { tableId }; if (!dbWithinTransaction) { db.beginTransaction(); } // Drop the table used for the formId db.execSQL("DROP TABLE IF EXISTS \"" + tableId + "\";"); // Delete the server sync ETags associated with this table seu.deleteAllSyncETags(db, tableId); // Delete the table definition for the tableId int count = db.delete(DatabaseConstants.TABLE_DEFS_TABLE_NAME, whereClause, whereArgs); // Delete the column definitions for this tableId db.delete(DatabaseConstants.COLUMN_DEFINITIONS_TABLE_NAME, whereClause, whereArgs); // Delete the uploads for the tableId String uploadWhereClause = InstanceColumns.DATA_TABLE_TABLE_ID + " = ?"; db.delete(DatabaseConstants.UPLOADS_TABLE_NAME, uploadWhereClause, whereArgs); // Delete the values from the 4 key value stores db.delete(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME, whereClause, whereArgs); db.delete(DatabaseConstants.KEY_VALULE_STORE_SYNC_TABLE_NAME, whereClause, whereArgs); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } // And delete the files from the SDCard... String tableDir = ODKFileUtils.getTablesFolder(appName, tableId); try { FileUtils.deleteDirectory(new File(tableDir)); } catch (IOException e1) { e1.printStackTrace(); throw new IllegalStateException("Unable to delete the " + tableDir + " directory", e1); } String assetsCsvDir = ODKFileUtils.getAssetsFolder(appName) + "/csv"; try { Collection<File> files = FileUtils.listFiles(new File(assetsCsvDir), new IOFileFilter() { @Override public boolean accept(File file) { String[] parts = file.getName().split("\\."); return (parts[0].equals(tableId) && parts[parts.length - 1].equals("csv") && (parts.length == 2 || parts.length == 3 || (parts.length == 4 && parts[parts.length - 2].equals("properties")))); } @Override public boolean accept(File dir, String name) { String[] parts = name.split("\\."); return (parts[0].equals(tableId) && parts[parts.length - 1].equals("csv") && (parts.length == 2 || parts.length == 3 || (parts.length == 4 && parts[parts.length - 2].equals("properties")))); } }, new IOFileFilter() { // don't traverse into directories @Override public boolean accept(File arg0) { return false; } // don't traverse into directories @Override public boolean accept(File arg0, String arg1) { return false; } }); FileUtils.deleteDirectory(new File(tableDir)); for (File f : files) { FileUtils.deleteQuietly(f); } } catch (IOException e1) { e1.printStackTrace(); throw new IllegalStateException("Unable to delete the " + tableDir + " directory", e1); } } /** * Update the schema and data-modification ETags of a given tableId. * * @param db * @param tableId * @param schemaETag * @param lastDataETag */ public void updateDBTableETags(SQLiteDatabase db, String tableId, String schemaETag, String lastDataETag) { if (tableId == null || tableId.length() <= 0) { throw new IllegalArgumentException(t + ": application name and table name must be specified"); } ContentValues cvTableDef = new ContentValues(); cvTableDef.put(TableDefinitionsColumns.SCHEMA_ETAG, schemaETag); cvTableDef.put(TableDefinitionsColumns.LAST_DATA_ETAG, lastDataETag); boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.update(DatabaseConstants.TABLE_DEFS_TABLE_NAME, cvTableDef, TableDefinitionsColumns.TABLE_ID + "=?", new String[] { tableId }); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * Update the timestamp of the last entirely-successful synchronization * attempt of this table. * * @param db * @param tableId */ public void updateDBTableLastSyncTime(SQLiteDatabase db, String tableId) { if (tableId == null || tableId.length() <= 0) { throw new IllegalArgumentException(t + ": application name and table name must be specified"); } ContentValues cvTableDef = new ContentValues(); cvTableDef.put(TableDefinitionsColumns.LAST_SYNC_TIME, TableConstants.nanoSecondsFromMillis(System.currentTimeMillis())); boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.update(DatabaseConstants.TABLE_DEFS_TABLE_NAME, cvTableDef, TableDefinitionsColumns.TABLE_ID + "=?", new String[] { tableId }); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * Get the table definition entry for a tableId. This specifies the schema * ETag, the data-modification ETag, and the date-time of the last successful * sync of the table to the server. * * @param db * @param tableId * @return */ public TableDefinitionEntry getTableDefinitionEntry(SQLiteDatabase db, String tableId) { TableDefinitionEntry e = null; Cursor c = null; try { StringBuilder b = new StringBuilder(); ArrayList<String> selArgs = new ArrayList<String>(); b.append(KeyValueStoreColumns.TABLE_ID).append("=?"); selArgs.add(tableId); c = db.query(DatabaseConstants.TABLE_DEFS_TABLE_NAME, null, b.toString(), selArgs.toArray(new String[selArgs.size()]), null, null, null); if (c.moveToFirst()) { int idxSchemaETag = c.getColumnIndex(TableDefinitionsColumns.SCHEMA_ETAG); int idxLastDataETag = c.getColumnIndex(TableDefinitionsColumns.LAST_DATA_ETAG); int idxLastSyncTime = c.getColumnIndex(TableDefinitionsColumns.LAST_SYNC_TIME); if (c.getCount() != 1) { throw new IllegalStateException( "Two or more TableDefinitionEntry records found for tableId " + tableId); } e = new TableDefinitionEntry(tableId); e.setSchemaETag(c.getString(idxSchemaETag)); e.setLastDataETag(c.getString(idxLastDataETag)); e.setLastSyncTime(c.getString(idxLastSyncTime)); } } finally { if (c != null && !c.isClosed()) { c.close(); } } return e; } /** * Insert or update a single table-level metadata KVS entry. * * @param db * @param entry */ public void replaceDBTableMetadata(SQLiteDatabase db, KeyValueStoreEntry entry) { ContentValues values = new ContentValues(); values.put(KeyValueStoreColumns.TABLE_ID, entry.tableId); values.put(KeyValueStoreColumns.PARTITION, entry.partition); values.put(KeyValueStoreColumns.ASPECT, entry.aspect); values.put(KeyValueStoreColumns.VALUE_TYPE, entry.type); values.put(KeyValueStoreColumns.VALUE, entry.value); values.put(KeyValueStoreColumns.KEY, entry.key); boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.replace(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME, null, values); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * Insert or update a list of table-level metadata KVS entries. If clear is * true, then delete the existing set of values for this tableId before * inserting the new values. * * @param db * @param tableId * @param metadata * a List<KeyValueStoreEntry> * @param clear * if true then delete the existing set of values for this tableId * before inserting the new ones. */ public void replaceDBTableMetadata(SQLiteDatabase db, String tableId, List<KeyValueStoreEntry> metadata, boolean clear) { boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } if (clear) { db.delete(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME, KeyValueStoreColumns.TABLE_ID + "=?", new String[] { tableId }); } for (KeyValueStoreEntry e : metadata) { ContentValues values = new ContentValues(); if (!tableId.equals(e.tableId)) { throw new IllegalArgumentException( "updateDBTableMetadata: expected all kvs entries to share the same tableId"); } if (e.value == null || e.value.trim().length() == 0) { deleteDBTableMetadata(db, e.tableId, e.partition, e.aspect, e.key); } else { values.put(KeyValueStoreColumns.TABLE_ID, e.tableId); values.put(KeyValueStoreColumns.PARTITION, e.partition); values.put(KeyValueStoreColumns.ASPECT, e.aspect); values.put(KeyValueStoreColumns.KEY, e.key); values.put(KeyValueStoreColumns.VALUE_TYPE, e.type); values.put(KeyValueStoreColumns.VALUE, e.value); db.replace(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME, null, values); } } if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * The deletion filter includes all non-null arguments. If all arguments * (except the db) are null, then all properties are removed. * * @param db * @param tableId * @param partition * @param aspect * @param key */ public void deleteDBTableMetadata(SQLiteDatabase db, String tableId, String partition, String aspect, String key) { StringBuilder b = new StringBuilder(); ArrayList<String> selArgs = new ArrayList<String>(); if (tableId != null) { b.append(KeyValueStoreColumns.TABLE_ID).append("=?"); selArgs.add(tableId); } if (partition != null) { if (b.length() != 0) { b.append(" AND "); } b.append(KeyValueStoreColumns.PARTITION).append("=?"); selArgs.add(partition); } if (aspect != null) { if (b.length() != 0) { b.append(" AND "); } b.append(KeyValueStoreColumns.ASPECT).append("=?"); selArgs.add(aspect); } if (key != null) { if (b.length() != 0) { b.append(" AND "); } b.append(KeyValueStoreColumns.KEY).append("=?"); selArgs.add(key); } boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.delete(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME, b.toString(), selArgs.toArray(new String[selArgs.size()])); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * Filters results by all non-null field values. * * @param db * @param tableId * @param partition * @param aspect * @param key * @return */ public ArrayList<KeyValueStoreEntry> getDBTableMetadata(SQLiteDatabase db, String tableId, String partition, String aspect, String key) { ArrayList<KeyValueStoreEntry> entries = new ArrayList<KeyValueStoreEntry>(); Cursor c = null; try { StringBuilder b = new StringBuilder(); ArrayList<String> selArgs = new ArrayList<String>(); if (tableId != null) { b.append(KeyValueStoreColumns.TABLE_ID).append("=?"); selArgs.add(tableId); } if (partition != null) { if (b.length() != 0) { b.append(" AND "); } b.append(KeyValueStoreColumns.PARTITION).append("=?"); selArgs.add(partition); } if (aspect != null) { if (b.length() != 0) { b.append(" AND "); } b.append(KeyValueStoreColumns.ASPECT).append("=?"); selArgs.add(aspect); } if (key != null) { if (b.length() != 0) { b.append(" AND "); } b.append(KeyValueStoreColumns.KEY).append("=?"); selArgs.add(key); } c = db.query(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME, null, b.toString(), selArgs.toArray(new String[selArgs.size()]), null, null, null); if (c.moveToFirst()) { int idxPartition = c.getColumnIndex(KeyValueStoreColumns.PARTITION); int idxAspect = c.getColumnIndex(KeyValueStoreColumns.ASPECT); int idxKey = c.getColumnIndex(KeyValueStoreColumns.KEY); int idxType = c.getColumnIndex(KeyValueStoreColumns.VALUE_TYPE); int idxValue = c.getColumnIndex(KeyValueStoreColumns.VALUE); do { KeyValueStoreEntry e = new KeyValueStoreEntry(); e.partition = c.getString(idxPartition); e.aspect = c.getString(idxAspect); e.key = c.getString(idxKey); e.type = c.getString(idxType); e.value = c.getString(idxValue); entries.add(e); } while (c.moveToNext()); } } finally { if (c != null && !c.isClosed()) { c.close(); } } return entries; } /** * Clean up the KVS row data types. This simplifies the migration process by * enforcing the proper data types regardless of what the values are in the * imported CSV files. * * @param db * @param tableId */ public void enforceTypesDBTableMetadata(SQLiteDatabase db, String tableId) { boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } StringBuilder b = new StringBuilder(); b.setLength(0); //@formatter:off b.append("UPDATE \"").append(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME).append("\" SET ") .append(KeyValueStoreColumns.VALUE_TYPE).append("=? WHERE ") .append(KeyValueStoreColumns.PARTITION).append("=? AND ").append(KeyValueStoreColumns.KEY) .append("=?"); //@formatter:on String sql = b.toString(); String[] fields = new String[3]; // for columns fields[0] = ElementDataType.array.name(); fields[1] = KeyValueStoreConstants.PARTITION_COLUMN; fields[2] = KeyValueStoreConstants.COLUMN_DISPLAY_CHOICES_LIST; db.execSQL(sql, fields); fields[0] = ElementDataType.string.name(); fields[1] = KeyValueStoreConstants.PARTITION_COLUMN; fields[2] = KeyValueStoreConstants.COLUMN_DISPLAY_FORMAT; db.execSQL(sql, fields); fields[0] = ElementDataType.object.name(); fields[1] = KeyValueStoreConstants.PARTITION_COLUMN; fields[2] = KeyValueStoreConstants.COLUMN_DISPLAY_NAME; db.execSQL(sql, fields); fields[0] = ElementDataType.bool.name(); fields[1] = KeyValueStoreConstants.PARTITION_COLUMN; fields[2] = KeyValueStoreConstants.COLUMN_DISPLAY_VISIBLE; db.execSQL(sql, fields); fields[0] = ElementDataType.array.name(); fields[1] = KeyValueStoreConstants.PARTITION_COLUMN; fields[2] = KeyValueStoreConstants.COLUMN_JOINS; db.execSQL(sql, fields); // and for the table... fields[0] = ElementDataType.array.name(); fields[1] = KeyValueStoreConstants.PARTITION_TABLE; fields[2] = KeyValueStoreConstants.TABLE_COL_ORDER; db.execSQL(sql, fields); fields[0] = ElementDataType.object.name(); fields[1] = KeyValueStoreConstants.PARTITION_TABLE; fields[2] = KeyValueStoreConstants.TABLE_DISPLAY_NAME; db.execSQL(sql, fields); fields[0] = ElementDataType.array.name(); fields[1] = KeyValueStoreConstants.PARTITION_TABLE; fields[2] = KeyValueStoreConstants.TABLE_GROUP_BY_COLS; db.execSQL(sql, fields); fields[0] = ElementDataType.string.name(); fields[1] = KeyValueStoreConstants.PARTITION_TABLE; fields[2] = KeyValueStoreConstants.TABLE_INDEX_COL; db.execSQL(sql, fields); fields[0] = ElementDataType.object.name(); fields[1] = KeyValueStoreConstants.PARTITION_TABLE; fields[2] = KeyValueStoreConstants.TABLE_SORT_COL; db.execSQL(sql, fields); fields[0] = ElementDataType.object.name(); fields[1] = KeyValueStoreConstants.PARTITION_TABLE; fields[2] = KeyValueStoreConstants.TABLE_SORT_ORDER; db.execSQL(sql, fields); // TODO: color rule groups if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /* * Create a user defined database table metadata - table definiton and KVS * values */ private void createDBTableMetadata(SQLiteDatabase db, String tableId) { if (tableId == null || tableId.length() <= 0) { throw new IllegalArgumentException(t + ": application name and table name must be specified"); } // Add the table id into table definitions ContentValues cvTableDef = new ContentValues(); cvTableDef.put(TableDefinitionsColumns.TABLE_ID, tableId); cvTableDef.putNull(TableDefinitionsColumns.SCHEMA_ETAG); cvTableDef.putNull(TableDefinitionsColumns.LAST_DATA_ETAG); cvTableDef.put(TableDefinitionsColumns.LAST_SYNC_TIME, -1); db.replaceOrThrow(DatabaseConstants.TABLE_DEFS_TABLE_NAME, null, cvTableDef); // Add the tables values into KVS ArrayList<ContentValues> cvTableValKVS = new ArrayList<ContentValues>(); ContentValues cvTableVal = null; cvTableVal = new ContentValues(); cvTableVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvTableVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_TABLE); cvTableVal.put(KeyValueStoreColumns.ASPECT, KeyValueStoreConstants.ASPECT_DEFAULT); cvTableVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.TABLE_COL_ORDER); cvTableVal.put(KeyValueStoreColumns.VALUE_TYPE, "array"); cvTableVal.put(KeyValueStoreColumns.VALUE, "[]"); cvTableValKVS.add(cvTableVal); cvTableVal = new ContentValues(); cvTableVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvTableVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_TABLE); cvTableVal.put(KeyValueStoreColumns.ASPECT, KeyValueStoreConstants.ASPECT_DEFAULT); cvTableVal.put(KeyValueStoreColumns.KEY, "defaultViewType"); cvTableVal.put(KeyValueStoreColumns.VALUE_TYPE, "string"); cvTableVal.put(KeyValueStoreColumns.VALUE, "SPREADSHEET"); cvTableValKVS.add(cvTableVal); cvTableVal = new ContentValues(); cvTableVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvTableVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_TABLE); cvTableVal.put(KeyValueStoreColumns.ASPECT, KeyValueStoreConstants.ASPECT_DEFAULT); cvTableVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.TABLE_DISPLAY_NAME); cvTableVal.put(KeyValueStoreColumns.VALUE_TYPE, "object"); cvTableVal.put(KeyValueStoreColumns.VALUE, "\"" + tableId + "\""); cvTableValKVS.add(cvTableVal); cvTableVal = new ContentValues(); cvTableVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvTableVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_TABLE); cvTableVal.put(KeyValueStoreColumns.ASPECT, KeyValueStoreConstants.ASPECT_DEFAULT); cvTableVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.TABLE_GROUP_BY_COLS); cvTableVal.put(KeyValueStoreColumns.VALUE_TYPE, "array"); cvTableVal.put(KeyValueStoreColumns.VALUE, "[]"); cvTableValKVS.add(cvTableVal); cvTableVal = new ContentValues(); cvTableVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvTableVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_TABLE); cvTableVal.put(KeyValueStoreColumns.ASPECT, KeyValueStoreConstants.ASPECT_DEFAULT); cvTableVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.TABLE_INDEX_COL); cvTableVal.put(KeyValueStoreColumns.VALUE_TYPE, "string"); cvTableVal.put(KeyValueStoreColumns.VALUE, ""); cvTableValKVS.add(cvTableVal); cvTableVal = new ContentValues(); cvTableVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvTableVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_TABLE); cvTableVal.put(KeyValueStoreColumns.ASPECT, KeyValueStoreConstants.ASPECT_DEFAULT); cvTableVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.TABLE_SORT_COL); cvTableVal.put(KeyValueStoreColumns.VALUE_TYPE, "string"); cvTableVal.put(KeyValueStoreColumns.VALUE, ""); cvTableValKVS.add(cvTableVal); cvTableVal = new ContentValues(); cvTableVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvTableVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_TABLE); cvTableVal.put(KeyValueStoreColumns.ASPECT, KeyValueStoreConstants.ASPECT_DEFAULT); cvTableVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.TABLE_SORT_ORDER); cvTableVal.put(KeyValueStoreColumns.VALUE_TYPE, "string"); cvTableVal.put(KeyValueStoreColumns.VALUE, ""); cvTableValKVS.add(cvTableVal); cvTableVal = new ContentValues(); cvTableVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvTableVal.put(KeyValueStoreColumns.PARTITION, "TableColorRuleGroup"); cvTableVal.put(KeyValueStoreColumns.ASPECT, KeyValueStoreConstants.ASPECT_DEFAULT); cvTableVal.put(KeyValueStoreColumns.KEY, "StatusColumn.ruleList"); cvTableVal.put(KeyValueStoreColumns.VALUE_TYPE, "object"); try { List<ColorRule> rules = ColorRuleUtil.getDefaultSyncStateColorRules(); List<TreeMap<String, Object>> jsonableList = new ArrayList<TreeMap<String, Object>>(); for (ColorRule rule : rules) { jsonableList.add(rule.getJsonRepresentation()); } String value = ODKFileUtils.mapper.writeValueAsString(jsonableList); cvTableVal.put(KeyValueStoreColumns.VALUE, value); cvTableValKVS.add(cvTableVal); } catch (JsonProcessingException e) { e.printStackTrace(); } // Now add Tables values into KVS for (int i = 0; i < cvTableValKVS.size(); i++) { db.replaceOrThrow(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME, null, cvTableValKVS.get(i)); } } /* * Build the start of a create table statement -- specifies all the metadata * columns. Caller must then add all the user-defined column definitions and * closing parentheses. */ private String getUserDefinedTableCreationStatement(String tableId) { /* * Resulting string should be the following String createTableCmd = * "CREATE TABLE IF NOT EXISTS " + tableId + " (" + DataTableColumns.ID + * " TEXT NOT NULL, " + DataTableColumns.ROW_ETAG + " TEXT NULL, " + * DataTableColumns.SYNC_STATE + " TEXT NOT NULL, " + * DataTableColumns.CONFLICT_TYPE + " INTEGER NULL," + * DataTableColumns.FILTER_TYPE + " TEXT NULL," + * DataTableColumns.FILTER_VALUE + " TEXT NULL," + DataTableColumns.FORM_ID * + " TEXT NULL," + DataTableColumns.LOCALE + " TEXT NULL," + * DataTableColumns.SAVEPOINT_TYPE + " TEXT NULL," + * DataTableColumns.SAVEPOINT_TIMESTAMP + " TEXT NOT NULL," + * DataTableColumns.SAVEPOINT_CREATOR + " TEXT NULL"; */ String createTableCmd = "CREATE TABLE IF NOT EXISTS " + tableId + " ("; List<String> cols = getAdminColumns(); String endSeq = ", "; for (int i = 0; i < cols.size(); ++i) { if (i == cols.size() - 1) { endSeq = ""; } String colName = cols.get(i); //@formatter:off if (colName.equals(DataTableColumns.ID) || colName.equals(DataTableColumns.SYNC_STATE) || colName.equals(DataTableColumns.SAVEPOINT_TIMESTAMP)) { createTableCmd = createTableCmd + colName + " TEXT NOT NULL" + endSeq; } else if (colName.equals(DataTableColumns.ROW_ETAG) || colName.equals(DataTableColumns.FILTER_TYPE) || colName.equals(DataTableColumns.FILTER_VALUE) || colName.equals(DataTableColumns.FORM_ID) || colName.equals(DataTableColumns.LOCALE) || colName.equals(DataTableColumns.SAVEPOINT_TYPE) || colName.equals(DataTableColumns.SAVEPOINT_CREATOR)) { createTableCmd = createTableCmd + colName + " TEXT NULL" + endSeq; } else if (colName.equals(DataTableColumns.CONFLICT_TYPE)) { createTableCmd = createTableCmd + colName + " INTEGER NULL" + endSeq; } //@formatter:on } return createTableCmd; } /* * Create a user defined database table metadata - table definiton and KVS * values */ public void createDBTableWithColumns(SQLiteDatabase db, String appName, String tableId, List<ColumnDefinition> orderedDefs) { if (tableId == null || tableId.length() <= 0) { throw new IllegalArgumentException(t + ": application name and table name must be specified"); } String createTableCmd = getUserDefinedTableCreationStatement(tableId); StringBuilder createTableCmdWithCols = new StringBuilder(); createTableCmdWithCols.append(createTableCmd); for (ColumnDefinition column : orderedDefs) { if (!column.isUnitOfRetention()) { continue; } ElementType elementType = column.getType(); ElementDataType dataType = elementType.getDataType(); String dbType; if (dataType == ElementDataType.array) { dbType = "TEXT"; } else if (dataType == ElementDataType.bool) { dbType = "INTEGER"; } else if (dataType == ElementDataType.configpath) { dbType = "TEXT"; } else if (dataType == ElementDataType.integer) { dbType = "INTEGER"; } else if (dataType == ElementDataType.number) { dbType = "REAL"; } else if (dataType == ElementDataType.object) { dbType = "TEXT"; } else if (dataType == ElementDataType.rowpath) { dbType = "TEXT"; } else if (dataType == ElementDataType.string) { dbType = "TEXT"; } else { throw new IllegalStateException("unexpected ElementDataType: " + dataType.name()); } //@formatter:off createTableCmdWithCols.append(", ").append(column.getElementKey()).append(" ").append(dbType) .append(" NULL"); //@formatter:on } createTableCmdWithCols.append(");"); db.execSQL(createTableCmdWithCols.toString()); // Create the metadata for the table - table def and KVS createDBTableMetadata(db, tableId); // Now need to call the function to write out all the column values for (ColumnDefinition column : orderedDefs) { createNewColumnMetadata(db, tableId, column); } // Need to address column order ContentValues cvTableVal = new ContentValues(); cvTableVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvTableVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_TABLE); cvTableVal.put(KeyValueStoreColumns.ASPECT, KeyValueStoreConstants.ASPECT_DEFAULT); cvTableVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.TABLE_COL_ORDER); cvTableVal.put(KeyValueStoreColumns.VALUE_TYPE, "array"); StringBuilder tableDefCol = new StringBuilder(); boolean needsComma = false; for (ColumnDefinition def : orderedDefs) { if (!def.isUnitOfRetention()) { continue; } if (needsComma) { tableDefCol.append(","); } needsComma = true; tableDefCol.append("\"").append(def.getElementKey()).append("\""); } WebLogger.getLogger(appName).i(t, "Column order for table " + tableId + " is " + tableDefCol.toString()); String colOrderVal = "[" + tableDefCol.toString() + "]"; cvTableVal.put(KeyValueStoreColumns.VALUE, colOrderVal); // Now add Tables values into KVS db.replaceOrThrow(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME, null, cvTableVal); } /* * Create a new column metadata in the database - add column values to KVS and * column definitions */ private void createNewColumnMetadata(SQLiteDatabase db, String tableId, ColumnDefinition column) { String colName = column.getElementKey(); ArrayList<ContentValues> cvColValKVS = new ArrayList<ContentValues>(); ContentValues cvColVal; cvColVal = new ContentValues(); cvColVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvColVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_COLUMN); cvColVal.put(KeyValueStoreColumns.ASPECT, colName); cvColVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.COLUMN_DISPLAY_CHOICES_LIST); cvColVal.put(KeyValueStoreColumns.VALUE_TYPE, ElementDataType.array.name()); cvColVal.put(KeyValueStoreColumns.VALUE, "[]"); cvColValKVS.add(cvColVal); cvColVal = new ContentValues(); cvColVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvColVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_COLUMN); cvColVal.put(KeyValueStoreColumns.ASPECT, colName); cvColVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.COLUMN_DISPLAY_FORMAT); cvColVal.put(KeyValueStoreColumns.VALUE_TYPE, ElementDataType.string.name()); cvColVal.put(KeyValueStoreColumns.VALUE, ""); cvColValKVS.add(cvColVal); cvColVal = new ContentValues(); cvColVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvColVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_COLUMN); cvColVal.put(KeyValueStoreColumns.ASPECT, colName); cvColVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.COLUMN_DISPLAY_NAME); cvColVal.put(KeyValueStoreColumns.VALUE_TYPE, ElementDataType.object.name()); String colDisplayName = "\"" + colName + "\""; cvColVal.put(KeyValueStoreColumns.VALUE, colDisplayName); cvColValKVS.add(cvColVal); // TODO: change bool to be integer valued in the KVS? cvColVal = new ContentValues(); cvColVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvColVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_COLUMN); cvColVal.put(KeyValueStoreColumns.ASPECT, colName); cvColVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.COLUMN_DISPLAY_VISIBLE); cvColVal.put(KeyValueStoreColumns.VALUE_TYPE, ElementDataType.bool.name()); cvColVal.put(KeyValueStoreColumns.VALUE, column.isUnitOfRetention() ? "true" : "false"); cvColValKVS.add(cvColVal); cvColVal = new ContentValues(); cvColVal.put(KeyValueStoreColumns.TABLE_ID, tableId); cvColVal.put(KeyValueStoreColumns.PARTITION, KeyValueStoreConstants.PARTITION_COLUMN); cvColVal.put(KeyValueStoreColumns.ASPECT, colName); cvColVal.put(KeyValueStoreColumns.KEY, KeyValueStoreConstants.COLUMN_JOINS); cvColVal.put(KeyValueStoreColumns.VALUE_TYPE, ElementDataType.object.name()); cvColVal.put(KeyValueStoreColumns.VALUE, ""); cvColValKVS.add(cvColVal); // Now add all this data into the database for (int i = 0; i < cvColValKVS.size(); i++) { db.replaceOrThrow(DatabaseConstants.KEY_VALUE_STORE_ACTIVE_TABLE_NAME, null, cvColValKVS.get(i)); } // Create column definition ContentValues cvColDefVal = null; cvColDefVal = new ContentValues(); cvColDefVal.put(ColumnDefinitionsColumns.TABLE_ID, tableId); cvColDefVal.put(ColumnDefinitionsColumns.ELEMENT_KEY, colName); cvColDefVal.put(ColumnDefinitionsColumns.ELEMENT_NAME, column.getElementName()); cvColDefVal.put(ColumnDefinitionsColumns.ELEMENT_TYPE, column.getElementType()); cvColDefVal.put(ColumnDefinitionsColumns.LIST_CHILD_ELEMENT_KEYS, column.getListChildElementKeys()); // Now add this data into the database db.replaceOrThrow(DatabaseConstants.COLUMN_DEFINITIONS_TABLE_NAME, null, cvColDefVal); } /** * If the tableId is not recorded in the TableDefinition metadata table, then * create the tableId with the indicated columns. This will synthesize * reasonable metadata KVS entries for table. * * If the tableId is present, then this is a no-op. * * @param db * @param appName * @param tableId * @param columns * @return the ArrayList<ColumnDefinition> of the user columns in the table. */ public ArrayList<ColumnDefinition> createOrOpenDBTableWithColumns(SQLiteDatabase db, String appName, String tableId, List<Column> columns) { boolean dbWithinTransaction = db.inTransaction(); boolean success = false; ArrayList<ColumnDefinition> orderedDefs = ColumnDefinition.buildColumnDefinitions(appName, tableId, columns); try { if (!dbWithinTransaction) { db.beginTransaction(); } if (!hasTableId(db, tableId)) { createDBTableWithColumns(db, appName, tableId, orderedDefs); } if (!dbWithinTransaction) { db.setTransactionSuccessful(); } success = true; return orderedDefs; } finally { if (!dbWithinTransaction) { db.endTransaction(); } if (success == false) { // Get the names of the columns StringBuilder colNames = new StringBuilder(); if (columns != null) { for (Column column : columns) { colNames.append(" ").append(column.getElementKey()).append(","); } if (colNames != null && colNames.length() > 0) { colNames.deleteCharAt(colNames.length() - 1); WebLogger.getLogger(appName).e(t, "createOrOpenDBTableWithColumns: Error while adding table " + tableId + " with columns:" + colNames.toString()); } } else { WebLogger.getLogger(appName).e(t, "createOrOpenDBTableWithColumns: Error while adding table " + tableId + " with columns: null"); } } } } /** * Call this when the schema on the server has changed w.r.t. the schema on * the device. In this case, we do not know whether the rows on the device * match those on the server. * * <ul> * <li>Reset all 'in_conflict' rows to their original local state (changed or * deleted).</li> * <li>Leave all 'deleted' rows in 'deleted' state.</li> * <li>Leave all 'changed' rows in 'changed' state.</li> * <li>Reset all 'synced' rows to 'new_row' to ensure they are sync'd to the * server.</li> * <li>Reset all 'synced_pending_files' rows to 'new_row' to ensure they are * sync'd to the server.</li> * </ul> * * @param db * @param tableId */ public void changeDataRowsToNewRowState(SQLiteDatabase db, String tableId) { StringBuilder b = new StringBuilder(); // remove server conflicting rows b.setLength(0); b.append("DELETE FROM \"").append(tableId).append("\" WHERE ").append(DataTableColumns.SYNC_STATE) .append(" =? AND ").append(DataTableColumns.CONFLICT_TYPE).append(" IN (?, ?)"); String sqlConflictingServer = b.toString(); //@formatter:off String argsConflictingServer[] = { SyncState.in_conflict.name(), Integer.toString(ConflictType.SERVER_DELETED_OLD_VALUES), Integer.toString(ConflictType.SERVER_UPDATED_UPDATED_VALUES) }; //@formatter:on // update local delete conflicts to deletes b.setLength(0); //@formatter:off b.append("UPDATE \"").append(tableId).append("\" SET ").append(DataTableColumns.SYNC_STATE).append(" =?, ") .append(DataTableColumns.CONFLICT_TYPE).append(" = null WHERE ") .append(DataTableColumns.CONFLICT_TYPE).append(" = ?"); //@formatter:on String sqlConflictingLocalDeleting = b.toString(); //@formatter:off String argsConflictingLocalDeleting[] = { SyncState.deleted.name(), Integer.toString(ConflictType.LOCAL_DELETED_OLD_VALUES) }; //@formatter:on // update local update conflicts to updates String sqlConflictingLocalUpdating = sqlConflictingLocalDeleting; //@formatter:off String argsConflictingLocalUpdating[] = { SyncState.changed.name(), Integer.toString(ConflictType.LOCAL_UPDATED_UPDATED_VALUES) }; //@formatter:on // reset all 'rest' rows to 'insert' b.setLength(0); //@formatter:off b.append("UPDATE \"").append(tableId).append("\" SET ").append(DataTableColumns.SYNC_STATE) .append(" =? WHERE ").append(DataTableColumns.SYNC_STATE).append(" =?"); //@formatter:on String sqlRest = b.toString(); //@formatter:off String argsRest[] = { SyncState.new_row.name(), SyncState.synced.name() }; //@formatter:on String sqlRestPendingFiles = sqlRest; //@formatter:off String argsRestPendingFiles[] = { SyncState.new_row.name(), SyncState.synced_pending_files.name() }; //@formatter:on boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.execSQL(sqlConflictingServer, argsConflictingServer); db.execSQL(sqlConflictingLocalDeleting, argsConflictingLocalDeleting); db.execSQL(sqlConflictingLocalUpdating, argsConflictingLocalUpdating); db.execSQL(sqlRest, argsRest); db.execSQL(sqlRestPendingFiles, argsRestPendingFiles); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * Deletes the server conflict row (if any) for this rowId in this tableId. * * @param db * @param tableId * @param rowId */ public void deleteServerConflictRowWithId(SQLiteDatabase db, String tableId, String rowId) { // delete the old server-values in_conflict row if it exists String whereClause = String.format("%s = ? AND %s = ? AND %s IN " + "( ?, ? )", DataTableColumns.ID, DataTableColumns.SYNC_STATE, DataTableColumns.CONFLICT_TYPE); String[] whereArgs = { rowId, SyncState.in_conflict.name(), String.valueOf(ConflictType.SERVER_DELETED_OLD_VALUES), String.valueOf(ConflictType.SERVER_UPDATED_UPDATED_VALUES) }; boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.delete(tableId, whereClause, whereArgs); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * Change the conflictType for the given row from null (not in conflict) to * the specified one. * * @param db * @param tableId * @param rowId * @param conflictType * expected to be one of ConflictType.LOCAL_DELETED_OLD_VALUES (0) or * ConflictType.LOCAL_UPDATED_UPDATED_VALUES (1) */ public void placeRowIntoConflict(SQLiteDatabase db, String tableId, String rowId, int conflictType) { String whereClause = String.format("%s = ? AND %s IS NULL", DataTableColumns.ID, DataTableColumns.CONFLICT_TYPE); String[] whereArgs = { rowId }; ContentValues cv = new ContentValues(); cv.put(DataTableColumns.SYNC_STATE, SyncState.in_conflict.name()); cv.put(DataTableColumns.CONFLICT_TYPE, conflictType); boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.update(tableId, cv, whereClause, whereArgs); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * Changes the conflictType for the given row from the specified one to null * and set the sync state of this row to the indicated value. In general, you * should first update the local conflict record with its new values, then * call deleteServerConflictRowWithId(...) and then call this method. * * @param db * @param tableId * @param rowId * @param syncState * @param conflictType */ public void restoreRowFromConflict(SQLiteDatabase db, String tableId, String rowId, SyncState syncState, int conflictType) { String whereClause = String.format("%s = ? AND %s = ?", DataTableColumns.ID, DataTableColumns.CONFLICT_TYPE); String[] whereArgs = { rowId, String.valueOf(conflictType) }; ContentValues cv = new ContentValues(); cv.putNull(DataTableColumns.CONFLICT_TYPE); cv.put(DataTableColumns.SYNC_STATE, syncState.name()); boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.update(tableId, cv, whereClause, whereArgs); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * * @param db * @param appName * @param tableId * @param rowId * @return the sync state of the row (see {@link SyncState}), or null if the * row does not exist. */ public SyncState getSyncState(SQLiteDatabase db, String appName, String tableId, String rowId) { Cursor c = null; try { c = db.query(tableId, new String[] { DataTableColumns.SYNC_STATE }, DataTableColumns.ID + " = ?", new String[] { rowId }, null, null, null); if (c.moveToFirst()) { int syncStateIndex = c.getColumnIndex(DataTableColumns.SYNC_STATE); if (!c.isNull(syncStateIndex)) { String val = getIndexAsString(c, syncStateIndex); return SyncState.valueOf(val); } } return null; } finally { if (c != null && !c.isClosed()) { c.close(); } } } /** * Delete the specified rowId in this tableId. Deletion respects sync * semantics. If the row is in the SyncState.new_row state, then the row and * its associated file attachments are immediately deleted. Otherwise, the row * is placed into the SyncState.deleted state and will be retained until the * device can delete the record on the server. * <p> * If you need to immediately delete a record that would otherwise sync to the * server, call updateRowETagAndSyncState(...) to set the row to * SyncState.new_row, and then call this method and it will be immediately * deleted (in this case, unless the record on the server was already deleted, * it will remain and not be deleted during any subsequent synchronizations). * * @param db * @param appName * @param tableId * @param rowId */ public void deleteDataInExistingDBTableWithId(SQLiteDatabase db, String appName, String tableId, String rowId) { SyncState syncState = getSyncState(db, appName, tableId, rowId); boolean dbWithinTransaction = db.inTransaction(); if (syncState == SyncState.new_row) { String[] whereArgs = { rowId }; String whereClause = DataTableColumns.ID + " = ?"; try { if (!dbWithinTransaction) { db.beginTransaction(); } db.delete(tableId, whereClause, whereArgs); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } File instanceFolder = new File(ODKFileUtils.getInstanceFolder(appName, tableId, rowId)); try { FileUtils.deleteDirectory(instanceFolder); } catch (IOException e) { // TODO Auto-generated catch block WebLogger.getLogger(appName).e(t, "Unable to delete this directory: " + instanceFolder.getAbsolutePath()); WebLogger.getLogger(appName).printStackTrace(e); } } else if (syncState == SyncState.synced || syncState == SyncState.changed) { String[] whereArgs = { rowId }; ContentValues values = new ContentValues(); values.put(DataTableColumns.SYNC_STATE, SyncState.deleted.name()); values.put(DataTableColumns.SAVEPOINT_TIMESTAMP, TableConstants.nanoSecondsFromMillis(System.currentTimeMillis())); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.update(tableId, values, DataTableColumns.ID + " = ?", whereArgs); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } } /* * Internal method to execute a delete statement with the given where clause */ private void rawDeleteDataInDBTable(SQLiteDatabase db, String tableId, String whereClause, String[] whereArgs) { boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.delete(tableId, whereClause, whereArgs); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * Delete any checkpoint rows for the given rowId in the tableId. Checkpoint * rows are created by ODK Survey to hold intermediate values during the * filling-in of the form. They act as restore points in the Survey, should * the application die. * * @param db * @param appName * @param tableId * @param rowId */ public void deleteCheckpointRowsWithId(SQLiteDatabase db, String appName, String tableId, String rowId) { rawDeleteDataInDBTable(db, tableId, DataTableColumns.ID + "=? AND " + DataTableColumns.SAVEPOINT_TYPE + " IS NULL", new String[] { rowId }); } /** * Update all rows for the given rowId to SavepointType 'INCOMPLETE' and * remove all but the most recent row. When used with a rowId that has * checkpoints, this updates to the most recent checkpoint and removes any * earlier checkpoints, incomplete or complete savepoints. Otherwise, it has * the general effect of resetting the rowId to an INCOMPLETE state. * * @param db * @param tableId * @param rowId */ public void saveAsIncompleteMostRecentCheckpointDataInDBTableWithId(SQLiteDatabase db, String tableId, String rowId) { boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.execSQL( "UPDATE \"" + tableId + "\" SET " + DataTableColumns.SAVEPOINT_TYPE + "= ? WHERE " + DataTableColumns.ID + "=?", new String[] { SavepointTypeManipulator.incomplete(), rowId }); db.delete(tableId, DataTableColumns.ID + "=? AND " + DataTableColumns.SAVEPOINT_TIMESTAMP + " NOT IN (SELECT MAX(" + DataTableColumns.SAVEPOINT_TIMESTAMP + ") FROM \"" + tableId + "\" WHERE " + DataTableColumns.ID + "=?)", new String[] { rowId, rowId }); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * Update the given rowId with the values in the cvValues. If certain metadata * values are not specified in the cvValues, then suitable default values may * be supplied for them. Furthermore, if the cvValues do not specify certain * metadata fields, then an exception may be thrown if there are more than one * row matching this rowId. * * @param db * @param tableId * @param orderedColumns * @param cvValues * @param rowId */ public void updateDataInExistingDBTableWithId(SQLiteDatabase db, String tableId, ArrayList<ColumnDefinition> orderedColumns, ContentValues cvValues, String rowId) { if (cvValues.size() <= 0) { throw new IllegalArgumentException(t + ": No values to add into table " + tableId); } ContentValues cvDataTableVal = new ContentValues(); cvDataTableVal.put(DataTableColumns.ID, rowId); cvDataTableVal.putAll(cvValues); upsertDataIntoExistingDBTable(db, tableId, orderedColumns, cvDataTableVal, true); } /** * Insert the given rowId with the values in the cvValues. If certain metadata * values are not specified in the cvValues, then suitable default values may * be supplied for them. * * If a row with this rowId and certain matching metadata fields is present, * then an exception is thrown. * * @param db * @param tableId * @param orderedColumns * @param cvValues * @param uuid */ public void insertDataIntoExistingDBTableWithId(SQLiteDatabase db, String tableId, ArrayList<ColumnDefinition> orderedColumns, ContentValues cvValues, String uuid) { if (cvValues.size() <= 0) { throw new IllegalArgumentException(t + ": No values to add into table " + tableId); } ContentValues cvDataTableVal = new ContentValues(); cvDataTableVal.put(DataTableColumns.ID, uuid); cvDataTableVal.putAll(cvValues); upsertDataIntoExistingDBTable(db, tableId, orderedColumns, cvDataTableVal, false); } /* * Write data into a user defined database table * * TODO: This is broken w.r.t. updates of partial fields */ private void upsertDataIntoExistingDBTable(SQLiteDatabase db, String tableId, ArrayList<ColumnDefinition> orderedColumns, ContentValues cvValues, boolean shouldUpdate) { String rowId = null; String whereClause = null; boolean specifiesConflictType = cvValues.containsKey(DataTableColumns.CONFLICT_TYPE); boolean nullConflictType = specifiesConflictType && (cvValues.get(DataTableColumns.CONFLICT_TYPE) == null); String[] whereArgs = new String[specifiesConflictType ? (1 + (nullConflictType ? 0 : 1)) : 1]; boolean update = false; if (cvValues.size() <= 0) { throw new IllegalArgumentException(t + ": No values to add into table " + tableId); } ContentValues cvDataTableVal = new ContentValues(); cvDataTableVal.putAll(cvValues); if (cvDataTableVal.containsKey(DataTableColumns.ID)) { // The user specified a row id; we need to determine whether to // insert or update the record, or to reject the action because // there are either checkpoint records for this row id, or, if // a server conflict is associated with this row, that the // _conflict_type to update was not specified. // // i.e., the tuple (_id, _conflict_type) should be unique. If // we find that there are more than 0 or 1 records matching this // tuple, then we should reject the update request. // // TODO: perhaps we want to allow updates to the local conflict // row if there are no checkpoints on it? I.e., change the // tri-state conflict type to a pair of states (local / remote). // and all local changes are flagged local. Remote only exists // if the server is in conflict. rowId = cvDataTableVal.getAsString(DataTableColumns.ID); if (rowId == null) { throw new IllegalArgumentException(DataTableColumns.ID + ", if specified, cannot be null"); } if (specifiesConflictType) { if (nullConflictType) { whereClause = DataTableColumns.ID + " = ?" + " AND " + DataTableColumns.CONFLICT_TYPE + " IS NULL"; whereArgs[0] = rowId; } else { whereClause = DataTableColumns.ID + " = ?" + " AND " + DataTableColumns.CONFLICT_TYPE + " = ?"; whereArgs[0] = rowId; whereArgs[1] = cvValues.getAsString(DataTableColumns.CONFLICT_TYPE); } } else { whereClause = DataTableColumns.ID + " = ?"; whereArgs[0] = rowId; } String sel = "SELECT * FROM " + tableId + " WHERE " + whereClause; String[] selArgs = whereArgs; Cursor cursor = rawQuery(db, sel, selArgs); // There must be only one row in the db for the update to work if (shouldUpdate) { if (cursor.getCount() == 1) { update = true; } else if (cursor.getCount() > 1) { throw new IllegalArgumentException( t + ": row id " + rowId + " has more than 1 row in table " + tableId); } } else { if (cursor.getCount() > 0) { throw new IllegalArgumentException( t + ": id " + rowId + " is already present in table " + tableId); } } } else { rowId = "uuid:" + UUID.randomUUID().toString(); } // TODO: This is broken w.r.t. updates of partial fields // TODO: This is broken w.r.t. updates of partial fields // TODO: This is broken w.r.t. updates of partial fields // TODO: This is broken w.r.t. updates of partial fields if (!cvDataTableVal.containsKey(DataTableColumns.ID)) { cvDataTableVal.put(DataTableColumns.ID, rowId); } if (update) { if (!cvDataTableVal.containsKey(DataTableColumns.SYNC_STATE) || (cvDataTableVal.get(DataTableColumns.SYNC_STATE) == null)) { cvDataTableVal.put(DataTableColumns.SYNC_STATE, SyncState.changed.name()); } if (cvDataTableVal.containsKey(DataTableColumns.LOCALE) && (cvDataTableVal.get(DataTableColumns.LOCALE) == null)) { cvDataTableVal.put(DataTableColumns.LOCALE, DataTableColumns.DEFAULT_LOCALE); } if (cvDataTableVal.containsKey(DataTableColumns.SAVEPOINT_TYPE) && (cvDataTableVal.get(DataTableColumns.SAVEPOINT_TYPE) == null)) { cvDataTableVal.put(DataTableColumns.SAVEPOINT_TYPE, SavepointTypeManipulator.complete()); } if (!cvDataTableVal.containsKey(DataTableColumns.SAVEPOINT_TIMESTAMP) || cvDataTableVal.get(DataTableColumns.SAVEPOINT_TIMESTAMP) == null) { String timeStamp = TableConstants.nanoSecondsFromMillis(System.currentTimeMillis()); cvDataTableVal.put(DataTableColumns.SAVEPOINT_TIMESTAMP, timeStamp); } if (!cvDataTableVal.containsKey(DataTableColumns.SAVEPOINT_CREATOR) || (cvDataTableVal.get(DataTableColumns.SAVEPOINT_CREATOR) == null)) { cvDataTableVal.put(DataTableColumns.SAVEPOINT_CREATOR, DataTableColumns.DEFAULT_SAVEPOINT_CREATOR); } } else { if (!cvDataTableVal.containsKey(DataTableColumns.ROW_ETAG) || cvDataTableVal.get(DataTableColumns.ROW_ETAG) == null) { cvDataTableVal.put(DataTableColumns.ROW_ETAG, DataTableColumns.DEFAULT_ROW_ETAG); } if (!cvDataTableVal.containsKey(DataTableColumns.SYNC_STATE) || (cvDataTableVal.get(DataTableColumns.SYNC_STATE) == null)) { cvDataTableVal.put(DataTableColumns.SYNC_STATE, SyncState.new_row.name()); } if (!cvDataTableVal.containsKey(DataTableColumns.CONFLICT_TYPE)) { cvDataTableVal.putNull(DataTableColumns.CONFLICT_TYPE); } if (!cvDataTableVal.containsKey(DataTableColumns.FILTER_TYPE) || (cvDataTableVal.get(DataTableColumns.FILTER_TYPE) == null)) { cvDataTableVal.put(DataTableColumns.FILTER_TYPE, DataTableColumns.DEFAULT_FILTER_TYPE); } if (!cvDataTableVal.containsKey(DataTableColumns.FILTER_VALUE) || (cvDataTableVal.get(DataTableColumns.FILTER_VALUE) == null)) { cvDataTableVal.put(DataTableColumns.FILTER_VALUE, DataTableColumns.DEFAULT_FILTER_VALUE); } if (!cvDataTableVal.containsKey(DataTableColumns.FORM_ID)) { cvDataTableVal.putNull(DataTableColumns.FORM_ID); } if (!cvDataTableVal.containsKey(DataTableColumns.LOCALE) || (cvDataTableVal.get(DataTableColumns.LOCALE) == null)) { cvDataTableVal.put(DataTableColumns.LOCALE, DataTableColumns.DEFAULT_LOCALE); } if (!cvDataTableVal.containsKey(DataTableColumns.SAVEPOINT_TYPE) || (cvDataTableVal.get(DataTableColumns.SAVEPOINT_TYPE) == null)) { cvDataTableVal.put(DataTableColumns.SAVEPOINT_TYPE, SavepointTypeManipulator.complete()); } if (!cvDataTableVal.containsKey(DataTableColumns.SAVEPOINT_TIMESTAMP) || cvDataTableVal.get(DataTableColumns.SAVEPOINT_TIMESTAMP) == null) { String timeStamp = TableConstants.nanoSecondsFromMillis(System.currentTimeMillis()); cvDataTableVal.put(DataTableColumns.SAVEPOINT_TIMESTAMP, timeStamp); } if (!cvDataTableVal.containsKey(DataTableColumns.SAVEPOINT_CREATOR) || (cvDataTableVal.get(DataTableColumns.SAVEPOINT_CREATOR) == null)) { cvDataTableVal.put(DataTableColumns.SAVEPOINT_CREATOR, DataTableColumns.DEFAULT_SAVEPOINT_CREATOR); } } cleanUpValuesMap(orderedColumns, cvDataTableVal); boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } if (update) { db.update(tableId, cvDataTableVal, whereClause, whereArgs); } else { db.insertOrThrow(tableId, null, cvDataTableVal); } if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * Update the ETag and SyncState of a given rowId. There should be exactly one * record for this rowId in thed database (i.e., no conflicts or checkpoints). * * @param db * @param tableId * @param rowId * @param rowETag * @param state */ public void updateRowETagAndSyncState(SQLiteDatabase db, String tableId, String rowId, String rowETag, SyncState state) { String whereClause = DataTableColumns.ID + " = ?"; String[] whereArgs = { rowId }; ContentValues cvDataTableVal = new ContentValues(); String sel = "SELECT * FROM " + tableId + " WHERE " + whereClause; String[] selArgs = whereArgs; Cursor cursor = rawQuery(db, sel, selArgs); // There must be only one row in the db if (cursor.getCount() != 1) { throw new IllegalArgumentException( t + ": row id " + rowId + " does not have exactly 1 row in table " + tableId); } cvDataTableVal.put(DataTableColumns.ROW_ETAG, rowETag); cvDataTableVal.put(DataTableColumns.SYNC_STATE, state.name()); boolean dbWithinTransaction = db.inTransaction(); try { if (!dbWithinTransaction) { db.beginTransaction(); } db.update(tableId, cvDataTableVal, whereClause, whereArgs); if (!dbWithinTransaction) { db.setTransactionSuccessful(); } } finally { if (!dbWithinTransaction) { db.endTransaction(); } } } /** * If the caller specified a complex json value for a structured type, flush * the value through to the individual columns. * * @param orderedColumns * @param values */ private void cleanUpValuesMap(ArrayList<ColumnDefinition> orderedColumns, ContentValues values) { Map<String, String> toBeResolved = new HashMap<String, String>(); for (String key : values.keySet()) { if (DataTableColumns.CONFLICT_TYPE.equals(key)) { continue; } else if (DataTableColumns.FILTER_TYPE.equals(key)) { continue; } else if (DataTableColumns.FILTER_TYPE.equals(key)) { continue; } else if (DataTableColumns.FILTER_VALUE.equals(key)) { continue; } else if (DataTableColumns.FORM_ID.equals(key)) { continue; } else if (DataTableColumns.ID.equals(key)) { continue; } else if (DataTableColumns.LOCALE.equals(key)) { continue; } else if (DataTableColumns.ROW_ETAG.equals(key)) { continue; } else if (DataTableColumns.SAVEPOINT_CREATOR.equals(key)) { continue; } else if (DataTableColumns.SAVEPOINT_TIMESTAMP.equals(key)) { continue; } else if (DataTableColumns.SAVEPOINT_TYPE.equals(key)) { continue; } else if (DataTableColumns.SYNC_STATE.equals(key)) { continue; } else if (DataTableColumns._ID.equals(key)) { continue; } // OK it is one of the data columns ColumnDefinition cp = ColumnDefinition.find(orderedColumns, key); if (!cp.isUnitOfRetention()) { toBeResolved.put(key, values.getAsString(key)); } } // remove these non-retained values from the values set... for (String key : toBeResolved.keySet()) { values.remove(key); } while (!toBeResolved.isEmpty()) { Map<String, String> moreToResolve = new HashMap<String, String>(); for (Map.Entry<String, String> entry : toBeResolved.entrySet()) { String key = entry.getKey(); String json = entry.getValue(); if (json == null) { // don't need to do anything // since the value is null continue; } ColumnDefinition cp = ColumnDefinition.find(orderedColumns, key); try { Map<String, Object> struct = ODKFileUtils.mapper.readValue(json, Map.class); for (ColumnDefinition child : cp.getChildren()) { String subkey = child.getElementKey(); ColumnDefinition subcp = ColumnDefinition.find(orderedColumns, subkey); if (subcp.isUnitOfRetention()) { ElementType subtype = subcp.getType(); ElementDataType type = subtype.getDataType(); if (type == ElementDataType.integer) { values.put(subkey, (Integer) struct.get(subcp.getElementName())); } else if (type == ElementDataType.number) { values.put(subkey, (Double) struct.get(subcp.getElementName())); } else if (type == ElementDataType.bool) { values.put(subkey, ((Boolean) struct.get(subcp.getElementName())) ? 1 : 0); } else { values.put(subkey, (String) struct.get(subcp.getElementName())); } } else { // this must be a javascript structure... re-JSON it and save (for // next round). moreToResolve.put(subkey, ODKFileUtils.mapper.writeValueAsString(struct.get(subcp.getElementName()))); } } } catch (JsonParseException e) { e.printStackTrace(); throw new IllegalStateException("should not be happening"); } catch (JsonMappingException e) { e.printStackTrace(); throw new IllegalStateException("should not be happening"); } catch (IOException e) { e.printStackTrace(); throw new IllegalStateException("should not be happening"); } } toBeResolved = moreToResolve; } } /** * Return the data stored in the cursor at the given index and given position * (ie the given row which the cursor is currently on) as null OR a String. * <p> * NB: Currently only checks for Strings, long, int, and double. * * @param c * @param i * @return */ @SuppressLint("NewApi") public String getIndexAsString(Cursor c, int i) { // If you add additional return types here be sure to modify the javadoc. if (i == -1) return null; if (c.isNull(i)) { return null; } switch (c.getType(i)) { case Cursor.FIELD_TYPE_STRING: return c.getString(i); case Cursor.FIELD_TYPE_FLOAT: return Double.toString(c.getDouble(i)); case Cursor.FIELD_TYPE_INTEGER: return Long.toString(c.getLong(i)); case Cursor.FIELD_TYPE_NULL: return c.getString(i); default: case Cursor.FIELD_TYPE_BLOB: throw new IllegalStateException("Unexpected data type in SQLite table"); } } /** * Retrieve the data type of the [i] field in the Cursor. * * @param c * @param i * @return */ public static final Class<?> getIndexDataType(Cursor c, int i) { switch (c.getType(i)) { case Cursor.FIELD_TYPE_STRING: return String.class; case Cursor.FIELD_TYPE_FLOAT: return Double.class; case Cursor.FIELD_TYPE_INTEGER: return Long.class; case Cursor.FIELD_TYPE_NULL: return String.class; default: case Cursor.FIELD_TYPE_BLOB: throw new IllegalStateException("Unexpected data type in SQLite table"); } } /** * Return the data stored in the cursor at the given index and given position * (ie the given row which the cursor is currently on) as null OR whatever * data type it is. * <p> * This does not actually convert data types from one type to the other. * Instead, it safely preserves null values and returns boxed data values. If * you specify ArrayList or HashMap, it JSON deserializes the value into one * of those. * * @param c * @param clazz * @param i * @return */ @SuppressLint("NewApi") public final <T> T getIndexAsType(Cursor c, Class<T> clazz, int i) { // If you add additional return types here be sure to modify the javadoc. try { if (i == -1) return null; if (c.isNull(i)) { return null; } if (clazz == Long.class) { Long l = c.getLong(i); return (T) l; } else if (clazz == Integer.class) { Integer l = c.getInt(i); return (T) l; } else if (clazz == Double.class) { Double d = c.getDouble(i); return (T) d; } else if (clazz == String.class) { String str = c.getString(i); return (T) str; } else if (clazz == Boolean.class) { // stored as integers Integer l = c.getInt(i); return (T) Boolean.valueOf(l != 0); } else if (clazz == ArrayList.class) { // json deserialization of an array String str = c.getString(i); return (T) ODKFileUtils.mapper.readValue(str, ArrayList.class); } else if (clazz == HashMap.class) { // json deserialization of an object String str = c.getString(i); return (T) ODKFileUtils.mapper.readValue(str, HashMap.class); } else { throw new IllegalStateException("Unexpected data type in SQLite table"); } } catch (ClassCastException e) { e.printStackTrace(); throw new IllegalStateException( "Unexpected data type conversion failure " + e.toString() + " in SQLite table "); } catch (JsonParseException e) { e.printStackTrace(); throw new IllegalStateException( "Unexpected data type conversion failure " + e.toString() + " on SQLite table"); } catch (JsonMappingException e) { e.printStackTrace(); throw new IllegalStateException( "Unexpected data type conversion failure " + e.toString() + " on SQLite table"); } catch (IOException e) { e.printStackTrace(); throw new IllegalStateException( "Unexpected data type conversion failure " + e.toString() + " on SQLite table"); } } }