org.opendatakit.tables.data.UserTable.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.tables.data.UserTable.java

Source

/*
 * Copyright (C) 2012 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.data;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.opendatakit.aggregate.odktables.rest.TableConstants;
import org.opendatakit.common.android.provider.DataTableColumns;
import org.opendatakit.common.android.provider.FileProvider;
import org.opendatakit.common.android.utilities.ODKFileUtils;
import org.opendatakit.tables.utils.TableFileUtils;

import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;

/**
 * This class represents a table. This can be conceptualized as a list of rows.
 * Each row comprises the user-defined columns, or data, as well as the
 * ODKTables-specified metadata.
 * <p>
 * This should be considered an immutable class, with the exception of the
 * footer. The footer is only important to the user when viewing a table in
 * certain conditions, and many other uses where the contents of a table need to
 * be accessed do not require the footer. For this reason it alone is mutable.
 *
 * @author unknown
 * @author sudar.sam@gmail.com
 *
 */
public class UserTable {

    private static final String TAG = UserTable.class.getSimpleName();

    private final String[] header;
    private String[] footer;
    private final ArrayList<Row> mRows;
    /**
     * The {@link TableProperties} associated with this table. Included so that
     * more intelligent things can be done with regards to interpretation of type.
     */
    private final TableProperties mTp;
    private final String[] mElementKeyForIndex;
    // array of ColumnProperties for these element keys
    // this can go stale when ColumnProperties are changed,
    // so it must be explicitly recomputed before being used
    // e.g., reloadCacheOfColumnProperties()
    private final ArrayList<ColumnProperties> mColumnProperties = new ArrayList<ColumnProperties>();

    /**
     * Maps the element key of user-defined columns to the corresponding index in
     * the Row objects.
     */
    private final Map<String, Integer> mDataKeyToIndex;
    /**
     * Maps the element key of ODKTables-specified metadata columns to the
     * corresponding indices in the Row objects.
     */
    private final Map<String, Integer> mMetadataKeyToIndex;

    private Map<String, Integer> mUnmodifiableCachedDataKeyToIndex = null;
    private Map<String, Integer> mUnmodifiableCachedMetadataKeyToIndex = null;

    private DateTimeZone tz;
    private DateTimeFormatter dateFormatter;
    private DateTimeFormatter dateTimeFormatter;
    private DateTimeFormatter timeFormatter;

    private void buildFormatters() {
        Locale l = Locale.getDefault();
        tz = DateTimeZone.forTimeZone(TimeZone.getDefault());
        dateFormatter = DateTimeFormat.forPattern(DateTimeFormat.patternForStyle("M-", l)).withZone(tz);
        dateTimeFormatter = DateTimeFormat.forPattern(DateTimeFormat.patternForStyle("ML", l)).withZone(tz);
        timeFormatter = DateTimeFormat.forPattern(DateTimeFormat.patternForStyle("-L", l)).withZone(tz);
    }

    public UserTable(TableProperties tp, String[] rowIds, String[] header, String[][] userDefinedData,
            String[] elementKeyForIndex, Map<String, Integer> dataElementKeyToIndex, String[][] odkTablesMetadata,
            Map<String, Integer> metadataElementKeyToIndex, String[] footer) {
        buildFormatters();
        this.header = header;
        mRows = new ArrayList<Row>(userDefinedData.length);
        for (int i = 0; i < userDefinedData.length; i++) {
            Row nextRow = new Row(rowIds[i], userDefinedData[i], odkTablesMetadata[i]);
            mRows.add(nextRow);
        }
        this.mTp = tp;
        this.footer = footer;
        mDataKeyToIndex = dataElementKeyToIndex;
        mMetadataKeyToIndex = metadataElementKeyToIndex;
        mElementKeyForIndex = elementKeyForIndex;
    }

