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.opendatakit.tables.utils; import java.io.File; import java.io.FileNotFoundException; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.apache.commons.lang3.CharEncoding; import org.opendatakit.common.android.data.ColumnDefinition; import org.opendatakit.common.android.data.UserTable; import org.opendatakit.common.android.database.DatabaseFactory; import org.opendatakit.common.android.utilities.ColumnUtil; import org.opendatakit.common.android.utilities.ODKDatabaseUtils; import org.opendatakit.common.android.utilities.ODKFileUtils; import org.opendatakit.common.android.utilities.TableUtil; import org.opendatakit.common.android.utilities.WebLogger; import org.opendatakit.tables.views.webkits.TableData; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParser; /** * Methods for dealing with things necessary for debugging in chrome. This * requires outputting json structures representing the state of the database. * * @author sudar.sam@gmail.com * */ public class OutputUtil { private static final String TAG = OutputUtil.class.getSimpleName(); /** The filename of the control object that will be written out. */ public static final String CONTROL_FILE_NAME = "control.json"; /** The suffix of the name for each data object that will be written. */ public static final String DATA_FILE_SUFFIX = "_data.json"; public static final String CTRL_KEY_TABLE_ID_TO_DISPLAY_NAME = "tableIdToDisplayName"; public static final String CTRL_KEY_TABLE_INFO = "tables"; /** The key for the default detail view file in the table. */ public static final String CTRL_KEY_DEFAULT_DETAIL_FILE = "defaultDetailFile"; /** The key for the default list view file in the table. */ public static final String CTRL_KEY_DEFAULT_LIST_FILE = "defaultListFile"; // These are keys we'll be outputting for use in debugging when we write // this object to a file. public static final String DATA_KEY_IN_COLLECTION_MODE = "inCollectionMode"; public static final String DATA_KEY_COUNT = "count"; public static final String DATA_KEY_COLUMNS = "columns"; public static final String DATA_KEY_IS_GROUPED_BY = "isGroupedBy"; public static final String DATA_KEY_DATA = "data"; public static final String DATA_KEY_COLUMN_DATA = "columnData"; public static final String DATA_KEY_TABLE_ID = "tableId"; public static final String DATA_KEY_ROW_IDS = "rowIds"; // Keys for the table object contained within control objects. public static final String CTRL_TABLE_KEY_ELEMENT_PATH_TO_KEY = "pathToKey"; public static final String CTRL_TABLE_KEY_ELEMENT_KEY_TO_DISPLAY_NAME = "pathToName"; public static final int NUM_ROWS_IN_DATA_OBJECT = 10; /** * Gets a string containing information necessary for the control object's * methods to be meaningful. E.g. there is a method to retrieve all the table * ids. This object will contain that information. * <p> * The object is as follows: <br> * { {@link #CTRL_TABLE_KEY_ELEMENT_KEY_TO_DISPLAY_NAME}: {tableIdOne: * displayName, ...}, {@link #CTRL_KEY_TABLE_INFO: tableIdOne: tableObjectOne, * ...} * * @return */ private static String getStringForControlObject(Context context, String appName) { Map<String, Object> controlMap = new HashMap<String, Object>(); Map<String, String> tableIdToDisplayName = new HashMap<String, String>(); Map<String, Map<String, Object>> tableIdToControlTable = new HashMap<String, Map<String, Object>>(); SQLiteDatabase db = null; try { db = DatabaseFactory.get().getDatabase(context, appName); ArrayList<String> tableIds = ODKDatabaseUtils.get().getAllTableIds(db); for (String tableId : tableIds) { String localizedDisplayName; localizedDisplayName = TableUtil.get().getLocalizedDisplayName(db, tableId); tableIdToDisplayName.put(tableId, localizedDisplayName); Map<String, Object> controlTable = getMapForControlTable(db, appName, tableId); tableIdToControlTable.put(tableId, controlTable); } } finally { if (db != null) { db.close(); } } Gson gson = new Gson(); controlMap.put(CTRL_KEY_TABLE_ID_TO_DISPLAY_NAME, tableIdToDisplayName); controlMap.put(CTRL_KEY_TABLE_INFO, tableIdToControlTable); String result = gson.toJson(controlMap); return result; } /** * The control.js object serves as a hub for some information about a table. * E.g. the control object needs to be able to return the elementKey based on * an elementPath for the table, as well as get display names for a column. * <p> * The string returned by this object should likely not be useful outside of * the context of a control object. { * {@link #CTRL_TABLE_KEY_ELEMENT_PATH_TO_KEY}: {elementPath: elementKey, * ...}, {@link #CTRL_TABLE_KEY_ELEMENT_KEY_TO_DISPLAY_NAME}: {elementPath: * displayName, ...}, * * } * * @param db * @param appName * @param tableId * @return */ public static Map<String, Object> getMapForControlTable(SQLiteDatabase db, String appName, String tableId) { Map<String, Object> controlTable = new HashMap<String, Object>(); Map<String, String> pathToKey = new HashMap<String, String>(); ArrayList<ColumnDefinition> orderedDefns; String defaultDetailFileName = null; String defaultListFileName = null; Map<String, String> keyToDisplayName = new HashMap<String, String>(); orderedDefns = TableUtil.get().getColumnDefinitions(db, appName, tableId); defaultDetailFileName = TableUtil.get().getDetailViewFilename(db, tableId); defaultListFileName = TableUtil.get().getListViewFilename(db, tableId); for (ColumnDefinition cd : orderedDefns) { String elementName = cd.getElementName(); if (elementName != null) { pathToKey.put(cd.getElementName(), cd.getElementKey()); String localizedDisplayName; localizedDisplayName = ColumnUtil.get().getLocalizedDisplayName(db, tableId, cd.getElementKey()); keyToDisplayName.put(cd.getElementKey(), localizedDisplayName); } } controlTable.put(CTRL_TABLE_KEY_ELEMENT_PATH_TO_KEY, pathToKey); controlTable.put(CTRL_TABLE_KEY_ELEMENT_KEY_TO_DISPLAY_NAME, keyToDisplayName); controlTable.put(CTRL_KEY_DEFAULT_DETAIL_FILE, defaultDetailFileName); controlTable.put(CTRL_KEY_DEFAULT_LIST_FILE, defaultListFileName); return controlTable; } /** * Gets a string containing information necessary for the data object for this * particular table. The object is something like the following:<br> * { {@link #DATA_KEY_IN_COLLECTION_MODE}: boolean, {@link #DATA_KEY_COUNT}: * int, {@link #DATA_KEY_COLLECTION_SIZE}: Array, (an array of ints) * {@link #DATA_KEY_IS_INDEXED}: boolean, {@link #DATA_KEY_DATA: Array, (2d, * array of rows) {@link #DATA_KEY_COLUMNS}: {elementKey: string, ...}, * {@link #DATA_KEY_ELEMENT_KEY_TO_PATH: elementPath: elementPath, ...}, * * @param db * @param appName * @param tableId * @return */ private static String getStringForDataObject(SQLiteDatabase db, String appName, String tableId, int numberOfRows) { ArrayList<ColumnDefinition> orderedDefns = TableUtil.get().getColumnDefinitions(db, appName, tableId); UserTable userTable = null; userTable = ODKDatabaseUtils.get().rawSqlQuery(db, appName, tableId, orderedDefns, null, null, null, null, null, null); // TODO: This is broken w.r.t. elementKey != elementPath // TODO: HACKED HACKED HACKED HACKED // Because the code is so freaked up we don't have an easy way to get the // information out of the UserTable without going through the TableData // object. So we need to create a dummy CustomTableView to get it to work. TableData tableData = new TableData(userTable); // We need to also store the element key to the index of the data so that // we know how to access it out of the array representing each row. Map<String, Integer> elementKeyToIndex = new HashMap<String, Integer>(); for (ColumnDefinition cd : orderedDefns) { if (cd.isUnitOfRetention()) { elementKeyToIndex.put(cd.getElementKey(), userTable.getColumnIndexOfElementKey(cd.getElementKey())); } } // We don't want to try and write more rows than we have. int numRowsToWrite = Math.min(numberOfRows, userTable.getNumberOfRows()); // First write the array of row ids. String[] rowIds = new String[numRowsToWrite]; for (int i = 0; i < rowIds.length; i++) { rowIds[i] = userTable.getRowAtIndex(i).getRowId(); } // Here we're using this object b/c these appear to be the columns // available to the client--are metadata columns exposed to them? It's // not obvious to me here. Set<String> columnKeys = elementKeyToIndex.keySet(); Map[] partialData = new Map[numRowsToWrite]; // Now construct up the partial data object. for (int i = 0; i < numRowsToWrite; i++) { Map<String, Object> rowOut = new HashMap<String, Object>(); for (String elementKey : columnKeys) { rowOut.put(elementKey, tableData.getData(i, elementKey)); } partialData[i] = rowOut; } // And now construct the object storing the columns data. JsonParser jsonParser = new JsonParser(); Map<String, Object> elementKeyToColumnData = new HashMap<String, Object>(); for (String elementKey : columnKeys) { // The tableData object returns a string, so we'll have to parse it back // into json. String strColumnData = tableData.getColumnDataForElementKey(elementKey, numRowsToWrite); if (strColumnData == null) continue; JsonArray columnData = (JsonArray) jsonParser.parse(strColumnData); // Now that it's json, we want to convert it to an array. Otherwise it // serializes to an object with a single key "elements". Oh gson. String[] columnDataArray = new String[columnData.size()]; for (int i = 0; i < columnDataArray.length; i++) { JsonElement e = columnData.get(i); if (e == JsonNull.INSTANCE) { columnDataArray[i] = null; } else { columnDataArray[i] = e.getAsString(); } } elementKeyToColumnData.put(elementKey, columnDataArray); } Gson gson = new Gson(); // We need to parse some of the String objects returned by TableData into // json so that they're output as objects rather than strings. String columnString = tableData.getColumns(); JsonObject columnJson = (JsonObject) jsonParser.parse(columnString); // Here, as with JsonArray, we need to convert this to a map or else we'll // serialize as the object to a "members" key. Map<String, Object> columnJsonMap = new HashMap<String, Object>(); for (Map.Entry<String, JsonElement> entry : columnJson.entrySet()) { JsonElement e = entry.getValue(); if (e == JsonNull.INSTANCE) { columnJsonMap.put(entry.getKey(), null); } else { columnJsonMap.put(entry.getKey(), e.getAsString()); } } Map<String, Object> outputObject = new HashMap<String, Object>(); outputObject.put(DATA_KEY_IS_GROUPED_BY, tableData.isGroupedBy()); // We don't want the real count, as that could interfere with for loops in // the code. We in fact want the number of rows that are written, as that // will be the number of rows available to the javascript. outputObject.put(DATA_KEY_TABLE_ID, userTable.getTableId()); outputObject.put(DATA_KEY_COUNT, numRowsToWrite); outputObject.put(DATA_KEY_COLUMNS, columnJsonMap); outputObject.put(DATA_KEY_COLUMN_DATA, elementKeyToColumnData); outputObject.put(DATA_KEY_DATA, partialData); outputObject.put(DATA_KEY_ROW_IDS, rowIds); String outputString = gson.toJson(outputObject); return outputString; } /** * Writes the control string to a json file in the debug folder. * * @param context * @param appName */ public static void writeControlObject(Context context, String appName) { String controlString = getStringForControlObject(context, appName); String fileName = ODKFileUtils.getTablesDebugObjectFolder(appName) + File.separator + CONTROL_FILE_NAME; PrintWriter writer; try { writer = new PrintWriter(fileName, CharEncoding.UTF_8); WebLogger.getLogger(appName).d(TAG, "writing control to: " + fileName); writer.print(controlString); writer.flush(); writer.close(); } catch (FileNotFoundException e) { WebLogger.getLogger(appName).printStackTrace(e); } catch (UnsupportedEncodingException e) { WebLogger.getLogger(appName).printStackTrace(e); } } /** * Writes the data objects for all the data tables in the database. * * @param context * @param appName * @param numberOfRows */ public static void writeAllDataObjects(Context context, String appName, int numberOfRows) { SQLiteDatabase db = null; try { db = DatabaseFactory.get().getDatabase(context, appName); ArrayList<String> tableIds = ODKDatabaseUtils.get().getAllTableIds(db); for (String tableId : tableIds) { writeDataObject(db, appName, tableId, numberOfRows); } } finally { if (db != null) { db.close(); } } } /** * Convenience method. Calls * {@link #writeAllDataObjects(Context, String, int)} with * {@link #NUM_ROWS_IN_DATA_OBJECT}. * * @param context * @param appName */ public static void writeAllDataObjects(Context context, String appName) { writeAllDataObjects(context, appName, NUM_ROWS_IN_DATA_OBJECT); } /** * Write the data object with the given number of rows to the debug folder. * The file is tableId_DATA_FILE_SUFFIX. * * @param db * @param tableId * @param numberOfRows */ public static void writeDataObject(SQLiteDatabase db, String appName, String tableId, int numberOfRows) { String dataString = getStringForDataObject(db, appName, tableId, numberOfRows); if (dataString == null) return; String fileName = ODKFileUtils.getTablesDebugObjectFolder(appName) + File.separator + tableId + DATA_FILE_SUFFIX; PrintWriter writer; try { writer = new PrintWriter(fileName, CharEncoding.UTF_8); WebLogger.getLogger(appName).d(TAG, "writing data object to: " + fileName); writer.print(dataString); writer.flush(); writer.close(); } catch (FileNotFoundException e) { WebLogger.getLogger(appName).printStackTrace(e); } catch (UnsupportedEncodingException e) { WebLogger.getLogger(appName).printStackTrace(e); } } }