Java tutorial
/* * Copyright (C) 2012-2013 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.aggregate.odktables; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang3.Validate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.opendatakit.aggregate.odktables.exception.BadColumnNameException; import org.opendatakit.aggregate.odktables.exception.ETagMismatchException; import org.opendatakit.aggregate.odktables.exception.InconsistentStateException; import org.opendatakit.aggregate.odktables.exception.PermissionDeniedException; import org.opendatakit.aggregate.odktables.exception.TableDataETagMismatchException; import org.opendatakit.aggregate.odktables.relation.DbColumnDefinitions; import org.opendatakit.aggregate.odktables.relation.DbColumnDefinitions.DbColumnDefinitionsEntity; import org.opendatakit.aggregate.odktables.relation.DbLogTable; import org.opendatakit.aggregate.odktables.relation.DbTable; import org.opendatakit.aggregate.odktables.relation.DbTableDefinitions; import org.opendatakit.aggregate.odktables.relation.DbTableDefinitions.DbTableDefinitionsEntity; import org.opendatakit.aggregate.odktables.relation.DbTableEntry; import org.opendatakit.aggregate.odktables.relation.DbTableEntry.DbTableEntryEntity; import org.opendatakit.aggregate.odktables.relation.EntityConverter; import org.opendatakit.aggregate.odktables.relation.EntityCreator; import org.opendatakit.aggregate.odktables.rest.entity.ChangeSetList; import org.opendatakit.aggregate.odktables.rest.entity.Row; import org.opendatakit.aggregate.odktables.rest.entity.RowList; import org.opendatakit.aggregate.odktables.rest.entity.RowOutcome; import org.opendatakit.aggregate.odktables.rest.entity.RowOutcome.OutcomeType; import org.opendatakit.aggregate.odktables.rest.entity.RowOutcomeList; import org.opendatakit.aggregate.odktables.rest.entity.Scope; import org.opendatakit.aggregate.odktables.rest.entity.TableRole.TablePermission; import org.opendatakit.aggregate.odktables.security.TablesUserPermissions; import org.opendatakit.common.ermodel.Entity; import org.opendatakit.common.ermodel.Query; import org.opendatakit.common.ermodel.Query.WebsafeQueryResult; import org.opendatakit.common.persistence.CommonFieldsBase; import org.opendatakit.common.persistence.PersistenceUtils; import org.opendatakit.common.persistence.Query.Direction; import org.opendatakit.common.persistence.QueryResumePoint; import org.opendatakit.common.persistence.exception.ODKDatastoreException; import org.opendatakit.common.persistence.exception.ODKEntityNotFoundException; import org.opendatakit.common.persistence.exception.ODKEntityPersistException; import org.opendatakit.common.persistence.exception.ODKTaskLockException; import org.opendatakit.common.web.CallingContext; import org.opendatakit.common.web.constants.BasicConsts; /** * Manages read, insert, update, and delete operations on the rows of a table. * * @author the.dylan.price@gmail.com * @author sudar.sam@gmail.com * */ public class DataManager { private static final Log logger = LogFactory.getLog(DataManager.class); public static class WebsafeRows { public final List<Row> rows; public final String dataETag; public final String websafeRefetchCursor; public final String websafeBackwardCursor; public final String websafeResumeCursor; public final boolean hasMore; public final boolean hasPrior; public WebsafeRows(List<Row> rows, String dataETag, String websafeRefetchCursor, String websafeBackwardCursor, String websafeResumeCursor, boolean hasMore, boolean hasPrior) { this.rows = rows; this.dataETag = dataETag; this.websafeRefetchCursor = websafeRefetchCursor; this.websafeBackwardCursor = websafeBackwardCursor; this.websafeResumeCursor = websafeResumeCursor; this.hasMore = hasMore; this.hasPrior = hasPrior; } } private CallingContext cc; private TablesUserPermissions userPermissions; private EntityConverter converter; private EntityCreator creator; private String appId; private String tableId; /** * Construct a new DataManager. * * @param tableId * the unique identifier of the table * @param userPermissions * the requesting user's permissions * @param cc * the calling context * @throws ODKEntityNotFoundException * if no table with the given id exists * @throws ODKDatastoreException * if there is an internal error in the datastore */ public DataManager(String appId, String tableId, TablesUserPermissions userPermissions, CallingContext cc) throws ODKEntityNotFoundException, ODKDatastoreException { Validate.notEmpty(appId); Validate.notEmpty(tableId); Validate.notNull(cc); this.cc = cc; this.userPermissions = userPermissions; this.converter = new EntityConverter(); this.creator = new EntityCreator(); this.appId = appId; this.tableId = tableId; } public String getAppId() { return appId; } public String getTableId() { return tableId; } private void revertPendingChanges(DbTableEntryEntity entry, List<DbColumnDefinitionsEntity> columns, DbTable table, DbLogTable logTable) throws ODKDatastoreException, BadColumnNameException { // we have nothing to do if the pending dataETag is null... String dataETag = entry.getPendingDataETag(); if (dataETag == null) { return; } logger.warn("Reverting changes for dataETag " + dataETag); // search for log entries matching the TableEntry dataETag // log entries are written first, so these should exist, and the // row entries may or may not reflect the log contents. Query query = logTable.query("DataManager.revertPendingChanges", cc); query.equal(DbLogTable.DATA_ETAG_AT_MODIFICATION, dataETag); List<Entity> logEntries = query.execute(); for (Entity logEntity : logEntries) { // Log entries maintain the history of previous rowETags // Chain back through that to get the previous log record. // If the previous rowETag is null, it means that the rowId // did not exist prior to this log entry. String priorETag = logEntity.getString(DbLogTable.PREVIOUS_ROW_ETAG); if (priorETag == null) { // no prior state -- so rowId may not exist now... try { // try to retrieve the rowId from the DbTable Entity rowEntity = table.getEntity(logEntity.getString(DbLogTable.ROW_ID), cc); // if found, delete it rowEntity.delete(cc); } catch (ODKEntityNotFoundException e) { // ignore... it was never created, which is OK! } // remove the entry in DbLogTable for the pending state logEntity.delete(cc); } else { // there is prior state, so the rowId should exist Entity rowEntity = table.getEntity(logEntity.getString(DbLogTable.ROW_ID), cc); // and the prior state should exist in the log... Entity priorLogEntity = logTable.getEntity(logEntity.getString(DbLogTable.PREVIOUS_ROW_ETAG), cc); // reset the row to the prior row state creator.setRowFields(rowEntity, priorLogEntity.getId(), priorLogEntity.getString(DbLogTable.DATA_ETAG_AT_MODIFICATION), priorLogEntity.getString(DbLogTable.LAST_UPDATE_USER), priorLogEntity.getBoolean(DbLogTable.DELETED), converter.getDbLogTableFilterScope(priorLogEntity), priorLogEntity.getString(DbLogTable.FORM_ID), priorLogEntity.getString(DbLogTable.LOCALE), priorLogEntity.getString(DbLogTable.SAVEPOINT_TYPE), priorLogEntity.getString(DbLogTable.SAVEPOINT_TIMESTAMP), priorLogEntity.getString(DbLogTable.SAVEPOINT_CREATOR), converter.getRowValues(priorLogEntity, columns), columns); // revert DbTable to the prior row state rowEntity.put(cc); // remove the entry in DbLogTable for the pending state logEntity.delete(cc); } } } /** * Retrieve all current rows of the table. * * @return all the rows of the table. * @throws ODKDatastoreException * @throws PermissionDeniedException * @throws ODKTaskLockException * @throws InconsistentStateException * @throws BadColumnNameException */ public WebsafeRows getRows(QueryResumePoint startCursor, int fetchLimit) throws ODKDatastoreException, PermissionDeniedException, ODKTaskLockException, InconsistentStateException, BadColumnNameException { userPermissions.checkPermission(appId, tableId, TablePermission.READ_ROW); String currentDataETag = null; List<DbColumnDefinitionsEntity> columns = null; WebsafeQueryResult result = null; LockTemplate propsLock = new LockTemplate(tableId, ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, cc); try { propsLock.acquire(); DbTableEntryEntity entry = DbTableEntry.getTableIdEntry(tableId, cc); String schemaETag = entry.getSchemaETag(); if (schemaETag == null) { throw new InconsistentStateException("Schema for table " + tableId + " is not yet defined."); } currentDataETag = entry.getDataETag(); DbTableDefinitionsEntity tableDefn = DbTableDefinitions.getDefinition(tableId, schemaETag, cc); columns = DbColumnDefinitions.query(tableId, schemaETag, cc); DbTable table = DbTable.getRelation(tableDefn, columns, cc); DbLogTable logTable = DbLogTable.getRelation(tableDefn, columns, cc); revertPendingChanges(entry, columns, table, logTable); Query query = buildRowsQuery(table); query.addSort(table.getDataField(CommonFieldsBase.CREATION_DATE_COLUMN_NAME), (startCursor == null || startCursor.isForwardCursor()) ? Direction.ASCENDING : Direction.DESCENDING); // we need the filter to activate the sort... query.addFilter(table.getDataField(CommonFieldsBase.CREATION_DATE_COLUMN_NAME), org.opendatakit.common.persistence.Query.FilterOperation.GREATER_THAN, BasicConsts.EPOCH); result = query.execute(startCursor, fetchLimit); } finally { propsLock.release(); } if (result.entities == null || columns == null) { throw new InconsistentStateException("Unable to retrieve rows for table " + tableId + "."); } ArrayList<Row> rows = new ArrayList<Row>(); for (Entity entity : result.entities) { Row row = converter.toRow(entity, columns); if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_READ)) { rows.add(row); } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.READ_ROW, row.getRowId(), row.getFilterScope())) { rows.add(row); } } return new WebsafeRows(rows, currentDataETag, result.websafeRefetchCursor, result.websafeBackwardCursor, result.websafeResumeCursor, result.hasMore, result.hasPrior); } /** * @return the query for current rows in the table */ private Query buildRowsQuery(DbTable table) { Query query = table.query("DataManager.buildRowsQuery", cc); query.equal(DbTable.DELETED, false); return query; } /** * Retrieves a set of rows representing the changes since the given data etag. * * @param dataETag * the data ETag * @return the rows which have changed or been added since the given data etag * @throws ODKDatastoreException * @throws ODKTaskLockException * @throws InconsistentStateException * @throws PermissionDeniedException * @throws BadColumnNameException */ public WebsafeRows getRowsSince(String dataETag, QueryResumePoint startCursor, int fetchLimit) throws ODKDatastoreException, ODKTaskLockException, InconsistentStateException, PermissionDeniedException, BadColumnNameException { userPermissions.checkPermission(appId, tableId, TablePermission.READ_ROW); String currentDataETag = null; List<DbColumnDefinitionsEntity> columns = null; WebsafeQueryResult result = null; LockTemplate propsLock = new LockTemplate(tableId, ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, cc); try { propsLock.acquire(); DbTableEntryEntity entry = DbTableEntry.getTableIdEntry(tableId, cc); String schemaETag = entry.getSchemaETag(); if (schemaETag == null) { throw new InconsistentStateException("Schema for table " + tableId + " is not yet defined."); } currentDataETag = entry.getDataETag(); DbTableDefinitionsEntity tableDefn = DbTableDefinitions.getDefinition(tableId, schemaETag, cc); columns = DbColumnDefinitions.query(tableId, schemaETag, cc); DbTable table = DbTable.getRelation(tableDefn, columns, cc); DbLogTable logTable = DbLogTable.getRelation(tableDefn, columns, cc); revertPendingChanges(entry, columns, table, logTable); String sequenceValue = null; if (dataETag != null) { try { sequenceValue = getSequenceValueForDataETag(logTable, dataETag); } catch (ODKEntityNotFoundException e) { // TODO: log this as a warning -- may be returning a very large set sequenceValue = null; } } Query query; if (sequenceValue == null) { query = buildRowsFromBeginningQuery(logTable, entry, (startCursor == null ? true : startCursor.isForwardCursor())); } else { query = buildRowsSinceQuery(logTable, sequenceValue, (startCursor == null ? true : startCursor.isForwardCursor())); } result = query.execute(startCursor, fetchLimit); } finally { propsLock.release(); } if (result.entities == null || columns == null) { throw new InconsistentStateException("Unable to retrieve rows for table " + tableId + "."); } // TODO: properly handle reporting of rows that the user no longer has // access to because of a access / permissions change for that user and / or // row. ArrayList<Row> rows = new ArrayList<Row>(); for (Entity entity : result.entities) { Row row = converter.toRowFromLogTable(entity, columns); if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_READ)) { rows.add(row); } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.READ_ROW, row.getRowId(), row.getFilterScope())) { rows.add(row); } } return new WebsafeRows(computeDiff(rows), currentDataETag, result.websafeRefetchCursor, result.websafeBackwardCursor, result.websafeResumeCursor, result.hasMore, result.hasPrior); } /** * Retrieves a set of rows representing the changes since the given timestamp. * @param dateToUse TODO * @param startTime - the timestamp to start at in the format of yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS * @param endTime - the timestamp to end at in the format of yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS * @param startCursor - the cursor to start with * @param fetchLimit - the number of rows to return in the response * * @return the rows which have changed or been added since the given timestamp * @throws ODKDatastoreException * @throws ODKTaskLockException * @throws InconsistentStateException * @throws PermissionDeniedException * @throws BadColumnNameException * @throws ParseException */ public WebsafeRows getRowsInTimeRange(String dateToUse, String startTime, String endTime, QueryResumePoint startCursor, int fetchLimit) throws ODKDatastoreException, ODKTaskLockException, InconsistentStateException, PermissionDeniedException, BadColumnNameException, ParseException { String query_col = DbLogTable.LAST_UPDATE_DATE_COLUMN_NAME; if (dateToUse.equals(DbLogTable.SAVEPOINT_TIMESTAMP.getName())) { query_col = DbLogTable.SAVEPOINT_TIMESTAMP.getName(); } userPermissions.checkPermission(appId, tableId, TablePermission.READ_ROW); String currentDataETag = null; List<DbColumnDefinitionsEntity> columns = null; WebsafeQueryResult result = null; LockTemplate propsLock = new LockTemplate(tableId, ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, cc); try { propsLock.acquire(); DbTableEntryEntity entry = DbTableEntry.getTableIdEntry(tableId, cc); String schemaETag = entry.getSchemaETag(); if (schemaETag == null) { throw new InconsistentStateException("Schema for table " + tableId + " is not yet defined."); } currentDataETag = entry.getDataETag(); DbTableDefinitionsEntity tableDefn = DbTableDefinitions.getDefinition(tableId, schemaETag, cc); columns = DbColumnDefinitions.query(tableId, schemaETag, cc); DbTable table = DbTable.getRelation(tableDefn, columns, cc); DbLogTable logTable = DbLogTable.getRelation(tableDefn, columns, cc); revertPendingChanges(entry, columns, table, logTable); SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS"); Date startDateToCompare = null; String startSequenceValue = null; if (startTime != null) { try { startDateToCompare = sf.parse(startTime); startSequenceValue = getSequenceValueForStartTime(logTable, query_col, startTime, startDateToCompare, Direction.ASCENDING); //(startCursor == null || startCursor.isForwardCursor()) ? Direction.ASCENDING : Direction.DESCENDING); } catch (ODKEntityNotFoundException e) { // No values to display should return empty list ArrayList<Row> rows = new ArrayList<Row>(); return new WebsafeRows(rows, currentDataETag, null, null, null, false, false); } } else { throw new IllegalArgumentException("startTime must be specified."); } // endTime is an optional parameter // and does not have to have a valid value Date endDateToCompare = null; String endSequenceValue = null; if (endTime != null) { try { endDateToCompare = sf.parse(endTime); // For the end time stamp we want the last one endSequenceValue = getSequenceValueForEndTime(logTable, query_col, endTime, endDateToCompare, Direction.DESCENDING); // (startCursor == null || startCursor.isForwardCursor()) ? Direction.DESCENDING : Direction.ASCENDING); } catch (ODKEntityNotFoundException e) { // If a sequence values is not found, // the query should still work } } // CAL: From getRowsSince Query query; if (startSequenceValue == null) { throw new IllegalArgumentException("No sequence value exists for the specified startTime."); } else { query = buildRowsIncludingQuery(logTable, startSequenceValue, endSequenceValue, (startCursor == null ? true : startCursor.isForwardCursor())); } result = query.execute(startCursor, fetchLimit); } finally { propsLock.release(); } if (result.entities == null || columns == null) { throw new InconsistentStateException("Unable to retrieve rows for table " + tableId + "."); } // TODO: properly handle reporting of rows that the user no longer has // access to because of a access / permissions change for that user and / or // row. ArrayList<Row> rows = new ArrayList<Row>(); for (Entity entity : result.entities) { Row row = converter.toRowFromLogTable(entity, columns); if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_READ)) { rows.add(row); } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.READ_ROW, row.getRowId(), row.getFilterScope())) { rows.add(row); } } List<Row> diffRows = computeDiff(rows); // Display rows in the order that they were meant to be // displayed List<Row> orderedRows = new ArrayList<Row>(); for (int i = 0; i < rows.size(); i++) { if (diffRows.contains(rows.get(i))) { orderedRows.add(rows.get(i)); } } return new WebsafeRows(orderedRows, currentDataETag, result.websafeRefetchCursor, result.websafeBackwardCursor, result.websafeResumeCursor, result.hasMore, result.hasPrior); } /** * Perform direct query on DATA_ETAG_AT_MODIFICATION to retrieve the * SEQUENCE_VALUE of that row. This is then used to construct the * get-rows-since queries. * * @param dataETag * @return SEQUENCE_VALUE of that row * @throws ODKDatastoreException */ private String getSequenceValueForDataETag(DbLogTable logTable, String dataETag) throws ODKDatastoreException { Query query = logTable.query("DataManager.getSequenceValueForDataETag", cc); query.equal(DbLogTable.DATA_ETAG_AT_MODIFICATION, dataETag); // we need the filter to activate the sort for the sequence value query.addFilter(DbLogTable.SEQUENCE_VALUE, org.opendatakit.common.persistence.Query.FilterOperation.GREATER_THAN, " "); query.addSort(DbLogTable.SEQUENCE_VALUE, Direction.DESCENDING); List<Entity> values = query.execute(); if (values == null || values.size() == 0) { throw new ODKEntityNotFoundException("ETag " + dataETag + " was not found in log table!"); } else if (values.size() != 1) { // the descending sort on the sequence value ensures we get the last change for // this dataETagAtModification. This assumes the client has gotten all records // matching this tag, and is requesting changes *after* the tag. logger.info("Multiple records for dataETagAtModification " + dataETag + " count: " + values.size()); } Entity e = values.get(0); return e.getString(DbLogTable.SEQUENCE_VALUE); } /** * Perform direct query on dateColToUseForCompare to retrieve the * SEQUENCE_VALUE of that row. This is then used to construct the * query for getting data with that using this start time. * * @param logTable - the log table to use * @param dateColToUseForCompare - the date field to use for the comparison * @param givenTimestamp - the original string value the user passed in * @param dateToCompare - the date to compare against the LAST_UPDATE_DATE_COLUMN_NAME * @param dir - the sort direction to use when retrieving the data * @return SEQUENCE_VALUE of that row * @throws ODKDatastoreException */ private String getSequenceValueForStartTime(DbLogTable logTable, String dateColToUseForCompare, String givenTimestamp, Date dateToCompare, Direction dir) throws ODKDatastoreException { Query query = logTable.query("DataManager.getSequenceValueForTimestamp", cc); // we need the filter to activate the sort for the sequence value query.addFilter(DbLogTable.SEQUENCE_VALUE, org.opendatakit.common.persistence.Query.FilterOperation.GREATER_THAN, " "); query.addSort(DbLogTable.SEQUENCE_VALUE, dir); // _LAST_UPDATE_DATE is a datetime field // _SAVEPOINT_TIMESTAMP is a String field if (dateColToUseForCompare.equals(DbLogTable.LAST_UPDATE_DATE_COLUMN_NAME)) { query.addFilter(dateColToUseForCompare, org.opendatakit.common.persistence.Query.FilterOperation.GREATER_THAN_OR_EQUAL, dateToCompare); } else if (dateColToUseForCompare.equals(DbLogTable.SAVEPOINT_TIMESTAMP.getName())) { query.addFilter(dateColToUseForCompare, org.opendatakit.common.persistence.Query.FilterOperation.GREATER_THAN_OR_EQUAL, givenTimestamp); } List<Entity> values = query.execute(); if (values == null || values.size() == 0) { throw new ODKEntityNotFoundException( "Timestamp " + dateToCompare.toString() + " was not found in log table!"); } Entity e = values.get(0); return e.getString(DbLogTable.SEQUENCE_VALUE); } /** * Perform direct query on dateColToUseForCompare to retrieve the * SEQUENCE_VALUE of that row. This is then used to construct the * query for getting data with that using this end time. * * @param logTable - the log table to use * @param dateColToUseForCompare - the date field to use for the comparison * @param givenTimestamp - the original string value the user passed in * @param dateToCompare - the date to compare against the LAST_UPDATE_DATE_COLUMN_NAME * @param dir - the sort direction to use when retrieving the data * @return SEQUENCE_VALUE of that row * @throws ODKDatastoreException */ private String getSequenceValueForEndTime(DbLogTable logTable, String dateColToUseForCompare, String givenTimestamp, Date dateToCompare, Direction dir) throws ODKDatastoreException { Query query = logTable.query("DataManager.getSequenceValueForTimestamp", cc); // we need the filter to activate the sort for the sequence value query.addFilter(DbLogTable.SEQUENCE_VALUE, org.opendatakit.common.persistence.Query.FilterOperation.GREATER_THAN, " "); query.addSort(DbLogTable.SEQUENCE_VALUE, dir); // _LAST_UPDATE_DATE is a datetime field // _SAVEPOINT_TIMESTAMP is a String field if (dateColToUseForCompare.equals(DbLogTable.LAST_UPDATE_DATE_COLUMN_NAME)) { query.addFilter(dateColToUseForCompare, org.opendatakit.common.persistence.Query.FilterOperation.LESS_THAN_OR_EQUAL, dateToCompare); } else if (dateColToUseForCompare.equals(DbLogTable.SAVEPOINT_TIMESTAMP.getName())) { query.addFilter(dateColToUseForCompare, org.opendatakit.common.persistence.Query.FilterOperation.LESS_THAN_OR_EQUAL, givenTimestamp); } List<Entity> values = query.execute(); if (values == null || values.size() == 0) { throw new ODKEntityNotFoundException( "Timestamp " + dateToCompare.toString() + " was not found in log table!"); } Entity e = values.get(0); return e.getString(DbLogTable.SEQUENCE_VALUE); } /** * @return the query for rows which have been changed or added from the * beginning * @throws ODKDatastoreException */ private Query buildRowsFromBeginningQuery(DbLogTable logTable, DbTableEntryEntity entry, boolean isForwardCursor) throws ODKDatastoreException { Query query = logTable.query("DataManager.buildRowsBeginningFromQuery", cc); query.greaterThanOrEqual(DbLogTable.SEQUENCE_VALUE, entry.getAprioriDataSequenceValue()); if (isForwardCursor) { query.sortAscending(DbLogTable.SEQUENCE_VALUE); } else { query.sortDescending(DbLogTable.SEQUENCE_VALUE); } return query; } /** * @param sequenceValue * @return the query for rows which have been changed or added since the given * sequenceValue * @throws ODKDatastoreException */ private Query buildRowsSinceQuery(DbLogTable logTable, String sequenceValue, boolean isForwardCursor) throws ODKDatastoreException { Query query = logTable.query("DataManager.buildRowsSinceQuery", cc); query.greaterThan(DbLogTable.SEQUENCE_VALUE, sequenceValue); if (isForwardCursor) { query.sortAscending(DbLogTable.SEQUENCE_VALUE); } else { query.sortDescending(DbLogTable.SEQUENCE_VALUE); } return query; } /** * @param startSequenceValue * @param endSequenceValue * @return the query for rows which have been changed or added since the given * sequenceValue * @throws ODKDatastoreException */ private Query buildRowsIncludingQuery(DbLogTable logTable, String startSequenceValue, String endSequenceValue, boolean isForwardCursor) throws ODKDatastoreException { Query query = logTable.query("DataManager.buildRowsIncludingQuery", cc); query.greaterThanOrEqual(DbLogTable.SEQUENCE_VALUE, startSequenceValue); if (endSequenceValue != null) { query.lessThanOrEqual(DbLogTable.SEQUENCE_VALUE, endSequenceValue); } if (isForwardCursor) { query.sortAscending(DbLogTable.SEQUENCE_VALUE); } else { query.sortDescending(DbLogTable.SEQUENCE_VALUE); } return query; } /** * Takes a list of rows which are not necessarily all unique and returns a * list of unique rows. In the case where there is more than one row with the * same rowId, only the last (highest index) row is included in the returned * list. * * @param rows * the rows * @return the list of unique rows */ private List<Row> computeDiff(List<Row> rows) { Map<String, Row> diff = new HashMap<String, Row>(); for (Row logRow : rows) { diff.put(logRow.getRowId(), logRow); } return new ArrayList<Row>(diff.values()); } /** * Retrieve a row from the table. * * @param rowId * the id of the row * @return the row * @throws ODKEntityNotFoundException * if the row with the given id does not exist * @throws ODKDatastoreException * @throws PermissionDeniedException * @throws InconsistentStateException * @throws ODKTaskLockException * @throws BadColumnNameException */ public Row getRow(String rowId) throws ODKEntityNotFoundException, ODKDatastoreException, PermissionDeniedException, InconsistentStateException, ODKTaskLockException, BadColumnNameException { try { Validate.notEmpty(rowId); userPermissions.checkPermission(appId, tableId, TablePermission.READ_ROW); List<DbColumnDefinitionsEntity> columns = null; Entity entity = null; LockTemplate propsLock = new LockTemplate(tableId, ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, cc); try { propsLock.acquire(); DbTableEntryEntity entry = DbTableEntry.getTableIdEntry(tableId, cc); String schemaETag = entry.getSchemaETag(); if (schemaETag == null) { throw new InconsistentStateException("Schema for table " + tableId + " is not yet defined."); } DbTableDefinitionsEntity tableDefn = DbTableDefinitions.getDefinition(tableId, schemaETag, cc); columns = DbColumnDefinitions.query(tableId, schemaETag, cc); DbTable table = DbTable.getRelation(tableDefn, columns, cc); DbLogTable logTable = DbLogTable.getRelation(tableDefn, columns, cc); revertPendingChanges(entry, columns, table, logTable); entity = table.getEntity(rowId, cc); } finally { propsLock.release(); } if (columns == null) { throw new InconsistentStateException("Unable to retrieve rows for table " + tableId + "."); } Row row = converter.toRow(entity, columns); if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_READ)) { return row; } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.READ_ROW, row.getRowId(), row.getFilterScope())) { return row; } throw new PermissionDeniedException(String.format("Denied table %s row %s access to user %s", tableId, rowId, userPermissions.getOdkTablesUserId())); } catch (NullPointerException e) { e.printStackTrace(); throw e; } catch (IllegalArgumentException e) { e.printStackTrace(); throw e; } catch (IndexOutOfBoundsException e) { e.printStackTrace(); throw e; } catch (ODKEntityPersistException e) { e.printStackTrace(); throw e; } catch (ODKEntityNotFoundException e) { e.printStackTrace(); throw e; } catch (ODKDatastoreException e) { e.printStackTrace(); throw e; } catch (ODKTaskLockException e) { e.printStackTrace(); throw e; } catch (BadColumnNameException e) { e.printStackTrace(); throw e; } catch (PermissionDeniedException e) { e.printStackTrace(); throw e; } catch (InconsistentStateException e) { e.printStackTrace(); throw e; } } private void prepareRowForInsertUpdateOrDelete(BulkRowObjWrapper rowWrapper, List<DbColumnDefinitionsEntity> columns, DbTable table, DataKeyValueDeepComparator dc) throws ODKDatastoreException, PermissionDeniedException { Row row = rowWrapper.getRow(); Entity entity = rowWrapper.getEntity(); String rowId = rowWrapper.getRowId(); Scope scope = rowWrapper.getFilterScope(); if (rowWrapper.hasNullIncomingScope() && entity.isFromDatabase()) { // preserve the scope of the existing entity if the incoming Row didn't // specify one. scope = converter.getDbTableFilterScope(entity); } // confirm that the user has the ability to read the row boolean hasPermissions = false; if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_READ)) { hasPermissions = true; } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.READ_ROW, rowId, scope)) { hasPermissions = true; } if (!hasPermissions) { rowWrapper.setOutcome(OutcomeType.DENIED); return; } if (row.isDeleted()) { // check for delete access hasPermissions = false; if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_DELETE)) { hasPermissions = true; } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.DELETE_ROW, rowId, scope)) { hasPermissions = true; } } else { // confirm they have the ability to write to it hasPermissions = false; if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_WRITE)) { hasPermissions = true; } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.WRITE_ROW, rowId, scope)) { hasPermissions = true; } } if (!hasPermissions) { rowWrapper.setOutcome(OutcomeType.DENIED); return; } if (entity.isFromDatabase()) { String rowETag = entity.getString(DbTable.ROW_ETAG); String currentETag = row.getRowETag(); // there was an existing record for the row in the database... if (currentETag == null || !currentETag.equals(rowETag)) { // Take the hit to convert the row we have. Row currentRow = converter.toRow(entity, columns); if (row.hasMatchingSignificantFieldValues(currentRow, dc)) { // If the row matches everywhere except on the rowETag, // return the row on the server. rowWrapper.setOutcome(currentRow, OutcomeType.SUCCESS); return; } // Otherwise, if there is a mis-match, then the client needs to // perform client-side conflict resolution on the changes already // up on the server. Return the row on the server. rowWrapper.setOutcome(currentRow, OutcomeType.IN_CONFLICT); return; } } } /** * The tableUri of the returned rowOutcomeList is null. * * @param rows * @return * @throws ODKEntityPersistException * @throws ODKEntityNotFoundException * @throws ODKDatastoreException * @throws ODKTaskLockException * @throws ETagMismatchException * @throws BadColumnNameException * @throws PermissionDeniedException * @throws InconsistentStateException * @throws TableDataETagMismatchException */ public RowOutcomeList insertOrUpdateRows(RowList rows) throws ODKEntityPersistException, ODKEntityNotFoundException, ODKDatastoreException, ODKTaskLockException, ETagMismatchException, BadColumnNameException, PermissionDeniedException, InconsistentStateException, TableDataETagMismatchException { long startTime = System.currentTimeMillis(); try { Validate.notNull(rows); ArrayList<RowOutcome> rowOutcomes = new ArrayList<RowOutcome>(); userPermissions.checkPermission(appId, tableId, TablePermission.WRITE_ROW); String dataETagAtModification = null; LockTemplate propsLock = new LockTemplate(tableId, ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, cc); List<DbColumnDefinitionsEntity> columns = null; try { propsLock.acquire(); Sequencer sequencer = new Sequencer(cc); DbTableEntryEntity entry = DbTableEntry.getTableIdEntry(tableId, cc); String schemaETag = entry.getSchemaETag(); if (schemaETag == null) { throw new InconsistentStateException("Schema for table " + tableId + " is not yet defined."); } String dataETag = entry.getDataETag(); if (!((dataETag == null) ? (rows.getDataETag() == null) : dataETag.equals(rows.getDataETag()))) { throw new TableDataETagMismatchException( "The dataETag for table " + tableId + " does not match that supplied in the RowList"); } DbTableDefinitionsEntity tableDefn = DbTableDefinitions.getDefinition(tableId, schemaETag, cc); columns = DbColumnDefinitions.query(tableId, schemaETag, cc); DbTable table = DbTable.getRelation(tableDefn, columns, cc); DbLogTable logTable = DbLogTable.getRelation(tableDefn, columns, cc); revertPendingChanges(entry, columns, table, logTable); logger.error("Before loop Time elpased: " + (System.currentTimeMillis() - startTime)); DataKeyValueDeepComparator dc = new DataKeyValueDeepComparator(columns); // mark as pending change. // get new dataETag dataETagAtModification = PersistenceUtils.newUri(); entry.setPendingDataETag(dataETagAtModification); entry.put(cc); List<Entity> entityInsertList = new ArrayList<Entity>(); List<Entity> entityUpdateList = new ArrayList<Entity>(); List<Entity> logEntityList = new ArrayList<Entity>(); ArrayList<BulkRowObjWrapper> rowWrapperList = new ArrayList<BulkRowObjWrapper>(); for (Row row : rows.getRows()) { BulkRowObjWrapper rowWrapper = new BulkRowObjWrapper(row); // and add row wrapper for bulk processing rowWrapperList.add(rowWrapper); Entity entity = null; try { entity = table.getEntity(rowWrapper.getRowId(), cc); if (rowWrapper.hasNewRowId()) { // yikes! -- generated UUID conflicts with an existing one. rowWrapper.setOutcome(OutcomeType.IN_CONFLICT); rowWrapperList.add(rowWrapper); continue; } } catch (ODKEntityNotFoundException e) { if (row.isDeleted()) { rowWrapper.setOutcome(OutcomeType.DENIED); rowWrapperList.add(rowWrapper); continue; } // presumptive initialization for insert... entity = table.newEntity(rowWrapper.getRowId(), cc); entity.set(DbTable.CREATE_USER, userPermissions.getOdkTablesUserId()); } // add entity to row wrapper rowWrapper.setEntity(entity); // determine whether the update or insert should go through or not. // if entity.isFromDatabase() is true, it is an update or delete prepareRowForInsertUpdateOrDelete(rowWrapper, columns, table, dc); // OK we are able to update or insert or delete the record if (!rowWrapper.outcomeAlreadySet()) { Scope scope = rowWrapper.getFilterScope(); String previousRowETag; if (row.isDeleted()) { // remember the previous row ETag so we can chain revisions in the // DbLogTable previousRowETag = entity.getString(DbTable.ROW_ETAG); // update the row ETag and deletion status entity.set(DbTable.ROW_ETAG, PersistenceUtils.newUri()); entity.set(DbTable.DELETED, true); } else { // this will be null of the entity is newly created... previousRowETag = row.getRowETag(); // update the fields in the DbTable entity... creator.setRowFields(entity, PersistenceUtils.newUri(), dataETagAtModification, userPermissions.getOdkTablesUserId(), false, scope, row.getFormId(), row.getLocale(), row.getSavepointType(), row.getSavepointTimestamp(), row.getSavepointCreator(), row.getValues(), columns); } // create log table entry Entity logEntity = creator.newLogEntity(logTable, dataETagAtModification, previousRowETag, entity, columns, sequencer, cc); logEntityList.add(logEntity); if (entity.isFromDatabase()) { entityUpdateList.add(entity); } else { entityInsertList.add(entity); } } } // commit the log change to the database (must be done first!) if (!logEntityList.isEmpty()) { logTable.bulkAlterEntities(logEntityList, cc); } // commit the row updates if (!entityUpdateList.isEmpty()) { table.bulkAlterEntities(entityUpdateList, cc); } // commit the row inserts if (!entityInsertList.isEmpty()) { table.bulkAlterEntities(entityInsertList, cc); } // commit change entry.setDataETag(entry.getPendingDataETag()); entry.setPendingDataETag(null); entry.put(cc); for (BulkRowObjWrapper rowWrapper : rowWrapperList) { if (!rowWrapper.outcomeAlreadySet()) { // we need to return the fields from the entity we upserted. Row newServer = converter.toRow(rowWrapper.getEntity(), columns); rowWrapper.setOutcome(newServer, OutcomeType.SUCCESS); } // update the outcomes set... rowOutcomes.add(rowWrapper.getOutcome()); } logger.error("End loop Time elpased: " + (System.currentTimeMillis() - startTime)); } finally { propsLock.release(); } if (columns == null) { throw new InconsistentStateException("Unable to retrieve rows for table " + tableId + "."); } if (rows != null) { long time = (System.currentTimeMillis() - startTime); int numRows = rows.getRows().size(); logger.error("Time: " + time + " size: " + numRows + " per iteration " + (time / numRows)); } return new RowOutcomeList(rowOutcomes, dataETagAtModification); } catch (NullPointerException e) { e.printStackTrace(); throw e; } catch (IllegalArgumentException e) { e.printStackTrace(); throw e; } catch (IndexOutOfBoundsException e) { e.printStackTrace(); throw e; } catch (ODKEntityPersistException e) { e.printStackTrace(); throw e; } catch (ODKEntityNotFoundException e) { e.printStackTrace(); throw e; } catch (ODKDatastoreException e) { e.printStackTrace(); throw e; } catch (ODKTaskLockException e) { e.printStackTrace(); throw e; } catch (BadColumnNameException e) { e.printStackTrace(); throw e; } catch (PermissionDeniedException e) { e.printStackTrace(); throw e; } catch (InconsistentStateException e) { e.printStackTrace(); throw e; } } /** * Inserts or Updates a row. If inserting, the row must not already exist or * the eTag for the row being inserted must exactly match that on the server. * * @param af * -- authentication filter to be applied to this action * @param row * the row to update. See * {@link Row#forInsert(String, String, java.util.Map)}. * {@link Row#forUpdate(String, String, java.util.Map)} * {@link Row#isDeleted()}, {@link Row#getCreateUser()}, and * {@link Row#getLastUpdateUser()} will be ignored if they are set. * @return a copy of the row that was inserted or updated, with the row's * rowETtag populated with the new rowETtag. If the original passed in * row had a null rowId, the row will contain the generated rowId. * @throws ODKEntityNotFoundException * if the passed in row does not exist * @throws ODKDatastoreException * @throws ODKTaskLockException * @throws ETagMismatchException * if the passed in row has a different rowETtag from the row in the * datastore (e.g., on insert, the row already exists, or on update, * there is conflict that needs to be resolved). * @throws BadColumnNameException * if the passed in row set a value for a column which doesn't exist * in the table * @throws PermissionDeniedException * @throws InconsistentStateException * */ public Row insertOrUpdateRow(Row row) throws ODKEntityPersistException, ODKEntityNotFoundException, ODKDatastoreException, ODKTaskLockException, ETagMismatchException, BadColumnNameException, PermissionDeniedException, InconsistentStateException { try { Validate.notNull(row); userPermissions.checkPermission(appId, tableId, TablePermission.WRITE_ROW); List<DbColumnDefinitionsEntity> columns = null; Entity entity = null; LockTemplate propsLock = new LockTemplate(tableId, ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, cc); try { propsLock.acquire(); Sequencer sequencer = new Sequencer(cc); DbTableEntryEntity entry = DbTableEntry.getTableIdEntry(tableId, cc); String schemaETag = entry.getSchemaETag(); if (schemaETag == null) { throw new InconsistentStateException("Schema for table " + tableId + " is not yet defined."); } DbTableDefinitionsEntity tableDefn = DbTableDefinitions.getDefinition(tableId, schemaETag, cc); columns = DbColumnDefinitions.query(tableId, schemaETag, cc); DbTable table = DbTable.getRelation(tableDefn, columns, cc); DbLogTable logTable = DbLogTable.getRelation(tableDefn, columns, cc); revertPendingChanges(entry, columns, table, logTable); DataKeyValueDeepComparator dc = new DataKeyValueDeepComparator(columns); String rowId = row.getRowId(); boolean newRowId = false; if (rowId == null) { newRowId = true; rowId = PersistenceUtils.newUri(); row.setRowId(rowId); } boolean nullIncomingScope = false; Scope scope = row.getFilterScope(); if (scope == null) { nullIncomingScope = true; scope = Scope.EMPTY_SCOPE; row.setFilterScope(scope); } try { entity = table.getEntity(rowId, cc); if (newRowId) { throw new InconsistentStateException( "Synthesized rowId collides with existing row in table " + tableId + "."); } if (nullIncomingScope) { // preserve the scope of the existing entity if the incoming Row // didn't specify one. scope = converter.getDbTableFilterScope(entity); } // confirm that the user has the ability to read the row boolean hasPermissions = false; if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_READ)) { hasPermissions = true; } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.READ_ROW, rowId, scope)) { hasPermissions = true; } if (!hasPermissions) { throw new PermissionDeniedException( String.format("Denied table %s row %s read access to user %s", tableId, rowId, userPermissions.getOdkTablesUserId())); } // confirm they have the ability to write to it hasPermissions = false; if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_WRITE)) { hasPermissions = true; } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.WRITE_ROW, rowId, scope)) { hasPermissions = true; } if (!hasPermissions) { throw new PermissionDeniedException( String.format("Denied table %s row %s read access to user %s", tableId, rowId, userPermissions.getOdkTablesUserId())); } String rowETag = entity.getString(DbTable.ROW_ETAG); String currentETag = row.getRowETag(); if (currentETag == null || !currentETag.equals(rowETag)) { // Take the hit to convert the row we have. // If the row matches everywhere except on the rowETag, return it. Row currentRow = converter.toRow(entity, columns); if (row.hasMatchingSignificantFieldValues(currentRow, dc)) { return currentRow; } // if null, then the client thinks they are creating a new row. // The rows may be identical, but leave that to the client to // determine // trigger client-side conflict resolution. // Otherwise, if there is a mis-match, then the client needs to pull // and // perform client-side conflict resolution on the changes already up // on the server. throw new ETagMismatchException(String.format( "rowETag %s does not match %s " + "for rowId %s", currentETag, rowETag, rowId)); } } catch (ODKEntityNotFoundException e) { // require unfiltered write permissions to create a new record userPermissions.checkPermission(appId, tableId, TablePermission.UNFILTERED_WRITE); newRowId = true; // initialization for insert... entity = table.newEntity(rowId, cc); entity.set(DbTable.CREATE_USER, userPermissions.getOdkTablesUserId()); } // OK we are able to update or insert the record -- mark as pending // change. // get new dataETag String dataETagAtModification = PersistenceUtils.newUri(); entry.setPendingDataETag(dataETagAtModification); entry.put(cc); // this will be null of the entity is newly created... String previousRowETag = row.getRowETag(); // update the fields in the DbTable entity... creator.setRowFields(entity, PersistenceUtils.newUri(), dataETagAtModification, userPermissions.getOdkTablesUserId(), false, scope, row.getFormId(), row.getLocale(), row.getSavepointType(), row.getSavepointTimestamp(), row.getSavepointCreator(), row.getValues(), columns); // create log table entry Entity logEntity = creator.newLogEntity(logTable, dataETagAtModification, previousRowETag, entity, columns, sequencer, cc); // update db DbLogTable.putEntity(logEntity, cc); DbTable.putEntity(entity, cc); // commit change entry.setDataETag(entry.getPendingDataETag()); entry.setPendingDataETag(null); entry.put(cc); } finally { propsLock.release(); } if (columns == null) { throw new InconsistentStateException("Unable to retrieve rows for table " + tableId + "."); } Row updatedRow = converter.toRow(entity, columns); return updatedRow; } catch (NullPointerException e) { e.printStackTrace(); throw e; } catch (IllegalArgumentException e) { e.printStackTrace(); throw e; } catch (IndexOutOfBoundsException e) { e.printStackTrace(); throw e; } catch (ODKEntityPersistException e) { e.printStackTrace(); throw e; } catch (ODKEntityNotFoundException e) { e.printStackTrace(); throw e; } catch (ETagMismatchException e) { e.printStackTrace(); throw e; } catch (ODKDatastoreException e) { e.printStackTrace(); throw e; } catch (ODKTaskLockException e) { e.printStackTrace(); throw e; } catch (BadColumnNameException e) { e.printStackTrace(); throw e; } catch (PermissionDeniedException e) { e.printStackTrace(); throw e; } catch (InconsistentStateException e) { e.printStackTrace(); throw e; } } /** * Delete a row. * * @param rowId * the row to delete. * @param currentRowETag * the ETag for that row, as known to the requester * @return returns the new dataETag that is current after deleting the row. * @throws ODKEntityNotFoundException * if there is no row with the given id in the datastore * @throws ODKDatastoreException * @throws ODKTaskLockException * @throws PermissionDeniedException * @throws InconsistentStateException * @throws BadColumnNameException * @throws ETagMismatchException */ public String deleteRow(String rowId, String currentRowETag) throws ODKEntityNotFoundException, ODKDatastoreException, ODKTaskLockException, PermissionDeniedException, InconsistentStateException, BadColumnNameException, ETagMismatchException { try { Validate.notNull(rowId); Validate.notBlank(rowId); userPermissions.checkPermission(appId, tableId, TablePermission.DELETE_ROW); String dataETagAtModification = null; LockTemplate propsLock = new LockTemplate(tableId, ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, cc); try { propsLock.acquire(); Sequencer sequencer = new Sequencer(cc); DbTableEntryEntity entry = DbTableEntry.getTableIdEntry(tableId, cc); String schemaETag = entry.getSchemaETag(); if (schemaETag == null) { throw new InconsistentStateException("Schema for table " + tableId + " is not yet defined."); } DbTableDefinitionsEntity tableDefn = DbTableDefinitions.getDefinition(tableId, schemaETag, cc); List<DbColumnDefinitionsEntity> columns = DbColumnDefinitions.query(tableId, schemaETag, cc); DbTable table = DbTable.getRelation(tableDefn, columns, cc); DbLogTable logTable = DbLogTable.getRelation(tableDefn, columns, cc); revertPendingChanges(entry, columns, table, logTable); Entity entity = table.getEntity(rowId, cc); // entity exists (or we would have thrown an // ODKEntityNotFoundException). String serverRowETag = entity.getString(DbTable.ROW_ETAG); if (!currentRowETag.equals(serverRowETag)) { // if null, then the client thinks they are creating a new row. // The rows may be identical, but leave that to the client to // determine // trigger client-side conflict resolution. // Otherwise, if there is a mis-match, then the client needs to pull // and // perform client-side conflict resolution on the changes already up // on the server. throw new ETagMismatchException(String.format("rowETag %s does not match %s " + "for rowId %s", currentRowETag, serverRowETag, rowId)); } Scope scope = converter.getDbTableFilterScope(entity); // check for read access boolean hasPermissions = false; if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_READ)) { hasPermissions = true; } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.READ_ROW, rowId, scope)) { hasPermissions = true; } if (!hasPermissions) { throw new PermissionDeniedException( String.format("Denied table %s row %s read access to user %s", tableId, rowId, userPermissions.getOdkTablesUserId())); } // check for delete access hasPermissions = false; if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_DELETE)) { hasPermissions = true; } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.DELETE_ROW, rowId, scope)) { hasPermissions = true; } if (!hasPermissions) { throw new PermissionDeniedException( String.format("Denied table %s row %s delete access to user %s", tableId, rowId, userPermissions.getOdkTablesUserId())); } // get new dataETag dataETagAtModification = PersistenceUtils.newUri(); entry.setPendingDataETag(dataETagAtModification); entry.put(cc); // remember the previous row ETag so we can chain revisions in the // DbLogTable String previousRowETag = entity.getString(DbTable.ROW_ETAG); // update the row ETag and deletion status entity.set(DbTable.ROW_ETAG, PersistenceUtils.newUri()); entity.set(DbTable.DELETED, true); // create log table entry Entity logEntity = creator.newLogEntity(logTable, dataETagAtModification, previousRowETag, entity, columns, sequencer, cc); // commit the log change to the database (must be done first!) DbLogTable.putEntity(logEntity, cc); // commit the row change DbTable.putEntity(entity, cc); // NOTE: the DbTableInstanceFiles objects are never deleted unless the // table is dropped. // They hold the file attachments and are eferred to by the records in // the DbLogTable // even if the row is deleted from the set of active records (i.e., // DbTable). // update the TableEntry to reflect the completion of the change entry.setDataETag(entry.getPendingDataETag()); entry.setPendingDataETag(null); entry.put(cc); } finally { propsLock.release(); } return dataETagAtModification; } catch (NullPointerException e) { e.printStackTrace(); throw e; } catch (IllegalArgumentException e) { e.printStackTrace(); throw e; } catch (IndexOutOfBoundsException e) { e.printStackTrace(); throw e; } catch (ODKEntityPersistException e) { e.printStackTrace(); throw e; } catch (ODKEntityNotFoundException e) { e.printStackTrace(); throw e; } catch (ETagMismatchException e) { e.printStackTrace(); throw e; } catch (ODKDatastoreException e) { e.printStackTrace(); throw e; } catch (ODKTaskLockException e) { e.printStackTrace(); throw e; } catch (BadColumnNameException e) { e.printStackTrace(); throw e; } catch (PermissionDeniedException e) { e.printStackTrace(); throw e; } catch (InconsistentStateException e) { e.printStackTrace(); throw e; } } /** * Returns the dataETag values that were applied after the given dataETag * and/or sequenceValue. * * There is no meaningful ordering of this returned set. For consistency, * the list is ordered by the dataETag values themselves. The returned object * includes the dataETag at the beginning of the processing of this request * and a sequenceValue. Either of these can be used to return any changes * made after this request. * * @param dataETag * @param sequenceValue * @return * @throws PermissionDeniedException * @throws ODKDatastoreException * @throws ODKTaskLockException * @throws InconsistentStateException * @throws BadColumnNameException */ public ChangeSetList getChangeSetsSince(String dataETag, String sequenceValue) throws PermissionDeniedException, ODKDatastoreException, ODKTaskLockException, InconsistentStateException, BadColumnNameException { userPermissions.checkPermission(appId, tableId, TablePermission.READ_ROW); String currentDataETag = null; String retrievalSequenceValue = null; List<DbColumnDefinitionsEntity> columns = null; List<?> result = null; LockTemplate propsLock = new LockTemplate(tableId, ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, cc); try { propsLock.acquire(); Sequencer sequencer = new Sequencer(cc); retrievalSequenceValue = sequencer.getNextSequenceValue(); DbTableEntryEntity entry = DbTableEntry.getTableIdEntry(tableId, cc); String schemaETag = entry.getSchemaETag(); if (schemaETag == null) { throw new InconsistentStateException("Schema for table " + tableId + " is not yet defined."); } currentDataETag = entry.getDataETag(); DbTableDefinitionsEntity tableDefn = DbTableDefinitions.getDefinition(tableId, schemaETag, cc); columns = DbColumnDefinitions.query(tableId, schemaETag, cc); DbTable table = DbTable.getRelation(tableDefn, columns, cc); DbLogTable logTable = DbLogTable.getRelation(tableDefn, columns, cc); revertPendingChanges(entry, columns, table, logTable); String unifiedSequenceValue = null; if (dataETag != null) { try { unifiedSequenceValue = getSequenceValueForDataETag(logTable, dataETag); } catch (ODKEntityNotFoundException e) { // TODO: log this as a warning -- may be returning a very large set unifiedSequenceValue = null; } } if (sequenceValue != null && (unifiedSequenceValue == null || (unifiedSequenceValue.compareTo(sequenceValue) < 0))) { unifiedSequenceValue = sequenceValue; } Query query; if (unifiedSequenceValue == null) { query = buildRowsFromBeginningQuery(logTable, entry, true); } else { query = buildRowsSinceQuery(logTable, unifiedSequenceValue, true); } result = query.getDistinct(DbLogTable.DATA_ETAG_AT_MODIFICATION); } finally { propsLock.release(); } if (result == null || result.isEmpty()) { return new ChangeSetList(null, currentDataETag, retrievalSequenceValue); } ArrayList<String> dataETags = new ArrayList<String>(); for (Object o : result) { String value = (String) o; dataETags.add(value); } return new ChangeSetList(dataETags, currentDataETag, retrievalSequenceValue); } /** * Returns the set of rows for a given dataETag (changeSet). * If the isActive flag is true, then return only the subset * of these that are the most current (in the DbTable). * Otherwise, return the full set of changes from the DbLogTable. * * @param dataETag * @param isActive * @param startCursor * @param fetchLimit * @return * @throws PermissionDeniedException * @throws ODKDatastoreException * @throws InconsistentStateException * @throws ODKTaskLockException * @throws BadColumnNameException */ public WebsafeRows getChangeSetRows(String dataETag, boolean isActive, QueryResumePoint startCursor, int fetchLimit) throws PermissionDeniedException, ODKDatastoreException, InconsistentStateException, ODKTaskLockException, BadColumnNameException { userPermissions.checkPermission(appId, tableId, TablePermission.READ_ROW); String currentDataETag = null; List<DbColumnDefinitionsEntity> columns = null; WebsafeQueryResult result = null; LockTemplate propsLock = new LockTemplate(tableId, ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, cc); try { propsLock.acquire(); DbTableEntryEntity entry = DbTableEntry.getTableIdEntry(tableId, cc); String schemaETag = entry.getSchemaETag(); if (schemaETag == null) { throw new InconsistentStateException("Schema for table " + tableId + " is not yet defined."); } currentDataETag = entry.getDataETag(); DbTableDefinitionsEntity tableDefn = DbTableDefinitions.getDefinition(tableId, schemaETag, cc); columns = DbColumnDefinitions.query(tableId, schemaETag, cc); DbTable table = DbTable.getRelation(tableDefn, columns, cc); DbLogTable logTable = DbLogTable.getRelation(tableDefn, columns, cc); revertPendingChanges(entry, columns, table, logTable); boolean isForwardCursor = (startCursor == null ? true : startCursor.isForwardCursor()); if (isActive) { // query is against DbTable Query query = table.query("DataManager.getChangeSetRows", cc); query.equal(DbTable.DATA_ETAG_AT_MODIFICATION, dataETag); if (isForwardCursor) { query.greaterThan(DbTable.ROW_ETAG, ""); query.sortAscending(DbTable.ROW_ETAG); } else { query.greaterThan(DbTable.ROW_ETAG, ""); query.sortDescending(DbTable.ROW_ETAG); } result = query.execute(startCursor, fetchLimit); } else { // query is against DbLogTable Query query = logTable.query("DataManager.getChangeSetRows", cc); query.equal(DbLogTable.DATA_ETAG_AT_MODIFICATION, dataETag); if (isForwardCursor) { query.greaterThan(DbLogTable.ROW_ID, ""); query.sortAscending(DbLogTable.ROW_ID); } else { query.greaterThan(DbLogTable.ROW_ID, ""); query.sortDescending(DbLogTable.ROW_ID); } result = query.execute(startCursor, fetchLimit); } } finally { propsLock.release(); } if (result.entities == null || columns == null) { throw new InconsistentStateException("Unable to retrieve rows for table " + tableId + "."); } // TODO: properly handle reporting of rows that the user no longer has // access to because of a access / permissions change for that user and / or // row. ArrayList<Row> rows = new ArrayList<Row>(); if (isActive) { // query is against DbTable for (Entity entity : result.entities) { Row row = converter.toRow(entity, columns); if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_READ)) { rows.add(row); } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.READ_ROW, row.getRowId(), row.getFilterScope())) { rows.add(row); } } } else { // query is against DbLogTable for (Entity entity : result.entities) { Row row = converter.toRowFromLogTable(entity, columns); if (userPermissions.hasPermission(appId, tableId, TablePermission.UNFILTERED_READ)) { rows.add(row); } else if (userPermissions.hasFilterScope(appId, tableId, TablePermission.READ_ROW, row.getRowId(), row.getFilterScope())) { rows.add(row); } } } return new WebsafeRows(computeDiff(rows), currentDataETag, result.websafeRefetchCursor, result.websafeBackwardCursor, result.websafeResumeCursor, result.hasMore, result.hasPrior); } }