    public UserTable(Cursor c, TableProperties tableProperties, List<String> userColumnOrder) {
        buildFormatters();
        mTp = tableProperties;
        List<String> adminColumnOrder = DbTable.getAdminColumns();
        int rowIdIndex = c.getColumnIndexOrThrow(DataTableColumns.ID);
        // These maps will map the element key to the corresponding index in
        // either data or metadata. If the user has defined a column with the
        // element key _my_data, and this column is at index 5 in the data
        // array, dataKeyToIndex would then have a mapping of _my_data:5.
        // The sync_state column, if present at index 7, would have a mapping
        // in metadataKeyToIndex of sync_state:7.
        mDataKeyToIndex = new HashMap<String, Integer>();
        mElementKeyForIndex = new String[userColumnOrder.size()];
        header = new String[userColumnOrder.size()];
        int[] userColumnCursorIndex = new int[userColumnOrder.size()];
        for (int i = 0; i < userColumnOrder.size(); i++) {
            String elementKey = userColumnOrder.get(i);
            mElementKeyForIndex[i] = elementKey;
            mDataKeyToIndex.put(elementKey, i);
            header[i] = mTp.getColumnByElementKey(elementKey).getDisplayName();
            userColumnCursorIndex[i] = c.getColumnIndexOrThrow(elementKey);
        }
        mMetadataKeyToIndex = new HashMap<String, Integer>();
        int[] adminColumnCursorIndex = new int[adminColumnOrder.size()];
        for (int i = 0; i < adminColumnOrder.size(); i++) {
            // TODO: problem is here. unclear how to best get just the
            // metadata in here. hmm.
            String elementKey = adminColumnOrder.get(i);
            mMetadataKeyToIndex.put(elementKey, i);
            adminColumnCursorIndex[i] = c.getColumnIndexOrThrow(elementKey);
        }

        c.moveToFirst();
        int rowCount = c.getCount();
        mRows = new ArrayList<Row>(rowCount);

        String[] rowData = new String[userColumnOrder.size()];
        String[] rowMetadata = new String[adminColumnOrder.size()];
        if (c.moveToFirst()) {
            do {
                String rowId = c.getString(rowIdIndex);
                // First get the user-defined data for this row.
                for (int i = 0; i < userColumnOrder.size(); i++) {
                    String value = getIndexAsString(c, userColumnCursorIndex[i]);
                    rowData[i] = value;
                }
                // Now get the metadata for this row.
                for (int i = 0; i < adminColumnOrder.size(); i++) {
                    String value = getIndexAsString(c, adminColumnCursorIndex[i]);
                    rowMetadata[i] = value;
                }
                Row nextRow = new Row(rowId, rowData.clone(), rowMetadata.clone());
                mRows.add(nextRow);
            } while (c.moveToNext());
        }
        footer = null;
    }

