diff --git a/announcements/src/org/labkey/announcements/model/AnnouncementManager.java b/announcements/src/org/labkey/announcements/model/AnnouncementManager.java index 4ccdffebc80..8fd49a60518 100644 --- a/announcements/src/org/labkey/announcements/model/AnnouncementManager.java +++ b/announcements/src/org/labkey/announcements/model/AnnouncementManager.java @@ -696,7 +696,7 @@ public static AnnouncementModel updateAnnouncement(User user, AnnouncementModel public static int updateContainer(List discussionSrcIds, Container targetContainer, User user) { - return ContainerManager.updateContainer(_comm.getTableInfoAnnouncements(), "discussionSrcIdentifier", discussionSrcIds, targetContainer, user, false); + return Table.updateContainer(_comm.getTableInfoAnnouncements(), "discussionSrcIdentifier", discussionSrcIds, targetContainer, user, false); } diff --git a/api/src/org/labkey/api/attachments/AttachmentService.java b/api/src/org/labkey/api/attachments/AttachmentService.java index 91e937f8007..538d25674f9 100644 --- a/api/src/org/labkey/api/attachments/AttachmentService.java +++ b/api/src/org/labkey/api/attachments/AttachmentService.java @@ -91,7 +91,7 @@ static AttachmentService get() void copyAttachment(AttachmentParent parent, Attachment a, String newName, User auditUser) throws IOException; - void moveAttachments(Container newContainer, List parents, User auditUser) throws IOException; + int moveAttachments(Container newContainer, List parents, User auditUser) throws IOException; @NotNull List getAttachmentFiles(AttachmentParent parent, Collection attachments) throws IOException; diff --git a/api/src/org/labkey/api/audit/AbstractAuditTypeProvider.java b/api/src/org/labkey/api/audit/AbstractAuditTypeProvider.java index ff5fdfdc722..da95c90b575 100644 --- a/api/src/org/labkey/api/audit/AbstractAuditTypeProvider.java +++ b/api/src/org/labkey/api/audit/AbstractAuditTypeProvider.java @@ -30,8 +30,7 @@ import org.labkey.api.data.DbSchemaType; import org.labkey.api.data.DbScope; import org.labkey.api.data.MutableColumnInfo; -import org.labkey.api.data.SQLFragment; -import org.labkey.api.data.SqlExecutor; +import org.labkey.api.data.Table; import org.labkey.api.data.TableInfo; import org.labkey.api.dataiterator.DataIterator; import org.labkey.api.dataiterator.ExistingRecordDataIterator; @@ -395,11 +394,6 @@ else if (value instanceof Date date) public int moveEvents(Container targetContainer, String idColumnName, Collection ids) { - TableInfo auditTable = createStorageTableInfo(); - SQLFragment sql = new SQLFragment("UPDATE ").append(auditTable) - .append(" SET container = ").appendValue(targetContainer) - .append(" WHERE ").append(idColumnName); - auditTable.getSchema().getSqlDialect().appendInClauseSql(sql, ids); - return new SqlExecutor(auditTable.getSchema()).execute(sql); + return Table.updateContainer(createStorageTableInfo(), idColumnName, ids, targetContainer, null, false); } } diff --git a/api/src/org/labkey/api/data/ContainerManager.java b/api/src/org/labkey/api/data/ContainerManager.java index ecae4509833..af7d9098e1a 100644 --- a/api/src/org/labkey/api/data/ContainerManager.java +++ b/api/src/org/labkey/api/data/ContainerManager.java @@ -43,6 +43,7 @@ import org.labkey.api.audit.provider.ContainerAuditProvider; import org.labkey.api.cache.Cache; import org.labkey.api.cache.CacheManager; +import org.labkey.api.collections.CaseInsensitiveHashMap; import org.labkey.api.collections.CaseInsensitiveHashSet; import org.labkey.api.collections.ConcurrentHashSet; import org.labkey.api.collections.IntHashMap; @@ -103,7 +104,6 @@ import org.labkey.api.view.ViewContext; import org.labkey.api.writer.MemoryVirtualFile; import org.labkey.folder.xml.FolderDocument; -import org.labkey.remoteapi.collections.CaseInsensitiveHashMap; import org.springframework.validation.BindException; import org.springframework.validation.Errors; @@ -138,7 +138,7 @@ /** * This class manages a hierarchy of collections, backed by a database table called Containers. - * Containers are named using filesystem-like paths e.g. /proteomics/comet/. Each path + * Containers are named using filesystem-like paths e.g., /proteomics/comet/. Each path * maps to a UID and set of permissions. The current security scheme allows ACLs * to be specified explicitly on the directory or completely inherited. ACLs are not combined. *

@@ -150,7 +150,7 @@ * a container is deleted, it should never get put back in the cache. We accomplish this by synchronizing on * the removal from the cache, and the database lookup/cache insertion. While a container is in the middle * of being deleted, it's OK for other clients to see it because FKs enforce that it's always internally - * consistent, even if some of the data has already been deleted. + * consistent, even if some data has already been deleted. */ public class ContainerManager { @@ -2683,8 +2683,7 @@ private static Container getContainerForIdOrPath(String targetContainer) return c; } - // targetContainer must be in the same app project at this time - // i.e. child of current project, project of current child, sibling within project + // Current and target containers must be within the same project tree at this time private static boolean isValidTargetContainer(Container current, Container target) { if (current.isRoot() || target.isRoot()) @@ -2694,31 +2693,16 @@ private static boolean isValidTargetContainer(Container current, Container targe if (current.equals(target)) return true; - boolean moveFromProjectToChild = current.isProject() && target.getParent().equals(current); - boolean moveFromChildToProject = !current.isProject() && current.getParent().isProject() && current.getParent().equals(target); - boolean moveFromChildToSibling = !current.isProject() && current.getParent().isProject() && current.getParent().equals(target.getParent()); + // from project to descendant + if (current.isProject()) + return target.isDescendant(current); - return moveFromProjectToChild || moveFromChildToProject || moveFromChildToSibling; - } - - public static int updateContainer(TableInfo dataTable, String idField, Collection ids, Container targetContainer, User user, boolean withModified) - { - try (DbScope.Transaction transaction = dataTable.getSchema().getScope().ensureTransaction()) - { - SQLFragment dataUpdate = new SQLFragment("UPDATE ").append(dataTable) - .append(" SET container = ").appendValue(targetContainer.getEntityId()); - if (withModified) - { - dataUpdate.append(", modified = ").appendValue(new Date()); - dataUpdate.append(", modifiedby = ").appendValue(user.getUserId()); - } - dataUpdate.append(" WHERE ").append(idField); - dataTable.getSchema().getSqlDialect().appendInClauseSql(dataUpdate, ids); - int numUpdated = new SqlExecutor(dataTable.getSchema()).execute(dataUpdate); - transaction.commit(); + // from descendant to project + if (target.isProject()) + return current.isDescendant(target); - return numUpdated; - } + // from descendant to descendant + return current.getProject() != null && current.getProject().equals(target.getProject()); } /** diff --git a/api/src/org/labkey/api/data/Table.java b/api/src/org/labkey/api/data/Table.java index f14716c0b14..ac80f5b39a5 100644 --- a/api/src/org/labkey/api/data/Table.java +++ b/api/src/org/labkey/api/data/Table.java @@ -21,7 +21,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; -import org.apache.poi.ss.formula.functions.T; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.Assert; @@ -37,6 +36,7 @@ import org.labkey.api.dataiterator.DataIteratorContext; import org.labkey.api.dataiterator.Pump; import org.labkey.api.dataiterator.SimpleTranslator; +import org.labkey.api.dataiterator.SimpleTranslator.SpecialColumn; import org.labkey.api.dataiterator.TableInsertDataIteratorBuilder; import org.labkey.api.di.DataIntegrationService; import org.labkey.api.exceptions.OptimisticConflictException; @@ -102,12 +102,12 @@ public class Table // Makes long parameter lists easier to read public static final int NO_OFFSET = 0; - public static final String ENTITY_ID_COLUMN_NAME = "EntityId"; - public static final String OWNER_COLUMN_NAME = "Owner"; - public static final String CREATED_BY_COLUMN_NAME = "CreatedBy"; - public static final String CREATED_COLUMN_NAME = "Created"; - public static final String MODIFIED_BY_COLUMN_NAME = "ModifiedBy"; - public static final String MODIFIED_COLUMN_NAME = "Modified"; + private static final String ENTITY_ID_COLUMN_NAME = SpecialColumn.EntityId.name(); + private static final String OWNER_COLUMN_NAME = "Owner"; + private static final String CREATED_BY_COLUMN_NAME = SpecialColumn.CreatedBy.name(); + private static final String CREATED_COLUMN_NAME = SpecialColumn.Created.name(); + private static final String MODIFIED_BY_COLUMN_NAME = SpecialColumn.ModifiedBy.name(); + private static final String MODIFIED_COLUMN_NAME = SpecialColumn.Modified.name(); /** Columns that are magically populated as part of an insert or update operation */ public static final Set AUTOPOPULATED_COLUMN_NAMES = CaseInsensitiveHashSet.of( @@ -819,7 +819,7 @@ else if (propName.endsWith("id")) */ public static K insert(@Nullable User user, TableInfo table, K fieldsIn) { - assert (table.getTableType() != DatabaseTableType.NOT_IN_DB): ("Table " + table.getSchema().getName() + "." + table.getName() + " is not in the physical database."); + assert assertInDb(table); // _executeTriggers(table, fields); @@ -963,7 +963,7 @@ public static K update(@Nullable User user, TableInfo table, K fieldsIn, Obj /* NOTE this does not enforce that keyColumn is an appropriately unique column! */ public static K update(@Nullable User user, TableInfo table, K fieldsIn, @Nullable ColumnInfo keyColumn, Object pkVals, @Nullable Filter filter, Level level) { - assert (table.getTableType() != DatabaseTableType.NOT_IN_DB): (table.getName() + " is not in the physical database."); + assert assertInDb(table); assert null != pkVals; // _executeTriggers(table, previous, fields); @@ -1128,6 +1128,71 @@ else if (pkVals instanceof Map) return (fieldsIn instanceof Map && !(fieldsIn instanceof BoundMap)) ? (K)fields : fieldsIn; } + /** + * Updates the container of specified rows in the provided database table. Optionally, the modification timestamp + * and the user who made the modification can also be updated if specified. + * + * @param table The table where the container update should be applied. + * @param idField The name of the identifier field used to locate the rows to update. + * @param ids A collection of identifier values specifying the rows to be updated. + * @param targetContainer The target container to set for the specified rows. + * @param user The user performing the update. If null, modified/modifiedBy details are not updated. + * @param withModified If true, updates the modified timestamp and the user who made the modification. + * @return The number of rows updated in the table. + */ + public static int updateContainer(TableInfo table, String idField, Collection ids, Container targetContainer, @Nullable User user, boolean withModified) + { + assert assertInDb(table); + ColumnInfo idColumn = table.getColumn(idField); + if (idColumn == null) + throw new IllegalArgumentException("Table " + fullTableName(table) + " has no column named '" + idField + "'."); + + if (ids == null || ids.isEmpty()) + return 0; + + SimpleFilter filter = new SimpleFilter(); + filter.addInClause(idColumn.getFieldKey(), ids); + + return updateContainer(table, targetContainer, filter, user, withModified); + } + + public static int updateContainer(TableInfo table, Container targetContainer, @NotNull SimpleFilter filter, @Nullable User user, boolean withModified) + { + assert assertInDb(table); + ColumnInfo containerColumn = table.getColumn(SpecialColumn.Container.name()); + if (containerColumn == null) + throw new IllegalArgumentException("Table " + fullTableName(table) + " has no column named '" + SpecialColumn.Container.name() + "'."); + + SQLFragment dataUpdate = new SQLFragment("UPDATE ").append(table) + .append(" SET ").appendIdentifier(containerColumn.getSelectIdentifier()) + .append(" = ") + .appendValue(targetContainer); + + if (withModified) + { + assert user != null : "User must be specified when updating modified/modifiedBy details."; + ColumnInfo colModified = table.getColumn(MODIFIED_COLUMN_NAME); + if (null != colModified) + { + dataUpdate.append(", ").appendIdentifier(colModified.getSelectIdentifier()) + .append(" = ") + .appendValue(new java.sql.Timestamp(System.currentTimeMillis())); + } + + ColumnInfo colModifiedBy = table.getColumn(MODIFIED_BY_COLUMN_NAME); + if (null != colModifiedBy) + { + dataUpdate.append(", ").appendIdentifier(colModifiedBy.getSelectIdentifier()) + .append(" = ") + .appendValue(user.getUserId()); + } + } + + SQLFragment whereClause = filter.getSQLFragment(table.getSqlDialect(), null, createMetaDataNameMap(table)); + dataUpdate.append("\n").append(whereClause); + + return new SqlExecutor(table.getSchema()).execute(dataUpdate); + } public static void delete(TableInfo table, Object rowId) { @@ -1154,17 +1219,16 @@ public static void delete(TableInfo table, Object rowId) public static int delete(TableInfo table) { - assert (table.getTableType() != DatabaseTableType.NOT_IN_DB): (table.getName() + " is not in the physical database."); + assert assertInDb(table); SqlExecutor sqlExecutor = new SqlExecutor(table.getSchema()); + return sqlExecutor.execute("DELETE FROM " + table.getSelectName()); } public static int delete(TableInfo table, Filter filter) { - assert (table.getTableType() != DatabaseTableType.NOT_IN_DB): (table.getName() + " is not in the physical database."); - + assert assertInDb(table); SQLFragment where = filter.getSQLFragment(table.getSqlDialect(), null, createMetaDataNameMap(table)); - SQLFragment deleteSQL = new SQLFragment("DELETE FROM ").append(table).append("\n\t").append(where); return new SqlExecutor(table.getSchema()).execute(deleteSQL); @@ -1172,7 +1236,7 @@ public static int delete(TableInfo table, Filter filter) public static void truncate(TableInfo table) { - assert (table.getTableType() != DatabaseTableType.NOT_IN_DB): (table.getName() + " is not in the physical database."); + assert assertInDb(table); SqlExecutor sqlExecutor = new SqlExecutor(table.getSchema()); sqlExecutor.execute(table.getSqlDialect().getTruncateSql(table.getSelectName())); } @@ -1569,7 +1633,7 @@ static public Map createColumnMap(@Nullable TableInfo tabl * Create a map that can be passed into Filter.getSQLFragment() that create a SQL fragment using getMetaDataName() instead of * getAlias(). */ - static private Map createMetaDataNameMap(TableInfo table) + private static Map createMetaDataNameMap(TableInfo table) { Map ret = new HashMap<>(); for (var column : table.getColumns()) @@ -1581,7 +1645,6 @@ static private Map createMetaDataNameMap(TableInfo table) return ret; } - public static boolean checkAllColumns(TableInfo table, Collection columns, String prefix) { return checkAllColumns(table, columns, prefix, false); @@ -1591,7 +1654,6 @@ public static boolean checkAllColumns(TableInfo table, Collection co { int bad = 0; -// Map mapFK = new HashMap<>(columns.size()*2); Map mapAlias = new HashMap<>(columns.size()*2); ColumnInfo prev; @@ -1599,8 +1661,6 @@ public static boolean checkAllColumns(TableInfo table, Collection co { if (!checkColumn(table, column, prefix)) bad++; -// if (enforceUnique && null != (prev=mapFK.put(column.getFieldKey(), column)) && prev != column) -// bad++; if (enforceUnique && !(column instanceof AliasedColumn) && null != (prev = mapAlias.put(column.getAlias().getId(), column)) && prev != column) { _log.warn(prefix + ": Column " + column + " from table: " + column.getParentTable() + " is mapped to the same alias (" + column.getAlias().getId() + ") as column " + prev + " from table: " + prev.getParentTable()); @@ -1621,7 +1681,6 @@ public static boolean checkAllColumns(TableInfo table, Collection co return 0 == bad; } - public static boolean checkColumn(TableInfo table, ColumnInfo column, String prefix) { if (column.getParentTable() != table) @@ -1635,8 +1694,7 @@ public static boolean checkColumn(TableInfo table, ColumnInfo column, String pre } } - - public static ParameterMapStatement deleteStatement(Connection conn, TableInfo tableDelete /*, Set columns */) throws SQLException + public static ParameterMapStatement deleteStatement(Connection conn, TableInfo tableDelete) throws SQLException { if (!(tableDelete instanceof UpdateableTableInfo updatable)) throw new IllegalArgumentException(); @@ -1732,6 +1790,17 @@ public static ParameterMapStatement deleteStatement(Connection conn, TableInfo t return new ParameterMapStatement(tableDelete.getSchema().getScope(), conn, sqlfDelete, updatable.remapSchemaColumns()); } + private static boolean assertInDb(TableInfo table) + { + if (table.getTableType() == DatabaseTableType.NOT_IN_DB) + throw new AssertionError("Table " + fullTableName(table) + " is not in the physical database."); + return true; + } + + private static String fullTableName(TableInfo table) + { + return table.getSchema().getName() + "." + table.getName(); + } public static class TestDataIterator extends AbstractDataIterator { diff --git a/api/src/org/labkey/api/exp/OntologyManager.java b/api/src/org/labkey/api/exp/OntologyManager.java index 2fab041f709..26d1b6d43ec 100644 --- a/api/src/org/labkey/api/exp/OntologyManager.java +++ b/api/src/org/labkey/api/exp/OntologyManager.java @@ -927,7 +927,7 @@ public static void updateObjectPropertyOrder(User user, Container container, Str */ public static int updateContainer(Container targetContainer, User user, @NotNull String objectLSID) { - return ContainerManager.updateContainer(getTinfoObject(), "objectURI", List.of(objectLSID), targetContainer, user, false); + return Table.updateContainer(getTinfoObject(), "objectURI", List.of(objectLSID), targetContainer, user, false); } /** diff --git a/api/src/org/labkey/api/exp/list/ListDefinition.java b/api/src/org/labkey/api/exp/list/ListDefinition.java index 20779eed733..95bc37c1a25 100644 --- a/api/src/org/labkey/api/exp/list/ListDefinition.java +++ b/api/src/org/labkey/api/exp/list/ListDefinition.java @@ -277,9 +277,11 @@ public static BodySetting getForValue(int value) int getListId(); void setPreferredListIds(Collection preferredListIds); // Attempts to use this list IDs when inserting Container getContainer(); - @Nullable Domain getDomain(); + @Nullable Domain getDomain(); @Nullable Domain getDomain(boolean forUpdate); + @NotNull Domain getDomainOrThrow(); + @NotNull Domain getDomainOrThrow(boolean forUpdate); String getName(); String getKeyName(); diff --git a/api/src/org/labkey/api/query/QueryService.java b/api/src/org/labkey/api/query/QueryService.java index 1039003aaaa..844b9cb3db6 100644 --- a/api/src/org/labkey/api/query/QueryService.java +++ b/api/src/org/labkey/api/query/QueryService.java @@ -462,7 +462,16 @@ public String getDefaultCommentSummary() List getQueryUpdateAuditRecords(User user, Container container, long transactionAuditId, @Nullable ContainerFilter containerFilter); AuditHandler getDefaultAuditHandler(); - int moveAuditEvents(Container targetContainer, List rowPks, String schemaName, String queryName); + /** + * Moves audit events associated with the specific rows, identified by primary key, to the target container. + * + * @param targetContainer The container to which audit events will be moved. + * @param rowPks A collection of primary key values identifying the rows whose audit events should be moved. + * @param schemaName The schema name of the table. + * @param queryName The query (table) name. + * @return The number of audit events moved. + */ + int moveAuditEvents(Container targetContainer, Collection rowPks, String schemaName, String queryName); /** * Returns a URL for the audit history for the table. diff --git a/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java b/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java index d6e4000a671..707d9644d66 100644 --- a/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java +++ b/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java @@ -23,6 +23,7 @@ import org.labkey.api.data.DbScope; import org.labkey.api.data.JdbcType; import org.labkey.api.data.PropertyStorageSpec; +import org.labkey.api.dataiterator.SimpleTranslator.SpecialColumn; import org.labkey.api.exp.OntologyManager; import org.labkey.api.exp.PropertyDescriptor; import org.labkey.api.exp.api.ExpProtocol; @@ -39,10 +40,6 @@ import java.util.Set; import static org.labkey.api.assay.AssayFileWriter.DIR_NAME; -import static org.labkey.api.data.Table.CREATED_BY_COLUMN_NAME; -import static org.labkey.api.data.Table.CREATED_COLUMN_NAME; -import static org.labkey.api.data.Table.MODIFIED_BY_COLUMN_NAME; -import static org.labkey.api.data.Table.MODIFIED_COLUMN_NAME; public class AssayResultDomainKind extends AssayDomainKind { @@ -89,10 +86,10 @@ public Set getBaseProperties(Domain domain) rowIdSpec.setAutoIncrement(true); rowIdSpec.setPrimaryKey(true); - PropertyStorageSpec createdSpec = new PropertyStorageSpec(CREATED_COLUMN_NAME, JdbcType.TIMESTAMP); - PropertyStorageSpec createdBySpec = new PropertyStorageSpec(CREATED_BY_COLUMN_NAME, JdbcType.INTEGER); - PropertyStorageSpec modifiedSpec = new PropertyStorageSpec(MODIFIED_COLUMN_NAME, JdbcType.TIMESTAMP); - PropertyStorageSpec modifiedBySpec = new PropertyStorageSpec(MODIFIED_BY_COLUMN_NAME, JdbcType.INTEGER); + PropertyStorageSpec createdSpec = new PropertyStorageSpec(SpecialColumn.Created.name(), JdbcType.TIMESTAMP); + PropertyStorageSpec createdBySpec = new PropertyStorageSpec(SpecialColumn.CreatedBy.name(), JdbcType.INTEGER); + PropertyStorageSpec modifiedSpec = new PropertyStorageSpec(SpecialColumn.Modified.name(), JdbcType.TIMESTAMP); + PropertyStorageSpec modifiedBySpec = new PropertyStorageSpec(SpecialColumn.ModifiedBy.name(), JdbcType.INTEGER); return PageFlowUtil.set(rowIdSpec, dataIdSpec, createdSpec, createdBySpec, modifiedSpec, modifiedBySpec); } @@ -107,8 +104,8 @@ public Set getPropertyIndices(Domain domain) public Set getPropertyForeignKeys(Container container) { return new HashSet<>(Arrays.asList( - new PropertyStorageSpec.ForeignKey(CREATED_BY_COLUMN_NAME, "core", "users", "userid", null, false), - new PropertyStorageSpec.ForeignKey(MODIFIED_BY_COLUMN_NAME, "core", "users", "userid", null, false) + new PropertyStorageSpec.ForeignKey(SpecialColumn.CreatedBy.name(), "core", "users", "userid", null, false), + new PropertyStorageSpec.ForeignKey(SpecialColumn.ModifiedBy.name(), "core", "users", "userid", null, false) )); } diff --git a/assay/api-src/org/labkey/api/assay/AssayResultTable.java b/assay/api-src/org/labkey/api/assay/AssayResultTable.java index 5e4dc295054..35711099c37 100644 --- a/assay/api-src/org/labkey/api/assay/AssayResultTable.java +++ b/assay/api-src/org/labkey/api/assay/AssayResultTable.java @@ -38,6 +38,7 @@ import org.labkey.api.data.dialect.SqlDialect; import org.labkey.api.dataiterator.DataIteratorBuilder; import org.labkey.api.dataiterator.DataIteratorContext; +import org.labkey.api.dataiterator.SimpleTranslator.SpecialColumn; import org.labkey.api.dataiterator.TableInsertDataIteratorBuilder; import org.labkey.api.exp.MvColumn; import org.labkey.api.exp.PropertyColumn; @@ -79,11 +80,6 @@ import java.util.Map; import java.util.Set; -import static org.labkey.api.data.Table.CREATED_BY_COLUMN_NAME; -import static org.labkey.api.data.Table.CREATED_COLUMN_NAME; -import static org.labkey.api.data.Table.MODIFIED_BY_COLUMN_NAME; -import static org.labkey.api.data.Table.MODIFIED_COLUMN_NAME; - public class AssayResultTable extends FilteredTable implements UpdateableTableInfo { protected final ExpProtocol _protocol; @@ -331,12 +327,16 @@ public static BaseColumnInfo createRowExpressionLsidColumn(FilteredTable CREATED_MODIFIED_COLUMN_NAMES = CaseInsensitiveHashSet.of( + SpecialColumn.Created.name(), + SpecialColumn.CreatedBy.name(), + SpecialColumn.Modified.name(), + SpecialColumn.ModifiedBy.name() + ); + private boolean isCreatedModifiedCol(String colName) { - return CREATED_COLUMN_NAME.equalsIgnoreCase(colName) || - CREATED_BY_COLUMN_NAME.equalsIgnoreCase(colName) || - MODIFIED_COLUMN_NAME.equalsIgnoreCase(colName) || - MODIFIED_BY_COLUMN_NAME.equalsIgnoreCase(colName); + return CREATED_MODIFIED_COLUMN_NAMES.contains(colName); } // Expensive render-time fetching of all ontology properties attached to the object row @@ -437,10 +437,10 @@ protected boolean shouldIncludeCreatedModified(Set selectedColumns) if (null == selectedColumns) // select all return true; - return selectedColumns.contains(new FieldKey(null, CREATED_COLUMN_NAME)) || - selectedColumns.contains(new FieldKey(null, CREATED_BY_COLUMN_NAME)) || - selectedColumns.contains(new FieldKey(null, MODIFIED_COLUMN_NAME)) || - selectedColumns.contains(new FieldKey(null, MODIFIED_BY_COLUMN_NAME)); + return selectedColumns.contains(new FieldKey(null, SpecialColumn.Created.name())) || + selectedColumns.contains(new FieldKey(null, SpecialColumn.CreatedBy.name())) || + selectedColumns.contains(new FieldKey(null, SpecialColumn.Modified.name())) || + selectedColumns.contains(new FieldKey(null, SpecialColumn.ModifiedBy.name())); } @NotNull @@ -468,10 +468,10 @@ public SQLFragment getFromSQLExpanded(String alias, Set selectedColumn // without any updates to the results rows, the created and modified date of the result should match the created date of the run // use run.created/by as default result modified/by String runSelectCol = propertyColumn.getName(); - if (MODIFIED_COLUMN_NAME.equalsIgnoreCase(runSelectCol)) - runSelectCol = CREATED_COLUMN_NAME; - else if (MODIFIED_BY_COLUMN_NAME.equalsIgnoreCase(runSelectCol)) - runSelectCol = CREATED_BY_COLUMN_NAME; + if (SpecialColumn.Modified.name().equalsIgnoreCase(runSelectCol)) + runSelectCol = SpecialColumn.Created.name(); + else if (SpecialColumn.ModifiedBy.name().equalsIgnoreCase(runSelectCol)) + runSelectCol = SpecialColumn.CreatedBy.name(); coalescedCol.append(runSelectCol); coalescedCol.append(") AS "); diff --git a/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java b/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java index 4f29e16b95e..b6e72f8779f 100644 --- a/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java +++ b/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java @@ -545,14 +545,17 @@ public void copyAttachment(AttachmentParent parent, Attachment a, String newName } @Override - public void moveAttachments(Container newContainer, List parents, User auditUser) throws IOException + public int moveAttachments(Container newContainer, List parents, User auditUser) throws IOException { + int totalRowsChanged = 0; + for (AttachmentParent parent : parents) { checkSecurityPolicy(auditUser, parent); int rowsChanged = new SqlExecutor(coreTables().getSchema()).execute(sqlMove(parent, newContainer)); if (rowsChanged > 0) { + totalRowsChanged += rowsChanged; List atts = getAttachments(parent); String filename; for (Attachment att : atts) @@ -575,6 +578,8 @@ public void moveAttachments(Container newContainer, List paren AttachmentCache.removeAttachments(parent); } } + + return totalRowsChanged; } /** may return fewer AttachmentFile than Attachment, if there have been deletions */ diff --git a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java index cb67b95ded2..5d2e5f94876 100644 --- a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java @@ -9561,11 +9561,7 @@ private int updateExpObjectContainers(List lsids, Container targetContai if (lsids == null || lsids.isEmpty()) return 0; - TableInfo objectTable = OntologyManager.getTinfoObject(); - SQLFragment objectUpdate = new SQLFragment("UPDATE ").append(objectTable).append(" SET container = ").appendValue(targetContainer.getEntityId()) - .append(" WHERE objecturi "); - objectTable.getSchema().getSqlDialect().appendInClauseSql(objectUpdate, lsids); - return new SqlExecutor(objectTable.getSchema()).execute(objectUpdate); + return Table.updateContainer(OntologyManager.getTinfoObject(), "objecturi", lsids, targetContainer, null, false); } @Override @@ -9614,7 +9610,7 @@ public Map moveDataClassObjects(Collection d TableInfo dataClassTable = schema.getTable(dataClass.getName()); // update exp.data.container - int updateCount = ContainerManager.updateContainer(getTinfoData(), "rowId", dataIds, targetContainer, user, true); + int updateCount = Table.updateContainer(getTinfoData(), "rowId", dataIds, targetContainer, user, true); updateCounts.put("sources", updateCounts.get("sources") + updateCount); // update for exp.object.container @@ -9636,7 +9632,7 @@ public Map moveDataClassObjects(Collection d // move audit events associated with the sources that are moving int auditEventCount = QueryService.get().moveAuditEvents(targetContainer, dataIds, "exp.data", dataClassTable.getName()); - updateCounts.compute("sourceAuditEvents", (k, c) -> c == null ? auditEventCount : c + auditEventCount ); + updateCounts.compute("sourceAuditEvents", (k, c) -> c == null ? auditEventCount : c + auditEventCount); // create summary audit entries for the source container only. The message is pretty generic, so having it // in both source and target doesn't help much. diff --git a/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java b/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java index 097331515b0..9109ecd0e27 100644 --- a/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java +++ b/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java @@ -59,6 +59,7 @@ import org.labkey.api.data.SimpleFilter; import org.labkey.api.data.SqlExecutor; import org.labkey.api.data.SqlSelector; +import org.labkey.api.data.Table; import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableSelector; import org.labkey.api.data.dialect.SqlDialect; @@ -1896,7 +1897,7 @@ public Map moveSamples(Collection sample List sampleIds = typeSamples.stream().map(ExpMaterial::getRowId).toList(); // update for exp.material.container - updateCounts.put("samples", updateCounts.get("samples") + ContainerManager.updateContainer(getTinfoMaterial(), "rowid", sampleIds, targetContainer, user, true)); + updateCounts.put("samples", updateCounts.get("samples") + Table.updateContainer(getTinfoMaterial(), "rowid", sampleIds, targetContainer, user, true)); // update for exp.object.container expService.updateExpObjectContainers(getTinfoMaterial(), sampleIds, targetContainer); @@ -1925,7 +1926,7 @@ public Map moveSamples(Collection sample // move the events associated with the samples that have moved SampleTimelineAuditProvider auditProvider = new SampleTimelineAuditProvider(); int auditEventCount = auditProvider.moveEvents(targetContainer, sampleIds); - updateCounts.compute("sampleAuditEvents", (k, c) -> c == null ? auditEventCount : c + auditEventCount ); + updateCounts.compute("sampleAuditEvents", (k, c) -> c == null ? auditEventCount : c + auditEventCount); AuditBehaviorType stAuditBehavior = samplesTable.getEffectiveAuditBehavior(auditBehavior); // create new events for each sample that was moved. diff --git a/list/src/org/labkey/list/controllers/ListController.java b/list/src/org/labkey/list/controllers/ListController.java index 474d2ff170a..78a641ffec7 100644 --- a/list/src/org/labkey/list/controllers/ListController.java +++ b/list/src/org/labkey/list/controllers/ListController.java @@ -146,11 +146,6 @@ import java.util.Set; import java.util.TreeSet; -/** - * User: adam - * Date: Dec 30, 2007 - * Time: 12:44:30 PM - */ public class ListController extends SpringActionController { private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(ListController.class, ClearDefaultValuesAction.class); @@ -848,27 +843,71 @@ private String getUrlParam(Enum param) return form.getReturnUrl(); } + public static class ListItemDetailsForm + { + private Integer _listId; + private String _name; + private Integer _rowId; + + public Integer getListId() + { + return _listId; + } + + public void setListId(Integer listId) + { + _listId = listId; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public Integer getRowId() + { + return _rowId; + } + + public void setRowId(Integer rowId) + { + _rowId = rowId; + } + } + @RequiresPermission(ReadPermission.class) - public class ListItemDetailsAction extends SimpleViewAction + public class ListItemDetailsAction extends SimpleViewAction { private ListDefinition _list; @Override - public ModelAndView getView(Object o, BindException errors) + public ModelAndView getView(ListItemDetailsForm form, BindException errors) { - int id = NumberUtils.toInt((String)getViewContext().get("rowId")); - int listId = NumberUtils.toInt((String)getViewContext().get("listId")); - _list = ListService.get().getList(getContainer(), listId); + String listName = form.getName(); + if (listName != null) + _list = ListService.get().getList(getContainer(), listName, true); + if (_list == null) { - return HtmlView.of("This list is no longer available."); + int listId = form.getListId(); + if (listId > 0) + _list = ListService.get().getList(getContainer(), listId); } + if (_list == null) + return HtmlView.of("This list is no longer available."); + String comment = null; String oldRecord = null; String newRecord = null; - ListAuditProvider.ListAuditEvent event = AuditLogService.get().getAuditEvent(getUser(), ListManager.LIST_AUDIT_EVENT, id); + int eventRowId = form.getRowId(); + ListAuditProvider.ListAuditEvent event = AuditLogService.get().getAuditEvent(getUser(), ListManager.LIST_AUDIT_EVENT, eventRowId); if (event != null) { diff --git a/list/src/org/labkey/list/model/ListAuditProvider.java b/list/src/org/labkey/list/model/ListAuditProvider.java index 3f25042cfd8..e91414869f8 100644 --- a/list/src/org/labkey/list/model/ListAuditProvider.java +++ b/list/src/org/labkey/list/model/ListAuditProvider.java @@ -44,10 +44,6 @@ import java.util.Map; import java.util.Set; -/** - * User: klum - * Date: 7/21/13 - */ public class ListAuditProvider extends AbstractAuditTypeProvider implements AuditTypeProvider { public static final String COLUMN_NAME_LIST_ID = "ListId"; @@ -108,7 +104,7 @@ protected void initColumn(MutableColumnInfo col) appendValueMapColumns(table, null, true); // Render a details URL only for rows that have a listItemEntityId - DetailsURL url = DetailsURL.fromString("list/listItemDetails.view?listId=${listId}&entityId=${listItemEntityId}&rowId=${rowId}", null, StringExpressionFactory.AbstractStringExpression.NullValueBehavior.NullResult); + DetailsURL url = DetailsURL.fromString("list/listItemDetails.view?listId=${listId}&name=${listName}&entityId=${listItemEntityId}&rowId=${rowId}", null, StringExpressionFactory.AbstractStringExpression.NullValueBehavior.NullResult); table.setDetailsURL(url); return table; @@ -143,6 +139,11 @@ public Class getEventClass() return (Class)ListAuditEvent.class; } + public int moveEvents(Container targetContainer, List listRowEntityIds) + { + return moveEvents(targetContainer, COLUMN_NAME_LIST_ITEM_ENTITY_ID, listRowEntityIds); + } + public static class ListAuditEvent extends DetailedAuditTypeEvent { private int _listId; diff --git a/list/src/org/labkey/list/model/ListDefinitionImpl.java b/list/src/org/labkey/list/model/ListDefinitionImpl.java index 07c3b39a477..d341b032912 100644 --- a/list/src/org/labkey/list/model/ListDefinitionImpl.java +++ b/list/src/org/labkey/list/model/ListDefinitionImpl.java @@ -153,6 +153,22 @@ public Domain getDomain(boolean forUpdate) } return _domain; } + + @Override + public @NotNull Domain getDomainOrThrow() + { + return getDomainOrThrow(false); + } + + @Override + public @NotNull Domain getDomainOrThrow(boolean forUpdate) + { + var domain = getDomain(forUpdate); + if (domain == null) + throw new IllegalArgumentException("Could not find domain for list \"" + getName() + "\"."); + return domain; + } + @Override public String getName() { @@ -510,7 +526,7 @@ private ListItem getListItem(SimpleFilter filter, User user, Container c) itm.setKey(row.get(getKeyName())); ListItemImpl impl = new ListItemImpl(this, itm); - for (DomainProperty prop : getDomain().getProperties()) + for (DomainProperty prop : getDomainOrThrow().getProperties()) { impl.setProperty(prop, row.get(prop.getName())); } @@ -555,12 +571,12 @@ public void delete(User user, @Nullable String auditUserComment) throws DomainNo { // remove related attachments, discussions, and indices ListManager.get().deleteIndexedList(this); - if (qus instanceof ListQueryUpdateService) - ((ListQueryUpdateService)qus).deleteRelatedListData(user, getContainer()); + if (qus instanceof ListQueryUpdateService listQus) + listQus.deleteRelatedListData(user, getContainer()); // then delete the list itself ListManager.get().deleteListDef(getContainer(), getListId()); - Domain domain = getDomain(); + Domain domain = getDomainOrThrow(); domain.delete(user, auditUserComment); ListManager.get().addAuditEvent(this, user, String.format("The list %s was deleted", _def.getName())); @@ -669,7 +685,6 @@ public void setLastIndexed(Date modified) edit().setLastIndexed(modified); } - /** NOTE consider using ListQuerySchema.getTable(), unless you have a good reason */ @Override @Nullable public TableInfo getTable(User user) @@ -677,7 +692,6 @@ public TableInfo getTable(User user) return getTable(user, getContainer()); } - /** NOTE consider using ListQuerySchema.getTable(), unless you have a good reason */ @Override @Nullable public TableInfo getTable(User user, Container c) diff --git a/list/src/org/labkey/list/model/ListItemImpl.java b/list/src/org/labkey/list/model/ListItemImpl.java index b8eb65807df..1a9c4bae87c 100644 --- a/list/src/org/labkey/list/model/ListItemImpl.java +++ b/list/src/org/labkey/list/model/ListItemImpl.java @@ -16,8 +16,6 @@ package org.labkey.list.model; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.labkey.api.exp.ObjectProperty; import org.labkey.api.exp.OntologyManager; import org.labkey.api.exp.OntologyObject; @@ -36,7 +34,6 @@ public class ListItemImpl implements ListItem ListItm _itm; Map _properties; Map _oldProperties; - private static final Logger _log = LogManager.getLogger(ListItemImpl.class); public ListItemImpl(ListDefinitionImpl list, ListItm item) { diff --git a/list/src/org/labkey/list/model/ListManager.java b/list/src/org/labkey/list/model/ListManager.java index ea2b6a046cc..497721ba2b4 100644 --- a/list/src/org/labkey/list/model/ListManager.java +++ b/list/src/org/labkey/list/model/ListManager.java @@ -100,7 +100,6 @@ public class ListManager implements SearchService.DocumentProvider public static final String LIST_AUDIT_EVENT = "ListAuditEvent"; public static final String LISTID_FIELD_NAME = "listId"; - private final Cache> _listDefCache = DatabaseCache.get(CoreSchema.getInstance().getScope(), CacheManager.UNLIMITED, CacheManager.DAY, "List definitions", new ListDefCacheLoader()) ; private class ListDefCacheLoader implements CacheLoader> @@ -1141,9 +1140,6 @@ void addAuditEvent(ListDefinitionImpl list, User user, String comment) } } - /** - * Modeled after ListItemImpl.addAuditEvent - */ void addAuditEvent(ListDefinitionImpl list, User user, Container c, String comment, String entityId, @Nullable String oldRecord, @Nullable String newRecord) { ListAuditProvider.ListAuditEvent event = new ListAuditProvider.ListAuditEvent(c, comment, list); diff --git a/list/src/org/labkey/list/model/ListQueryUpdateService.java b/list/src/org/labkey/list/model/ListQueryUpdateService.java index c6a0609682b..44787baad7b 100644 --- a/list/src/org/labkey/list/model/ListQueryUpdateService.java +++ b/list/src/org/labkey/list/model/ListQueryUpdateService.java @@ -23,31 +23,46 @@ import org.labkey.api.attachments.AttachmentParent; import org.labkey.api.attachments.AttachmentParentFactory; import org.labkey.api.attachments.AttachmentService; +import org.labkey.api.audit.AbstractAuditTypeProvider; +import org.labkey.api.audit.AuditLogService; +import org.labkey.api.audit.TransactionAuditProvider; import org.labkey.api.collections.CaseInsensitiveHashMap; import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.CompareType; import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; import org.labkey.api.data.DbScope; import org.labkey.api.data.LookupResolutionType; +import org.labkey.api.data.RuntimeSQLException; import org.labkey.api.data.Selector.ForEachBatchBlock; import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Table; import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableSelector; import org.labkey.api.dataiterator.DataIteratorBuilder; import org.labkey.api.dataiterator.DataIteratorContext; +import org.labkey.api.dataiterator.DetailedAuditLogDataIterator; import org.labkey.api.exp.ObjectProperty; +import org.labkey.api.exp.PropertyType; +import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.exp.list.ListDefinition; import org.labkey.api.exp.list.ListImportProgress; import org.labkey.api.exp.list.ListItem; +import org.labkey.api.exp.list.ListService; import org.labkey.api.exp.property.Domain; import org.labkey.api.exp.property.DomainProperty; import org.labkey.api.exp.property.IPropertyValidator; import org.labkey.api.exp.property.ValidatorContext; +import org.labkey.api.gwt.client.AuditBehaviorType; import org.labkey.api.lists.permissions.ManagePicklistsPermission; +import org.labkey.api.query.AbstractQueryUpdateService; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.DefaultQueryUpdateService; import org.labkey.api.query.FieldKey; import org.labkey.api.query.InvalidKeyException; import org.labkey.api.query.PropertyValidationError; +import org.labkey.api.query.QueryService; import org.labkey.api.query.QueryUpdateServiceException; import org.labkey.api.query.ValidationError; import org.labkey.api.query.ValidationException; @@ -55,9 +70,15 @@ import org.labkey.api.security.ElevatedUser; import org.labkey.api.security.User; import org.labkey.api.security.permissions.DeletePermission; +import org.labkey.api.security.permissions.MoveEntitiesPermission; +import org.labkey.api.security.permissions.ReadPermission; import org.labkey.api.security.permissions.UpdatePermission; import org.labkey.api.security.roles.EditorRole; +import org.labkey.api.usageMetrics.SimpleMetricsService; +import org.labkey.api.util.GUID; +import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.Pair; +import org.labkey.api.util.StringUtilsLabKey; import org.labkey.api.util.UnexpectedException; import org.labkey.api.view.UnauthorizedException; import org.labkey.api.writer.VirtualFile; @@ -68,8 +89,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.labkey.api.util.IntegerUtils.isIntegral; @@ -126,7 +149,7 @@ protected Map getRow(User user, Container container, Map updateRow(User user, Container container, Map dps = new HashMap<>(); - for (DomainProperty dp : _list.getDomain().getProperties()) + for (DomainProperty dp : _list.getDomainOrThrow().getProperties()) { dps.put(dp.getPropertyURI(), dp); } @@ -335,7 +358,7 @@ protected Map updateRow(User user, Container container, Map moveRows( + User _user, + Container container, + Container targetContainer, + List> rows, + BatchValidationException errors, + @Nullable Map configParameters, + @Nullable Map extraScriptContext + ) throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException + { + // Ensure the list is in scope for the target container + if (null == ListService.get().getList(targetContainer, _list.getName(), true)) + { + errors.addRowError(new ValidationException(String.format("List '%s' is not accessible from folder %s.", _list.getName(), targetContainer.getPath()))); + throw errors; + } + + User user = getListUser(_user, container); + Map> containerRows = getListRowsForMoveRows(container, user, targetContainer, rows, errors); + if (errors.hasErrors()) + throw errors; + + int fileAttachmentsMovedCount = 0; + int listAuditEventsCreatedCount = 0; + int listAuditEventsMovedCount = 0; + int listRecordsCount = 0; + int queryAuditEventsMovedCount = 0; + + if (containerRows.isEmpty()) + return moveRowsCounts(fileAttachmentsMovedCount, listAuditEventsCreatedCount, listAuditEventsMovedCount, listRecordsCount, queryAuditEventsMovedCount); + + AuditBehaviorType auditBehavior = configParameters != null ? (AuditBehaviorType) configParameters.get(DetailedAuditLogDataIterator.AuditConfigs.AuditBehavior) : null; + String auditUserComment = configParameters != null ? (String) configParameters.get(DetailedAuditLogDataIterator.AuditConfigs.AuditUserComment) : null; + boolean hasAttachmentProperties = _list.getDomainOrThrow() + .getProperties() + .stream() + .anyMatch(prop -> PropertyType.ATTACHMENT.equals(prop.getPropertyType())); + + ListAuditProvider listAuditProvider = new ListAuditProvider(); + final int BATCH_SIZE = 5_000; + boolean isAuditEnabled = auditBehavior != null && AuditBehaviorType.NONE != auditBehavior; + + try (DbScope.Transaction tx = getDbTable().getSchema().getScope().ensureTransaction()) + { + if (isAuditEnabled && tx.getAuditEvent() == null) + { + TransactionAuditProvider.TransactionAuditEvent auditEvent = createTransactionAuditEvent(targetContainer, QueryService.AuditAction.UPDATE); + auditEvent.updateCommentRowCount(containerRows.values().stream().mapToInt(List::size).sum()); + AbstractQueryUpdateService.addTransactionAuditEvent(tx, user, auditEvent); + } + + List listAuditEvents = new ArrayList<>(); + + for (GUID containerId : containerRows.keySet()) + { + Container sourceContainer = ContainerManager.getForId(containerId); + if (sourceContainer == null) + throw new InvalidKeyException("Container '" + containerId + "' does not exist."); + + if (!sourceContainer.hasPermission(user, MoveEntitiesPermission.class)) + throw new UnauthorizedException("You do not have permission to move list records out of '" + sourceContainer.getName() + "'."); + + TableInfo listTable = _list.getTable(user, sourceContainer); + if (listTable == null) + throw new QueryUpdateServiceException(String.format("Failed to retrieve table for list '%s' in folder %s.", _list.getName(), sourceContainer.getPath())); + + List records = containerRows.get(containerId); + int numRecords = records.size(); + + for (int start = 0; start < numRecords; start += BATCH_SIZE) + { + int end = Math.min(start + BATCH_SIZE, numRecords); + List batch = records.subList(start, end); + List rowPks = batch.stream().map(ListRecord::key).toList(); + + // Before trigger per batch + Map extraContext = Map.of("targetContainer", targetContainer, "keys", rowPks); + listTable.fireBatchTrigger(sourceContainer, user, TableInfo.TriggerType.MOVE, true, errors, extraContext); + if (errors.hasErrors()) + throw errors; + + listRecordsCount += Table.updateContainer(getDbTable(), _list.getKeyName(), rowPks, targetContainer, user, true); + if (errors.hasErrors()) + throw errors; + + if (hasAttachmentProperties) + { + fileAttachmentsMovedCount += moveAttachments(user, sourceContainer, targetContainer, batch, errors); + if (errors.hasErrors()) + throw errors; + } + + queryAuditEventsMovedCount += QueryService.get().moveAuditEvents(targetContainer, rowPks, ListQuerySchema.NAME, _list.getName()); + listAuditEventsMovedCount += listAuditProvider.moveEvents(targetContainer, batch.stream().map(ListRecord::entityId).toList()); + + // Detailed audit events per row + if (AuditBehaviorType.DETAILED == listTable.getEffectiveAuditBehavior(auditBehavior)) + listAuditEventsCreatedCount += addDetailedMoveAuditEvents(user, sourceContainer, targetContainer, batch); + + // After trigger per batch + listTable.fireBatchTrigger(sourceContainer, user, TableInfo.TriggerType.MOVE, false, errors, extraContext); + if (errors.hasErrors()) + throw errors; + } + + // Create a summary audit event for the source container + if (isAuditEnabled) + { + String comment = String.format("Moved %s to %s", StringUtilsLabKey.pluralize(numRecords, "row"), targetContainer.getPath()); + ListAuditProvider.ListAuditEvent event = new ListAuditProvider.ListAuditEvent(sourceContainer, comment, _list); + event.setUserComment(auditUserComment); + listAuditEvents.add(event); + } + + // Create a summary audit event for the target container + if (isAuditEnabled) + { + String comment = String.format("Moved %s from %s", StringUtilsLabKey.pluralize(numRecords, "row"), sourceContainer.getPath()); + ListAuditProvider.ListAuditEvent event = new ListAuditProvider.ListAuditEvent(targetContainer, comment, _list); + event.setUserComment(auditUserComment); + listAuditEvents.add(event); + } + } + + if (!listAuditEvents.isEmpty()) + { + AuditLogService.get().addEvents(user, listAuditEvents, true); + listAuditEventsCreatedCount += listAuditEvents.size(); + } + + tx.addCommitTask(() -> ListManager.get().indexList(_list), DbScope.CommitTaskOption.POSTCOMMIT); + + tx.commit(); + + SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "moveEntities", "list"); + } + + return moveRowsCounts(fileAttachmentsMovedCount, listAuditEventsCreatedCount, listAuditEventsMovedCount, listRecordsCount, queryAuditEventsMovedCount); + } + + private Map moveRowsCounts(int fileAttachmentsMovedCount, int listAuditEventsCreated, int listAuditEventsMoved, int listRecords, int queryAuditEventsMoved) + { + return Map.of( + "fileAttachmentsMoved", fileAttachmentsMovedCount, + "listAuditEventsCreated", listAuditEventsCreated, + "listAuditEventsMoved", listAuditEventsMoved, + "listRecords", listRecords, + "queryAuditEventsMoved", queryAuditEventsMoved + ); + } + + private Map> getListRowsForMoveRows( + Container container, + User user, + Container targetContainer, + List> rows, + BatchValidationException errors + ) throws QueryUpdateServiceException + { + if (rows.isEmpty()) + return Collections.emptyMap(); + + String keyName = _list.getKeyName(); + List keys = new ArrayList<>(); + for (var row : rows) + { + Object key = getField(row, keyName); + if (key == null) + { + errors.addRowError(new ValidationException("Key field value required for moving list rows.")); + return Collections.emptyMap(); + } + + keys.add(getKeyFilterValue(key)); + } + + SimpleFilter filter = new SimpleFilter(); + FieldKey fieldKey = FieldKey.fromParts(keyName); + filter.addInClause(fieldKey, keys); + filter.addCondition(FieldKey.fromParts("Container"), targetContainer.getId(), CompareType.NEQ); + + // Request all rows without a container filter so that rows are more easily resolved across the list scope. + // Read permissions are subsequently checked upon loading a row. + TableInfo table = _list.getTable(user, container, ContainerFilter.getUnsafeEverythingFilter()); + if (table == null) + throw new QueryUpdateServiceException(String.format("Failed to resolve table for list %s in %s", _list.getName(), container.getPath())); + + Map> containerRows = new HashMap<>(); + try (var result = new TableSelector(table, PageFlowUtil.set(keyName, "Container", "EntityId"), filter, null).getResults()) + { + while (result.next()) + { + GUID containerId = new GUID(result.getString("Container")); + if (!containerRows.containsKey(containerId)) + { + var c = ContainerManager.getForId(containerId); + if (c == null) + throw new QueryUpdateServiceException(String.format("Failed to resolve container for row in list %s in %s.", _list.getName(), container.getPath())); + else if (!c.hasPermission(user, ReadPermission.class)) + throw new UnauthorizedException("You do not have permission to read list rows in all source containers."); + } + + containerRows.computeIfAbsent(containerId, k -> new ArrayList<>()); + containerRows.get(containerId).add(new ListRecord(result.getObject(fieldKey), result.getString("EntityId"))); + } + } + catch (SQLException e) + { + throw new RuntimeSQLException(e); + } + + return containerRows; + } + + private int moveAttachments(User user, Container sourceContainer, Container targetContainer, List records, BatchValidationException errors) + { + List parents = new ArrayList<>(); + for (ListRecord record : records) + parents.add(new ListItemAttachmentParent(record.entityId, sourceContainer)); + + int count = 0; + try + { + count = AttachmentService.get().moveAttachments(targetContainer, parents, user); + } + catch (IOException e) + { + errors.addRowError(new ValidationException("Failed to move attachments when moving list rows. Error: " + e.getMessage())); + } + + return count; + } + + private int addDetailedMoveAuditEvents(User user, Container sourceContainer, Container targetContainer, List records) + { + List auditEvents = new ArrayList<>(records.size()); + String keyName = _list.getKeyName(); + String sourcePath = sourceContainer.getPath(); + String targetPath = targetContainer.getPath(); + + for (ListRecord record : records) + { + ListAuditProvider.ListAuditEvent event = new ListAuditProvider.ListAuditEvent(targetContainer, "An existing list record was moved", _list); + event.setListItemEntityId(record.entityId); + event.setOldRecordMap(AbstractAuditTypeProvider.encodeForDataMap(CaseInsensitiveHashMap.of("Folder", sourcePath, keyName, record.key.toString()))); + event.setNewRecordMap(AbstractAuditTypeProvider.encodeForDataMap(CaseInsensitiveHashMap.of("Folder", targetPath, keyName, record.key.toString()))); + auditEvents.add(event); + } + + AuditLogService.get().addEvents(user, auditEvents, true); + + return auditEvents.size(); + } + @Override protected Map deleteRow(User user, Container container, Map oldRowMap) throws InvalidKeyException, QueryUpdateServiceException, SQLException { @@ -544,32 +824,33 @@ protected int truncateRows(User user, Container container) throws QueryUpdateSer return result; } - @Nullable public SimpleFilter getKeyFilter(Map map) throws InvalidKeyException { String keyName = _list.getKeyName(); - ListDefinition.KeyType type = _list.getKeyType(); - - Object key = getField(map, _list.getKeyName()); + Object key = getField(map, keyName); if (null == key) { // Auto-increment lists might not provide a key so allow them to pass through - if (type.equals(ListDefinition.KeyType.AutoIncrementInteger)) + if (ListDefinition.KeyType.AutoIncrementInteger.equals(_list.getKeyType())) return null; throw new InvalidKeyException("No " + keyName + " provided for list \"" + _list.getName() + "\""); } + return new SimpleFilter(FieldKey.fromParts(keyName), getKeyFilterValue(key)); + } + + @NotNull + private Object getKeyFilterValue(@NotNull Object key) + { + ListDefinition.KeyType type = _list.getKeyType(); + // Check the type of the list to ensure proper casting of the key type - if (type.equals(ListDefinition.KeyType.Integer) || type.equals(ListDefinition.KeyType.AutoIncrementInteger)) - { - if (isIntegral(key)) - return new SimpleFilter(FieldKey.fromParts(keyName), key); - return new SimpleFilter(FieldKey.fromParts(keyName), Integer.valueOf(key.toString())); - } + if (ListDefinition.KeyType.Integer.equals(type) || ListDefinition.KeyType.AutoIncrementInteger.equals(type)) + return isIntegral(key) ? key : Integer.valueOf(key.toString()); - return new SimpleFilter(FieldKey.fromParts(keyName), key.toString()); + return key.toString(); } @Nullable diff --git a/query/src/org/labkey/query/QueryServiceImpl.java b/query/src/org/labkey/query/QueryServiceImpl.java index c094ca989ce..009b239df58 100644 --- a/query/src/org/labkey/query/QueryServiceImpl.java +++ b/query/src/org/labkey/query/QueryServiceImpl.java @@ -3098,7 +3098,7 @@ public void clearEnvironment() } @Override - public int moveAuditEvents(Container targetContainer, List rowPks, String schemaName, String queryName) + public int moveAuditEvents(Container targetContainer, Collection rowPks, String schemaName, String queryName) { QueryUpdateAuditProvider provider = new QueryUpdateAuditProvider(); return provider.moveEvents(targetContainer, rowPks, schemaName, queryName); @@ -3133,23 +3133,33 @@ private QueryUpdateAuditProvider.QueryUpdateAuditEvent createAuditRecord(Contain event.setQueryName(tinfo.getPublicName()); FieldKey rowPk = tinfo.getAuditRowPk(); - if (rowPk != null) + if (rowPk != null && (row != null || existingRow != null)) { - String rowPkStr = rowPk.toString(); - Object pk = null; - if (row != null && row.containsKey(rowPkStr)) - { - pk = row.get(rowPkStr); - } - if (pk == null && existingRow != null && existingRow.containsKey(rowPkStr)) - { - pk = existingRow.get(rowPkStr); - } + Object pk = resolvePkValue(rowPk, row); + if (pk == null) + pk = resolvePkValue(rowPk, existingRow); + if (pk != null) event.setRowPk(String.valueOf(pk)); } + return event; } + + private static @Nullable Object resolvePkValue(@NotNull FieldKey fieldKey, @Nullable Map row) + { + Object pk = null; + if (row != null) + { + String rowPkStr = fieldKey.toString(); + if (row.containsKey(rowPkStr)) + pk = row.get(rowPkStr); + if (pk == null && row.containsKey(fieldKey.getName())) + pk = row.get(fieldKey.getName()); + } + + return pk; + } }; } diff --git a/query/src/org/labkey/query/audit/QueryUpdateAuditProvider.java b/query/src/org/labkey/query/audit/QueryUpdateAuditProvider.java index 77e37f019e9..76f970a6e05 100644 --- a/query/src/org/labkey/query/audit/QueryUpdateAuditProvider.java +++ b/query/src/org/labkey/query/audit/QueryUpdateAuditProvider.java @@ -26,9 +26,8 @@ import org.labkey.api.data.Container; import org.labkey.api.data.ContainerFilter; import org.labkey.api.data.MutableColumnInfo; -import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SimpleFilter; -import org.labkey.api.data.SqlExecutor; +import org.labkey.api.data.Table; import org.labkey.api.data.TableInfo; import org.labkey.api.exp.PropertyDescriptor; import org.labkey.api.exp.PropertyType; @@ -50,10 +49,6 @@ import java.util.Map; import java.util.Set; -/** - * User: klum - * Date: 7/21/13 - */ public class QueryUpdateAuditProvider extends AbstractAuditTypeProvider implements AuditTypeProvider { public static final String QUERY_UPDATE_AUDIT_EVENT = "QueryUpdateAuditEvent"; @@ -192,15 +187,14 @@ public static QueryView createDetailsQueryView(ViewContext context, String schem return null; } - public int moveEvents(Container targetContainer, Collection rowIds, String schemaName, String queryName) + public int moveEvents(Container targetContainer, Collection rowPks, String schemaName, String queryName) { - TableInfo auditTable = createStorageTableInfo(); - SQLFragment sql = new SQLFragment("UPDATE ").append(auditTable) - .append(" SET container = ").appendValue(targetContainer) - .append(" WHERE RowPk "); - auditTable.getSchema().getSqlDialect().appendInClauseSql(sql, rowIds.stream().map(Object::toString).toList()); - sql.append(" AND SchemaName = ").appendValue(schemaName).append(" AND QueryName = ").appendValue(queryName); - return new SqlExecutor(auditTable.getSchema()).execute(sql); + SimpleFilter filter = new SimpleFilter(); + filter.addInClause(FieldKey.fromParts(COLUMN_NAME_ROW_PK), rowPks.stream().map(Object::toString).toList()); + filter.addCondition(FieldKey.fromParts(COLUMN_NAME_SCHEMA_NAME), schemaName); + filter.addCondition(FieldKey.fromParts(COLUMN_NAME_QUERY_NAME), queryName); + + return Table.updateContainer(createStorageTableInfo(), targetContainer, filter, null, false); } public static class QueryUpdateAuditEvent extends DetailedAuditTypeEvent