    /**
     * 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 a String.
     * <p>
     * NB: Currently only checks for Strings, long, int, and double.
     *
     * @param c
     * @param i
     * @return
     */
    @SuppressLint("NewApi")
    private static final String getIndexAsString(Cursor c, int i) {
        // If you add additional return types here be sure to modify the javadoc.
        int version = android.os.Build.VERSION.SDK_INT;
        if (version < 11) {
            // getType() is not yet supported.
            String str = null;
            try {
                str = c.getString(i);
            } catch (Exception e1) {
                try {
                    str = Long.toString(c.getLong(i));
                } catch (Exception e2) {
                    try {
                        str = Double.toString(c.getDouble(i));
                    } catch (Exception e3) {
                        throw new IllegalStateException("Unexpected data type in SQLite table");
                    }
                }
            }
            return str;
        } else {
            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");
            }
        }
    }

    public Long getTimestamp(int rowNum) {
        return TableConstants
                .milliSecondsFromNanos(getMetadataByElementKey(rowNum, DataTableColumns.SAVEPOINT_TIMESTAMP));
    }

    public Row getRowAtIndex(int index) {
        return this.mRows.get(index);
    }

    public String getHeader(int colNum) {
        return header[colNum];
    }

    public String getElementKey(int colNum) {
        return mElementKeyForIndex[colNum];
    }

    /**
     * Get the index of the element key for the user-defined columns.
     * @param elementKey
     * @return null if the column is not found
     */
    public Integer getColumnIndexOfElementKey(String elementKey) {
        return mDataKeyToIndex.get(elementKey);
    }

    public String getData(int cellNum) {
        int rowNum = cellNum / getWidth();
        int colNum = cellNum % getWidth();
        return getData(rowNum, colNum);
    }

    public String getData(int rowNum, int colNum) {
        return mRows.get(rowNum).getDataAtIndex(colNum);
    }

    /**
     * True if the table has been grouped by a value. This is referred to in some
     * places of the code as an "indexed" table, which also and irritatingly
     * means that a column has been set to "prime". 
     * @return
     */
    public boolean isGroupedBy() {
        // All this mess of terminology is incredibly confusing and frustrating.
        // This method comes from CustomView#TableData.
        return !mTp.getPrimeColumns().isEmpty();
    }

    public String getDisplayTextOfData(Context context, int cellNum) {
        int rowNum = cellNum / getWidth();
        int colNum = cellNum % getWidth();
        return getDisplayTextOfData(context, rowNum, colNum, true);
    }

    public String getDisplayTextOfData(Context context, int rowNum, int colNum, boolean showErrorText) {
        // TODO: share processing with CollectUtil.writeRowDataToBeEdited(...)
        String raw = getData(rowNum, colNum);
        if (raw == null) {
            return null;
        }
        ColumnProperties cp = mColumnProperties.get(colNum);
        ColumnType type = cp.getColumnType();
        if (type == ColumnType.AUDIOURI || type == ColumnType.IMAGEURI || type == ColumnType.MIMEURI
                || type == ColumnType.VIDEOURI) {
            try {
                if (raw.length() == 0) {
                    return raw;
                }
                @SuppressWarnings("rawtypes")
                Map m = ODKFileUtils.mapper.readValue(raw, Map.class);
                String uriFragment = (String) m.get("uriFragment");
                File f = FileProvider.getAsFile(context, TableFileUtils.ODK_TABLES_APP_NAME, uriFragment);
                return f.getName();
            } catch (JsonParseException e) {
                e.printStackTrace();
            } catch (JsonMappingException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return raw;
        } else if (type == ColumnType.DATE) {
            DataUtil du = DataUtil.getDefaultDataUtil();
            DateTime d = du.parseDateTimeFromDb(raw);
            return dateFormatter.print(d);
        } else if (type == ColumnType.DATETIME) {
            DataUtil du = DataUtil.getDefaultDataUtil();
            DateTime d = du.parseDateTimeFromDb(raw);
            return dateTimeFormatter.print(d);
        } else if (type == ColumnType.TIME) {
            DataUtil du = DataUtil.getDefaultDataUtil();
            DateTime d = du.parseDateTimeFromDb(raw);
            return timeFormatter.print(d);
        } else if (type == ColumnType.TABLE_JOIN) {
            return raw;
        } else {
            return raw;
        }
    }

    /**
     * The cache should be reloaded before using getDisplayTextOfData
     * (above) because the column properties could change due to
     * changes in the property values for those columns.
     */
    public void reloadCacheOfColumnProperties() {
        mColumnProperties.clear();
        for (int i = 0; i < mElementKeyForIndex.length; ++i) {
            String elementKey = mElementKeyForIndex[i];
            mColumnProperties.add(mTp.getColumnByElementKey(elementKey));
        }
    }

    /**
     * Retrieve the metadata datum in the column specified by elementKey at the
     * given row number.
     *
     * @param rowNum
     * @param elementKey
     * @return
     */
    public String getMetadataByElementKey(int rowNum, String elementKey) {
        return mRows.get(rowNum).getMetadataAtIndex(mMetadataKeyToIndex.get(elementKey));
    }

    /**
     * Return the data or metadata value in the given row by element key.
     * @param rowNum
     * @param elementKey
     * @return
     */
    public String getDataByElementKey(int rowNum, String elementKey) {
        return mRows.get(rowNum).getDataOrMetadataByElementKey(elementKey);
    }

    public String getFooter(int colNum) {
        return footer[colNum];
    }

    public void setFooter(String[] footer) {
        this.footer = footer;
    }

    /**
     * Return a map containing the mapping of the element keys for the user-
     * defined columns to their index in array returned by
     * {@link Row#getAllData()}.
     *
     * @return
     */
    public Map<String, Integer> getMapOfUserDataToIndex() {
        if (this.mUnmodifiableCachedDataKeyToIndex == null) {
            this.mUnmodifiableCachedDataKeyToIndex = Collections.unmodifiableMap(this.mDataKeyToIndex);
        }
        return this.mUnmodifiableCachedDataKeyToIndex;
    }

    public TableProperties getTableProperties() {
        return mTp;
    }

    /**
     * Return a map containing the mapping of the element keys for the
     * ODKTables-specified metadata columns to their index in the array returned
     * by {@link Row#getAllMetadata()}.
     *
     * @return
     */
    public Map<String, Integer> getMapOfMetadataToIndex() {
        if (this.mUnmodifiableCachedMetadataKeyToIndex == null) {
            this.mUnmodifiableCachedMetadataKeyToIndex = Collections.unmodifiableMap(this.mMetadataKeyToIndex);
        }
        return this.mUnmodifiableCachedMetadataKeyToIndex;
    }

    public String[] getElementKeysForIndex() {
        return this.mElementKeyForIndex.clone();
    }

    public int getWidth() {
        return header.length;
    }

    /**
     * Get the number of metadata columns.
     *
     * @return
     */
    public int getNumberOfMetadataColumns() {
        return mMetadataKeyToIndex.size();
    }

    public String[] getAllMetadataForRow(int rowNum) {
        return mRows.get(rowNum).getAllMetadata();
    }

    public int getNumberOfRows() {
        return this.mRows.size();
    }

    /**
     * Scan the rowIds to get the row number. As the rowIds are not sorted, this
     * is a potentially expensive operation, scanning the entire array, as well as
     * the cost of checking String equality. Should be used only when necessary.
     * <p>
     * Return -1 if the row Id is not found.
     *
     * @param rowId
     * @return
     */
    public int getRowNumFromId(String rowId) {
        for (int i = 0; i < this.mRows.size(); i++) {
            if (this.mRows.get(i).mRowId.equals(rowId)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * This represents a single row of data in a table.
     *
     * @author sudar.sam@gmail.com
     *
     */
    /*
     * This class is final to try and reduce overhead. As final there is no
     * extended-class pointer. Not positive this is really a thing, need to
     * investigate. Nothing harmed by finalizing, though.
     */
    public final class Row {

        /**
         * The id of the row.
         */
        private final String mRowId;

        /**
         * Holds the actual data in the row. To index into the array correctly, must
         * use the information contained in UserTable.
         */
        private final String[] mData;

        /**
         * Holds the metadata for the row. to index into the array correctly, must
         * use the information contained in UserTable.
         */
        private final String[] mMetadata;

        /**
         * Construct the row.
         *
         * @param rowId
         * @param data
         *          the user-defined data of the row
         * @param metadata
         *          the ODKTables-specified metadata for the row.
         */
        public Row(String rowId, String[] data, String[] metadata) {
            this.mRowId = rowId;
            this.mData = data;
            this.mMetadata = metadata;
        }

        /**
         * Return the value of the row at the given index.
         *
         * @param index
         * @return
         */
        public String getDataAtIndex(int index) {
            return mData[index];
        }

        /**
         * Return the metadata value at the given index.
         *
         * @param index
         * @return
         */
        public String getMetadataAtIndex(int index) {
            return mMetadata[index];
        }

        /**
         * Return the id of this row.
         * @return
         */
        public String getRowId() {
            return this.mRowId;
        }

        /**
         * Return the String representing the contents of the column represented by
         * the passed in elementKey. This can be either the element key of a
         * user-defined column or a ODKTabes-specified metadata column.
         * <p>
         * Null values are returned as an empty string. Null is returned if the
         * elementKey is not found in the table.
         *
         * @param elementKey
         *          elementKey of data or metadata column
         * @return String representation of contents of column. Null values are
         *         converted to an empty string. If the elementKey is not contained
         *         in the table, returns null.
         */
        public String getDataOrMetadataByElementKey(String elementKey) {
            String result;
            if (UserTable.this.mDataKeyToIndex.containsKey(elementKey)) {
                result = this.mData[UserTable.this.mDataKeyToIndex.get(elementKey)];
            } else if (UserTable.this.mMetadataKeyToIndex.containsKey(elementKey)) {
                result = this.mMetadata[UserTable.this.mMetadataKeyToIndex.get(elementKey)];
            } else {
                // The elementKey was not in the table. Probable error or misuse.
                Log.e(TAG, "elementKey [" + elementKey + "] was not found in table");
                return null;
            }
            if (result == null) {
                result = "";
            }
            return result;
        }

        /**
         * Get the array backing the entire row.
         *
         * @return
         */
        public String[] getAllData() {
            return mData;
        }

        public String[] getAllMetadata() {
            return mMetadata;
        }

        @Override
        public int hashCode() {
            final int PRIME = 31;
            int result = 1;
            result = result * PRIME + this.mRowId.hashCode();
            result = result * PRIME + this.mData.hashCode();
            result = result * PRIME + this.mMetadata.hashCode();
            return result;
        }

    }
}