From 8ec8d70febd1be8b1164bb9fb105c936e4e7137e Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 25 May 2026 18:08:11 -0700 Subject: [PATCH] GitHub Issue 903: Remove Cross-Container Sample and Data Class Import Feature --- .../api/dataiterator/DataIteratorContext.java | 11 - .../api/query/AbstractQueryImportAction.java | 6 - .../api/query/AbstractQueryUpdateService.java | 3410 ++-- .../api/query/DefaultQueryUpdateService.java | 1887 +- experiment/package-lock.json | 8 +- experiment/package.json | 2 +- .../labkey/experiment/ExpDataIterators.java | 224 +- .../api/ExpDataClassDataTableImpl.java | 25 +- .../api/SampleTypeUpdateServiceDI.java | 14 +- .../controllers/exp/ExperimentController.java | 16601 ++++++++-------- 10 files changed, 10950 insertions(+), 11238 deletions(-) diff --git a/api/src/org/labkey/api/dataiterator/DataIteratorContext.java b/api/src/org/labkey/api/dataiterator/DataIteratorContext.java index 12713d9605b..6107285824a 100644 --- a/api/src/org/labkey/api/dataiterator/DataIteratorContext.java +++ b/api/src/org/labkey/api/dataiterator/DataIteratorContext.java @@ -54,7 +54,6 @@ public class DataIteratorContext LookupResolutionType _lookupResolutionType = LookupResolutionType.primaryKey; QueryImportPipelineJob _backgroundJob = null; boolean _crossTypeImport = false; - boolean _crossFolderImport = false; boolean _allowCreateStorage = false; boolean _useTransactionAuditCache = false; private final Set _passThroughBuiltInColumnNames = new CaseInsensitiveHashSet(); @@ -202,16 +201,6 @@ public void setCrossTypeImport(boolean crossTypeImport) _crossTypeImport = crossTypeImport; } - public boolean isCrossFolderImport() - { - return _crossFolderImport; - } - - public void setCrossFolderImport(boolean crossFolderImport) - { - _crossFolderImport = crossFolderImport; - } - public boolean isAllowCreateStorage() { return _allowCreateStorage; diff --git a/api/src/org/labkey/api/query/AbstractQueryImportAction.java b/api/src/org/labkey/api/query/AbstractQueryImportAction.java index 9e489a5165a..719ab8e16cb 100644 --- a/api/src/org/labkey/api/query/AbstractQueryImportAction.java +++ b/api/src/org/labkey/api/query/AbstractQueryImportAction.java @@ -311,7 +311,6 @@ public enum Params crossTypeImport, allowCreateStorage, importLookupByAlternateKey, // deprecated. Prefer lookupResolutionType - crossFolderImport, useTransactionAuditCache, lookupResolutionType, auditDetails, @@ -331,7 +330,6 @@ protected Map getOptionParamsMap() _optionParamsMap.put(Params.importIdentity, Boolean.valueOf(getParam(Params.importIdentity))); _optionParamsMap.put(Params.crossTypeImport, Boolean.valueOf(getParam(Params.crossTypeImport))); _optionParamsMap.put(Params.allowCreateStorage, Boolean.valueOf(getParam(Params.allowCreateStorage))); - _optionParamsMap.put(Params.crossFolderImport, Boolean.valueOf(getParam(Params.crossFolderImport))); _optionParamsMap.put(Params.useTransactionAuditCache, Boolean.valueOf(getParam(Params.useTransactionAuditCache))); } return _optionParamsMap; @@ -345,8 +343,6 @@ protected Set getTransactionImportParams(String insertOption, boolean us importParams.add("backgroundImport"); if (Boolean.valueOf(getParam(Params.crossTypeImport))) importParams.add(Params.crossTypeImport.name()); - if (Boolean.valueOf(getParam(Params.crossFolderImport))) - importParams.add(Params.crossFolderImport.name()); if (Boolean.valueOf(getParam(Params.useTransactionAuditCache))) importParams.add(Params.useTransactionAuditCache.name()); if (Boolean.valueOf(getParam(Params.allowCreateStorage))) @@ -852,7 +848,6 @@ public static DataIteratorContext createDataIteratorContext(QueryUpdateService.I boolean importIdentity = optionParamsMap.getOrDefault(AbstractQueryImportAction.Params.importIdentity, false); boolean crossTypeImport = optionParamsMap.getOrDefault(AbstractQueryImportAction.Params.crossTypeImport, false); boolean allowCreateStorage = optionParamsMap.getOrDefault(AbstractQueryImportAction.Params.allowCreateStorage, false); - boolean crossFolderImport = optionParamsMap.getOrDefault(AbstractQueryImportAction.Params.crossFolderImport, false); boolean useTransactionAuditCache = optionParamsMap.getOrDefault(Params.useTransactionAuditCache, false); DataIteratorContext context = new DataIteratorContext(errors); @@ -874,7 +869,6 @@ public static DataIteratorContext createDataIteratorContext(QueryUpdateService.I context.setSupportAutoIncrementKey(true); } context.setCrossTypeImport(crossTypeImport); - context.setCrossFolderImport(crossFolderImport && container != null && container.hasProductFolders()); context.setAllowCreateStorage(allowCreateStorage); context.setUseTransactionAuditCache(useTransactionAuditCache); context.setLogger(logger); diff --git a/api/src/org/labkey/api/query/AbstractQueryUpdateService.java b/api/src/org/labkey/api/query/AbstractQueryUpdateService.java index 19453164ec4..d92b2b613ea 100644 --- a/api/src/org/labkey/api/query/AbstractQueryUpdateService.java +++ b/api/src/org/labkey/api/query/AbstractQueryUpdateService.java @@ -1,1705 +1,1705 @@ -/* - * Copyright (c) 2008-2019 LabKey Corporation - * - * 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.labkey.api.query; - -import org.apache.commons.beanutils.ConversionException; -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.labkey.api.assay.AssayFileWriter; -import org.labkey.api.attachments.AttachmentFile; -import org.labkey.api.attachments.AttachmentParentFactory; -import org.labkey.api.attachments.SpringAttachmentFile; -import org.labkey.api.audit.AuditLogService; -import org.labkey.api.audit.TransactionAuditProvider; -import org.labkey.api.audit.provider.FileSystemAuditProvider; -import org.labkey.api.collections.ArrayListMap; -import org.labkey.api.collections.CaseInsensitiveHashMap; -import org.labkey.api.collections.CaseInsensitiveHashSet; -import org.labkey.api.collections.Sets; -import org.labkey.api.data.ColumnInfo; -import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerManager; -import org.labkey.api.data.ConvertHelper; -import org.labkey.api.data.DbScope; -import org.labkey.api.data.DbSequenceManager; -import org.labkey.api.data.ExpDataFileConverter; -import org.labkey.api.data.ImportAliasable; -import org.labkey.api.data.MultiValuedForeignKey; -import org.labkey.api.data.PropertyStorageSpec; -import org.labkey.api.data.RuntimeSQLException; -import org.labkey.api.data.Sort; -import org.labkey.api.data.TableInfo; -import org.labkey.api.data.TableSelector; -import org.labkey.api.data.UpdateableTableInfo; -import org.labkey.api.data.dialect.SqlDialect; -import org.labkey.api.dataiterator.AttachmentDataIterator; -import org.labkey.api.dataiterator.DataIterator; -import org.labkey.api.dataiterator.DataIteratorBuilder; -import org.labkey.api.dataiterator.DataIteratorContext; -import org.labkey.api.dataiterator.DataIteratorUtil; -import org.labkey.api.dataiterator.DetailedAuditLogDataIterator; -import org.labkey.api.dataiterator.ExistingRecordDataIterator; -import org.labkey.api.dataiterator.MapDataIterator; -import org.labkey.api.dataiterator.Pump; -import org.labkey.api.dataiterator.StandardDataIteratorBuilder; -import org.labkey.api.dataiterator.TriggerDataBuilderHelper; -import org.labkey.api.dataiterator.WrapperDataIterator; -import org.labkey.api.exceptions.OptimisticConflictException; -import org.labkey.api.exp.ExperimentException; -import org.labkey.api.exp.MvColumn; -import org.labkey.api.exp.PropertyType; -import org.labkey.api.exp.api.ExpData; -import org.labkey.api.exp.api.ExperimentService; -import org.labkey.api.exp.list.ListDefinition; -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.files.FileContentService; -import org.labkey.api.gwt.client.AuditBehaviorType; -import org.labkey.api.ontology.OntologyService; -import org.labkey.api.pipeline.PipeRoot; -import org.labkey.api.pipeline.PipelineService; -import org.labkey.api.reader.TabLoader; -import org.labkey.api.security.User; -import org.labkey.api.security.UserPrincipal; -import org.labkey.api.security.permissions.AdminPermission; -import org.labkey.api.security.permissions.DeletePermission; -import org.labkey.api.security.permissions.InsertPermission; -import org.labkey.api.security.permissions.Permission; -import org.labkey.api.security.permissions.ReadPermission; -import org.labkey.api.security.permissions.UpdatePermission; -import org.labkey.api.test.TestWhen; -import org.labkey.api.util.FileUtil; -import org.labkey.api.util.GUID; -import org.labkey.api.util.JunitUtil; -import org.labkey.api.util.TestContext; -import org.labkey.api.util.URIUtil; -import org.labkey.api.view.NotFoundException; -import org.labkey.api.view.UnauthorizedException; -import org.labkey.api.workflow.WorkflowService; -import org.labkey.api.writer.VirtualFile; -import org.labkey.vfs.FileLike; -import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.io.IOException; -import java.io.StringReader; -import java.nio.file.Path; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.function.Function; - -import static java.util.Objects.requireNonNull; -import static org.labkey.api.audit.TransactionAuditProvider.DB_SEQUENCE_NAME; -import static org.labkey.api.dataiterator.DetailedAuditLogDataIterator.AuditConfigs.AuditBehavior; -import static org.labkey.api.dataiterator.DetailedAuditLogDataIterator.AuditConfigs.AuditUserComment; -import static org.labkey.api.files.FileContentService.UPLOADED_FILE; -import static org.labkey.api.util.FileUtil.toFileForRead; -import static org.labkey.api.util.FileUtil.toFileForWrite; - -public abstract class AbstractQueryUpdateService implements QueryUpdateService -{ - protected final TableInfo _queryTable; - - private boolean _bulkLoad = false; - private CaseInsensitiveHashMap _columnImportMap = null; - private VirtualFile _att = null; - - /* AbstractQueryUpdateService is generally responsible for some shared functionality - * - triggers - * - coercion/validation - * - detailed logging - * - attachments - * - * If a subclass wants to disable some of these features (w/o subclassing), put flags here... - */ - protected boolean _enableExistingRecordsDataIterator = true; - protected Set _previouslyUpdatedRows = new HashSet<>(); - - protected AbstractQueryUpdateService(TableInfo queryTable) - { - if (queryTable == null) - throw new IllegalArgumentException(); - _queryTable = queryTable; - } - - protected TableInfo getQueryTable() - { - return _queryTable; - } - - public @NotNull Set getPreviouslyUpdatedRows() - { - return _previouslyUpdatedRows == null ? new HashSet<>() : _previouslyUpdatedRows; - } - - @Override - public boolean hasPermission(@NotNull UserPrincipal user, @NotNull Class acl) - { - return getQueryTable().hasPermission(user, acl); - } - - protected Map getRow(User user, Container container, Map keys, boolean allowCrossContainer) - throws InvalidKeyException, QueryUpdateServiceException, SQLException - { - return getRow(user, container, keys); - } - - protected abstract Map getRow(User user, Container container, Map keys) - throws InvalidKeyException, QueryUpdateServiceException, SQLException; - - @Override - public List> getRows(User user, Container container, List> keys) - throws InvalidKeyException, QueryUpdateServiceException, SQLException - { - if (!hasPermission(user, ReadPermission.class)) - throw new UnauthorizedException("You do not have permission to read data from this table."); - - List> result = new ArrayList<>(); - for (Map rowKeys : keys) - { - Map row = getRow(user, container, rowKeys); - if (row != null) - result.add(row); - } - return result; - } - - @Override - public Map> getExistingRows(User user, Container container, Map> keys, boolean verifyNoCrossFolderData, boolean verifyExisting, @Nullable Set columns) - throws InvalidKeyException, QueryUpdateServiceException, SQLException - { - if (!hasPermission(user, ReadPermission.class)) - throw new UnauthorizedException("You do not have permission to read data from this table."); - - Map> result = new LinkedHashMap<>(); - for (Map.Entry> key : keys.entrySet()) - { - Map row = getRow(user, container, key.getValue(), verifyNoCrossFolderData); - if (row != null && !row.isEmpty()) - { - result.put(key.getKey(), row); - if (verifyNoCrossFolderData) - { - String dataContainer = (String) row.get("container"); - if (StringUtils.isEmpty(dataContainer)) - dataContainer = (String) row.get("folder"); - if (!container.getId().equals(dataContainer)) - throw new InvalidKeyException("Data does not belong to folder '" + container.getName() + "': " + key.getValue().values()); - } - } - else if (verifyExisting) - throw new InvalidKeyException("Data not found for " + key.getValue().values()); - } - return result; - } - - @Override - public boolean hasExistingRowsInOtherContainers(Container container, Map> keys) - { - return false; - } - - public static TransactionAuditProvider.TransactionAuditEvent createTransactionAuditEvent(Container container, QueryService.AuditAction auditAction) - { - return createTransactionAuditEvent(container, auditAction, null); - } - - public static TransactionAuditProvider.TransactionAuditEvent createTransactionAuditEvent(Container container, QueryService.AuditAction auditAction, @Nullable Map details) - { - long auditId = DbSequenceManager.get(ContainerManager.getRoot(), DB_SEQUENCE_NAME).next(); - TransactionAuditProvider.TransactionAuditEvent event = new TransactionAuditProvider.TransactionAuditEvent(container, auditAction, auditId); - if (details != null) - event.addDetails(details); - return event; - } - - public static void addTransactionAuditEvent(DbScope.Transaction transaction, User user, TransactionAuditProvider.TransactionAuditEvent auditEvent) - { - UserSchema schema = AuditLogService.getAuditLogSchema(user, ContainerManager.getRoot()); - - if (schema != null) - { - // This is a little hack to ensure that the audit table has actually been created and gets put into the table cache by the time the - // pre-commit task is executed. Otherwise, since the creation of the table happens while within the commit for the - // outermost transaction, it looks like there is a close that hasn't happened when trying to commit the transaction for creating the - // table. - schema.getTable(auditEvent.getEventType(), false); - - transaction.addCommitTask(() -> AuditLogService.get().addEvent(user, auditEvent), DbScope.CommitTaskOption.PRECOMMIT); - - transaction.setAuditEvent(auditEvent); - } - } - - protected final DataIteratorContext getDataIteratorContext(BatchValidationException errors, InsertOption forImport, Map configParameters) - { - if (null == errors) - errors = new BatchValidationException(); - DataIteratorContext context = new DataIteratorContext(errors); - context.setInsertOption(forImport); - context.setConfigParameters(configParameters); - configureDataIteratorContext(context); - recordDataIteratorUsed(configParameters); - - return context; - } - - protected void recordDataIteratorUsed(@Nullable Map configParameters) - { - if (configParameters == null) - return; - - try - { - configParameters.put(TransactionAuditProvider.TransactionDetail.DataIteratorUsed, true); - } - catch (UnsupportedOperationException ignore) - { - // configParameters is immutable, likely originated from a junit test - } - } - - /** - * If QUS wants to use something other than PKs to select existing rows for merge, it can override this method. - * Used only for generating ExistingRecordDataIterator at the moment. - */ - protected Set getSelectKeys(DataIteratorContext context) - { - if (!context.getAlternateKeys().isEmpty()) - return context.getAlternateKeys(); - return null; - } - - /* - * construct the core DataIterator transformation pipeline for this table, may be just StandardDataIteratorBuilder. - * does NOT handle triggers or the insert/update iterator. - */ - public DataIteratorBuilder createImportDIB(User user, Container container, DataIteratorBuilder data, DataIteratorContext context) - { - DataIteratorBuilder dib = StandardDataIteratorBuilder.forInsert(getQueryTable(), data, container, user); - - if (_enableExistingRecordsDataIterator || context.getInsertOption().updateOnly) - { - // some tables need to generate PKs, so they need to add ExistingRecordDataIterator in persistRows() (after generating PK, before inserting) - dib = ExistingRecordDataIterator.createBuilder(dib, getQueryTable(), getSelectKeys(context)); - } - - dib = ((UpdateableTableInfo) getQueryTable()).persistRows(dib, context); - dib = AttachmentDataIterator.getAttachmentDataIteratorBuilder(getQueryTable(), dib, user, context.getInsertOption().batch ? getAttachmentDirectory() : null, container, getAttachmentParentFactory()); - dib = DetailedAuditLogDataIterator.getDataIteratorBuilder(getQueryTable(), dib, context.getInsertOption(), user, container, null); - return dib; - } - - - /** - * Implementation to use insertRows() while we migrate to using DIB for all code paths - *

- * DataIterator should/must use the same error collection as passed in - */ - @Deprecated - protected int _importRowsUsingInsertRows(User user, Container container, DataIterator rows, BatchValidationException errors, Map extraScriptContext) - { - MapDataIterator mapIterator = DataIteratorUtil.wrapMap(rows, true); - List> list = new ArrayList<>(); - List> ret; - Exception rowException; - - try - { - while (mapIterator.next()) - list.add(mapIterator.getMap()); - ret = insertRows(user, container, list, errors, null, extraScriptContext); - if (errors.hasErrors()) - return 0; - return ret.size(); - } - catch (BatchValidationException x) - { - assert x == errors; - assert x.hasErrors(); - return 0; - } - catch (QueryUpdateServiceException | DuplicateKeyException | SQLException x) - { - rowException = x; - } - finally - { - DataIteratorUtil.closeQuietly(mapIterator); - } - errors.addRowError(new ValidationException(rowException.getMessage())); - return 0; - } - - protected boolean hasImportRowsPermission(User user, Container container, DataIteratorContext context) - { - return hasPermission(user, context.getInsertOption().updateOnly ? UpdatePermission.class : InsertPermission.class); - } - - protected boolean hasInsertRowsPermission(User user) - { - return hasPermission(user, InsertPermission.class); - } - - protected boolean hasDeleteRowsPermission(User user) - { - return hasPermission(user, DeletePermission.class); - } - - protected boolean hasUpdateRowsPermission(User user) - { - return hasPermission(user, UpdatePermission.class); - } - - // override this - protected void preImportDIBValidation(@Nullable DataIteratorBuilder in, @Nullable Collection inputColumns) - { - } - - protected int _importRowsUsingDIB(User user, Container container, DataIteratorBuilder in, @Nullable final ArrayList> outputRows, DataIteratorContext context, @Nullable Map extraScriptContext) - { - if (!hasImportRowsPermission(user, container, context)) - throw new UnauthorizedException("You do not have permission to " + (context.getInsertOption().updateOnly ? "update data in this table." : "insert data into this table.")); - - if (!context.getConfigParameterBoolean(ConfigParameters.SkipInsertOptionValidation)) - assert(getQueryTable().supportsInsertOption(context.getInsertOption())); - - context.getErrors().setExtraContext(extraScriptContext); - if (extraScriptContext != null) - { - context.setDataSource((String) extraScriptContext.get(DataIteratorUtil.DATA_SOURCE)); - if (extraScriptContext.containsKey(AbstractQueryImportAction.Params.useTransactionAuditCache.name())) - { - boolean useTransactionAuditCache = Boolean.TRUE.equals(extraScriptContext.get(AbstractQueryImportAction.Params.useTransactionAuditCache.name())); - context.setUseTransactionAuditCache(useTransactionAuditCache); - } - } - - preImportDIBValidation(in, null); - - boolean skipTriggers = context.getConfigParameterBoolean(ConfigParameters.SkipTriggers) || context.isCrossTypeImport() || context.isCrossFolderImport(); - boolean hasTableScript = hasTableScript(container); - TriggerDataBuilderHelper helper = new TriggerDataBuilderHelper(getQueryTable(), container, user, extraScriptContext, context.getInsertOption().useImportAliases); - if (!skipTriggers) - { - in = preTriggerDataIterator(in, context); - if (hasTableScript) - in = helper.before(in); - } - DataIteratorBuilder importDIB = createImportDIB(user, container, in, context); - DataIteratorBuilder out = importDIB; - - if (!skipTriggers) - { - if (hasTableScript) - out = helper.after(importDIB); - - out = postTriggerDataIterator(out, context); - } - - if (hasTableScript) - { - context.setFailFast(false); - context.setMaxRowErrors(Math.max(context.getMaxRowErrors(),1000)); - } - int count = _pump(out, outputRows, context); - - if (context.getErrors().hasErrors()) - return 0; - - if (!context.getConfigParameterBoolean(ConfigParameters.SkipAuditSummary)) - _addSummaryAuditEvent(container, user, context, count); - - return count; - } - - protected DataIteratorBuilder preTriggerDataIterator(DataIteratorBuilder in, DataIteratorContext context) - { - return in; - } - - protected DataIteratorBuilder postTriggerDataIterator(DataIteratorBuilder out, DataIteratorContext context) - { - return out; - } - - /** this is extracted so subclasses can add wrap */ - protected int _pump(DataIteratorBuilder etl, final @Nullable ArrayList> rows, DataIteratorContext context) - { - DataIterator it = etl.getDataIterator(context); - - if (null == it) - return 0; - - try - { - if (null != rows) - { - MapDataIterator maps = DataIteratorUtil.wrapMap(it, false); - it = new WrapperDataIterator(maps) - { - @Override - public boolean next() throws BatchValidationException - { - boolean ret = super.next(); - if (ret) - rows.add(((MapDataIterator)_delegate).getMapExcludeExistingRecord()); - return ret; - } - }; - } - - Pump pump = new Pump(it, context); - pump.run(); - - return pump.getRowCount(); - } - finally - { - DataIteratorUtil.closeQuietly(it); - } - } - - /* can be used for simple bookkeeping tasks, per row processing belongs in a data iterator */ - protected void afterInsertUpdate(int count, BatchValidationException errors, boolean isUpdate) - { - afterInsertUpdate(count, errors); - } - - protected void afterInsertUpdate(int count, BatchValidationException errors) - {} - - @Override - public int loadRows(User user, Container container, DataIteratorBuilder rows, DataIteratorContext context, @Nullable Map extraScriptContext) - { - return loadRows(user, container, rows, null, context, extraScriptContext); - } - - public int loadRows(User user, Container container, DataIteratorBuilder rows, @Nullable final ArrayList> outputRows, DataIteratorContext context, @Nullable Map extraScriptContext) - { - configureDataIteratorContext(context); - int count = _importRowsUsingDIB(user, container, rows, outputRows, context, extraScriptContext); - afterInsertUpdate(count, context.getErrors(), context.getInsertOption().updateOnly); - return count; - } - - @Override - public int importRows(User user, Container container, DataIteratorBuilder rows, BatchValidationException errors, Map configParameters, @Nullable Map extraScriptContext) - { - DataIteratorContext context = getDataIteratorContext(errors, InsertOption.IMPORT, configParameters); - int count = _importRowsUsingInsertRows(user, container, rows.getDataIterator(context), errors, extraScriptContext); - afterInsertUpdate(count, errors, context.getInsertOption().updateOnly); - return count; - } - - @Override - public int mergeRows(User user, Container container, DataIteratorBuilder rows, BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) - { - throw new UnsupportedOperationException("merge is not supported for all tables"); - } - - private boolean hasTableScript(Container container) - { - return getQueryTable().hasTriggers(container); - } - - - protected Map insertRow(User user, Container container, Map row) - throws DuplicateKeyException, ValidationException, QueryUpdateServiceException, SQLException - { - throw new UnsupportedOperationException("Not implemented by this QueryUpdateService"); - } - - - protected @Nullable List> _insertRowsUsingDIB(User user, Container container, List> rows, - DataIteratorContext context, @Nullable Map extraScriptContext) - { - if (!hasInsertRowsPermission(user)) - throw new UnauthorizedException("You do not have permission to insert data into this table."); - - return _insertUpdateRowsUsingDIB(user, container, rows, context, extraScriptContext); - } - - protected @Nullable List> _insertUpdateRowsUsingDIB(User user, Container container, List> rows, - DataIteratorContext context, @Nullable Map extraScriptContext) - { - DataIteratorBuilder dib = _toDataIteratorBuilder(getClass().getSimpleName() + (context.getInsertOption().updateOnly ? ".updateRows" : ".insertRows()"), rows); - ArrayList> outputRows = new ArrayList<>(); - int count = _importRowsUsingDIB(user, container, dib, outputRows, context, extraScriptContext); - afterInsertUpdate(count, context.getErrors(), context.getInsertOption().updateOnly); - - if (context.getErrors().hasErrors()) - return null; - - return outputRows; - } - - // not yet supported - protected @Nullable List> _updateRowsUsingDIB(User user, Container container, List> rows, - DataIteratorContext context, @Nullable Map extraScriptContext) - { - if (!hasUpdateRowsPermission(user)) - throw new UnauthorizedException("You do not have permission to update data in this table."); - - return _insertUpdateRowsUsingDIB(user, container, rows, context, extraScriptContext); - } - - - protected DataIteratorBuilder _toDataIteratorBuilder(String debugName, List> rows) - { - // TODO probably can't assume all rows have all columns - // TODO can we assume that all rows refer to columns consistently? (not PTID and MouseId for the same column) - // TODO optimize ArrayListMap? - Set colNames; - - if (!rows.isEmpty() && rows.getFirst() instanceof ArrayListMap) - { - colNames = ((ArrayListMap)rows.getFirst()).getFindMap().keySet(); - } - else - { - // Preserve casing by using wrapped CaseInsensitiveHashMap instead of CaseInsensitiveHashSet - colNames = Sets.newCaseInsensitiveHashSet(); - for (Map row : rows) - colNames.addAll(row.keySet()); - } - - preImportDIBValidation(null, colNames); - return MapDataIterator.of(colNames, rows, debugName); - } - - - /** @deprecated switch to using DIB based method */ - @Deprecated - protected List> _insertRowsUsingInsertRow(User user, Container container, List> rows, BatchValidationException errors, Map extraScriptContext) - throws DuplicateKeyException, BatchValidationException, QueryUpdateServiceException, SQLException - { - if (!hasInsertRowsPermission(user)) - throw new UnauthorizedException("You do not have permission to insert data into this table."); - - assert(getQueryTable().supportsInsertOption(InsertOption.INSERT)); - - boolean hasTableScript = hasTableScript(container); - - errors.setExtraContext(extraScriptContext); - if (hasTableScript) - getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.INSERT, null, true, errors, extraScriptContext); - - List> result = new ArrayList<>(rows.size()); - List> providedValues = new ArrayList<>(rows.size()); - for (int i = 0; i < rows.size(); i++) - { - Map row = rows.get(i); - row = normalizeColumnNames(row); - try - { - providedValues.add(new CaseInsensitiveHashMap<>()); - row = coerceTypes(row, providedValues.get(i), false); - if (hasTableScript) - { - getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.INSERT, true, i, row, null, extraScriptContext); - } - row = insertRow(user, container, row); - if (row == null) - continue; - - if (hasTableScript) - getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.INSERT, false, i, row, null, extraScriptContext); - result.add(row); - } - catch (SQLException sqlx) - { - if (StringUtils.startsWith(sqlx.getSQLState(), "22") || RuntimeSQLException.isConstraintException(sqlx)) - { - ValidationException vex = new ValidationException(sqlx.getMessage()); - vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), row, i+1); - errors.addRowError(vex); - } - else if (SqlDialect.isTransactionException(sqlx) && errors.hasErrors()) - { - // if we already have some errors, just break - break; - } - else - { - throw sqlx; - } - } - catch (ValidationException vex) - { - errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), row, i)); - } - catch (RuntimeValidationException rvex) - { - ValidationException vex = rvex.getValidationException(); - errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), row, i)); - } - } - - if (hasTableScript) - getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.INSERT, null, false, errors, extraScriptContext); - - addAuditEvent(user, container, QueryService.AuditAction.INSERT, null, result, null, providedValues); - - return result; - } - - protected void addAuditEvent(User user, Container container, QueryService.AuditAction auditAction, @Nullable Map configParameters, @Nullable List> rows, @Nullable List> existingRows, @Nullable List> providedValues) - { - if (!isBulkLoad()) - { - AuditBehaviorType auditBehavior = configParameters != null ? (AuditBehaviorType) configParameters.get(AuditBehavior) : null; - String userComment = configParameters == null ? null : (String) configParameters.get(AuditUserComment); - getQueryTable().getAuditHandler(auditBehavior) - .addAuditEvent(user, container, getQueryTable(), auditBehavior, userComment, auditAction, rows, existingRows, providedValues); - } - } - - private Map normalizeColumnNames(Map row) - { - if(_columnImportMap == null) - { - _columnImportMap = (CaseInsensitiveHashMap)ImportAliasable.Helper.createImportMap(getQueryTable().getColumns(), false); - } - - Map newRow = new CaseInsensitiveHashMap<>(); - CaseInsensitiveHashSet columns = new CaseInsensitiveHashSet(); - columns.addAll(row.keySet()); - - String newName; - for(String key : row.keySet()) - { - if(_columnImportMap.containsKey(key)) - { - //it is possible for a normalized name to conflict with an existing property. if so, defer to the original - newName = _columnImportMap.get(key).getName(); - if(!columns.contains(newName)){ - newRow.put(newName, row.get(key)); - continue; - } - } - newRow.put(key, row.get(key)); - } - - return newRow; - } - - @Override - public List> insertRows(User user, Container container, List> rows, BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) - throws DuplicateKeyException, QueryUpdateServiceException, SQLException - { - try - { - List> ret = _insertRowsUsingInsertRow(user, container, rows, errors, extraScriptContext); - afterInsertUpdate(null==ret?0:ret.size(), errors); - if (errors.hasErrors()) - return null; - return ret; - } - catch (BatchValidationException x) - { - assert x == errors; - assert x.hasErrors(); - } - return null; - } - - protected Object coerceTypesValue(ColumnInfo col, Map providedValues, String key, Object value) - { - if (col != null && value != null && - !col.getJavaObjectClass().isInstance(value) && - !(value instanceof AttachmentFile) && - !(value instanceof MultipartFile) && - !(value instanceof String[]) && - !(col.isMultiValued() || col.getFk() instanceof MultiValuedForeignKey)) - { - try - { - if (col.getKindOfQuantity() != null) - providedValues.put(key, value); - if (PropertyType.FILE_LINK.equals(col.getPropertyType())) - value = ExpDataFileConverter.convert(value); - else - value = col.convert(value); - } - catch (ConvertHelper.FileConversionException e) - { - throw e; - } - catch (ConversionException e) - { - // That's OK, the transformation script may be able to fix up the value before it gets inserted - } - } - - return value; - } - - /** Attempt to make the passed in types match the expected types so the script doesn't have to do the conversion */ - @Deprecated - protected Map coerceTypes(Map row, Map providedValues, boolean isUpdate) - { - Map result = new CaseInsensitiveHashMap<>(row.size()); - Map columnMap = ImportAliasable.Helper.createImportMap(_queryTable.getColumns(), true); - for (Map.Entry entry : row.entrySet()) - { - ColumnInfo col = columnMap.get(entry.getKey()); - Object value = coerceTypesValue(col, providedValues, entry.getKey(), entry.getValue()); - result.put(entry.getKey(), value); - } - - return result; - } - - protected abstract Map updateRow(User user, Container container, Map row, @NotNull Map oldRow, @Nullable Map configParameters) - throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException; - - - protected boolean firstUpdateRow = true; - Function,Map> updateTransform = Function.identity(); - - /* Do standard AQUS stuff here, then call the subclass specific implementation of updateRow() */ - final protected Map updateOneRow(User user, Container container, Map row, @NotNull Map oldRow, @Nullable Map configParameters) - throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException - { - if (firstUpdateRow) - { - firstUpdateRow = false; - if (null != OntologyService.get()) - { - var t = OntologyService.get().getConceptUpdateHandler(_queryTable); - if (null != t) - updateTransform = t; - } - } - row = updateTransform.apply(row); - return updateRow(user, container, row, oldRow, configParameters); - } - - // used by updateRows to check if all rows have the same set of keys - // prepared statement can only be used to updateRows if all rows have the same set of keys - protected static boolean hasUniformKeys(List> rowsToUpdate) - { - if (rowsToUpdate == null || rowsToUpdate.isEmpty()) - return false; - - if (rowsToUpdate.size() == 1) - return true; - - Set keys = rowsToUpdate.getFirst().keySet(); - int keySize = keys.size(); - - for (int i = 1 ; i < rowsToUpdate.size(); i ++) - { - Set otherKeys = rowsToUpdate.get(i).keySet(); - if (otherKeys.size() != keySize) - return false; - if (!otherKeys.containsAll(keys)) - return false; - } - - return true; - } - - @Override - public List> updateRows(User user, Container container, List> rows, List> oldKeys, - BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) - throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException - { - if (!hasUpdateRowsPermission(user)) - throw new UnauthorizedException("You do not have permission to update data in this table."); - - if (oldKeys != null && rows.size() != oldKeys.size()) - throw new IllegalArgumentException("rows and oldKeys are required to be the same length, but were " + rows.size() + " and " + oldKeys + " in length, respectively"); - - assert(getQueryTable().supportsInsertOption(InsertOption.UPDATE)); - - errors.setExtraContext(extraScriptContext); - getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.UPDATE, null, true, errors, extraScriptContext); - - List> result = new ArrayList<>(rows.size()); - List> oldRows = new ArrayList<>(rows.size()); - List> providedValues = new ArrayList<>(rows.size()); - // TODO: Support update/delete without selecting the existing row -- unfortunately, we currently get the existing row to check its container matches the incoming container - boolean streaming = false; //_queryTable.canStreamTriggers(container) && _queryTable.getAuditBehavior() != AuditBehaviorType.NONE; - - for (int i = 0; i < rows.size(); i++) - { - Map row = rows.get(i); - providedValues.add(new CaseInsensitiveHashMap<>()); - row = coerceTypes(row, providedValues.get(i), true); - try - { - Map oldKey = oldKeys == null ? row : oldKeys.get(i); - Map oldRow = null; - if (!streaming) - { - oldRow = getRow(user, container, oldKey); - if (oldRow == null) - throw new NotFoundException("The existing row was not found."); - } - - getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.UPDATE, true, i, row, oldRow, extraScriptContext); - Map updatedRow = updateOneRow(user, container, row, oldRow, configParameters); - if (!streaming && updatedRow == null) - continue; - - getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.UPDATE, false, i, updatedRow, oldRow, extraScriptContext); - if (!streaming) - { - result.add(updatedRow); - oldRows.add(oldRow); - } - } - catch (ValidationException vex) - { - errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), row, i)); - } - catch (RuntimeValidationException rvex) - { - ValidationException vex = rvex.getValidationException(); - errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), row, i)); - } - catch (OptimisticConflictException e) - { - errors.addRowError(new ValidationException("Unable to update. Row may have been deleted.")); - } - } - - // Fire triggers, if any, and also throw if there are any errors - getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.UPDATE, null, false, errors, extraScriptContext); - afterInsertUpdate(null==result?0:result.size(), errors, true); - - if (errors.hasErrors()) - throw errors; - - addAuditEvent(user, container, QueryService.AuditAction.UPDATE, configParameters, result, oldRows, providedValues); - WorkflowService service = WorkflowService.get(); - if (service != null && configParameters != null && configParameters.containsKey(WorkflowService.WorkflowConfigs.ActionId)) - service.onActionComplete(container, user, (Long) configParameters.get(WorkflowService.WorkflowConfigs.ActionId)); - - return result; - } - - /** - * Hook called by {@link #updateRowsUsingPartitionedDIB} once per partition before that partition is processed. - * A partition is a maximal consecutive run of rows that all share the same set of column names (keys). - *

- * Subclasses can override to enforce invariants on a particular key-column combination — for example, - * requiring that certain columns always appear together. Throw a runtime exception to reject the partition. - * - * @param columns the set of column names present in the rows of the current partition - */ - protected void validatePartitionedRowKeys(Collection columns) - { - // do nothing - } - - /** - * Updates a list of rows through the DataIteratorBuilder pipeline, automatically partitioning the input - * into consecutive groups that share the same set of column names (keys). Each partition is processed - * independently through {@link #_updateRowsUsingDIB}, allowing a single call to handle heterogeneous - * row maps (e.g. some rows keyed by RowId, others by Name) without requiring the caller to pre-split them. - *

- * Audit summary logging is suppressed for individual partitions and emitted once at the end. - * Cross-partition duplicate keys are detected and reported as errors. - * If any partition produces errors, a {@link DbScope.RetryPassthroughException} is thrown so that - * an enclosing {@code executeWithRetry()} block will not attempt to commit the transaction. - */ - protected List> updateRowsUsingPartitionedDIB( - DbScope.Transaction tx, - User user, - Container container, - List> rows, - BatchValidationException errors, - @Nullable Map configParameters, - Map extraScriptContext - ) - { - int index = 0; - int numPartitions = 0; - List> ret = new ArrayList<>(); - - Set observedRowIds = new HashSet<>(); - Set observedNames = new CaseInsensitiveHashSet(); - - while (index < rows.size()) - { - CaseInsensitiveHashSet rowKeys = new CaseInsensitiveHashSet(rows.get(index).keySet()); - - validatePartitionedRowKeys(rowKeys); - - int nextIndex = index + 1; - while (nextIndex < rows.size() && rowKeys.equals(new CaseInsensitiveHashSet(rows.get(nextIndex).keySet()))) - nextIndex++; - - List> rowsToProcess = rows.subList(index, nextIndex); - index = nextIndex; - numPartitions++; - - DataIteratorContext context = getDataIteratorContext(errors, InsertOption.UPDATE, configParameters); - - // skip audit summary for the partitions, we will perform it once at the end - context.putConfigParameter(ConfigParameters.SkipAuditSummary, true); - - List> subRet = _updateRowsUsingDIB(user, container, rowsToProcess, context, extraScriptContext); - - // we need to throw if we don't want executeWithRetry() attempt commit() - if (context.getErrors().hasErrors()) - throw new DbScope.RetryPassthroughException(context.getErrors()); - - if (subRet != null) - { - ret.addAll(subRet); - - // Check if duplicate rows have been processed across the partitions - // Only start checking for duplicates after the first partition has been processed. - if (numPartitions > 1) - { - // If we are on the second partition, then lazily check all previous rows, otherwise check only the current partition - checkPartitionForDuplicates(numPartitions == 2 ? ret : subRet, observedRowIds, observedNames, errors); - } - - if (errors.hasErrors()) - throw new DbScope.RetryPassthroughException(errors); - } - } - - if (numPartitions > 1) - { - var auditEvent = tx.getAuditEvent(); - if (auditEvent != null) - auditEvent.addDetail(TransactionAuditProvider.TransactionDetail.DataIteratorPartitions, numPartitions); - } - - _addSummaryAuditEvent(container, user, getDataIteratorContext(errors, InsertOption.UPDATE, configParameters), ret.size()); - - return ret; - } - - /** - * Identifies the column names used to detect duplicate keys across partitions in - * {@link #updateRowsUsingPartitionedDIB}. Return {@code null} for either field to skip that check. - * The base implementation returns {@code (null, null)}, disabling duplicate detection. - * Subclasses whose tables support partitioned updates should override with the appropriate column names. - */ - public record PartitionKeyColumns(@Nullable String numericKey, @Nullable String stringKey) {} - - protected PartitionKeyColumns getPartitionKeyColumns() - { - return new PartitionKeyColumns(null, null); - } - - private void checkPartitionForDuplicates(List> partitionRows, Set globalRowIds, Set globalNames, BatchValidationException errors) - { - PartitionKeyColumns keys = getPartitionKeyColumns(); - for (Map row : partitionRows) - { - if (keys.numericKey() != null) - { - Long rowId = MapUtils.getLong(row, keys.numericKey()); - if (rowId != null && !globalRowIds.add(rowId)) - { - errors.addRowError(new ValidationException("Duplicate key provided: " + rowId)); - return; - } - } - - if (keys.stringKey() != null) - { - Object nameObj = row.get(keys.stringKey()); - if (nameObj != null && !globalNames.add(nameObj.toString())) - { - errors.addRowError(new ValidationException("Duplicate key provided: " + nameObj)); - return; - } - } - } - } - - protected void checkDuplicateUpdate(Object pkVals) throws ValidationException - { - if (pkVals == null) - return; - - Set updatedRows = getPreviouslyUpdatedRows(); - - Object[] keysObj; - if (pkVals.getClass().isArray()) - keysObj = (Object[]) pkVals; - else if (pkVals instanceof Map map) - { - List orderedKeyVals = new ArrayList<>(); - SortedSet sortedKeys = new TreeSet<>(map.keySet()); - for (String key : sortedKeys) - orderedKeyVals.add(map.get(key)); - keysObj = orderedKeyVals.toArray(); - } - else - keysObj = new Object[]{pkVals}; - - if (keysObj.length == 1) - { - if (updatedRows.contains(keysObj[0])) - throw new ValidationException("Duplicate key provided: " + keysObj[0]); - updatedRows.add(keysObj[0]); - return; - } - - List keys = new ArrayList<>(); - for (Object key : keysObj) - keys.add(String.valueOf(key)); - if (updatedRows.contains(keys)) - throw new ValidationException("Duplicate key provided: " + StringUtils.join(keys, ", ")); - updatedRows.add(keys); - } - - @Override - public Map moveRows(User user, Container container, Container targetContainer, List> rows, BatchValidationException errors, @Nullable Map configParameters, @Nullable Map extraScriptContext) throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException - { - throw new UnsupportedOperationException("Move is not supported for this table type."); - } - - protected abstract Map deleteRow(User user, Container container, Map oldRow) - throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException; - - protected Map deleteRow(User user, Container container, Map oldRow, @Nullable Map configParameters, @Nullable Map extraScriptContext) - throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException - { - return deleteRow(user, container, oldRow); - } - - @Override - public List> deleteRows(User user, Container container, List> keys, @Nullable Map configParameters, @Nullable Map extraScriptContext) - throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException - { - if (!hasDeleteRowsPermission(user)) - throw new UnauthorizedException("You do not have permission to delete data from this table."); - - BatchValidationException errors = new BatchValidationException(); - errors.setExtraContext(extraScriptContext); - getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.DELETE, null, true, errors, extraScriptContext); - - // TODO: Support update/delete without selecting the existing row -- unfortunately, we currently get the existing row to check its container matches the incoming container - boolean streaming = false; //_queryTable.canStreamTriggers(container) && _queryTable.getAuditBehavior() != AuditBehaviorType.NONE; - - List> result = new ArrayList<>(keys.size()); - for (int i = 0; i < keys.size(); i++) - { - Map key = keys.get(i); - try - { - Map oldRow = null; - if (!streaming) - { - oldRow = getRow(user, container, key); - // if row doesn't exist, bail early - if (oldRow == null) - continue; - } - - getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.DELETE, true, i, null, oldRow, extraScriptContext); - Map updatedRow = deleteRow(user, container, oldRow, configParameters, extraScriptContext); - if (!streaming && updatedRow == null) - continue; - - getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.DELETE, false, i, null, updatedRow, extraScriptContext); - result.add(updatedRow); - } - catch (InvalidKeyException ex) - { - ValidationException vex = new ValidationException(ex.getMessage()); - errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), key, i)); - } - catch (ValidationException vex) - { - errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), key, i)); - } - catch (RuntimeValidationException rvex) - { - ValidationException vex = rvex.getValidationException(); - errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), key, i)); - } - } - - // Fire triggers, if any, and also throw if there are any errors - getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.DELETE, null, false, errors, extraScriptContext); - - addAuditEvent(user, container, QueryService.AuditAction.DELETE, configParameters, result, null, null); - - return result; - } - - protected int truncateRows(User user, Container container) - throws QueryUpdateServiceException, SQLException - { - throw new UnsupportedOperationException(); - } - - @Override - public int truncateRows(User user, Container container, @Nullable Map configParameters, @Nullable Map extraScriptContext) - throws BatchValidationException, QueryUpdateServiceException, SQLException - { - if (!container.hasPermission(user, AdminPermission.class) && !hasDeleteRowsPermission(user)) - throw new UnauthorizedException("You do not have permission to truncate this table."); - - BatchValidationException errors = new BatchValidationException(); - errors.setExtraContext(extraScriptContext); - getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.TRUNCATE, null, true, errors, extraScriptContext); - - int result = truncateRows(user, container); - - getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.TRUNCATE, null, false, errors, extraScriptContext); - addAuditEvent(user, container, QueryService.AuditAction.TRUNCATE, configParameters, null, null, null); - - return result; - } - - @Override - public void setBulkLoad(boolean bulkLoad) - { - _bulkLoad = bulkLoad; - } - - @Override - public boolean isBulkLoad() - { - return _bulkLoad; - } - - public static Object saveFile(User user, Container container, String name, Object value, @Nullable String dirName) throws ValidationException, QueryUpdateServiceException - { - FileLike dirPath = AssayFileWriter.getUploadDirectoryPath(container, dirName); - return saveFile(user, container, name, value, dirPath); - } - - /** - * Save uploaded file to dirName directory under file or pipeline root. - */ - public static Object saveFile(User user, Container container, String name, Object value, @Nullable FileLike dirPath) throws ValidationException, QueryUpdateServiceException - { - if (!(value instanceof MultipartFile) && !(value instanceof SpringAttachmentFile)) - throw new ValidationException("Invalid file value"); - - String auditMessageFormat = "Saved file '%s' for field '%s' in folder %s."; - FileLike file = null; - try - { - FileLike dir = AssayFileWriter.ensureUploadDirectory(dirPath); - - FileSystemAuditProvider.FileSystemAuditEvent event = new FileSystemAuditProvider.FileSystemAuditEvent(container, null); - if (value instanceof MultipartFile multipartFile) - { - // Once we've found one, write it to disk and replace the row's value with just the File reference to it - if (multipartFile.isEmpty()) - { - throw new ValidationException("File " + multipartFile.getOriginalFilename() + " for field " + name + " has no content"); - } - file = FileUtil.findUniqueFileName(multipartFile.getOriginalFilename(), dir); - checkFileUnderRoot(container, file); - multipartFile.transferTo(toFileForWrite(file)); - event.setComment(String.format(auditMessageFormat, multipartFile.getOriginalFilename(), name, container.getPath())); - event.setProvidedFileName(multipartFile.getOriginalFilename()); - } - else - { - SpringAttachmentFile saf = (SpringAttachmentFile) value; - file = FileUtil.findUniqueFileName(saf.getFilename(), dir); - checkFileUnderRoot(container, file); - saf.saveTo(file); - event.setComment(String.format(auditMessageFormat, saf.getFilename(), name, container.getPath())); - event.setProvidedFileName(saf.getFilename()); - } - event.setFile(file.getName()); - event.setFieldName(name); - event.setDirectory(file.getParent().toURI().getPath()); - AuditLogService.get().addEvent(user, event); - } - catch (IOException | ExperimentException e) - { - throw new QueryUpdateServiceException(e); - } - - ensureExpData(user, container, file.toNioPathForRead().toFile()); - return file; - } - - public static ExpData ensureExpData(User user, Container container, File file) - { - ExpData existingData = ExperimentService.get().getExpDataByURL(file, container); - // create exp.data record - if (existingData == null) - { - File canonicalFile = FileUtil.getAbsoluteCaseSensitiveFile(file); - ExpData data = ExperimentService.get().createData(container, UPLOADED_FILE); - data.setName(file.getName()); - data.setDataFileURI(canonicalFile.toPath().toUri()); - if (data.getDataFileUrl() != null && data.getDataFileUrl().length() <= ExperimentService.get().getTinfoData().getColumn("DataFileURL").getScale()) - { - // If the path is too long to store, bail out without creating an exp.data row - data.save(user); - } - - return data; - } - - return existingData; - } - - // For security reasons, make sure the user hasn't tried to reference a file that's not under - // the pipeline root or @assayfiles root. Otherwise, they could get access to any file on the server - static FileLike checkFileUnderRoot(Container container, FileLike file) throws ExperimentException - { - Path assayFilesRoot = FileContentService.get().getFileRootPath(container, FileContentService.ContentType.assayfiles); - if (assayFilesRoot != null && URIUtil.isDescendant(assayFilesRoot.toUri(), file.toURI())) - return file; - - PipeRoot root = PipelineService.get().findPipelineRoot(container); - if (root == null) - throw new ExperimentException("Pipeline root not available in container " + container.getPath()); - - if (!root.isUnderRoot(toFileForRead(file))) - { - throw new ExperimentException("Cannot reference file '" + file + "' from " + container.getPath()); - } - - return file; - } - - protected void _addSummaryAuditEvent(Container container, User user, DataIteratorContext context, int count) - { - if (!context.isCrossTypeImport() && !context.isCrossFolderImport()) // audit handled at table level - { - AuditBehaviorType auditType = (AuditBehaviorType) context.getConfigParameter(DetailedAuditLogDataIterator.AuditConfigs.AuditBehavior); - String auditUserComment = (String) context.getConfigParameter(DetailedAuditLogDataIterator.AuditConfigs.AuditUserComment); - boolean skipAuditLevelCheck = false; - if (context.getConfigParameterBoolean(QueryUpdateService.ConfigParameters.BulkLoad)) - { - if (getQueryTable().getEffectiveAuditBehavior(auditType) == AuditBehaviorType.DETAILED) // allow ETL to demote audit level for bulkLoad - skipAuditLevelCheck = true; - } - getQueryTable().getAuditHandler(auditType).addSummaryAuditEvent(user, container, getQueryTable(), context.getInsertOption().auditAction, count, auditType, auditUserComment, skipAuditLevelCheck); - } - } - - /** - * Is used by the AttachmentDataIterator to point to the location of the serialized - * attachment files. - */ - public void setAttachmentDirectory(VirtualFile att) - { - _att = att; - } - - @Nullable - protected VirtualFile getAttachmentDirectory() - { - return _att; - } - - /** - * QUS instances that allow import of attachments through the AttachmentDataIterator should furnish a factory - * implementation in order to resolve the attachment parent on incoming attachment files. - */ - @Nullable - protected AttachmentParentFactory getAttachmentParentFactory() - { - return null; - } - - /** Translate between the column name that query is exposing to the column name that actually lives in the database */ - protected static void aliasColumns(Map columnMapping, Map row) - { - for (Map.Entry entry : columnMapping.entrySet()) - { - if (row.containsKey(entry.getValue()) && !row.containsKey(entry.getKey())) - { - row.put(entry.getKey(), row.get(entry.getValue())); - } - } - } - - /** - * The database table has underscores for MV column names, but we expose a column without the underscore. - * Therefore, we need to translate between the two sets of column names. - * @return database column name -> exposed TableInfo column name - */ - protected static Map createMVMapping(Domain domain) - { - Map result = new CaseInsensitiveHashMap<>(); - if (domain != null) - { - for (DomainProperty domainProperty : domain.getProperties()) - { - if (domainProperty.isMvEnabled()) - { - result.put(PropertyStorageSpec.getMvIndicatorStorageColumnName(domainProperty.getPropertyDescriptor()), domainProperty.getName() + MvColumn.MV_INDICATOR_SUFFIX); - } - } - } - return result; - } - - @TestWhen(TestWhen.When.BVT) - public static class TestCase extends Assert - { - private boolean _useAlias = false; - - static TabLoader getTestData() throws IOException - { - TabLoader testData = new TabLoader(new StringReader("pk,i,s\n0,0,zero\n1,1,one\n2,2,two"),true); - testData.parseAsCSV(); - testData.getColumns()[0].clazz = Integer.class; - testData.getColumns()[1].clazz = Integer.class; - testData.getColumns()[2].clazz = String.class; - return testData; - } - - @BeforeClass - public static void createList() throws Exception - { - if (null == ListService.get()) - return; - deleteList(); - - TabLoader testData = getTestData(); - String hash = GUID.makeHash(); - User user = TestContext.get().getUser(); - Container c = JunitUtil.getTestContainer(); - ListService s = ListService.get(); - UserSchema lists = (UserSchema)DefaultSchema.get(user, c).getSchema("lists"); - assertNotNull(lists); - - ListDefinition R = s.createList(c, "R", ListDefinition.KeyType.Integer); - R.setKeyName("pk"); - Domain d = requireNonNull(R.getDomain()); - for (int i=0 ; i> getRows() - { - User user = TestContext.get().getUser(); - Container c = JunitUtil.getTestContainer(); - UserSchema lists = (UserSchema)DefaultSchema.get(user, c).getSchema("lists"); - TableInfo rTableInfo = requireNonNull(lists.getTable("R", null)); - return Arrays.asList(new TableSelector(rTableInfo, TableSelector.ALL_COLUMNS, null, new Sort("PK")).getMapArray()); - } - - @Before - public void resetList() throws Exception - { - if (null == ListService.get()) - return; - User user = TestContext.get().getUser(); - Container c = JunitUtil.getTestContainer(); - TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); - QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); - qus.truncateRows(user, c, null, null); - } - - @AfterClass - public static void deleteList() throws Exception - { - if (null == ListService.get()) - return; - User user = TestContext.get().getUser(); - Container c = JunitUtil.getTestContainer(); - ListService s = ListService.get(); - Map m = s.getLists(c); - if (m.containsKey("R")) - m.get("R").delete(user); - } - - void validateDefaultData(List> rows) - { - assertEquals(3, rows.size()); - - assertEquals(0, rows.get(0).get("pk")); - assertEquals(1, rows.get(1).get("pk")); - assertEquals(2, rows.get(2).get("pk")); - - assertEquals(0, rows.get(0).get("i")); - assertEquals(1, rows.get(1).get("i")); - assertEquals(2, rows.get(2).get("i")); - - assertEquals("zero", rows.get(0).get("s")); - assertEquals("one", rows.get(1).get("s")); - assertEquals("two", rows.get(2).get("s")); - } - - @Test - public void INSERT() throws Exception - { - if (null == ListService.get()) - return; - User user = TestContext.get().getUser(); - Container c = JunitUtil.getTestContainer(); - TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); - assert(getRows().isEmpty()); - QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); - BatchValidationException errors = new BatchValidationException(); - var rows = qus.insertRows(user, c, getTestData().load(), errors, null, null); - assertFalse(errors.hasErrors()); - validateDefaultData(rows); - validateDefaultData(getRows()); - - qus.insertRows(user, c, getTestData().load(), errors, null, null); - assertTrue(errors.hasErrors()); - } - - @Test - public void UPSERT() throws Exception - { - if (null == ListService.get()) - return; - /* not sure how you use/test ImportOptions.UPSERT - * the only row returning QUS method is insertRows(), which doesn't let you specify the InsertOption? - */ - } - - @Test - public void IMPORT() throws Exception - { - if (null == ListService.get()) - return; - User user = TestContext.get().getUser(); - Container c = JunitUtil.getTestContainer(); - TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); - assert(getRows().isEmpty()); - QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); - BatchValidationException errors = new BatchValidationException(); - var count = qus.importRows(user, c, getTestData(), errors, null, null); - assertFalse(errors.hasErrors()); - assert(count == 3); - validateDefaultData(getRows()); - - qus.importRows(user, c, getTestData(), errors, null, null); - assertTrue(errors.hasErrors()); - } - - @Test - public void MERGE() throws Exception - { - if (null == ListService.get()) - return; - INSERT(); - assertEquals("Wrong number of rows after INSERT", 3, getRows().size()); - - User user = TestContext.get().getUser(); - Container c = JunitUtil.getTestContainer(); - TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); - QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); - var mergeRows = new ArrayList>(); - String colName = _useAlias ? "s_alias" : "s"; - String pkName = _useAlias ? "pk_alias" : "pk"; - mergeRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO")); - mergeRows.add(CaseInsensitiveHashMap.of(pkName,3,colName,"THREE")); - BatchValidationException errors = new BatchValidationException() - { - @Override - public void addRowError(ValidationException vex) - { - LogManager.getLogger(AbstractQueryUpdateService.class).error("test error", vex); - fail(vex.getMessage()); - } - }; - int count=0; - try (var tx = rTableInfo.getSchema().getScope().ensureTransaction()) - { - var ret = qus.mergeRows(user, c, MapDataIterator.of(mergeRows.getFirst().keySet(), mergeRows), errors, null, null); - if (!errors.hasErrors()) - { - tx.commit(); - count = ret; - } - } - assertFalse("mergeRows error(s): " + errors.getMessage(), errors.hasErrors()); - assertEquals(2, count); - var rows = getRows(); - // test existing row value is updated - assertEquals("TWO", rows.get(2).get("s")); - // test existing row value is not updated - assertEquals(2, rows.get(2).get("i")); - // test new row - assertEquals("THREE", rows.get(3).get("s")); - assertNull(rows.get(3).get("i")); - - // merge should fail if duplicate keys are provided - errors = new BatchValidationException(); - mergeRows = new ArrayList<>(); - mergeRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP-2")); - mergeRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP-UP-2")); - qus.mergeRows(user, c, MapDataIterator.of(mergeRows.getFirst().keySet(), mergeRows), errors, null, null); - assertTrue(errors.hasErrors()); - assertTrue("Duplicate key error: " + errors.getMessage(), errors.getMessage().contains("Duplicate key provided: 2")); - } - - @Test - public void UPDATE() throws Exception - { - if (null == ListService.get()) - return; - INSERT(); - assertEquals("Wrong number of rows after INSERT", 3, getRows().size()); - - User user = TestContext.get().getUser(); - Container c = JunitUtil.getTestContainer(); - TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); - QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); - var updateRows = new ArrayList>(); - String colName = _useAlias ? "s_alias" : "s"; - String pkName = _useAlias ? "pk_alias" : "pk"; - - // update using data iterator - updateRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP")); - DataIteratorContext context = new DataIteratorContext(); - context.setInsertOption(InsertOption.UPDATE); - var count = qus.loadRows(user, c, MapDataIterator.of(updateRows.getFirst().keySet(), updateRows), context, null); - assertFalse(context.getErrors().hasErrors()); - assertEquals(1, count); - var rows = getRows(); - // test existing row value is updated - assertEquals("TWO-UP", rows.get(2).get("s")); - // test existing row value is not updated/erased - assertEquals(2, rows.get(2).get("i")); - - // update should fail if a new record is provided - updateRows = new ArrayList<>(); - updateRows.add(CaseInsensitiveHashMap.of(pkName,123,colName,"NEW")); - updateRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP-2")); - qus.loadRows(user, c, MapDataIterator.of(updateRows.getFirst().keySet(), updateRows), context, null); - assertTrue(context.getErrors().hasErrors()); - - // Issue 52728: update should fail if duplicate key is provide - updateRows = new ArrayList<>(); - updateRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP-2")); - updateRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP-UP-2")); - - // use DIB - context = new DataIteratorContext(); - context.setInsertOption(InsertOption.UPDATE); - qus.loadRows(user, c, MapDataIterator.of(updateRows.getFirst().keySet(), updateRows), context, null); - assertTrue(context.getErrors().hasErrors()); - assertTrue("Duplicate key error: " + context.getErrors().getMessage(), context.getErrors().getMessage().contains("Duplicate key provided: 2")); - - // use updateRows - if (!_useAlias) // _update using alias is not supported - { - BatchValidationException errors = new BatchValidationException(); - try - { - qus.updateRows(user, c, updateRows, null, errors, null, null); - } - catch (Exception e) - { - - } - assertTrue(errors.hasErrors()); - assertTrue("Duplicate key error: " + errors.getMessage(), errors.getMessage().contains("Duplicate key provided: 2")); - - } - } - - @Test - public void REPLACE() throws Exception - { - if (null == ListService.get()) - return; - assert(getRows().isEmpty()); - INSERT(); - - User user = TestContext.get().getUser(); - Container c = JunitUtil.getTestContainer(); - TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); - QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); - var mergeRows = new ArrayList>(); - String colName = _useAlias ? "s_alias" : "s"; - String pkName = _useAlias ? "pk_alias" : "pk"; - mergeRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO")); - mergeRows.add(CaseInsensitiveHashMap.of(pkName,3,colName,"THREE")); - DataIteratorContext context = new DataIteratorContext(); - context.setInsertOption(InsertOption.REPLACE); - var count = qus.loadRows(user, c, MapDataIterator.of(mergeRows.getFirst().keySet(), mergeRows), context, null); - assertFalse(context.getErrors().hasErrors()); - assertEquals(2, count); - var rows = getRows(); - // test existing row value is updated - assertEquals("TWO", rows.get(2).get("s")); - // test existing row value is updated - assertNull(rows.get(2).get("i")); - // test new row - assertEquals("THREE", rows.get(3).get("s")); - assertNull(rows.get(3).get("i")); - } - - @Test - public void IMPORT_IDENTITY() - { - if (null == ListService.get()) - return; - // TODO - } - - @Test - public void ALIAS_MERGE() throws Exception - { - _useAlias = true; - MERGE(); - } - - @Test - public void ALIAS_REPLACE() throws Exception - { - _useAlias = true; - REPLACE(); - } - - @Test - public void ALIAS_UPDATE() throws Exception - { - _useAlias = true; - UPDATE(); - } - } -} +/* + * Copyright (c) 2008-2019 LabKey Corporation + * + * 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.labkey.api.query; + +import org.apache.commons.beanutils.ConversionException; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.labkey.api.assay.AssayFileWriter; +import org.labkey.api.attachments.AttachmentFile; +import org.labkey.api.attachments.AttachmentParentFactory; +import org.labkey.api.attachments.SpringAttachmentFile; +import org.labkey.api.audit.AuditLogService; +import org.labkey.api.audit.TransactionAuditProvider; +import org.labkey.api.audit.provider.FileSystemAuditProvider; +import org.labkey.api.collections.ArrayListMap; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.collections.CaseInsensitiveHashSet; +import org.labkey.api.collections.Sets; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.ConvertHelper; +import org.labkey.api.data.DbScope; +import org.labkey.api.data.DbSequenceManager; +import org.labkey.api.data.ExpDataFileConverter; +import org.labkey.api.data.ImportAliasable; +import org.labkey.api.data.MultiValuedForeignKey; +import org.labkey.api.data.PropertyStorageSpec; +import org.labkey.api.data.RuntimeSQLException; +import org.labkey.api.data.Sort; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.TableSelector; +import org.labkey.api.data.UpdateableTableInfo; +import org.labkey.api.data.dialect.SqlDialect; +import org.labkey.api.dataiterator.AttachmentDataIterator; +import org.labkey.api.dataiterator.DataIterator; +import org.labkey.api.dataiterator.DataIteratorBuilder; +import org.labkey.api.dataiterator.DataIteratorContext; +import org.labkey.api.dataiterator.DataIteratorUtil; +import org.labkey.api.dataiterator.DetailedAuditLogDataIterator; +import org.labkey.api.dataiterator.ExistingRecordDataIterator; +import org.labkey.api.dataiterator.MapDataIterator; +import org.labkey.api.dataiterator.Pump; +import org.labkey.api.dataiterator.StandardDataIteratorBuilder; +import org.labkey.api.dataiterator.TriggerDataBuilderHelper; +import org.labkey.api.dataiterator.WrapperDataIterator; +import org.labkey.api.exceptions.OptimisticConflictException; +import org.labkey.api.exp.ExperimentException; +import org.labkey.api.exp.MvColumn; +import org.labkey.api.exp.PropertyType; +import org.labkey.api.exp.api.ExpData; +import org.labkey.api.exp.api.ExperimentService; +import org.labkey.api.exp.list.ListDefinition; +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.files.FileContentService; +import org.labkey.api.gwt.client.AuditBehaviorType; +import org.labkey.api.ontology.OntologyService; +import org.labkey.api.pipeline.PipeRoot; +import org.labkey.api.pipeline.PipelineService; +import org.labkey.api.reader.TabLoader; +import org.labkey.api.security.User; +import org.labkey.api.security.UserPrincipal; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.security.permissions.DeletePermission; +import org.labkey.api.security.permissions.InsertPermission; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.security.permissions.UpdatePermission; +import org.labkey.api.test.TestWhen; +import org.labkey.api.util.FileUtil; +import org.labkey.api.util.GUID; +import org.labkey.api.util.JunitUtil; +import org.labkey.api.util.TestContext; +import org.labkey.api.util.URIUtil; +import org.labkey.api.view.NotFoundException; +import org.labkey.api.view.UnauthorizedException; +import org.labkey.api.workflow.WorkflowService; +import org.labkey.api.writer.VirtualFile; +import org.labkey.vfs.FileLike; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.function.Function; + +import static java.util.Objects.requireNonNull; +import static org.labkey.api.audit.TransactionAuditProvider.DB_SEQUENCE_NAME; +import static org.labkey.api.dataiterator.DetailedAuditLogDataIterator.AuditConfigs.AuditBehavior; +import static org.labkey.api.dataiterator.DetailedAuditLogDataIterator.AuditConfigs.AuditUserComment; +import static org.labkey.api.files.FileContentService.UPLOADED_FILE; +import static org.labkey.api.util.FileUtil.toFileForRead; +import static org.labkey.api.util.FileUtil.toFileForWrite; + +public abstract class AbstractQueryUpdateService implements QueryUpdateService +{ + protected final TableInfo _queryTable; + + private boolean _bulkLoad = false; + private CaseInsensitiveHashMap _columnImportMap = null; + private VirtualFile _att = null; + + /* AbstractQueryUpdateService is generally responsible for some shared functionality + * - triggers + * - coercion/validation + * - detailed logging + * - attachments + * + * If a subclass wants to disable some of these features (w/o subclassing), put flags here... + */ + protected boolean _enableExistingRecordsDataIterator = true; + protected Set _previouslyUpdatedRows = new HashSet<>(); + + protected AbstractQueryUpdateService(TableInfo queryTable) + { + if (queryTable == null) + throw new IllegalArgumentException(); + _queryTable = queryTable; + } + + protected TableInfo getQueryTable() + { + return _queryTable; + } + + public @NotNull Set getPreviouslyUpdatedRows() + { + return _previouslyUpdatedRows == null ? new HashSet<>() : _previouslyUpdatedRows; + } + + @Override + public boolean hasPermission(@NotNull UserPrincipal user, @NotNull Class acl) + { + return getQueryTable().hasPermission(user, acl); + } + + protected Map getRow(User user, Container container, Map keys, boolean allowCrossContainer) + throws InvalidKeyException, QueryUpdateServiceException, SQLException + { + return getRow(user, container, keys); + } + + protected abstract Map getRow(User user, Container container, Map keys) + throws InvalidKeyException, QueryUpdateServiceException, SQLException; + + @Override + public List> getRows(User user, Container container, List> keys) + throws InvalidKeyException, QueryUpdateServiceException, SQLException + { + if (!hasPermission(user, ReadPermission.class)) + throw new UnauthorizedException("You do not have permission to read data from this table."); + + List> result = new ArrayList<>(); + for (Map rowKeys : keys) + { + Map row = getRow(user, container, rowKeys); + if (row != null) + result.add(row); + } + return result; + } + + @Override + public Map> getExistingRows(User user, Container container, Map> keys, boolean verifyNoCrossFolderData, boolean verifyExisting, @Nullable Set columns) + throws InvalidKeyException, QueryUpdateServiceException, SQLException + { + if (!hasPermission(user, ReadPermission.class)) + throw new UnauthorizedException("You do not have permission to read data from this table."); + + Map> result = new LinkedHashMap<>(); + for (Map.Entry> key : keys.entrySet()) + { + Map row = getRow(user, container, key.getValue(), verifyNoCrossFolderData); + if (row != null && !row.isEmpty()) + { + result.put(key.getKey(), row); + if (verifyNoCrossFolderData) + { + String dataContainer = (String) row.get("container"); + if (StringUtils.isEmpty(dataContainer)) + dataContainer = (String) row.get("folder"); + if (!container.getId().equals(dataContainer)) + throw new InvalidKeyException("Data does not belong to folder '" + container.getName() + "': " + key.getValue().values()); + } + } + else if (verifyExisting) + throw new InvalidKeyException("Data not found for " + key.getValue().values()); + } + return result; + } + + @Override + public boolean hasExistingRowsInOtherContainers(Container container, Map> keys) + { + return false; + } + + public static TransactionAuditProvider.TransactionAuditEvent createTransactionAuditEvent(Container container, QueryService.AuditAction auditAction) + { + return createTransactionAuditEvent(container, auditAction, null); + } + + public static TransactionAuditProvider.TransactionAuditEvent createTransactionAuditEvent(Container container, QueryService.AuditAction auditAction, @Nullable Map details) + { + long auditId = DbSequenceManager.get(ContainerManager.getRoot(), DB_SEQUENCE_NAME).next(); + TransactionAuditProvider.TransactionAuditEvent event = new TransactionAuditProvider.TransactionAuditEvent(container, auditAction, auditId); + if (details != null) + event.addDetails(details); + return event; + } + + public static void addTransactionAuditEvent(DbScope.Transaction transaction, User user, TransactionAuditProvider.TransactionAuditEvent auditEvent) + { + UserSchema schema = AuditLogService.getAuditLogSchema(user, ContainerManager.getRoot()); + + if (schema != null) + { + // This is a little hack to ensure that the audit table has actually been created and gets put into the table cache by the time the + // pre-commit task is executed. Otherwise, since the creation of the table happens while within the commit for the + // outermost transaction, it looks like there is a close that hasn't happened when trying to commit the transaction for creating the + // table. + schema.getTable(auditEvent.getEventType(), false); + + transaction.addCommitTask(() -> AuditLogService.get().addEvent(user, auditEvent), DbScope.CommitTaskOption.PRECOMMIT); + + transaction.setAuditEvent(auditEvent); + } + } + + protected final DataIteratorContext getDataIteratorContext(BatchValidationException errors, InsertOption forImport, Map configParameters) + { + if (null == errors) + errors = new BatchValidationException(); + DataIteratorContext context = new DataIteratorContext(errors); + context.setInsertOption(forImport); + context.setConfigParameters(configParameters); + configureDataIteratorContext(context); + recordDataIteratorUsed(configParameters); + + return context; + } + + protected void recordDataIteratorUsed(@Nullable Map configParameters) + { + if (configParameters == null) + return; + + try + { + configParameters.put(TransactionAuditProvider.TransactionDetail.DataIteratorUsed, true); + } + catch (UnsupportedOperationException ignore) + { + // configParameters is immutable, likely originated from a junit test + } + } + + /** + * If QUS wants to use something other than PKs to select existing rows for merge, it can override this method. + * Used only for generating ExistingRecordDataIterator at the moment. + */ + protected Set getSelectKeys(DataIteratorContext context) + { + if (!context.getAlternateKeys().isEmpty()) + return context.getAlternateKeys(); + return null; + } + + /* + * construct the core DataIterator transformation pipeline for this table, may be just StandardDataIteratorBuilder. + * does NOT handle triggers or the insert/update iterator. + */ + public DataIteratorBuilder createImportDIB(User user, Container container, DataIteratorBuilder data, DataIteratorContext context) + { + DataIteratorBuilder dib = StandardDataIteratorBuilder.forInsert(getQueryTable(), data, container, user); + + if (_enableExistingRecordsDataIterator || context.getInsertOption().updateOnly) + { + // some tables need to generate PKs, so they need to add ExistingRecordDataIterator in persistRows() (after generating PK, before inserting) + dib = ExistingRecordDataIterator.createBuilder(dib, getQueryTable(), getSelectKeys(context)); + } + + dib = ((UpdateableTableInfo) getQueryTable()).persistRows(dib, context); + dib = AttachmentDataIterator.getAttachmentDataIteratorBuilder(getQueryTable(), dib, user, context.getInsertOption().batch ? getAttachmentDirectory() : null, container, getAttachmentParentFactory()); + dib = DetailedAuditLogDataIterator.getDataIteratorBuilder(getQueryTable(), dib, context.getInsertOption(), user, container, null); + return dib; + } + + + /** + * Implementation to use insertRows() while we migrate to using DIB for all code paths + *

+ * DataIterator should/must use the same error collection as passed in + */ + @Deprecated + protected int _importRowsUsingInsertRows(User user, Container container, DataIterator rows, BatchValidationException errors, Map extraScriptContext) + { + MapDataIterator mapIterator = DataIteratorUtil.wrapMap(rows, true); + List> list = new ArrayList<>(); + List> ret; + Exception rowException; + + try + { + while (mapIterator.next()) + list.add(mapIterator.getMap()); + ret = insertRows(user, container, list, errors, null, extraScriptContext); + if (errors.hasErrors()) + return 0; + return ret.size(); + } + catch (BatchValidationException x) + { + assert x == errors; + assert x.hasErrors(); + return 0; + } + catch (QueryUpdateServiceException | DuplicateKeyException | SQLException x) + { + rowException = x; + } + finally + { + DataIteratorUtil.closeQuietly(mapIterator); + } + errors.addRowError(new ValidationException(rowException.getMessage())); + return 0; + } + + protected boolean hasImportRowsPermission(User user, Container container, DataIteratorContext context) + { + return hasPermission(user, context.getInsertOption().updateOnly ? UpdatePermission.class : InsertPermission.class); + } + + protected boolean hasInsertRowsPermission(User user) + { + return hasPermission(user, InsertPermission.class); + } + + protected boolean hasDeleteRowsPermission(User user) + { + return hasPermission(user, DeletePermission.class); + } + + protected boolean hasUpdateRowsPermission(User user) + { + return hasPermission(user, UpdatePermission.class); + } + + // override this + protected void preImportDIBValidation(@Nullable DataIteratorBuilder in, @Nullable Collection inputColumns) + { + } + + protected int _importRowsUsingDIB(User user, Container container, DataIteratorBuilder in, @Nullable final ArrayList> outputRows, DataIteratorContext context, @Nullable Map extraScriptContext) + { + if (!hasImportRowsPermission(user, container, context)) + throw new UnauthorizedException("You do not have permission to " + (context.getInsertOption().updateOnly ? "update data in this table." : "insert data into this table.")); + + if (!context.getConfigParameterBoolean(ConfigParameters.SkipInsertOptionValidation)) + assert(getQueryTable().supportsInsertOption(context.getInsertOption())); + + context.getErrors().setExtraContext(extraScriptContext); + if (extraScriptContext != null) + { + context.setDataSource((String) extraScriptContext.get(DataIteratorUtil.DATA_SOURCE)); + if (extraScriptContext.containsKey(AbstractQueryImportAction.Params.useTransactionAuditCache.name())) + { + boolean useTransactionAuditCache = Boolean.TRUE.equals(extraScriptContext.get(AbstractQueryImportAction.Params.useTransactionAuditCache.name())); + context.setUseTransactionAuditCache(useTransactionAuditCache); + } + } + + preImportDIBValidation(in, null); + + boolean skipTriggers = context.getConfigParameterBoolean(ConfigParameters.SkipTriggers) || context.isCrossTypeImport(); + boolean hasTableScript = hasTableScript(container); + TriggerDataBuilderHelper helper = new TriggerDataBuilderHelper(getQueryTable(), container, user, extraScriptContext, context.getInsertOption().useImportAliases); + if (!skipTriggers) + { + in = preTriggerDataIterator(in, context); + if (hasTableScript) + in = helper.before(in); + } + DataIteratorBuilder importDIB = createImportDIB(user, container, in, context); + DataIteratorBuilder out = importDIB; + + if (!skipTriggers) + { + if (hasTableScript) + out = helper.after(importDIB); + + out = postTriggerDataIterator(out, context); + } + + if (hasTableScript) + { + context.setFailFast(false); + context.setMaxRowErrors(Math.max(context.getMaxRowErrors(),1000)); + } + int count = _pump(out, outputRows, context); + + if (context.getErrors().hasErrors()) + return 0; + + if (!context.getConfigParameterBoolean(ConfigParameters.SkipAuditSummary)) + _addSummaryAuditEvent(container, user, context, count); + + return count; + } + + protected DataIteratorBuilder preTriggerDataIterator(DataIteratorBuilder in, DataIteratorContext context) + { + return in; + } + + protected DataIteratorBuilder postTriggerDataIterator(DataIteratorBuilder out, DataIteratorContext context) + { + return out; + } + + /** this is extracted so subclasses can add wrap */ + protected int _pump(DataIteratorBuilder etl, final @Nullable ArrayList> rows, DataIteratorContext context) + { + DataIterator it = etl.getDataIterator(context); + + if (null == it) + return 0; + + try + { + if (null != rows) + { + MapDataIterator maps = DataIteratorUtil.wrapMap(it, false); + it = new WrapperDataIterator(maps) + { + @Override + public boolean next() throws BatchValidationException + { + boolean ret = super.next(); + if (ret) + rows.add(((MapDataIterator)_delegate).getMapExcludeExistingRecord()); + return ret; + } + }; + } + + Pump pump = new Pump(it, context); + pump.run(); + + return pump.getRowCount(); + } + finally + { + DataIteratorUtil.closeQuietly(it); + } + } + + /* can be used for simple bookkeeping tasks, per row processing belongs in a data iterator */ + protected void afterInsertUpdate(int count, BatchValidationException errors, boolean isUpdate) + { + afterInsertUpdate(count, errors); + } + + protected void afterInsertUpdate(int count, BatchValidationException errors) + {} + + @Override + public int loadRows(User user, Container container, DataIteratorBuilder rows, DataIteratorContext context, @Nullable Map extraScriptContext) + { + return loadRows(user, container, rows, null, context, extraScriptContext); + } + + public int loadRows(User user, Container container, DataIteratorBuilder rows, @Nullable final ArrayList> outputRows, DataIteratorContext context, @Nullable Map extraScriptContext) + { + configureDataIteratorContext(context); + int count = _importRowsUsingDIB(user, container, rows, outputRows, context, extraScriptContext); + afterInsertUpdate(count, context.getErrors(), context.getInsertOption().updateOnly); + return count; + } + + @Override + public int importRows(User user, Container container, DataIteratorBuilder rows, BatchValidationException errors, Map configParameters, @Nullable Map extraScriptContext) + { + DataIteratorContext context = getDataIteratorContext(errors, InsertOption.IMPORT, configParameters); + int count = _importRowsUsingInsertRows(user, container, rows.getDataIterator(context), errors, extraScriptContext); + afterInsertUpdate(count, errors, context.getInsertOption().updateOnly); + return count; + } + + @Override + public int mergeRows(User user, Container container, DataIteratorBuilder rows, BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) + { + throw new UnsupportedOperationException("merge is not supported for all tables"); + } + + private boolean hasTableScript(Container container) + { + return getQueryTable().hasTriggers(container); + } + + + protected Map insertRow(User user, Container container, Map row) + throws DuplicateKeyException, ValidationException, QueryUpdateServiceException, SQLException + { + throw new UnsupportedOperationException("Not implemented by this QueryUpdateService"); + } + + + protected @Nullable List> _insertRowsUsingDIB(User user, Container container, List> rows, + DataIteratorContext context, @Nullable Map extraScriptContext) + { + if (!hasInsertRowsPermission(user)) + throw new UnauthorizedException("You do not have permission to insert data into this table."); + + return _insertUpdateRowsUsingDIB(user, container, rows, context, extraScriptContext); + } + + protected @Nullable List> _insertUpdateRowsUsingDIB(User user, Container container, List> rows, + DataIteratorContext context, @Nullable Map extraScriptContext) + { + DataIteratorBuilder dib = _toDataIteratorBuilder(getClass().getSimpleName() + (context.getInsertOption().updateOnly ? ".updateRows" : ".insertRows()"), rows); + ArrayList> outputRows = new ArrayList<>(); + int count = _importRowsUsingDIB(user, container, dib, outputRows, context, extraScriptContext); + afterInsertUpdate(count, context.getErrors(), context.getInsertOption().updateOnly); + + if (context.getErrors().hasErrors()) + return null; + + return outputRows; + } + + // not yet supported + protected @Nullable List> _updateRowsUsingDIB(User user, Container container, List> rows, + DataIteratorContext context, @Nullable Map extraScriptContext) + { + if (!hasUpdateRowsPermission(user)) + throw new UnauthorizedException("You do not have permission to update data in this table."); + + return _insertUpdateRowsUsingDIB(user, container, rows, context, extraScriptContext); + } + + + protected DataIteratorBuilder _toDataIteratorBuilder(String debugName, List> rows) + { + // TODO probably can't assume all rows have all columns + // TODO can we assume that all rows refer to columns consistently? (not PTID and MouseId for the same column) + // TODO optimize ArrayListMap? + Set colNames; + + if (!rows.isEmpty() && rows.getFirst() instanceof ArrayListMap) + { + colNames = ((ArrayListMap)rows.getFirst()).getFindMap().keySet(); + } + else + { + // Preserve casing by using wrapped CaseInsensitiveHashMap instead of CaseInsensitiveHashSet + colNames = Sets.newCaseInsensitiveHashSet(); + for (Map row : rows) + colNames.addAll(row.keySet()); + } + + preImportDIBValidation(null, colNames); + return MapDataIterator.of(colNames, rows, debugName); + } + + + /** @deprecated switch to using DIB based method */ + @Deprecated + protected List> _insertRowsUsingInsertRow(User user, Container container, List> rows, BatchValidationException errors, Map extraScriptContext) + throws DuplicateKeyException, BatchValidationException, QueryUpdateServiceException, SQLException + { + if (!hasInsertRowsPermission(user)) + throw new UnauthorizedException("You do not have permission to insert data into this table."); + + assert(getQueryTable().supportsInsertOption(InsertOption.INSERT)); + + boolean hasTableScript = hasTableScript(container); + + errors.setExtraContext(extraScriptContext); + if (hasTableScript) + getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.INSERT, null, true, errors, extraScriptContext); + + List> result = new ArrayList<>(rows.size()); + List> providedValues = new ArrayList<>(rows.size()); + for (int i = 0; i < rows.size(); i++) + { + Map row = rows.get(i); + row = normalizeColumnNames(row); + try + { + providedValues.add(new CaseInsensitiveHashMap<>()); + row = coerceTypes(row, providedValues.get(i), false); + if (hasTableScript) + { + getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.INSERT, true, i, row, null, extraScriptContext); + } + row = insertRow(user, container, row); + if (row == null) + continue; + + if (hasTableScript) + getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.INSERT, false, i, row, null, extraScriptContext); + result.add(row); + } + catch (SQLException sqlx) + { + if (StringUtils.startsWith(sqlx.getSQLState(), "22") || RuntimeSQLException.isConstraintException(sqlx)) + { + ValidationException vex = new ValidationException(sqlx.getMessage()); + vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), row, i+1); + errors.addRowError(vex); + } + else if (SqlDialect.isTransactionException(sqlx) && errors.hasErrors()) + { + // if we already have some errors, just break + break; + } + else + { + throw sqlx; + } + } + catch (ValidationException vex) + { + errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), row, i)); + } + catch (RuntimeValidationException rvex) + { + ValidationException vex = rvex.getValidationException(); + errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), row, i)); + } + } + + if (hasTableScript) + getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.INSERT, null, false, errors, extraScriptContext); + + addAuditEvent(user, container, QueryService.AuditAction.INSERT, null, result, null, providedValues); + + return result; + } + + protected void addAuditEvent(User user, Container container, QueryService.AuditAction auditAction, @Nullable Map configParameters, @Nullable List> rows, @Nullable List> existingRows, @Nullable List> providedValues) + { + if (!isBulkLoad()) + { + AuditBehaviorType auditBehavior = configParameters != null ? (AuditBehaviorType) configParameters.get(AuditBehavior) : null; + String userComment = configParameters == null ? null : (String) configParameters.get(AuditUserComment); + getQueryTable().getAuditHandler(auditBehavior) + .addAuditEvent(user, container, getQueryTable(), auditBehavior, userComment, auditAction, rows, existingRows, providedValues); + } + } + + private Map normalizeColumnNames(Map row) + { + if(_columnImportMap == null) + { + _columnImportMap = (CaseInsensitiveHashMap)ImportAliasable.Helper.createImportMap(getQueryTable().getColumns(), false); + } + + Map newRow = new CaseInsensitiveHashMap<>(); + CaseInsensitiveHashSet columns = new CaseInsensitiveHashSet(); + columns.addAll(row.keySet()); + + String newName; + for(String key : row.keySet()) + { + if(_columnImportMap.containsKey(key)) + { + //it is possible for a normalized name to conflict with an existing property. if so, defer to the original + newName = _columnImportMap.get(key).getName(); + if(!columns.contains(newName)){ + newRow.put(newName, row.get(key)); + continue; + } + } + newRow.put(key, row.get(key)); + } + + return newRow; + } + + @Override + public List> insertRows(User user, Container container, List> rows, BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) + throws DuplicateKeyException, QueryUpdateServiceException, SQLException + { + try + { + List> ret = _insertRowsUsingInsertRow(user, container, rows, errors, extraScriptContext); + afterInsertUpdate(null==ret?0:ret.size(), errors); + if (errors.hasErrors()) + return null; + return ret; + } + catch (BatchValidationException x) + { + assert x == errors; + assert x.hasErrors(); + } + return null; + } + + protected Object coerceTypesValue(ColumnInfo col, Map providedValues, String key, Object value) + { + if (col != null && value != null && + !col.getJavaObjectClass().isInstance(value) && + !(value instanceof AttachmentFile) && + !(value instanceof MultipartFile) && + !(value instanceof String[]) && + !(col.isMultiValued() || col.getFk() instanceof MultiValuedForeignKey)) + { + try + { + if (col.getKindOfQuantity() != null) + providedValues.put(key, value); + if (PropertyType.FILE_LINK.equals(col.getPropertyType())) + value = ExpDataFileConverter.convert(value); + else + value = col.convert(value); + } + catch (ConvertHelper.FileConversionException e) + { + throw e; + } + catch (ConversionException e) + { + // That's OK, the transformation script may be able to fix up the value before it gets inserted + } + } + + return value; + } + + /** Attempt to make the passed in types match the expected types so the script doesn't have to do the conversion */ + @Deprecated + protected Map coerceTypes(Map row, Map providedValues, boolean isUpdate) + { + Map result = new CaseInsensitiveHashMap<>(row.size()); + Map columnMap = ImportAliasable.Helper.createImportMap(_queryTable.getColumns(), true); + for (Map.Entry entry : row.entrySet()) + { + ColumnInfo col = columnMap.get(entry.getKey()); + Object value = coerceTypesValue(col, providedValues, entry.getKey(), entry.getValue()); + result.put(entry.getKey(), value); + } + + return result; + } + + protected abstract Map updateRow(User user, Container container, Map row, @NotNull Map oldRow, @Nullable Map configParameters) + throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException; + + + protected boolean firstUpdateRow = true; + Function,Map> updateTransform = Function.identity(); + + /* Do standard AQUS stuff here, then call the subclass specific implementation of updateRow() */ + final protected Map updateOneRow(User user, Container container, Map row, @NotNull Map oldRow, @Nullable Map configParameters) + throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException + { + if (firstUpdateRow) + { + firstUpdateRow = false; + if (null != OntologyService.get()) + { + var t = OntologyService.get().getConceptUpdateHandler(_queryTable); + if (null != t) + updateTransform = t; + } + } + row = updateTransform.apply(row); + return updateRow(user, container, row, oldRow, configParameters); + } + + // used by updateRows to check if all rows have the same set of keys + // prepared statement can only be used to updateRows if all rows have the same set of keys + protected static boolean hasUniformKeys(List> rowsToUpdate) + { + if (rowsToUpdate == null || rowsToUpdate.isEmpty()) + return false; + + if (rowsToUpdate.size() == 1) + return true; + + Set keys = rowsToUpdate.getFirst().keySet(); + int keySize = keys.size(); + + for (int i = 1 ; i < rowsToUpdate.size(); i ++) + { + Set otherKeys = rowsToUpdate.get(i).keySet(); + if (otherKeys.size() != keySize) + return false; + if (!otherKeys.containsAll(keys)) + return false; + } + + return true; + } + + @Override + public List> updateRows(User user, Container container, List> rows, List> oldKeys, + BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) + throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException + { + if (!hasUpdateRowsPermission(user)) + throw new UnauthorizedException("You do not have permission to update data in this table."); + + if (oldKeys != null && rows.size() != oldKeys.size()) + throw new IllegalArgumentException("rows and oldKeys are required to be the same length, but were " + rows.size() + " and " + oldKeys + " in length, respectively"); + + assert(getQueryTable().supportsInsertOption(InsertOption.UPDATE)); + + errors.setExtraContext(extraScriptContext); + getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.UPDATE, null, true, errors, extraScriptContext); + + List> result = new ArrayList<>(rows.size()); + List> oldRows = new ArrayList<>(rows.size()); + List> providedValues = new ArrayList<>(rows.size()); + // TODO: Support update/delete without selecting the existing row -- unfortunately, we currently get the existing row to check its container matches the incoming container + boolean streaming = false; //_queryTable.canStreamTriggers(container) && _queryTable.getAuditBehavior() != AuditBehaviorType.NONE; + + for (int i = 0; i < rows.size(); i++) + { + Map row = rows.get(i); + providedValues.add(new CaseInsensitiveHashMap<>()); + row = coerceTypes(row, providedValues.get(i), true); + try + { + Map oldKey = oldKeys == null ? row : oldKeys.get(i); + Map oldRow = null; + if (!streaming) + { + oldRow = getRow(user, container, oldKey); + if (oldRow == null) + throw new NotFoundException("The existing row was not found."); + } + + getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.UPDATE, true, i, row, oldRow, extraScriptContext); + Map updatedRow = updateOneRow(user, container, row, oldRow, configParameters); + if (!streaming && updatedRow == null) + continue; + + getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.UPDATE, false, i, updatedRow, oldRow, extraScriptContext); + if (!streaming) + { + result.add(updatedRow); + oldRows.add(oldRow); + } + } + catch (ValidationException vex) + { + errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), row, i)); + } + catch (RuntimeValidationException rvex) + { + ValidationException vex = rvex.getValidationException(); + errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), row, i)); + } + catch (OptimisticConflictException e) + { + errors.addRowError(new ValidationException("Unable to update. Row may have been deleted.")); + } + } + + // Fire triggers, if any, and also throw if there are any errors + getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.UPDATE, null, false, errors, extraScriptContext); + afterInsertUpdate(null==result?0:result.size(), errors, true); + + if (errors.hasErrors()) + throw errors; + + addAuditEvent(user, container, QueryService.AuditAction.UPDATE, configParameters, result, oldRows, providedValues); + WorkflowService service = WorkflowService.get(); + if (service != null && configParameters != null && configParameters.containsKey(WorkflowService.WorkflowConfigs.ActionId)) + service.onActionComplete(container, user, (Long) configParameters.get(WorkflowService.WorkflowConfigs.ActionId)); + + return result; + } + + /** + * Hook called by {@link #updateRowsUsingPartitionedDIB} once per partition before that partition is processed. + * A partition is a maximal consecutive run of rows that all share the same set of column names (keys). + *

+ * Subclasses can override to enforce invariants on a particular key-column combination — for example, + * requiring that certain columns always appear together. Throw a runtime exception to reject the partition. + * + * @param columns the set of column names present in the rows of the current partition + */ + protected void validatePartitionedRowKeys(Collection columns) + { + // do nothing + } + + /** + * Updates a list of rows through the DataIteratorBuilder pipeline, automatically partitioning the input + * into consecutive groups that share the same set of column names (keys). Each partition is processed + * independently through {@link #_updateRowsUsingDIB}, allowing a single call to handle heterogeneous + * row maps (e.g. some rows keyed by RowId, others by Name) without requiring the caller to pre-split them. + *

+ * Audit summary logging is suppressed for individual partitions and emitted once at the end. + * Cross-partition duplicate keys are detected and reported as errors. + * If any partition produces errors, a {@link DbScope.RetryPassthroughException} is thrown so that + * an enclosing {@code executeWithRetry()} block will not attempt to commit the transaction. + */ + protected List> updateRowsUsingPartitionedDIB( + DbScope.Transaction tx, + User user, + Container container, + List> rows, + BatchValidationException errors, + @Nullable Map configParameters, + Map extraScriptContext + ) + { + int index = 0; + int numPartitions = 0; + List> ret = new ArrayList<>(); + + Set observedRowIds = new HashSet<>(); + Set observedNames = new CaseInsensitiveHashSet(); + + while (index < rows.size()) + { + CaseInsensitiveHashSet rowKeys = new CaseInsensitiveHashSet(rows.get(index).keySet()); + + validatePartitionedRowKeys(rowKeys); + + int nextIndex = index + 1; + while (nextIndex < rows.size() && rowKeys.equals(new CaseInsensitiveHashSet(rows.get(nextIndex).keySet()))) + nextIndex++; + + List> rowsToProcess = rows.subList(index, nextIndex); + index = nextIndex; + numPartitions++; + + DataIteratorContext context = getDataIteratorContext(errors, InsertOption.UPDATE, configParameters); + + // skip audit summary for the partitions, we will perform it once at the end + context.putConfigParameter(ConfigParameters.SkipAuditSummary, true); + + List> subRet = _updateRowsUsingDIB(user, container, rowsToProcess, context, extraScriptContext); + + // we need to throw if we don't want executeWithRetry() attempt commit() + if (context.getErrors().hasErrors()) + throw new DbScope.RetryPassthroughException(context.getErrors()); + + if (subRet != null) + { + ret.addAll(subRet); + + // Check if duplicate rows have been processed across the partitions + // Only start checking for duplicates after the first partition has been processed. + if (numPartitions > 1) + { + // If we are on the second partition, then lazily check all previous rows, otherwise check only the current partition + checkPartitionForDuplicates(numPartitions == 2 ? ret : subRet, observedRowIds, observedNames, errors); + } + + if (errors.hasErrors()) + throw new DbScope.RetryPassthroughException(errors); + } + } + + if (numPartitions > 1) + { + var auditEvent = tx.getAuditEvent(); + if (auditEvent != null) + auditEvent.addDetail(TransactionAuditProvider.TransactionDetail.DataIteratorPartitions, numPartitions); + } + + _addSummaryAuditEvent(container, user, getDataIteratorContext(errors, InsertOption.UPDATE, configParameters), ret.size()); + + return ret; + } + + /** + * Identifies the column names used to detect duplicate keys across partitions in + * {@link #updateRowsUsingPartitionedDIB}. Return {@code null} for either field to skip that check. + * The base implementation returns {@code (null, null)}, disabling duplicate detection. + * Subclasses whose tables support partitioned updates should override with the appropriate column names. + */ + public record PartitionKeyColumns(@Nullable String numericKey, @Nullable String stringKey) {} + + protected PartitionKeyColumns getPartitionKeyColumns() + { + return new PartitionKeyColumns(null, null); + } + + private void checkPartitionForDuplicates(List> partitionRows, Set globalRowIds, Set globalNames, BatchValidationException errors) + { + PartitionKeyColumns keys = getPartitionKeyColumns(); + for (Map row : partitionRows) + { + if (keys.numericKey() != null) + { + Long rowId = MapUtils.getLong(row, keys.numericKey()); + if (rowId != null && !globalRowIds.add(rowId)) + { + errors.addRowError(new ValidationException("Duplicate key provided: " + rowId)); + return; + } + } + + if (keys.stringKey() != null) + { + Object nameObj = row.get(keys.stringKey()); + if (nameObj != null && !globalNames.add(nameObj.toString())) + { + errors.addRowError(new ValidationException("Duplicate key provided: " + nameObj)); + return; + } + } + } + } + + protected void checkDuplicateUpdate(Object pkVals) throws ValidationException + { + if (pkVals == null) + return; + + Set updatedRows = getPreviouslyUpdatedRows(); + + Object[] keysObj; + if (pkVals.getClass().isArray()) + keysObj = (Object[]) pkVals; + else if (pkVals instanceof Map map) + { + List orderedKeyVals = new ArrayList<>(); + SortedSet sortedKeys = new TreeSet<>(map.keySet()); + for (String key : sortedKeys) + orderedKeyVals.add(map.get(key)); + keysObj = orderedKeyVals.toArray(); + } + else + keysObj = new Object[]{pkVals}; + + if (keysObj.length == 1) + { + if (updatedRows.contains(keysObj[0])) + throw new ValidationException("Duplicate key provided: " + keysObj[0]); + updatedRows.add(keysObj[0]); + return; + } + + List keys = new ArrayList<>(); + for (Object key : keysObj) + keys.add(String.valueOf(key)); + if (updatedRows.contains(keys)) + throw new ValidationException("Duplicate key provided: " + StringUtils.join(keys, ", ")); + updatedRows.add(keys); + } + + @Override + public Map moveRows(User user, Container container, Container targetContainer, List> rows, BatchValidationException errors, @Nullable Map configParameters, @Nullable Map extraScriptContext) throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException + { + throw new UnsupportedOperationException("Move is not supported for this table type."); + } + + protected abstract Map deleteRow(User user, Container container, Map oldRow) + throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException; + + protected Map deleteRow(User user, Container container, Map oldRow, @Nullable Map configParameters, @Nullable Map extraScriptContext) + throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException + { + return deleteRow(user, container, oldRow); + } + + @Override + public List> deleteRows(User user, Container container, List> keys, @Nullable Map configParameters, @Nullable Map extraScriptContext) + throws InvalidKeyException, BatchValidationException, QueryUpdateServiceException, SQLException + { + if (!hasDeleteRowsPermission(user)) + throw new UnauthorizedException("You do not have permission to delete data from this table."); + + BatchValidationException errors = new BatchValidationException(); + errors.setExtraContext(extraScriptContext); + getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.DELETE, null, true, errors, extraScriptContext); + + // TODO: Support update/delete without selecting the existing row -- unfortunately, we currently get the existing row to check its container matches the incoming container + boolean streaming = false; //_queryTable.canStreamTriggers(container) && _queryTable.getAuditBehavior() != AuditBehaviorType.NONE; + + List> result = new ArrayList<>(keys.size()); + for (int i = 0; i < keys.size(); i++) + { + Map key = keys.get(i); + try + { + Map oldRow = null; + if (!streaming) + { + oldRow = getRow(user, container, key); + // if row doesn't exist, bail early + if (oldRow == null) + continue; + } + + getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.DELETE, true, i, null, oldRow, extraScriptContext); + Map updatedRow = deleteRow(user, container, oldRow, configParameters, extraScriptContext); + if (!streaming && updatedRow == null) + continue; + + getQueryTable().fireRowTrigger(container, user, TableInfo.TriggerType.DELETE, false, i, null, updatedRow, extraScriptContext); + result.add(updatedRow); + } + catch (InvalidKeyException ex) + { + ValidationException vex = new ValidationException(ex.getMessage()); + errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), key, i)); + } + catch (ValidationException vex) + { + errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), key, i)); + } + catch (RuntimeValidationException rvex) + { + ValidationException vex = rvex.getValidationException(); + errors.addRowError(vex.fillIn(getQueryTable().getPublicSchemaName(), getQueryTable().getName(), key, i)); + } + } + + // Fire triggers, if any, and also throw if there are any errors + getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.DELETE, null, false, errors, extraScriptContext); + + addAuditEvent(user, container, QueryService.AuditAction.DELETE, configParameters, result, null, null); + + return result; + } + + protected int truncateRows(User user, Container container) + throws QueryUpdateServiceException, SQLException + { + throw new UnsupportedOperationException(); + } + + @Override + public int truncateRows(User user, Container container, @Nullable Map configParameters, @Nullable Map extraScriptContext) + throws BatchValidationException, QueryUpdateServiceException, SQLException + { + if (!container.hasPermission(user, AdminPermission.class) && !hasDeleteRowsPermission(user)) + throw new UnauthorizedException("You do not have permission to truncate this table."); + + BatchValidationException errors = new BatchValidationException(); + errors.setExtraContext(extraScriptContext); + getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.TRUNCATE, null, true, errors, extraScriptContext); + + int result = truncateRows(user, container); + + getQueryTable().fireBatchTrigger(container, user, TableInfo.TriggerType.TRUNCATE, null, false, errors, extraScriptContext); + addAuditEvent(user, container, QueryService.AuditAction.TRUNCATE, configParameters, null, null, null); + + return result; + } + + @Override + public void setBulkLoad(boolean bulkLoad) + { + _bulkLoad = bulkLoad; + } + + @Override + public boolean isBulkLoad() + { + return _bulkLoad; + } + + public static Object saveFile(User user, Container container, String name, Object value, @Nullable String dirName) throws ValidationException, QueryUpdateServiceException + { + FileLike dirPath = AssayFileWriter.getUploadDirectoryPath(container, dirName); + return saveFile(user, container, name, value, dirPath); + } + + /** + * Save uploaded file to dirName directory under file or pipeline root. + */ + public static Object saveFile(User user, Container container, String name, Object value, @Nullable FileLike dirPath) throws ValidationException, QueryUpdateServiceException + { + if (!(value instanceof MultipartFile) && !(value instanceof SpringAttachmentFile)) + throw new ValidationException("Invalid file value"); + + String auditMessageFormat = "Saved file '%s' for field '%s' in folder %s."; + FileLike file = null; + try + { + FileLike dir = AssayFileWriter.ensureUploadDirectory(dirPath); + + FileSystemAuditProvider.FileSystemAuditEvent event = new FileSystemAuditProvider.FileSystemAuditEvent(container, null); + if (value instanceof MultipartFile multipartFile) + { + // Once we've found one, write it to disk and replace the row's value with just the File reference to it + if (multipartFile.isEmpty()) + { + throw new ValidationException("File " + multipartFile.getOriginalFilename() + " for field " + name + " has no content"); + } + file = FileUtil.findUniqueFileName(multipartFile.getOriginalFilename(), dir); + checkFileUnderRoot(container, file); + multipartFile.transferTo(toFileForWrite(file)); + event.setComment(String.format(auditMessageFormat, multipartFile.getOriginalFilename(), name, container.getPath())); + event.setProvidedFileName(multipartFile.getOriginalFilename()); + } + else + { + SpringAttachmentFile saf = (SpringAttachmentFile) value; + file = FileUtil.findUniqueFileName(saf.getFilename(), dir); + checkFileUnderRoot(container, file); + saf.saveTo(file); + event.setComment(String.format(auditMessageFormat, saf.getFilename(), name, container.getPath())); + event.setProvidedFileName(saf.getFilename()); + } + event.setFile(file.getName()); + event.setFieldName(name); + event.setDirectory(file.getParent().toURI().getPath()); + AuditLogService.get().addEvent(user, event); + } + catch (IOException | ExperimentException e) + { + throw new QueryUpdateServiceException(e); + } + + ensureExpData(user, container, file.toNioPathForRead().toFile()); + return file; + } + + public static ExpData ensureExpData(User user, Container container, File file) + { + ExpData existingData = ExperimentService.get().getExpDataByURL(file, container); + // create exp.data record + if (existingData == null) + { + File canonicalFile = FileUtil.getAbsoluteCaseSensitiveFile(file); + ExpData data = ExperimentService.get().createData(container, UPLOADED_FILE); + data.setName(file.getName()); + data.setDataFileURI(canonicalFile.toPath().toUri()); + if (data.getDataFileUrl() != null && data.getDataFileUrl().length() <= ExperimentService.get().getTinfoData().getColumn("DataFileURL").getScale()) + { + // If the path is too long to store, bail out without creating an exp.data row + data.save(user); + } + + return data; + } + + return existingData; + } + + // For security reasons, make sure the user hasn't tried to reference a file that's not under + // the pipeline root or @assayfiles root. Otherwise, they could get access to any file on the server + static FileLike checkFileUnderRoot(Container container, FileLike file) throws ExperimentException + { + Path assayFilesRoot = FileContentService.get().getFileRootPath(container, FileContentService.ContentType.assayfiles); + if (assayFilesRoot != null && URIUtil.isDescendant(assayFilesRoot.toUri(), file.toURI())) + return file; + + PipeRoot root = PipelineService.get().findPipelineRoot(container); + if (root == null) + throw new ExperimentException("Pipeline root not available in container " + container.getPath()); + + if (!root.isUnderRoot(toFileForRead(file))) + { + throw new ExperimentException("Cannot reference file '" + file + "' from " + container.getPath()); + } + + return file; + } + + protected void _addSummaryAuditEvent(Container container, User user, DataIteratorContext context, int count) + { + if (!context.isCrossTypeImport()) // audit handled at table level + { + AuditBehaviorType auditType = (AuditBehaviorType) context.getConfigParameter(DetailedAuditLogDataIterator.AuditConfigs.AuditBehavior); + String auditUserComment = (String) context.getConfigParameter(DetailedAuditLogDataIterator.AuditConfigs.AuditUserComment); + boolean skipAuditLevelCheck = false; + if (context.getConfigParameterBoolean(QueryUpdateService.ConfigParameters.BulkLoad)) + { + if (getQueryTable().getEffectiveAuditBehavior(auditType) == AuditBehaviorType.DETAILED) // allow ETL to demote audit level for bulkLoad + skipAuditLevelCheck = true; + } + getQueryTable().getAuditHandler(auditType).addSummaryAuditEvent(user, container, getQueryTable(), context.getInsertOption().auditAction, count, auditType, auditUserComment, skipAuditLevelCheck); + } + } + + /** + * Is used by the AttachmentDataIterator to point to the location of the serialized + * attachment files. + */ + public void setAttachmentDirectory(VirtualFile att) + { + _att = att; + } + + @Nullable + protected VirtualFile getAttachmentDirectory() + { + return _att; + } + + /** + * QUS instances that allow import of attachments through the AttachmentDataIterator should furnish a factory + * implementation in order to resolve the attachment parent on incoming attachment files. + */ + @Nullable + protected AttachmentParentFactory getAttachmentParentFactory() + { + return null; + } + + /** Translate between the column name that query is exposing to the column name that actually lives in the database */ + protected static void aliasColumns(Map columnMapping, Map row) + { + for (Map.Entry entry : columnMapping.entrySet()) + { + if (row.containsKey(entry.getValue()) && !row.containsKey(entry.getKey())) + { + row.put(entry.getKey(), row.get(entry.getValue())); + } + } + } + + /** + * The database table has underscores for MV column names, but we expose a column without the underscore. + * Therefore, we need to translate between the two sets of column names. + * @return database column name -> exposed TableInfo column name + */ + protected static Map createMVMapping(Domain domain) + { + Map result = new CaseInsensitiveHashMap<>(); + if (domain != null) + { + for (DomainProperty domainProperty : domain.getProperties()) + { + if (domainProperty.isMvEnabled()) + { + result.put(PropertyStorageSpec.getMvIndicatorStorageColumnName(domainProperty.getPropertyDescriptor()), domainProperty.getName() + MvColumn.MV_INDICATOR_SUFFIX); + } + } + } + return result; + } + + @TestWhen(TestWhen.When.BVT) + public static class TestCase extends Assert + { + private boolean _useAlias = false; + + static TabLoader getTestData() throws IOException + { + TabLoader testData = new TabLoader(new StringReader("pk,i,s\n0,0,zero\n1,1,one\n2,2,two"),true); + testData.parseAsCSV(); + testData.getColumns()[0].clazz = Integer.class; + testData.getColumns()[1].clazz = Integer.class; + testData.getColumns()[2].clazz = String.class; + return testData; + } + + @BeforeClass + public static void createList() throws Exception + { + if (null == ListService.get()) + return; + deleteList(); + + TabLoader testData = getTestData(); + String hash = GUID.makeHash(); + User user = TestContext.get().getUser(); + Container c = JunitUtil.getTestContainer(); + ListService s = ListService.get(); + UserSchema lists = (UserSchema)DefaultSchema.get(user, c).getSchema("lists"); + assertNotNull(lists); + + ListDefinition R = s.createList(c, "R", ListDefinition.KeyType.Integer); + R.setKeyName("pk"); + Domain d = requireNonNull(R.getDomain()); + for (int i=0 ; i> getRows() + { + User user = TestContext.get().getUser(); + Container c = JunitUtil.getTestContainer(); + UserSchema lists = (UserSchema)DefaultSchema.get(user, c).getSchema("lists"); + TableInfo rTableInfo = requireNonNull(lists.getTable("R", null)); + return Arrays.asList(new TableSelector(rTableInfo, TableSelector.ALL_COLUMNS, null, new Sort("PK")).getMapArray()); + } + + @Before + public void resetList() throws Exception + { + if (null == ListService.get()) + return; + User user = TestContext.get().getUser(); + Container c = JunitUtil.getTestContainer(); + TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); + QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); + qus.truncateRows(user, c, null, null); + } + + @AfterClass + public static void deleteList() throws Exception + { + if (null == ListService.get()) + return; + User user = TestContext.get().getUser(); + Container c = JunitUtil.getTestContainer(); + ListService s = ListService.get(); + Map m = s.getLists(c); + if (m.containsKey("R")) + m.get("R").delete(user); + } + + void validateDefaultData(List> rows) + { + assertEquals(3, rows.size()); + + assertEquals(0, rows.get(0).get("pk")); + assertEquals(1, rows.get(1).get("pk")); + assertEquals(2, rows.get(2).get("pk")); + + assertEquals(0, rows.get(0).get("i")); + assertEquals(1, rows.get(1).get("i")); + assertEquals(2, rows.get(2).get("i")); + + assertEquals("zero", rows.get(0).get("s")); + assertEquals("one", rows.get(1).get("s")); + assertEquals("two", rows.get(2).get("s")); + } + + @Test + public void INSERT() throws Exception + { + if (null == ListService.get()) + return; + User user = TestContext.get().getUser(); + Container c = JunitUtil.getTestContainer(); + TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); + assert(getRows().isEmpty()); + QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); + BatchValidationException errors = new BatchValidationException(); + var rows = qus.insertRows(user, c, getTestData().load(), errors, null, null); + assertFalse(errors.hasErrors()); + validateDefaultData(rows); + validateDefaultData(getRows()); + + qus.insertRows(user, c, getTestData().load(), errors, null, null); + assertTrue(errors.hasErrors()); + } + + @Test + public void UPSERT() throws Exception + { + if (null == ListService.get()) + return; + /* not sure how you use/test ImportOptions.UPSERT + * the only row returning QUS method is insertRows(), which doesn't let you specify the InsertOption? + */ + } + + @Test + public void IMPORT() throws Exception + { + if (null == ListService.get()) + return; + User user = TestContext.get().getUser(); + Container c = JunitUtil.getTestContainer(); + TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); + assert(getRows().isEmpty()); + QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); + BatchValidationException errors = new BatchValidationException(); + var count = qus.importRows(user, c, getTestData(), errors, null, null); + assertFalse(errors.hasErrors()); + assert(count == 3); + validateDefaultData(getRows()); + + qus.importRows(user, c, getTestData(), errors, null, null); + assertTrue(errors.hasErrors()); + } + + @Test + public void MERGE() throws Exception + { + if (null == ListService.get()) + return; + INSERT(); + assertEquals("Wrong number of rows after INSERT", 3, getRows().size()); + + User user = TestContext.get().getUser(); + Container c = JunitUtil.getTestContainer(); + TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); + QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); + var mergeRows = new ArrayList>(); + String colName = _useAlias ? "s_alias" : "s"; + String pkName = _useAlias ? "pk_alias" : "pk"; + mergeRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO")); + mergeRows.add(CaseInsensitiveHashMap.of(pkName,3,colName,"THREE")); + BatchValidationException errors = new BatchValidationException() + { + @Override + public void addRowError(ValidationException vex) + { + LogManager.getLogger(AbstractQueryUpdateService.class).error("test error", vex); + fail(vex.getMessage()); + } + }; + int count=0; + try (var tx = rTableInfo.getSchema().getScope().ensureTransaction()) + { + var ret = qus.mergeRows(user, c, MapDataIterator.of(mergeRows.getFirst().keySet(), mergeRows), errors, null, null); + if (!errors.hasErrors()) + { + tx.commit(); + count = ret; + } + } + assertFalse("mergeRows error(s): " + errors.getMessage(), errors.hasErrors()); + assertEquals(2, count); + var rows = getRows(); + // test existing row value is updated + assertEquals("TWO", rows.get(2).get("s")); + // test existing row value is not updated + assertEquals(2, rows.get(2).get("i")); + // test new row + assertEquals("THREE", rows.get(3).get("s")); + assertNull(rows.get(3).get("i")); + + // merge should fail if duplicate keys are provided + errors = new BatchValidationException(); + mergeRows = new ArrayList<>(); + mergeRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP-2")); + mergeRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP-UP-2")); + qus.mergeRows(user, c, MapDataIterator.of(mergeRows.getFirst().keySet(), mergeRows), errors, null, null); + assertTrue(errors.hasErrors()); + assertTrue("Duplicate key error: " + errors.getMessage(), errors.getMessage().contains("Duplicate key provided: 2")); + } + + @Test + public void UPDATE() throws Exception + { + if (null == ListService.get()) + return; + INSERT(); + assertEquals("Wrong number of rows after INSERT", 3, getRows().size()); + + User user = TestContext.get().getUser(); + Container c = JunitUtil.getTestContainer(); + TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); + QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); + var updateRows = new ArrayList>(); + String colName = _useAlias ? "s_alias" : "s"; + String pkName = _useAlias ? "pk_alias" : "pk"; + + // update using data iterator + updateRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP")); + DataIteratorContext context = new DataIteratorContext(); + context.setInsertOption(InsertOption.UPDATE); + var count = qus.loadRows(user, c, MapDataIterator.of(updateRows.getFirst().keySet(), updateRows), context, null); + assertFalse(context.getErrors().hasErrors()); + assertEquals(1, count); + var rows = getRows(); + // test existing row value is updated + assertEquals("TWO-UP", rows.get(2).get("s")); + // test existing row value is not updated/erased + assertEquals(2, rows.get(2).get("i")); + + // update should fail if a new record is provided + updateRows = new ArrayList<>(); + updateRows.add(CaseInsensitiveHashMap.of(pkName,123,colName,"NEW")); + updateRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP-2")); + qus.loadRows(user, c, MapDataIterator.of(updateRows.getFirst().keySet(), updateRows), context, null); + assertTrue(context.getErrors().hasErrors()); + + // Issue 52728: update should fail if duplicate key is provide + updateRows = new ArrayList<>(); + updateRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP-2")); + updateRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO-UP-UP-2")); + + // use DIB + context = new DataIteratorContext(); + context.setInsertOption(InsertOption.UPDATE); + qus.loadRows(user, c, MapDataIterator.of(updateRows.getFirst().keySet(), updateRows), context, null); + assertTrue(context.getErrors().hasErrors()); + assertTrue("Duplicate key error: " + context.getErrors().getMessage(), context.getErrors().getMessage().contains("Duplicate key provided: 2")); + + // use updateRows + if (!_useAlias) // _update using alias is not supported + { + BatchValidationException errors = new BatchValidationException(); + try + { + qus.updateRows(user, c, updateRows, null, errors, null, null); + } + catch (Exception e) + { + + } + assertTrue(errors.hasErrors()); + assertTrue("Duplicate key error: " + errors.getMessage(), errors.getMessage().contains("Duplicate key provided: 2")); + + } + } + + @Test + public void REPLACE() throws Exception + { + if (null == ListService.get()) + return; + assert(getRows().isEmpty()); + INSERT(); + + User user = TestContext.get().getUser(); + Container c = JunitUtil.getTestContainer(); + TableInfo rTableInfo = ((UserSchema)DefaultSchema.get(user, c).getSchema("lists")).getTable("R", null); + QueryUpdateService qus = requireNonNull(rTableInfo.getUpdateService()); + var mergeRows = new ArrayList>(); + String colName = _useAlias ? "s_alias" : "s"; + String pkName = _useAlias ? "pk_alias" : "pk"; + mergeRows.add(CaseInsensitiveHashMap.of(pkName,2,colName,"TWO")); + mergeRows.add(CaseInsensitiveHashMap.of(pkName,3,colName,"THREE")); + DataIteratorContext context = new DataIteratorContext(); + context.setInsertOption(InsertOption.REPLACE); + var count = qus.loadRows(user, c, MapDataIterator.of(mergeRows.getFirst().keySet(), mergeRows), context, null); + assertFalse(context.getErrors().hasErrors()); + assertEquals(2, count); + var rows = getRows(); + // test existing row value is updated + assertEquals("TWO", rows.get(2).get("s")); + // test existing row value is updated + assertNull(rows.get(2).get("i")); + // test new row + assertEquals("THREE", rows.get(3).get("s")); + assertNull(rows.get(3).get("i")); + } + + @Test + public void IMPORT_IDENTITY() + { + if (null == ListService.get()) + return; + // TODO + } + + @Test + public void ALIAS_MERGE() throws Exception + { + _useAlias = true; + MERGE(); + } + + @Test + public void ALIAS_REPLACE() throws Exception + { + _useAlias = true; + REPLACE(); + } + + @Test + public void ALIAS_UPDATE() throws Exception + { + _useAlias = true; + UPDATE(); + } + } +} diff --git a/api/src/org/labkey/api/query/DefaultQueryUpdateService.java b/api/src/org/labkey/api/query/DefaultQueryUpdateService.java index a2b32cffcb3..3e993bff998 100644 --- a/api/src/org/labkey/api/query/DefaultQueryUpdateService.java +++ b/api/src/org/labkey/api/query/DefaultQueryUpdateService.java @@ -1,953 +1,934 @@ -/* - * Copyright (c) 2009-2019 LabKey Corporation - * - * 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.labkey.api.query; - -import org.apache.commons.beanutils.ConversionException; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.attachments.AttachmentFile; -import org.labkey.api.collections.ArrayListMap; -import org.labkey.api.collections.CaseInsensitiveHashMap; -import org.labkey.api.collections.CaseInsensitiveMapWrapper; -import org.labkey.api.data.ColumnInfo; -import org.labkey.api.data.Container; -import org.labkey.api.data.ConvertHelper; -import org.labkey.api.data.ExpDataFileConverter; -import org.labkey.api.data.ImportAliasable; -import org.labkey.api.data.JdbcType; -import org.labkey.api.data.MvUtil; -import org.labkey.api.data.Parameter; -import org.labkey.api.data.SQLFragment; -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.data.UpdateableTableInfo; -import org.labkey.api.data.validator.ColumnValidator; -import org.labkey.api.data.validator.ColumnValidators; -import org.labkey.api.dataiterator.DataIteratorBuilder; -import org.labkey.api.dataiterator.DataIteratorContext; -import org.labkey.api.dataiterator.DataIteratorUtil; -import org.labkey.api.dataiterator.MapDataIterator; -import org.labkey.api.exp.OntologyManager; -import org.labkey.api.exp.OntologyObject; -import org.labkey.api.exp.PropertyColumn; -import org.labkey.api.exp.PropertyDescriptor; -import org.labkey.api.exp.PropertyType; -import org.labkey.api.exp.api.ExperimentService; -import org.labkey.api.exp.property.Domain; -import org.labkey.api.exp.property.DomainProperty; -import org.labkey.api.exp.property.ValidatorContext; -import org.labkey.api.reader.ColumnDescriptor; -import org.labkey.api.reader.DataLoader; -import org.labkey.api.security.User; -import org.labkey.api.security.permissions.DeletePermission; -import org.labkey.api.security.permissions.InsertPermission; -import org.labkey.api.security.permissions.Permission; -import org.labkey.api.security.permissions.UpdatePermission; -import org.labkey.api.util.CachingSupplier; -import org.labkey.api.util.Pair; -import org.labkey.api.view.UnauthorizedException; -import org.labkey.vfs.FileLike; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.nio.file.Path; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Supplier; - -/** - * QueryUpdateService implementation that supports Query TableInfos that are backed by both a hard table and a Domain. - * To update the Domain, a DomainUpdateHelper is required, otherwise the DefaultQueryUpdateService will only update the - * hard table columns. - */ -public class DefaultQueryUpdateService extends AbstractQueryUpdateService -{ - private final TableInfo _dbTable; - private DomainUpdateHelper _helper = null; - /** - * Map from DbTable column names to QueryTable column names, if they have been aliased - */ - protected Map _columnMapping = Collections.emptyMap(); - /** - * Hold onto the ColumnInfos, so we don't have to regenerate them for every row we process - */ - private final Supplier> _tableMapSupplier = new CachingSupplier<>(() -> DataIteratorUtil.createTableMap(getQueryTable(), true)); - private final ValidatorContext _validatorContext; - private final FileColumnValueMapper _fileColumnValueMapping = new FileColumnValueMapper(); - - public DefaultQueryUpdateService(@NotNull TableInfo queryTable, TableInfo dbTable) - { - super(queryTable); - _dbTable = dbTable; - - if (queryTable.getUserSchema() == null) - throw new RuntimeValidationException("User schema not defined for " + queryTable.getName()); - - _validatorContext = new ValidatorContext(queryTable.getUserSchema().getContainer(), queryTable.getUserSchema().getUser()); - } - - public DefaultQueryUpdateService(TableInfo queryTable, TableInfo dbTable, DomainUpdateHelper helper) - { - this(queryTable, dbTable); - _helper = helper; - } - - /** - * @param columnMapping Map from DbTable column names to QueryTable column names, if they have been aliased - */ - public DefaultQueryUpdateService(TableInfo queryTable, TableInfo dbTable, Map columnMapping) - { - this(queryTable, dbTable); - _columnMapping = columnMapping; - } - - protected TableInfo getDbTable() - { - return _dbTable; - } - - protected Domain getDomain() - { - return _helper == null ? null : _helper.getDomain(); - } - - protected ColumnInfo getObjectUriColumn() - { - return _helper == null ? null : _helper.getObjectUriColumn(); - } - - protected String createObjectURI() - { - return _helper == null ? null : _helper.createObjectURI(); - } - - protected Iterable getPropertyColumns() - { - return _helper == null ? Collections.emptyList() : _helper.getPropertyColumns(); - } - - protected Map getColumnMapping() - { - return _columnMapping; - } - - /** - * Returns the container that the domain is defined - */ - protected Container getDomainContainer(Container c) - { - return _helper == null ? c : _helper.getDomainContainer(c); - } - - /** - * Returns the container to insert/update values into - */ - protected Container getDomainObjContainer(Container c) - { - return _helper == null ? c : _helper.getDomainObjContainer(c); - } - - protected Set getAutoPopulatedColumns() - { - return Table.AUTOPOPULATED_COLUMN_NAMES; - } - - public interface DomainUpdateHelper - { - Domain getDomain(); - - ColumnInfo getObjectUriColumn(); - - String createObjectURI(); - - // Could probably be just Iterable or be removed and just get all PropertyDescriptors in the Domain. - Iterable getPropertyColumns(); - - Container getDomainContainer(Container c); - - Container getDomainObjContainer(Container c); - } - - public class ImportHelper implements OntologyManager.ImportHelper - { - ImportHelper() - { - } - - @Override - public String beforeImportObject(Map map) - { - ColumnInfo objectUriCol = getObjectUriColumn(); - - // Get existing Lsid - String lsid = (String) map.get(objectUriCol.getName()); - if (lsid != null) - return lsid; - - // Generate a new Lsid - lsid = createObjectURI(); - map.put(objectUriCol.getName(), lsid); - return lsid; - } - - @Override - public void afterBatchInsert(int currentRow) - { - } - - @Override - public void updateStatistics(int currentRow) - { - } - } - - @Override - protected Map getRow(User user, Container container, Map keys) - throws InvalidKeyException, QueryUpdateServiceException, SQLException - { - aliasColumns(_columnMapping, keys); - Map row = _select(container, getKeys(keys, container)); - - //PostgreSQL includes a column named _row for the row index, but since this is selecting by - //primary key, it will always be 1, which is not only unnecessary, but confusing, so strip it - if (null != row) - { - if (row instanceof ArrayListMap) - ((ArrayListMap) row).getFindMap().remove("_row"); - else - row.remove("_row"); - } - - return row; - } - - protected Map _select(Container container, Object[] keys) throws ConversionException - { - TableInfo table = getDbTable(); - Object[] typedParameters = convertToTypedValues(keys, table.getPkColumns()); - - Map row = new TableSelector(table).getMap(typedParameters); - - ColumnInfo objectUriCol = getObjectUriColumn(); - Domain domain = getDomain(); - if (objectUriCol != null && domain != null && !domain.getProperties().isEmpty() && row != null) - { - String lsid = (String) row.get(objectUriCol.getName()); - if (lsid != null) - { - Map propertyValues = OntologyManager.getProperties(getDomainObjContainer(container), lsid); - if (!propertyValues.isEmpty()) - { - // convert PropertyURI->value map into "Property name"->value map - Map propertyMap = domain.createImportMap(false); - for (Map.Entry entry : propertyValues.entrySet()) - { - String propertyURI = entry.getKey(); - DomainProperty dp = propertyMap.get(propertyURI); - PropertyDescriptor pd = dp != null ? dp.getPropertyDescriptor() : null; - if (pd != null) - row.put(pd.getName(), entry.getValue()); - } - } - } - // Issue 46985: Be tolerant of a row not having an LSID value (as the row may have been - // inserted before the table was made extensible), but make sure that we got an LSID field - // when fetching the row - else if (!row.containsKey(objectUriCol.getName())) - { - throw new IllegalStateException("LSID value not returned when querying table - " + table.getName()); - } - } - - return row; - } - - - private Object[] convertToTypedValues(Object[] keys, List cols) - { - Object[] typedParameters = new Object[keys.length]; - int t = 0; - for (int i = 0; i < keys.length; i++) - { - if (i >= cols.size() || keys[i] instanceof Parameter.TypedValue) - { - typedParameters[t++] = keys[i]; - continue; - } - Object v = keys[i]; - JdbcType type = cols.get(i).getJdbcType(); - if (v instanceof String) - v = type.convert(v); - Parameter.TypedValue tv = new Parameter.TypedValue(v, type); - typedParameters[t++] = tv; - } - return typedParameters; - } - - - @Override - protected Map insertRow(User user, Container container, Map row) - throws DuplicateKeyException, ValidationException, QueryUpdateServiceException, SQLException - { - aliasColumns(_columnMapping, row); - convertTypes(user, container, row); - setSpecialColumns(container, row, user, InsertPermission.class); - validateInsertRow(row); - return _insert(user, container, row); - } - - protected Map _insert(User user, Container c, Map row) - throws SQLException, ValidationException - { - assert (getQueryTable().supportsInsertOption(InsertOption.INSERT)); - - try - { - ColumnInfo objectUriCol = getObjectUriColumn(); - Domain domain = getDomain(); - if (objectUriCol != null && domain != null && !domain.getProperties().isEmpty()) - { - // convert "Property name"->value map into PropertyURI->value map - List pds = new ArrayList<>(); - Map values = new CaseInsensitiveMapWrapper<>(new HashMap<>()); - for (PropertyColumn pc : getPropertyColumns()) - { - PropertyDescriptor pd = pc.getPropertyDescriptor(); - pds.add(pd); - Object value = getPropertyValue(row, pd); - values.put(pd.getPropertyURI(), value); - } - - LsidCollector collector = new LsidCollector(); - OntologyManager.insertTabDelimited(getDomainObjContainer(c), user, null, new ImportHelper(), pds, MapDataIterator.of(Collections.singletonList(values)).getDataIterator(new DataIteratorContext()), true, collector); - String lsid = collector.getLsid(); - - // Add the new lsid to the row map. - row.put(objectUriCol.getName(), lsid); - } - - return Table.insert(user, getDbTable(), row); - } - catch (RuntimeValidationException e) - { - throw e.getValidationException(); - } - catch (BatchValidationException e) - { - throw e.getLastRowError(); - } - } - - @Override - protected Map updateRow(User user, Container container, Map row, @NotNull Map oldRow, @Nullable Map configParameters) - throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException - { - return updateRow(user, container, row, oldRow, false, false); - } - - protected Map updateRow(User user, Container container, Map row, @NotNull Map oldRow, boolean allowOwner, boolean retainCreation) - throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException - { - Map rowStripped = new CaseInsensitiveHashMap<>(row.size()); - - // Flip the key/value pairs around for easy lookup - Map queryToDb = new CaseInsensitiveHashMap<>(); - for (Map.Entry entry : _columnMapping.entrySet()) - { - queryToDb.put(entry.getValue(), entry.getKey()); - } - - setSpecialColumns(container, row, user, UpdatePermission.class); - - Map tableAliasesMap = _tableMapSupplier.get(); - Map> colFrequency = new HashMap<>(); - - //resolve passed in row including columns in the table and other properties (vocabulary properties) not in the Domain/table - for (Map.Entry entry: row.entrySet()) - { - if (!rowStripped.containsKey(entry.getKey())) - { - ColumnInfo col = getQueryTable().getColumn(entry.getKey()); - - if (null == col) - { - col = tableAliasesMap.get(entry.getKey()); - } - - if (null != col) - { - final String name = col.getName(); - - // Skip readonly and wrapped columns. The wrapped column is usually a pk column and can't be updated. - if (col.isReadOnly() || col.isCalculated()) - continue; - - //when updating a row, we should strip the following fields, as they are - //automagically maintained by the table layer, and should not be allowed - //to change once the record exists. - //unfortunately, the Table.update() method doesn't strip these, so we'll - //do that here. - // Owner, CreatedBy, Created, EntityId - if ((!retainCreation && (name.equalsIgnoreCase("CreatedBy") || name.equalsIgnoreCase("Created"))) - || (!allowOwner && name.equalsIgnoreCase("Owner")) - || name.equalsIgnoreCase("EntityId")) - continue; - - // Throw error if more than one row properties having different values match up to the same column. - if (!colFrequency.containsKey(col)) - { - colFrequency.put(col, Pair.of(entry.getKey(),entry.getValue())); - } - else - { - if (!Objects.equals(colFrequency.get(col).second, entry.getValue())) - { - throw new ValidationException("Property key - " + colFrequency.get(col).first + " and " + entry.getKey() + " matched for the same column."); - } - } - - // We want a map using the DbTable column names as keys, so figure out the right name to use - String dbName = queryToDb.getOrDefault(name, name); - rowStripped.put(dbName, entry.getValue()); - } - } - } - - convertTypes(user, container, rowStripped); - validateUpdateRow(rowStripped); - - if (row.get("container") != null) - { - Container rowContainer = UserSchema.translateRowSuppliedContainer(row.get("container"), container, user, getQueryTable(), UpdatePermission.class, null); - if (rowContainer == null) - { - throw new ValidationException("Unknown container: " + row.get("container")); - } - else - { - Container oldContainer = UserSchema.translateRowSuppliedContainer(new CaseInsensitiveHashMap<>(oldRow).get("container"), container, user, getQueryTable(), UpdatePermission.class, null); - if (null != oldContainer && !rowContainer.equals(oldContainer)) - throw new UnauthorizedException("The row is from the wrong container."); - } - } - - Map updatedRow = _update(user, container, rowStripped, oldRow, oldRow == null ? getKeys(row, container) : getKeys(oldRow, container)); - - //when passing a map for the row, the Table layer returns the map of fields it updated, which excludes - //the primary key columns as well as those marked read-only. So we can't simply return the map returned - //from Table.update(). Instead, we need to copy values from updatedRow into row and return that. - row.putAll(updatedRow); - return row; - } - - protected void validateValue(ColumnInfo column, Object value, Object providedValue) throws ValidationException - { - DomainProperty dp = getDomain() == null ? null : getDomain().getPropertyByName(column.getColumnName()); - List validators = ColumnValidators.create(column, dp); - for (ColumnValidator v : validators) - { - String msg = v.validate(-1, value, _validatorContext, providedValue); - if (msg != null) - throw new ValidationException(msg, column.getName()); - } - } - - protected void validateInsertRow(Map row) throws ValidationException - { - for (ColumnInfo col : getQueryTable().getColumns()) - { - Object value = row.get(col.getColumnName()); - - // Check required values aren't null or empty - if (null == value || value instanceof String s && s.isEmpty()) - { - if (!col.isAutoIncrement() && col.isRequired() && - !getAutoPopulatedColumns().contains(col.getName()) && - col.getJdbcDefaultValue() == null) - { - throw new ValidationException("A value is required for field '" + col.getName() + "'", col.getName()); - } - } - else - { - validateValue(col, value, null); - } - } - } - - protected void validateUpdateRow(Map row) throws ValidationException - { - for (ColumnInfo col : getQueryTable().getColumns()) - { - // Only validate incoming values - if (row.containsKey(col.getColumnName())) - { - Object value = row.get(col.getColumnName()); - validateValue(col, value, null); - } - } - } - - protected Map _update(User user, Container c, Map row, Map oldRow, Object[] keys) - throws SQLException, ValidationException - { - assert(getQueryTable().supportsInsertOption(InsertOption.UPDATE)); - - try - { - ColumnInfo objectUriCol = getObjectUriColumn(); - Domain domain = getDomain(); - - // The lsid may be null for the row until a property has been inserted - String lsid = null; - if (objectUriCol != null) - lsid = (String) oldRow.get(objectUriCol.getName()); - - List tableProperties = new ArrayList<>(); - if (objectUriCol != null && domain != null && !domain.getProperties().isEmpty()) - { - // convert "Property name"->value map into PropertyURI->value map - Map newValues = new CaseInsensitiveMapWrapper<>(new HashMap<>()); - - for (PropertyColumn pc : getPropertyColumns()) - { - PropertyDescriptor pd = pc.getPropertyDescriptor(); - tableProperties.add(pd); - - // clear out the old value if it exists and is contained in the new row (it may be incoming as null) - if (lsid != null && (hasProperty(row, pd) && hasProperty(oldRow, pd))) - OntologyManager.deleteProperty(lsid, pd.getPropertyURI(), getDomainObjContainer(c), getDomainContainer(c)); - - Object value = getPropertyValue(row, pd); - if (value != null) - newValues.put(pd.getPropertyURI(), value); - } - - // Note: copy lsid into newValues map so it will be found by the ImportHelper.beforeImportObject() - newValues.put(objectUriCol.getName(), lsid); - - LsidCollector collector = new LsidCollector(); - OntologyManager.insertTabDelimited(getDomainObjContainer(c), user, null, new ImportHelper(), tableProperties, MapDataIterator.of(Collections.singletonList(newValues)).getDataIterator(new DataIteratorContext()), true, collector); - - // Update the lsid in the row: the lsid may have not existed in the row before the update. - lsid = collector.getLsid(); - row.put(objectUriCol.getName(), lsid); - } - - // Get lsid value if it hasn't been set. - // This should only happen if the QueryUpdateService doesn't have a DomainUpdateHelper (DataClass and SampleType) - if (lsid == null && getQueryTable() instanceof UpdateableTableInfo updateableTableInfo) - { - String objectUriColName = updateableTableInfo.getObjectURIColumnName(); - if (objectUriColName != null) - lsid = (String) row.getOrDefault(objectUriColName, oldRow.get(objectUriColName)); - } - - // handle vocabulary properties - if (lsid != null) - { - for (Map.Entry rowEntry : row.entrySet()) - { - String colName = rowEntry.getKey(); - Object value = rowEntry.getValue(); - - ColumnInfo col = getQueryTable().getColumn(colName); - if (col instanceof PropertyColumn propCol) - { - PropertyDescriptor pd = propCol.getPropertyDescriptor(); - if (pd.isVocabulary() && !tableProperties.contains(pd)) - { - OntologyManager.updateObjectProperty(user, c, pd, lsid, value, null, false); - } - } - } - } - } - catch (BatchValidationException e) - { - throw e.getLastRowError(); - } - - checkDuplicateUpdate(keys); - - return Table.update(user, getDbTable(), row, keys); // Cache-invalidation handled in caller (TreatmentManager.saveAssaySpecimen()) - } - - private static class LsidCollector implements OntologyManager.RowCallback - { - private String _lsid; - - @Override - public void rowProcessed(Map row, String lsid) - { - if (_lsid != null) - { - throw new IllegalStateException("Only expected a single LSID"); - } - _lsid = lsid; - } - - public String getLsid() - { - if (_lsid == null) - { - throw new IllegalStateException("No LSID returned"); - } - return _lsid; - } - } - - // Get value from row map where the keys are column names. - private Object getPropertyValue(Map row, PropertyDescriptor pd) - { - if (row.containsKey(pd.getName())) - return row.get(pd.getName()); - - if (row.containsKey(pd.getLabel())) - return row.get(pd.getLabel()); - - for (String alias : pd.getImportAliasSet()) - { - if (row.containsKey(alias)) - return row.get(alias); - } - - return null; - } - - // Checks a value exists in the row map (value may be null) - private boolean hasProperty(Map row, PropertyDescriptor pd) - { - if (row.containsKey(pd.getName())) - return true; - - if (row.containsKey(pd.getLabel())) - return true; - - for (String alias : pd.getImportAliasSet()) - { - if (row.containsKey(alias)) - return true; - } - - return false; - } - - @Override - protected Map deleteRow(User user, Container container, Map oldRowMap) throws QueryUpdateServiceException, SQLException, InvalidKeyException - { - if (oldRowMap == null) - return null; - - aliasColumns(_columnMapping, oldRowMap); - - if (container != null && getDbTable().getColumn("container") != null) - { - // UNDONE: 9077: check container permission on each row before delete - Container rowContainer = UserSchema.translateRowSuppliedContainer(new CaseInsensitiveHashMap<>(oldRowMap).get("container"), container, user, getQueryTable(), DeletePermission.class, null); - if (null != rowContainer && !container.equals(rowContainer)) - { - //Issue 15301: allow workbooks records to be deleted/updated from the parent container - if (container.allowRowMutationForContainer(rowContainer)) - container = rowContainer; - else - throw new UnauthorizedException("The row is from the container: " + rowContainer.getId() + " which does not allow deletes from the container: " + container.getPath()); - } - } - - _delete(container, oldRowMap); - return oldRowMap; - } - - protected void _delete(Container c, Map row) throws InvalidKeyException - { - ColumnInfo objectUriCol = getObjectUriColumn(); - if (objectUriCol != null) - { - String lsid = (String)row.get(objectUriCol.getName()); - if (lsid != null) - { - OntologyObject oo = OntologyManager.getOntologyObject(c, lsid); - if (oo != null) - OntologyManager.deleteProperties(c, oo.getObjectId()); - } - } - Table.delete(getDbTable(), getKeys(row, c)); - } - - // classes should override this method if they need to do more work than delete all the rows from the table - // this implementation will delete all rows from the table for the given container as well as delete - // any properties associated with the table - @Override - protected int truncateRows(User user, Container container) throws QueryUpdateServiceException, SQLException - { - // get rid of the properties for this table - if (null != getObjectUriColumn()) - { - SQLFragment lsids = new SQLFragment() - .append("SELECT t.").append(getObjectUriColumn().getColumnName()) - .append(" FROM ").append(getDbTable(), "t") - .append(" WHERE t.").append(getObjectUriColumn().getColumnName()).append(" IS NOT NULL"); - if (null != getDbTable().getColumn("container")) - { - lsids.append(" AND t.Container = ?"); - lsids.add(container.getId()); - } - - OntologyManager.deleteOntologyObjects(ExperimentService.get().getSchema(), lsids, container); - } - - // delete all the rows in this table, scoping to the container if the column - // is available - if (null != getDbTable().getColumn("container")) - return Table.delete(getDbTable(), SimpleFilter.createContainerFilter(container)); - - return Table.delete(getDbTable()); - } - - protected Object[] getKeys(Map map, Container container) throws InvalidKeyException - { - //build an array of pk values based on the table info - TableInfo table = getDbTable(); - List pks = table.getPkColumns(); - Object[] pkVals = new Object[pks.size()]; - - if (map == null || map.isEmpty()) - return pkVals; - - for (int idx = 0; idx < pks.size(); ++idx) - { - ColumnInfo pk = pks.get(idx); - Object pkValue = map.get(pk.getName()); - // Check the type and coerce if needed - if (pkValue != null && !pk.getJavaObjectClass().isInstance(pkValue)) - { - try - { - pkValue = pk.convert(pkValue); - } - catch (ConversionException ignored) { /* Maybe the database can do the conversion */ } - } - pkVals[idx] = pkValue; - if (null == pkVals[idx] && pk.getColumnName().equalsIgnoreCase("Container")) - { - pkVals[idx] = container; - } - if(null == pkVals[idx]) - { - throw new InvalidKeyException("Value for key field '" + pk.getName() + "' was null or not supplied!", map); - } - } - return pkVals; - } - - private Map _missingValues = null; - private Container _missingValuesContainer; - - protected boolean validMissingValue(Container c, String mv) - { - if (null == c) - return false; - if (null == _missingValues || !c.getId().equals(_missingValuesContainer.getId())) - { - _missingValues = MvUtil.getIndicatorsAndLabels(c); - _missingValuesContainer = c; - } - return _missingValues.containsKey(mv); - } - - final protected void convertTypes(User user, Container c, Map row) throws ValidationException - { - convertTypes(user, c, row, getDbTable(), null); - } - - // TODO Path->FileObject - // why is coerceTypes() in AbstractQueryUpdateService and convertTypes() in DefaultQueryUpdateService? - protected void convertTypes(User user, Container c, Map row, TableInfo t, @Nullable Path fileLinkDirPath) throws ValidationException - { - for (ColumnInfo col : t.getColumns()) - { - if (col.isMvIndicatorColumn()) - continue; - boolean isColumnPresent = row.containsKey(col.getName()) || col.isMvEnabled() && row.containsKey(col.getMvColumnName().getName()); - if (!isColumnPresent) - continue; - - Object value = row.get(col.getName()); - - /* NOTE: see MissingValueConvertColumn.convert() these methods should have similar behavior. - * If you update this code, check that code as well. */ - if (col.isMvEnabled()) - { - if (value instanceof String s && StringUtils.isEmpty(s)) - value = null; - - Object mvObj = row.get(col.getMvColumnName().getName()); - String mv = Objects.toString(mvObj, null); - if (StringUtils.isEmpty(mv)) - mv = null; - - if (null != mv) - { - if (!validMissingValue(c, mv)) - throw new ValidationException("Value is not a valid missing value indicator: " + mv); - } - else if (null != value) - { - String s = Objects.toString(value, null); - if (validMissingValue(c, s)) - { - mv = s; - value = null; - } - } - row.put(col.getMvColumnName().getName(), mv); - } - - value = convertColumnValue(col, value, user, c, fileLinkDirPath); - row.put(col.getName(), value); - } - } - - protected Object convertColumnValue(ColumnInfo col, Object value, User user, Container c, @Nullable Path fileLinkDirPath) throws ValidationException - { - // Issue 13951: PSQLException from org.labkey.api.query.DefaultQueryUpdateService._update() - // improve handling of conversion errors - try - { - if (PropertyType.FILE_LINK == col.getPropertyType()) - { - if ((value instanceof MultipartFile || value instanceof AttachmentFile)) - { - FileLike fl = (FileLike)_fileColumnValueMapping.saveFileColumnValue(user, c, fileLinkDirPath, col.getName(), value); - value = fl.toNioPathForRead().toString(); - } - return ExpDataFileConverter.convert(value); - } - return col.getConvertFn().convert(value); - } - catch (ConvertHelper.FileConversionException e) - { - throw new ValidationException(e.getMessage()); - } - catch (ConversionException e) - { - String type = ColumnInfo.getFriendlyTypeName(col.getJdbcType().getJavaClass()); - throw new ValidationException("Unable to convert value '" + value.toString() + "' to " + type, col.getName()); - } - catch (QueryUpdateServiceException e) - { - throw new ValidationException("Save file link failed: " + col.getName()); - } - } - - /** - * Override this method to alter the row before insert or update. - * For example, you can automatically adjust certain column values based on context. - * @param container The current container - * @param row The row data - * @param user The current user - * @param clazz A permission class to test - */ - protected void setSpecialColumns(Container container, Map row, User user, Class clazz) - { - if (null != container) - { - //Issue 15301: allow workbooks records to be deleted/updated from the parent container - if (row.get("container") != null) - { - Container rowContainer = UserSchema.translateRowSuppliedContainer(row.get("container"), container, user, getQueryTable(), clazz, null); - if (rowContainer != null && container.allowRowMutationForContainer(rowContainer)) - { - row.put("container", rowContainer.getId()); //normalize to container ID - return; //accept the row-provided value - } - } - row.put("container", container.getId()); - } - } - - public boolean hasAttachmentProperties() - { - Domain domain = getDomain(); - if (null != domain) - { - for (DomainProperty dp : domain.getProperties()) - if (null != dp && isAttachmentProperty(dp)) - return true; - } - return false; - } - - protected boolean isAttachmentProperty(@NotNull DomainProperty dp) - { - PropertyDescriptor pd = dp.getPropertyDescriptor(); - return PropertyType.ATTACHMENT.equals(pd.getPropertyType()); - } - - protected boolean isAttachmentProperty(String name) - { - DomainProperty dp = getDomain().getPropertyByName(name); - if (dp != null) - return isAttachmentProperty(dp); - return false; - } - - protected void configureCrossFolderImport(DataIteratorBuilder rows, DataIteratorContext context) throws IOException - { - if (!context.getInsertOption().updateOnly && context.isCrossFolderImport() && rows instanceof DataLoader dataLoader) - { - boolean hasContainerField = false; - for (ColumnDescriptor columnDescriptor : dataLoader.getColumns()) - { - String fieldName = columnDescriptor.getColumnName(); - if (fieldName.equalsIgnoreCase("Container") || fieldName.equalsIgnoreCase("Folder")) - { - hasContainerField = true; - break; - } - } - if (!hasContainerField) - context.setCrossFolderImport(false); - } - } - - public static @Nullable String getKeyColumnAliasForUpdate(TableInfo tableInfo, @NotNull Map columnNameMap) - { - // Currently, SampleUpdateAddColumnsDataIterator and DataClassUpdateAddColumnsDataIterator is being called before a translator is invoked to - // remap column labels to columns (e.g., "Row Id" -> "RowId"). Due to this, we need to search the - // map of columns for the key column. - var rowIdAliases = ImportAliasable.Helper.createImportSet(tableInfo.getColumn(FieldKey.fromParts("RowId"))); - rowIdAliases.retainAll(columnNameMap.keySet()); - - if (rowIdAliases.size() == 1) - return rowIdAliases.iterator().next(); - if (rowIdAliases.isEmpty()) - return "Name"; - - return null; - } - -} +/* + * Copyright (c) 2009-2019 LabKey Corporation + * + * 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.labkey.api.query; + +import org.apache.commons.beanutils.ConversionException; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.attachments.AttachmentFile; +import org.labkey.api.collections.ArrayListMap; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.collections.CaseInsensitiveMapWrapper; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; +import org.labkey.api.data.ConvertHelper; +import org.labkey.api.data.ExpDataFileConverter; +import org.labkey.api.data.ImportAliasable; +import org.labkey.api.data.JdbcType; +import org.labkey.api.data.MvUtil; +import org.labkey.api.data.Parameter; +import org.labkey.api.data.SQLFragment; +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.data.UpdateableTableInfo; +import org.labkey.api.data.validator.ColumnValidator; +import org.labkey.api.data.validator.ColumnValidators; +import org.labkey.api.dataiterator.DataIteratorBuilder; +import org.labkey.api.dataiterator.DataIteratorContext; +import org.labkey.api.dataiterator.DataIteratorUtil; +import org.labkey.api.dataiterator.MapDataIterator; +import org.labkey.api.exp.OntologyManager; +import org.labkey.api.exp.OntologyObject; +import org.labkey.api.exp.PropertyColumn; +import org.labkey.api.exp.PropertyDescriptor; +import org.labkey.api.exp.PropertyType; +import org.labkey.api.exp.api.ExperimentService; +import org.labkey.api.exp.property.Domain; +import org.labkey.api.exp.property.DomainProperty; +import org.labkey.api.exp.property.ValidatorContext; +import org.labkey.api.reader.ColumnDescriptor; +import org.labkey.api.reader.DataLoader; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.DeletePermission; +import org.labkey.api.security.permissions.InsertPermission; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.permissions.UpdatePermission; +import org.labkey.api.util.CachingSupplier; +import org.labkey.api.util.Pair; +import org.labkey.api.view.UnauthorizedException; +import org.labkey.vfs.FileLike; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +/** + * QueryUpdateService implementation that supports Query TableInfos that are backed by both a hard table and a Domain. + * To update the Domain, a DomainUpdateHelper is required, otherwise the DefaultQueryUpdateService will only update the + * hard table columns. + */ +public class DefaultQueryUpdateService extends AbstractQueryUpdateService +{ + private final TableInfo _dbTable; + private DomainUpdateHelper _helper = null; + /** + * Map from DbTable column names to QueryTable column names, if they have been aliased + */ + protected Map _columnMapping = Collections.emptyMap(); + /** + * Hold onto the ColumnInfos, so we don't have to regenerate them for every row we process + */ + private final Supplier> _tableMapSupplier = new CachingSupplier<>(() -> DataIteratorUtil.createTableMap(getQueryTable(), true)); + private final ValidatorContext _validatorContext; + private final FileColumnValueMapper _fileColumnValueMapping = new FileColumnValueMapper(); + + public DefaultQueryUpdateService(@NotNull TableInfo queryTable, TableInfo dbTable) + { + super(queryTable); + _dbTable = dbTable; + + if (queryTable.getUserSchema() == null) + throw new RuntimeValidationException("User schema not defined for " + queryTable.getName()); + + _validatorContext = new ValidatorContext(queryTable.getUserSchema().getContainer(), queryTable.getUserSchema().getUser()); + } + + public DefaultQueryUpdateService(TableInfo queryTable, TableInfo dbTable, DomainUpdateHelper helper) + { + this(queryTable, dbTable); + _helper = helper; + } + + /** + * @param columnMapping Map from DbTable column names to QueryTable column names, if they have been aliased + */ + public DefaultQueryUpdateService(TableInfo queryTable, TableInfo dbTable, Map columnMapping) + { + this(queryTable, dbTable); + _columnMapping = columnMapping; + } + + protected TableInfo getDbTable() + { + return _dbTable; + } + + protected Domain getDomain() + { + return _helper == null ? null : _helper.getDomain(); + } + + protected ColumnInfo getObjectUriColumn() + { + return _helper == null ? null : _helper.getObjectUriColumn(); + } + + protected String createObjectURI() + { + return _helper == null ? null : _helper.createObjectURI(); + } + + protected Iterable getPropertyColumns() + { + return _helper == null ? Collections.emptyList() : _helper.getPropertyColumns(); + } + + protected Map getColumnMapping() + { + return _columnMapping; + } + + /** + * Returns the container that the domain is defined + */ + protected Container getDomainContainer(Container c) + { + return _helper == null ? c : _helper.getDomainContainer(c); + } + + /** + * Returns the container to insert/update values into + */ + protected Container getDomainObjContainer(Container c) + { + return _helper == null ? c : _helper.getDomainObjContainer(c); + } + + protected Set getAutoPopulatedColumns() + { + return Table.AUTOPOPULATED_COLUMN_NAMES; + } + + public interface DomainUpdateHelper + { + Domain getDomain(); + + ColumnInfo getObjectUriColumn(); + + String createObjectURI(); + + // Could probably be just Iterable or be removed and just get all PropertyDescriptors in the Domain. + Iterable getPropertyColumns(); + + Container getDomainContainer(Container c); + + Container getDomainObjContainer(Container c); + } + + public class ImportHelper implements OntologyManager.ImportHelper + { + ImportHelper() + { + } + + @Override + public String beforeImportObject(Map map) + { + ColumnInfo objectUriCol = getObjectUriColumn(); + + // Get existing Lsid + String lsid = (String) map.get(objectUriCol.getName()); + if (lsid != null) + return lsid; + + // Generate a new Lsid + lsid = createObjectURI(); + map.put(objectUriCol.getName(), lsid); + return lsid; + } + + @Override + public void afterBatchInsert(int currentRow) + { + } + + @Override + public void updateStatistics(int currentRow) + { + } + } + + @Override + protected Map getRow(User user, Container container, Map keys) + throws InvalidKeyException, QueryUpdateServiceException, SQLException + { + aliasColumns(_columnMapping, keys); + Map row = _select(container, getKeys(keys, container)); + + //PostgreSQL includes a column named _row for the row index, but since this is selecting by + //primary key, it will always be 1, which is not only unnecessary, but confusing, so strip it + if (null != row) + { + if (row instanceof ArrayListMap) + ((ArrayListMap) row).getFindMap().remove("_row"); + else + row.remove("_row"); + } + + return row; + } + + protected Map _select(Container container, Object[] keys) throws ConversionException + { + TableInfo table = getDbTable(); + Object[] typedParameters = convertToTypedValues(keys, table.getPkColumns()); + + Map row = new TableSelector(table).getMap(typedParameters); + + ColumnInfo objectUriCol = getObjectUriColumn(); + Domain domain = getDomain(); + if (objectUriCol != null && domain != null && !domain.getProperties().isEmpty() && row != null) + { + String lsid = (String) row.get(objectUriCol.getName()); + if (lsid != null) + { + Map propertyValues = OntologyManager.getProperties(getDomainObjContainer(container), lsid); + if (!propertyValues.isEmpty()) + { + // convert PropertyURI->value map into "Property name"->value map + Map propertyMap = domain.createImportMap(false); + for (Map.Entry entry : propertyValues.entrySet()) + { + String propertyURI = entry.getKey(); + DomainProperty dp = propertyMap.get(propertyURI); + PropertyDescriptor pd = dp != null ? dp.getPropertyDescriptor() : null; + if (pd != null) + row.put(pd.getName(), entry.getValue()); + } + } + } + // Issue 46985: Be tolerant of a row not having an LSID value (as the row may have been + // inserted before the table was made extensible), but make sure that we got an LSID field + // when fetching the row + else if (!row.containsKey(objectUriCol.getName())) + { + throw new IllegalStateException("LSID value not returned when querying table - " + table.getName()); + } + } + + return row; + } + + + private Object[] convertToTypedValues(Object[] keys, List cols) + { + Object[] typedParameters = new Object[keys.length]; + int t = 0; + for (int i = 0; i < keys.length; i++) + { + if (i >= cols.size() || keys[i] instanceof Parameter.TypedValue) + { + typedParameters[t++] = keys[i]; + continue; + } + Object v = keys[i]; + JdbcType type = cols.get(i).getJdbcType(); + if (v instanceof String) + v = type.convert(v); + Parameter.TypedValue tv = new Parameter.TypedValue(v, type); + typedParameters[t++] = tv; + } + return typedParameters; + } + + + @Override + protected Map insertRow(User user, Container container, Map row) + throws DuplicateKeyException, ValidationException, QueryUpdateServiceException, SQLException + { + aliasColumns(_columnMapping, row); + convertTypes(user, container, row); + setSpecialColumns(container, row, user, InsertPermission.class); + validateInsertRow(row); + return _insert(user, container, row); + } + + protected Map _insert(User user, Container c, Map row) + throws SQLException, ValidationException + { + assert (getQueryTable().supportsInsertOption(InsertOption.INSERT)); + + try + { + ColumnInfo objectUriCol = getObjectUriColumn(); + Domain domain = getDomain(); + if (objectUriCol != null && domain != null && !domain.getProperties().isEmpty()) + { + // convert "Property name"->value map into PropertyURI->value map + List pds = new ArrayList<>(); + Map values = new CaseInsensitiveMapWrapper<>(new HashMap<>()); + for (PropertyColumn pc : getPropertyColumns()) + { + PropertyDescriptor pd = pc.getPropertyDescriptor(); + pds.add(pd); + Object value = getPropertyValue(row, pd); + values.put(pd.getPropertyURI(), value); + } + + LsidCollector collector = new LsidCollector(); + OntologyManager.insertTabDelimited(getDomainObjContainer(c), user, null, new ImportHelper(), pds, MapDataIterator.of(Collections.singletonList(values)).getDataIterator(new DataIteratorContext()), true, collector); + String lsid = collector.getLsid(); + + // Add the new lsid to the row map. + row.put(objectUriCol.getName(), lsid); + } + + return Table.insert(user, getDbTable(), row); + } + catch (RuntimeValidationException e) + { + throw e.getValidationException(); + } + catch (BatchValidationException e) + { + throw e.getLastRowError(); + } + } + + @Override + protected Map updateRow(User user, Container container, Map row, @NotNull Map oldRow, @Nullable Map configParameters) + throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException + { + return updateRow(user, container, row, oldRow, false, false); + } + + protected Map updateRow(User user, Container container, Map row, @NotNull Map oldRow, boolean allowOwner, boolean retainCreation) + throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException + { + Map rowStripped = new CaseInsensitiveHashMap<>(row.size()); + + // Flip the key/value pairs around for easy lookup + Map queryToDb = new CaseInsensitiveHashMap<>(); + for (Map.Entry entry : _columnMapping.entrySet()) + { + queryToDb.put(entry.getValue(), entry.getKey()); + } + + setSpecialColumns(container, row, user, UpdatePermission.class); + + Map tableAliasesMap = _tableMapSupplier.get(); + Map> colFrequency = new HashMap<>(); + + //resolve passed in row including columns in the table and other properties (vocabulary properties) not in the Domain/table + for (Map.Entry entry: row.entrySet()) + { + if (!rowStripped.containsKey(entry.getKey())) + { + ColumnInfo col = getQueryTable().getColumn(entry.getKey()); + + if (null == col) + { + col = tableAliasesMap.get(entry.getKey()); + } + + if (null != col) + { + final String name = col.getName(); + + // Skip readonly and wrapped columns. The wrapped column is usually a pk column and can't be updated. + if (col.isReadOnly() || col.isCalculated()) + continue; + + //when updating a row, we should strip the following fields, as they are + //automagically maintained by the table layer, and should not be allowed + //to change once the record exists. + //unfortunately, the Table.update() method doesn't strip these, so we'll + //do that here. + // Owner, CreatedBy, Created, EntityId + if ((!retainCreation && (name.equalsIgnoreCase("CreatedBy") || name.equalsIgnoreCase("Created"))) + || (!allowOwner && name.equalsIgnoreCase("Owner")) + || name.equalsIgnoreCase("EntityId")) + continue; + + // Throw error if more than one row properties having different values match up to the same column. + if (!colFrequency.containsKey(col)) + { + colFrequency.put(col, Pair.of(entry.getKey(),entry.getValue())); + } + else + { + if (!Objects.equals(colFrequency.get(col).second, entry.getValue())) + { + throw new ValidationException("Property key - " + colFrequency.get(col).first + " and " + entry.getKey() + " matched for the same column."); + } + } + + // We want a map using the DbTable column names as keys, so figure out the right name to use + String dbName = queryToDb.getOrDefault(name, name); + rowStripped.put(dbName, entry.getValue()); + } + } + } + + convertTypes(user, container, rowStripped); + validateUpdateRow(rowStripped); + + if (row.get("container") != null) + { + Container rowContainer = UserSchema.translateRowSuppliedContainer(row.get("container"), container, user, getQueryTable(), UpdatePermission.class, null); + if (rowContainer == null) + { + throw new ValidationException("Unknown container: " + row.get("container")); + } + else + { + Container oldContainer = UserSchema.translateRowSuppliedContainer(new CaseInsensitiveHashMap<>(oldRow).get("container"), container, user, getQueryTable(), UpdatePermission.class, null); + if (null != oldContainer && !rowContainer.equals(oldContainer)) + throw new UnauthorizedException("The row is from the wrong container."); + } + } + + Map updatedRow = _update(user, container, rowStripped, oldRow, oldRow == null ? getKeys(row, container) : getKeys(oldRow, container)); + + //when passing a map for the row, the Table layer returns the map of fields it updated, which excludes + //the primary key columns as well as those marked read-only. So we can't simply return the map returned + //from Table.update(). Instead, we need to copy values from updatedRow into row and return that. + row.putAll(updatedRow); + return row; + } + + protected void validateValue(ColumnInfo column, Object value, Object providedValue) throws ValidationException + { + DomainProperty dp = getDomain() == null ? null : getDomain().getPropertyByName(column.getColumnName()); + List validators = ColumnValidators.create(column, dp); + for (ColumnValidator v : validators) + { + String msg = v.validate(-1, value, _validatorContext, providedValue); + if (msg != null) + throw new ValidationException(msg, column.getName()); + } + } + + protected void validateInsertRow(Map row) throws ValidationException + { + for (ColumnInfo col : getQueryTable().getColumns()) + { + Object value = row.get(col.getColumnName()); + + // Check required values aren't null or empty + if (null == value || value instanceof String s && s.isEmpty()) + { + if (!col.isAutoIncrement() && col.isRequired() && + !getAutoPopulatedColumns().contains(col.getName()) && + col.getJdbcDefaultValue() == null) + { + throw new ValidationException("A value is required for field '" + col.getName() + "'", col.getName()); + } + } + else + { + validateValue(col, value, null); + } + } + } + + protected void validateUpdateRow(Map row) throws ValidationException + { + for (ColumnInfo col : getQueryTable().getColumns()) + { + // Only validate incoming values + if (row.containsKey(col.getColumnName())) + { + Object value = row.get(col.getColumnName()); + validateValue(col, value, null); + } + } + } + + protected Map _update(User user, Container c, Map row, Map oldRow, Object[] keys) + throws SQLException, ValidationException + { + assert(getQueryTable().supportsInsertOption(InsertOption.UPDATE)); + + try + { + ColumnInfo objectUriCol = getObjectUriColumn(); + Domain domain = getDomain(); + + // The lsid may be null for the row until a property has been inserted + String lsid = null; + if (objectUriCol != null) + lsid = (String) oldRow.get(objectUriCol.getName()); + + List tableProperties = new ArrayList<>(); + if (objectUriCol != null && domain != null && !domain.getProperties().isEmpty()) + { + // convert "Property name"->value map into PropertyURI->value map + Map newValues = new CaseInsensitiveMapWrapper<>(new HashMap<>()); + + for (PropertyColumn pc : getPropertyColumns()) + { + PropertyDescriptor pd = pc.getPropertyDescriptor(); + tableProperties.add(pd); + + // clear out the old value if it exists and is contained in the new row (it may be incoming as null) + if (lsid != null && (hasProperty(row, pd) && hasProperty(oldRow, pd))) + OntologyManager.deleteProperty(lsid, pd.getPropertyURI(), getDomainObjContainer(c), getDomainContainer(c)); + + Object value = getPropertyValue(row, pd); + if (value != null) + newValues.put(pd.getPropertyURI(), value); + } + + // Note: copy lsid into newValues map so it will be found by the ImportHelper.beforeImportObject() + newValues.put(objectUriCol.getName(), lsid); + + LsidCollector collector = new LsidCollector(); + OntologyManager.insertTabDelimited(getDomainObjContainer(c), user, null, new ImportHelper(), tableProperties, MapDataIterator.of(Collections.singletonList(newValues)).getDataIterator(new DataIteratorContext()), true, collector); + + // Update the lsid in the row: the lsid may have not existed in the row before the update. + lsid = collector.getLsid(); + row.put(objectUriCol.getName(), lsid); + } + + // Get lsid value if it hasn't been set. + // This should only happen if the QueryUpdateService doesn't have a DomainUpdateHelper (DataClass and SampleType) + if (lsid == null && getQueryTable() instanceof UpdateableTableInfo updateableTableInfo) + { + String objectUriColName = updateableTableInfo.getObjectURIColumnName(); + if (objectUriColName != null) + lsid = (String) row.getOrDefault(objectUriColName, oldRow.get(objectUriColName)); + } + + // handle vocabulary properties + if (lsid != null) + { + for (Map.Entry rowEntry : row.entrySet()) + { + String colName = rowEntry.getKey(); + Object value = rowEntry.getValue(); + + ColumnInfo col = getQueryTable().getColumn(colName); + if (col instanceof PropertyColumn propCol) + { + PropertyDescriptor pd = propCol.getPropertyDescriptor(); + if (pd.isVocabulary() && !tableProperties.contains(pd)) + { + OntologyManager.updateObjectProperty(user, c, pd, lsid, value, null, false); + } + } + } + } + } + catch (BatchValidationException e) + { + throw e.getLastRowError(); + } + + checkDuplicateUpdate(keys); + + return Table.update(user, getDbTable(), row, keys); // Cache-invalidation handled in caller (TreatmentManager.saveAssaySpecimen()) + } + + private static class LsidCollector implements OntologyManager.RowCallback + { + private String _lsid; + + @Override + public void rowProcessed(Map row, String lsid) + { + if (_lsid != null) + { + throw new IllegalStateException("Only expected a single LSID"); + } + _lsid = lsid; + } + + public String getLsid() + { + if (_lsid == null) + { + throw new IllegalStateException("No LSID returned"); + } + return _lsid; + } + } + + // Get value from row map where the keys are column names. + private Object getPropertyValue(Map row, PropertyDescriptor pd) + { + if (row.containsKey(pd.getName())) + return row.get(pd.getName()); + + if (row.containsKey(pd.getLabel())) + return row.get(pd.getLabel()); + + for (String alias : pd.getImportAliasSet()) + { + if (row.containsKey(alias)) + return row.get(alias); + } + + return null; + } + + // Checks a value exists in the row map (value may be null) + private boolean hasProperty(Map row, PropertyDescriptor pd) + { + if (row.containsKey(pd.getName())) + return true; + + if (row.containsKey(pd.getLabel())) + return true; + + for (String alias : pd.getImportAliasSet()) + { + if (row.containsKey(alias)) + return true; + } + + return false; + } + + @Override + protected Map deleteRow(User user, Container container, Map oldRowMap) throws QueryUpdateServiceException, SQLException, InvalidKeyException + { + if (oldRowMap == null) + return null; + + aliasColumns(_columnMapping, oldRowMap); + + if (container != null && getDbTable().getColumn("container") != null) + { + // UNDONE: 9077: check container permission on each row before delete + Container rowContainer = UserSchema.translateRowSuppliedContainer(new CaseInsensitiveHashMap<>(oldRowMap).get("container"), container, user, getQueryTable(), DeletePermission.class, null); + if (null != rowContainer && !container.equals(rowContainer)) + { + //Issue 15301: allow workbooks records to be deleted/updated from the parent container + if (container.allowRowMutationForContainer(rowContainer)) + container = rowContainer; + else + throw new UnauthorizedException("The row is from the container: " + rowContainer.getId() + " which does not allow deletes from the container: " + container.getPath()); + } + } + + _delete(container, oldRowMap); + return oldRowMap; + } + + protected void _delete(Container c, Map row) throws InvalidKeyException + { + ColumnInfo objectUriCol = getObjectUriColumn(); + if (objectUriCol != null) + { + String lsid = (String)row.get(objectUriCol.getName()); + if (lsid != null) + { + OntologyObject oo = OntologyManager.getOntologyObject(c, lsid); + if (oo != null) + OntologyManager.deleteProperties(c, oo.getObjectId()); + } + } + Table.delete(getDbTable(), getKeys(row, c)); + } + + // classes should override this method if they need to do more work than delete all the rows from the table + // this implementation will delete all rows from the table for the given container as well as delete + // any properties associated with the table + @Override + protected int truncateRows(User user, Container container) throws QueryUpdateServiceException, SQLException + { + // get rid of the properties for this table + if (null != getObjectUriColumn()) + { + SQLFragment lsids = new SQLFragment() + .append("SELECT t.").append(getObjectUriColumn().getColumnName()) + .append(" FROM ").append(getDbTable(), "t") + .append(" WHERE t.").append(getObjectUriColumn().getColumnName()).append(" IS NOT NULL"); + if (null != getDbTable().getColumn("container")) + { + lsids.append(" AND t.Container = ?"); + lsids.add(container.getId()); + } + + OntologyManager.deleteOntologyObjects(ExperimentService.get().getSchema(), lsids, container); + } + + // delete all the rows in this table, scoping to the container if the column + // is available + if (null != getDbTable().getColumn("container")) + return Table.delete(getDbTable(), SimpleFilter.createContainerFilter(container)); + + return Table.delete(getDbTable()); + } + + protected Object[] getKeys(Map map, Container container) throws InvalidKeyException + { + //build an array of pk values based on the table info + TableInfo table = getDbTable(); + List pks = table.getPkColumns(); + Object[] pkVals = new Object[pks.size()]; + + if (map == null || map.isEmpty()) + return pkVals; + + for (int idx = 0; idx < pks.size(); ++idx) + { + ColumnInfo pk = pks.get(idx); + Object pkValue = map.get(pk.getName()); + // Check the type and coerce if needed + if (pkValue != null && !pk.getJavaObjectClass().isInstance(pkValue)) + { + try + { + pkValue = pk.convert(pkValue); + } + catch (ConversionException ignored) { /* Maybe the database can do the conversion */ } + } + pkVals[idx] = pkValue; + if (null == pkVals[idx] && pk.getColumnName().equalsIgnoreCase("Container")) + { + pkVals[idx] = container; + } + if(null == pkVals[idx]) + { + throw new InvalidKeyException("Value for key field '" + pk.getName() + "' was null or not supplied!", map); + } + } + return pkVals; + } + + private Map _missingValues = null; + private Container _missingValuesContainer; + + protected boolean validMissingValue(Container c, String mv) + { + if (null == c) + return false; + if (null == _missingValues || !c.getId().equals(_missingValuesContainer.getId())) + { + _missingValues = MvUtil.getIndicatorsAndLabels(c); + _missingValuesContainer = c; + } + return _missingValues.containsKey(mv); + } + + final protected void convertTypes(User user, Container c, Map row) throws ValidationException + { + convertTypes(user, c, row, getDbTable(), null); + } + + // TODO Path->FileObject + // why is coerceTypes() in AbstractQueryUpdateService and convertTypes() in DefaultQueryUpdateService? + protected void convertTypes(User user, Container c, Map row, TableInfo t, @Nullable Path fileLinkDirPath) throws ValidationException + { + for (ColumnInfo col : t.getColumns()) + { + if (col.isMvIndicatorColumn()) + continue; + boolean isColumnPresent = row.containsKey(col.getName()) || col.isMvEnabled() && row.containsKey(col.getMvColumnName().getName()); + if (!isColumnPresent) + continue; + + Object value = row.get(col.getName()); + + /* NOTE: see MissingValueConvertColumn.convert() these methods should have similar behavior. + * If you update this code, check that code as well. */ + if (col.isMvEnabled()) + { + if (value instanceof String s && StringUtils.isEmpty(s)) + value = null; + + Object mvObj = row.get(col.getMvColumnName().getName()); + String mv = Objects.toString(mvObj, null); + if (StringUtils.isEmpty(mv)) + mv = null; + + if (null != mv) + { + if (!validMissingValue(c, mv)) + throw new ValidationException("Value is not a valid missing value indicator: " + mv); + } + else if (null != value) + { + String s = Objects.toString(value, null); + if (validMissingValue(c, s)) + { + mv = s; + value = null; + } + } + row.put(col.getMvColumnName().getName(), mv); + } + + value = convertColumnValue(col, value, user, c, fileLinkDirPath); + row.put(col.getName(), value); + } + } + + protected Object convertColumnValue(ColumnInfo col, Object value, User user, Container c, @Nullable Path fileLinkDirPath) throws ValidationException + { + // Issue 13951: PSQLException from org.labkey.api.query.DefaultQueryUpdateService._update() + // improve handling of conversion errors + try + { + if (PropertyType.FILE_LINK == col.getPropertyType()) + { + if ((value instanceof MultipartFile || value instanceof AttachmentFile)) + { + FileLike fl = (FileLike)_fileColumnValueMapping.saveFileColumnValue(user, c, fileLinkDirPath, col.getName(), value); + value = fl.toNioPathForRead().toString(); + } + return ExpDataFileConverter.convert(value); + } + return col.getConvertFn().convert(value); + } + catch (ConvertHelper.FileConversionException e) + { + throw new ValidationException(e.getMessage()); + } + catch (ConversionException e) + { + String type = ColumnInfo.getFriendlyTypeName(col.getJdbcType().getJavaClass()); + throw new ValidationException("Unable to convert value '" + value.toString() + "' to " + type, col.getName()); + } + catch (QueryUpdateServiceException e) + { + throw new ValidationException("Save file link failed: " + col.getName()); + } + } + + /** + * Override this method to alter the row before insert or update. + * For example, you can automatically adjust certain column values based on context. + * @param container The current container + * @param row The row data + * @param user The current user + * @param clazz A permission class to test + */ + protected void setSpecialColumns(Container container, Map row, User user, Class clazz) + { + if (null != container) + { + //Issue 15301: allow workbooks records to be deleted/updated from the parent container + if (row.get("container") != null) + { + Container rowContainer = UserSchema.translateRowSuppliedContainer(row.get("container"), container, user, getQueryTable(), clazz, null); + if (rowContainer != null && container.allowRowMutationForContainer(rowContainer)) + { + row.put("container", rowContainer.getId()); //normalize to container ID + return; //accept the row-provided value + } + } + row.put("container", container.getId()); + } + } + + public boolean hasAttachmentProperties() + { + Domain domain = getDomain(); + if (null != domain) + { + for (DomainProperty dp : domain.getProperties()) + if (null != dp && isAttachmentProperty(dp)) + return true; + } + return false; + } + + protected boolean isAttachmentProperty(@NotNull DomainProperty dp) + { + PropertyDescriptor pd = dp.getPropertyDescriptor(); + return PropertyType.ATTACHMENT.equals(pd.getPropertyType()); + } + + protected boolean isAttachmentProperty(String name) + { + DomainProperty dp = getDomain().getPropertyByName(name); + if (dp != null) + return isAttachmentProperty(dp); + return false; + } + + public static @Nullable String getKeyColumnAliasForUpdate(TableInfo tableInfo, @NotNull Map columnNameMap) + { + // Currently, SampleUpdateAddColumnsDataIterator and DataClassUpdateAddColumnsDataIterator is being called before a translator is invoked to + // remap column labels to columns (e.g., "Row Id" -> "RowId"). Due to this, we need to search the + // map of columns for the key column. + var rowIdAliases = ImportAliasable.Helper.createImportSet(tableInfo.getColumn(FieldKey.fromParts("RowId"))); + rowIdAliases.retainAll(columnNameMap.keySet()); + + if (rowIdAliases.size() == 1) + return rowIdAliases.iterator().next(); + if (rowIdAliases.isEmpty()) + return "Name"; + + return null; + } + +} diff --git a/experiment/package-lock.json b/experiment/package-lock.json index 52a797dff86..6cdba926b71 100644 --- a/experiment/package-lock.json +++ b/experiment/package-lock.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@labkey/build": "9.1.3", - "@labkey/test": "1.13.1", + "@labkey/test": "1.13.2-fb-dropCrossFolder.1", "@types/jest": "30.0.0", "@types/react": "18.3.27", "@types/react-dom": "18.3.7", @@ -3655,9 +3655,9 @@ } }, "node_modules/@labkey/test": { - "version": "1.13.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/test/-/@labkey/test-1.13.1.tgz", - "integrity": "sha512-Zom/IYzX8UxALFW3rWprL14kBSBgKKrbL6q64kz/WbEbceA6r/tKMs72bsTG4fp5R/adrZ7EUpitb1TICCXwpg==", + "version": "1.13.2-fb-dropCrossFolder.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/test/-/@labkey/test-1.13.2-fb-dropCrossFolder.1.tgz", + "integrity": "sha512-byxmuX+mVFUhK06QJvtUPsJEmxJohOTB7mxWWMfRwW4k2X282uklttVykSQ2Ft2b54XlELQs/md5YtGuKTlfQQ==", "dev": true, "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { diff --git a/experiment/package.json b/experiment/package.json index 934f5ad9bce..45353b1bfdb 100644 --- a/experiment/package.json +++ b/experiment/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@labkey/build": "9.1.3", - "@labkey/test": "1.13.1", + "@labkey/test": "1.13.2-fb-dropCrossFolder.1", "@types/jest": "30.0.0", "@types/react": "18.3.27", "@types/react-dom": "18.3.7", diff --git a/experiment/src/org/labkey/experiment/ExpDataIterators.java b/experiment/src/org/labkey/experiment/ExpDataIterators.java index f5f81746a2c..8c17afc5389 100644 --- a/experiment/src/org/labkey/experiment/ExpDataIterators.java +++ b/experiment/src/org/labkey/experiment/ExpDataIterators.java @@ -2673,27 +2673,22 @@ record TypeData( private final DataIteratorContext _context; private final boolean _isCrossType; - private final boolean _isCrossFolder; private final boolean _isSamples; private final ExpObject _dataType; private final Container _container; private final User _user; private Integer _typeColIndex = null; private String _typeColName = null; - private Integer _folderColIndex = null; // want to process the sample types in the order given in the original file, unless we have dependencies private final Map> _typeFolderDataMap = new TreeMap<>(); private final Map> _orderDependencies = new HashMap<>(); private final int _dataIdIndex; - private final FieldKey _dataKey; - private final boolean _dataKeyIsNumeric; private final Map> _idsPerType = new HashMap<>(); private final Map> _parentIdsPerType = new HashMap<>(); private final Map _containerMap = new CaseInsensitiveHashMap<>(); - private final boolean _isCrossFolderUpdate; private final TSVWriter _tsvWriter; - private MultiDataTypeCrossProjectDataIterator(DataIterator di, DataIteratorContext context, Container container, User user, boolean isCrossType, boolean isCrossFolder, ExpObject dataType, boolean isSamples) + private MultiDataTypeCrossProjectDataIterator(DataIterator di, DataIteratorContext context, Container container, User user, boolean isCrossType, ExpObject dataType, boolean isSamples) { super(di); _context = context; @@ -2702,14 +2697,11 @@ private MultiDataTypeCrossProjectDataIterator(DataIterator di, DataIteratorConte _dataType = dataType; _user = user; _isCrossType = isCrossType; - _isCrossFolder = isCrossFolder; Map map = DataIteratorUtil.createColumnNameMap(di); // Determine the dataId column { int index; - FieldKey dataKey; - boolean isNumeric; var foundId = RowId.namesAndLabels().stream() .filter(map::containsKey) @@ -2718,19 +2710,13 @@ private MultiDataTypeCrossProjectDataIterator(DataIterator di, DataIteratorConte if (foundId.isPresent()) { index = map.get(foundId.get()); - dataKey = RowId.fieldKey(); - isNumeric = true; } else { index = map.getOrDefault(Name.name(), -1); - dataKey = Name.fieldKey(); - isNumeric = false; } _dataIdIndex = index; - _dataKey = dataKey; - _dataKeyIsNumeric = isNumeric; } _tsvWriter = new TSVWriter() // Used to quote values with newline/tabs/quotes @@ -2743,8 +2729,6 @@ protected int write() }; _tsvWriter.setAdditionalQuotedChars(TSVWriter.BACKSLASH_CHAR_STRING); - _isCrossFolderUpdate = isCrossFolder && context.getInsertOption().updateOnly; - if (_isCrossType && _isSamples) //cross type only supported for samples { SAMPLE_TYPE_FIELD_NAMES.forEach(name -> { @@ -2759,51 +2743,6 @@ protected int write() if (_typeColIndex == null) _context.getErrors().addRowError(new ValidationException("Could not determine sample type. Please provide a 'Sample Type' column in the data.")); } - - if (_isCrossFolder) - { - CONTAINER_FIELD_NAMES.forEach(name -> { - if (map.get(name) != null) - { - if (_folderColIndex != null) - _context.getErrors().addRowError(new ValidationException("Only one of [" + CONTAINER_FIELD_NAMES.stream().sorted().collect(Collectors.joining(", ")) + "] allowed for import.")); - _folderColIndex = map.get(name); - } - }); - - if (_folderColIndex != null || _isCrossFolderUpdate) - { - ContainerFilter cf; - if (container.isProductFoldersEnabled()) - { - // Note that this is slightly different from our treatment of lookups: - // - when in a project, we allow import or update to all subfolders, - // - when in a folder, we only allow references to data up the folder tree - if (container.isProject()) - cf = new ContainerFilter.AllInProjectPlusShared(container, user); - else - cf = new ContainerFilter.CurrentPlusProjectAndShared(container, user); - } - else - cf = ContainerFilter.current(container, user); - - Collection validContainerIds; - if (cf instanceof ContainerFilter.ContainerFilterWithPermission cfp) - validContainerIds = cfp.generateIds(container, context.getInsertOption().allowUpdate ? UpdatePermission.class : InsertPermission.class, null); - else - validContainerIds = cf.getIds(); - - if (validContainerIds != null) - { - for (GUID containerId : validContainerIds) - { - Container validContainer = ContainerManager.getForId(containerId); - _containerMap.put(validContainer.getId(), validContainer); - _containerMap.put(validContainer.getName(), validContainer); // for multi-type import, container column lookup is not yet resolved - } - } - } - } } private int _importSplitFile(TypeData typeData, File splitFile, Container dataContainer, TableInfo dataTable) @@ -2844,32 +2783,6 @@ private int _importPartition(TypeData typeData) if (_context.getErrors().hasErrors()) return 0; - int totalRowCount = 0; - if (_isCrossFolderUpdate && !typeData.folderFiles.keySet().isEmpty()) - { - boolean hasCrossFolderData = typeData.folderFiles.keySet().stream().anyMatch(id -> id != _container.getRowId()); - - if (hasCrossFolderData) - { - for (Map.Entry containerSplitFile : typeData.folderFiles.entrySet()) - { - Container splitContainer = ContainerManager.getForRowId(containerSplitFile.getKey()); - AbstractExpSchema schema = _isSamples ? new SamplesSchema(_user, splitContainer) : new DataClassUserSchema(splitContainer, _user); - QueryDefinition qDef = schema.getQueryDefForTable(typeData.dataType.getName()); - setContainerFilterForImport(qDef, splitContainer, _user); - TableInfo dataTable = qDef.getTable(schema, new ArrayList<>(), true); - - if (dataTable == null) - { - _context.getErrors().addRowError(new ValidationException("Table for " + (_isSamples ? "sample type" : "dataclass") + " '" + typeData.dataType.getName() + "' not found.")); - return totalRowCount; - } - totalRowCount += _importSplitFile(typeData, containerSplitFile.getValue(), splitContainer, dataTable); - } - return totalRowCount; - } - } - return _importSplitFile(typeData, typeData.dataFile, typeData.container, typeData.tableInfo); } @@ -2887,16 +2800,12 @@ public boolean next() throws BatchValidationException if (!_context.getErrors().hasErrors()) { _context.setCrossTypeImport(false); - _context.setCrossFolderImport(false); _context.putConfigParameter(QueryUpdateService.ConfigParameters.ProcessingPartition, true); - boolean hasCrossFolderImport = false; - // process the individual files for (String key : importOrderKeys) { Map typeFolderData = _typeFolderDataMap.get(key); - hasCrossFolderImport = hasCrossFolderImport || typeFolderData.keySet().size() > 1; for (TypeData typeData : typeFolderData.values()) { writeRowsToFile(typeData); // write the last rows that have been collected since the last write, if any @@ -2905,12 +2814,8 @@ public boolean next() throws BatchValidationException } } - if (_isCrossFolder && !_context.getInsertOption().updateOnly && hasCrossFolderImport) // all updates are cross-folder due to lack of Container column - SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, _isSamples ? "sampleImport" : "dataClassImport", "multiFolderImport"); - _context.putConfigParameter(QueryUpdateService.ConfigParameters.ProcessingPartition, false); _context.setCrossTypeImport(_isCrossType); - _context.setCrossFolderImport(_isCrossFolder); } return false; @@ -2935,21 +2840,6 @@ public boolean next() throws BatchValidationException _context.getErrors().addRowError(new ValidationException("No value provided for '" + _typeColName + "'.")); else { - // Issue 52626 and Issue 52609 - don't check folders during update - if (_isCrossFolder && _folderColIndex != null && !_context.getInsertOption().updateOnly) - { - String rowFolderId = StringUtils.trim((String) get(_folderColIndex)); - if (!StringUtils.isEmpty(rowFolderId)) - { - targetContainer = _containerMap.get(rowFolderId); - if (targetContainer == null) - { - _context.getErrors().addRowError(new ValidationException(String.format(INVALID_FOLDER_MESSAGE, rowFolderId, _container.getName()))); - return true; - } - } - } - Map typeFolderMap = _typeFolderDataMap.computeIfAbsent(typeName, k -> new LinkedHashMap<>()); typeFolderData = typeFolderMap.get(targetContainer.getId()); if (typeFolderData == null) @@ -3265,23 +3155,6 @@ private void addDataRow(TypeData typeData) { String dataString = data.toString(); _idsPerType.computeIfAbsent(typeData.dataType.getName(), k -> new HashSet<>()).add(dataString); - if (_isCrossFolderUpdate) - { - if (_dataKeyIsNumeric) - { - try - { - typeData.dataIds.add(JdbcType.BIGINT.convert(data)); - } - catch (ConversionException e) - { - _context.getErrors().addRowError(new ValidationException(e.getMessage() + " on row " + get(0), _dataKey.getName())); - return; - } - } - else - typeData.dataIds.add(dataString); - } } // if the data represents a derivation dependency between types, and we're creating ids within the file, @@ -3296,11 +3169,6 @@ private void addDataRow(TypeData typeData) _orderDependencies.computeIfAbsent(typeData.dataType.getName(), i -> new HashSet<>()).add(parentTypeName); } } - else if (index == _dataIdIndex && _isCrossFolderUpdate) - { - // Issue 52922: Samples with blank sample id in the file are getting ignored - throw new IllegalArgumentException(_dataKey.getName() + " value not provided on row " + get(0)); - } }); typeData.dataRows.add(StringUtils.join(dataRow, "\t")); } @@ -3310,90 +3178,6 @@ private void writeRowsToFile(TypeData typeData) if (typeData.dataRows.isEmpty()) return; - // for cross-folder import, write to further partitions - if (_isCrossFolderUpdate) - { - ExpObject dataType = typeData.dataType; - Map> containerRows = new HashMap<>(); - - TableInfo tableInfo; - SimpleFilter filter; - - if (_isSamples) - { - filter = new SimpleFilter(MaterialSourceId.fieldKey(), dataType.getRowId()); - filter.addCondition(_dataKey, typeData.dataIds, CompareType.IN); - tableInfo = ExperimentService.get().getTinfoMaterial(); - } - else - { - filter = new SimpleFilter(FieldKey.fromParts("ClassId"), dataType.getRowId()); - filter.addCondition(_dataKey, typeData.dataIds, CompareType.IN); - tableInfo = ExperimentService.get().getTinfoData(); - } - - Map[] rows = new TableSelector(tableInfo, Set.of(_dataKey.getName(), "container"), filter, null).getMapArray(); - - Set notFoundIds = new HashSet<>(typeData.dataIds); - for (Map row : rows) - { - Object raw = row.get(_dataKey.getName()); - Object identifier = _dataKeyIsNumeric ? asLong(raw) : raw; - notFoundIds.remove(identifier); - String dataContainer = (String) row.get("container"); - // could be updating the same data multiple times in a single import, the import will later be rejected - List dataRowIds = - IntStream.range(0, typeData.dataIds.size()).boxed() - .filter(i -> typeData.dataIds.get(i).equals(identifier)) - .toList(); - containerRows.computeIfAbsent(dataContainer, k -> new ArrayList<>()).addAll(dataRowIds); - } - if (!notFoundIds.isEmpty()) - { - _context.getErrors().addRowError(new ValidationException((_isSamples ? "Samples" : "Data") + " not found for " + StringUtils.join(notFoundIds, ", "))); - return; - } - - for (String containerId : containerRows.keySet()) - { - Container container = _containerMap.get(containerId); - if (container == null) - { - Container folder = ContainerManager.getForId(containerId); - _context.getErrors().addRowError(new ValidationException(String.format(INVALID_FOLDER_MESSAGE, (folder != null ? folder.getName() : containerId), _container.getName()))); - return; - } - - int containerRowId = container.getRowId(); - File splitFile = typeData.folderFiles.get(containerRowId); - - if (splitFile == null) - { - splitFile = writeSplitFile(typeData.dataType.getName(), "~containerSplit~", containerRowId + "-" + typeData.dataFile.getName(), typeData.headerRow); - if (splitFile == null) - return; - typeData.folderFiles.put(containerRowId, splitFile); - } - - List dataRows = new ArrayList<>(); - List dataRowIndexes = containerRows.get(containerId); - Collections.sort(dataRowIndexes); - for (Integer dataRowIndex : dataRowIndexes) - dataRows.add(typeData.dataRows.get(dataRowIndex)); - - try (FileWriter writer = new FileWriter(splitFile, true)) - { - writer.write(StringUtils.join(dataRows, System.lineSeparator())); - writer.write(System.lineSeparator()); // Issue 48442: add a new line to the end so the next written rows start on a new line - } - catch (IOException e) - { - _context.getErrors().addRowError(new ValidationException("Unable to write data for '" + typeData.dataType.getName() + "'.")); - return; - } - } - } - try (FileWriter writer = new FileWriter(typeData.dataFile, true)) { writer.write(StringUtils.join(typeData.dataRows, System.lineSeparator())); @@ -3414,17 +3198,15 @@ public static class MultiDataTypeCrossProjectDataIteratorBuilder implements Data private final Container _container; private final User _user; private final boolean _isCrossType; - private final boolean _isCrossFolder; private final ExpObject _dataType; private final boolean _isSamples; - public MultiDataTypeCrossProjectDataIteratorBuilder(@NotNull User user, @NotNull Container container, @NotNull DataIteratorBuilder in, boolean isCrossType, boolean isCrossFolder, ExpObject dataType, boolean isSamples) + public MultiDataTypeCrossProjectDataIteratorBuilder(@NotNull User user, @NotNull Container container, @NotNull DataIteratorBuilder in, boolean isCrossType, ExpObject dataType, boolean isSamples) { _in = in; _container = container; _user = user; _isCrossType = isCrossType; - _isCrossFolder = isCrossFolder; _dataType = dataType; _isSamples = isSamples; } @@ -3436,7 +3218,7 @@ public DataIterator getDataIterator(DataIteratorContext context) if (di == null) return null; // can happen if context has errors - return LoggingDataIterator.wrap(new MultiDataTypeCrossProjectDataIterator(di, context, _container, _user, _isCrossType, _isCrossFolder, _dataType, _isSamples)); + return LoggingDataIterator.wrap(new MultiDataTypeCrossProjectDataIterator(di, context, _container, _user, _isCrossType, _dataType, _isSamples)); } } diff --git a/experiment/src/org/labkey/experiment/api/ExpDataClassDataTableImpl.java b/experiment/src/org/labkey/experiment/api/ExpDataClassDataTableImpl.java index 2c47efa7184..b3d10fdfb70 100644 --- a/experiment/src/org/labkey/experiment/api/ExpDataClassDataTableImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExpDataClassDataTableImpl.java @@ -991,13 +991,7 @@ public DataIterator getDataIterator(DataIteratorContext context) { String name = input.getColumnInfo(i).getName(); - boolean isContainerField = name.equalsIgnoreCase("Container") || name.equalsIgnoreCase("Folder"); - if (isContainerField) - { - if (isUpdate || !context.isCrossFolderImport()) - drop.add(name); - } - else if (ExpDataTable.Column.Name.name().equalsIgnoreCase(name)) + if (ExpDataTable.Column.Name.name().equalsIgnoreCase(name)) { keysCheck.add(ExpDataTable.Column.Name.name()); } @@ -1227,20 +1221,6 @@ public int importRows(User user, Container container, DataIteratorBuilder rows, return _importRowsUsingDIB(user, container, rows, null, getDataIteratorContext(errors, InsertOption.IMPORT, configParameters), extraScriptContext); } - @Override - public int loadRows(User user, Container container, DataIteratorBuilder rows, DataIteratorContext context, @Nullable Map extraScriptContext) - { - try - { - configureCrossFolderImport(rows, context); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - return super.loadRows(user, container, rows, context, extraScriptContext); - } - @Override public int mergeRows(User user, Container container, DataIteratorBuilder rows, BatchValidationException errors, @Nullable Map configParameters, Map extraScriptContext) { @@ -1542,9 +1522,6 @@ protected void preImportDIBValidation(@Nullable DataIteratorBuilder in, @Nullabl @Override public DataIteratorBuilder createImportDIB(User user, Container container, DataIteratorBuilder data, DataIteratorContext context) { - if (context.isCrossFolderImport()) - return new ExpDataIterators.MultiDataTypeCrossProjectDataIteratorBuilder(user, container, data, context.isCrossTypeImport(), context.isCrossFolderImport(), _dataClass, false); - StandardDataIteratorBuilder standard = StandardDataIteratorBuilder.forInsert(getQueryTable(), data, container, user, context); DataIteratorBuilder dib = ((UpdateableTableInfo)getQueryTable()).persistRows(standard, context); dib = AttachmentDataIterator.getAttachmentDataIteratorBuilder(getQueryTable(), dib, user, context.getInsertOption().batch ? getAttachmentDirectory() : null, diff --git a/experiment/src/org/labkey/experiment/api/SampleTypeUpdateServiceDI.java b/experiment/src/org/labkey/experiment/api/SampleTypeUpdateServiceDI.java index 92510061fc5..1834ffd3df8 100644 --- a/experiment/src/org/labkey/experiment/api/SampleTypeUpdateServiceDI.java +++ b/experiment/src/org/labkey/experiment/api/SampleTypeUpdateServiceDI.java @@ -398,8 +398,8 @@ protected Map extractProvidedAmountsAndUnits(@NotNull Map - { - @Override - public VBox getView(Object o, BindException errors) - { - VBox result = new VBox(); - - VBox runListView = createRunListView(20); - result.addView(runListView); - - RunGroupWebPart runGroups = new RunGroupWebPart(getViewContext(), false); - runGroups.showHeader(); - result.addView(runGroups); - - result.addView(new ProtocolWebPart(false, getViewContext())); - result.addView(new SampleTypeWebPart(false, getViewContext())); - result.addView(new DataClassWebPart(false, getViewContext(), null)); - - return result; - } - - @Override - public void addNavTrail(NavTree root) - { - addRootNavTrail(root); - root.addChild("Experiment"); - } - } - - @RequiresPermission(ReadPermission.class) - public class ShowRunsAction extends SimpleViewAction - { - @Override - public VBox getView(Object o, BindException errors) - { - return createRunListView(100); - } - - @Override - public void addNavTrail(NavTree root) - { - addRootNavTrail(root); - root.addChild("Experiment Runs"); - } - } - - private VBox createRunListView(int defaultMaxRows) - { - Set types = ExperimentService.get().getExperimentRunTypes(getContainer()); - ChooseExperimentTypeBean bean = new ChooseExperimentTypeBean(types, ExperimentRunType.getSelectedFilter(types, getViewContext().getRequest().getParameter("experimentRunFilter")), getViewContext().getActionURL().clone(), Collections.emptyList()); - JspView chooserView = new JspView<>("/org/labkey/experiment/experimentRunQueryHeader.jsp", bean); - - ExperimentRunListView view = ExperimentService.get().createExperimentRunWebPart(getViewContext(), bean.getSelectedFilter()); - view.setFrame(WebPartView.FrameType.NONE); - - // When paginated and the user hasn't explicitly set a maxRows, use the default maxRows size. - QuerySettings settings = view.getSettings(); - if (!settings.isMaxRowsSet() && settings.getShowRows() == ShowRows.PAGINATED) - { - settings.setMaxRows(defaultMaxRows); - } - - VBox result = new VBox(chooserView, view); - result.setFrame(WebPartView.FrameType.PORTAL); - return result; - } - - @RequiresPermission(ReadPermission.class) - @ActionNames("showRunGroups, showExperiments") - public class ShowRunGroupsAction extends SimpleViewAction - { - @Override - public ModelAndView getView(Object o, BindException errors) - { - RunGroupWebPart webPart = new RunGroupWebPart(getViewContext(), false); - webPart.setFrame(WebPartView.FrameType.NONE); - return webPart; - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("runGroups"); - addRootNavTrail(root); - root.addChild("Run Groups"); - } - } - - public record Field(String domainURI, String domainName, String name, Container container) {} - public record MiniExpObject(Object rowId, String name) {} - public record TimelineSummary(MiniExpObject miniExpObject, String mostRecentValue) {} - public record ProblemType(String tableName, String fieldName, String pkName) { - public Object toHtml(List summaries) - { - return DOM.DIV( - DOM.H4(tableName), - DOM.TABLE(at(cl("table-condensed", "labkey-data-region", "table-bordered")), - DOM.THEAD(DOM.TH(pkName), DOM.TH(fieldName)), - summaries.stream().map(summary -> - DOM.TR(DOM.TD(summary.miniExpObject.name), DOM.TD(summary.mostRecentValue))) - )); - } - } - - @RequiresPermission(SiteAdminPermission.class) - public static class ReportLostFieldValuesAction extends SimpleViewAction - { - @Override - public ModelAndView getView(Object o, BindException errors) - { - // Find all the fields that could have lost data due to issue 52666 - TableInfo t = new ExpSchema(getUser(), ContainerManager.getRoot()).getTable(ExpSchema.TableType.Fields.name(), ContainerFilter.getUnsafeEverythingFilter()); - List fields = new TableSelector(t, - new SimpleFilter(FieldKey.fromParts("StorageColumnNameMatch"), false). - addCondition(FieldKey.fromParts("DomainURI"), ":AssayDomain-Data.", CompareType.DOES_NOT_CONTAIN), - null). - getArrayList(Field.class); - - // Prep audit table for querying - UserSchema auditSchema = AuditLogService.get().createSchema(getUser(), ContainerManager.getRoot()); - - Map> sampleTypeSummaries = new HashMap<>(); - Map> dataClassSummaries = new HashMap<>(); - Map> listSummaries = new HashMap<>(); - - Map> problematicFields = new LinkedHashMap<>(); - - for (Field field : fields) - { - String domainURI = field.domainURI; - String fieldName = field.name; - Container container = field.container; - Domain domain = PropertyService.get().getDomain(container, domainURI); - if (domain != null && domain.getDomainKind() != null) - { - TableInfo table = domain.getDomainKind().getTableInfo(getUser(), container, domain, ContainerFilter.getUnsafeEverythingFilter()); - - if (table != null) - { - // Drill into sample types - if (domain.getDomainKind().getClass().equals(SampleTypeDomainKind.class)) - { - // rows that currently have no value for the field with potential for data loss - List rowsWithNull = new TableSelector(table, - new HashSet<>(List.of("RowId", "Name")), - new SimpleFilter(new CompareType.CompareClause(FieldKey.fromParts(fieldName), CompareType.ISBLANK, null)), - null). - getArrayList(MiniExpObject.class); - - List fixupsNeeded = checkData( - rowsWithNull, - fieldName, - obj -> new SimpleFilter(FieldKey.fromParts("SampleId"), obj.rowId), - auditSchema.getTable(SampleTimelineAuditEvent.EVENT_TYPE, ContainerFilter.getUnsafeEverythingFilter())); - if (!fixupsNeeded.isEmpty()) - { - sampleTypeSummaries.put(new ProblemType(table.getName(), fieldName, "SampleID"), fixupsNeeded); - } - } - // and data classes/sample sources - if (domain.getDomainKind().getClass().equals(DataClassDomainKind.class)) - { - // rows samples that current have no value for the field with potential for data loss - List rowsWithNull = new TableSelector(table, - new HashSet<>(List.of("RowId", "Name")), - new SimpleFilter(new CompareType.CompareClause(FieldKey.fromParts(fieldName), CompareType.ISBLANK, null)), - null). - getArrayList(MiniExpObject.class); - - List fixupsNeeded = checkData( - rowsWithNull, - fieldName, - obj -> new SimpleFilter(FieldKey.fromParts("RowPk"), Objects.toString(obj.rowId)). - addCondition(FieldKey.fromParts("SchemaName"), "exp.data"). - addCondition(FieldKey.fromParts("QueryName"), domain.getName()), - auditSchema.getTable("QueryUpdateAuditEvent", ContainerFilter.getUnsafeEverythingFilter())); - - if (!fixupsNeeded.isEmpty()) - { - dataClassSummaries.put(new ProblemType(table.getName(), fieldName, "SourceID"), fixupsNeeded); - } - } - // and lists - if ("lists".equals(table.getUserSchema().getName())) - { - // rows samples that current have no value for the field with potential for data loss - List rowsWithNull = new ArrayList<>(); - - ColumnInfo entityIdCol = table.getColumn("EntityId"); - ColumnInfo pkCol = table.getPkColumns().getFirst(); - - new TableSelector(table, - List.of(entityIdCol, pkCol), - new SimpleFilter(new CompareType.CompareClause(FieldKey.fromParts(fieldName), CompareType.ISBLANK, null)), - null). - forEachResults(r -> - { - Object entityId = entityIdCol.getValue(r); - Object pk = pkCol.getValue(r); - rowsWithNull.add(new MiniExpObject(entityId, pk.toString())); - }); - - - List fixupsNeeded = checkData( - rowsWithNull, - fieldName, - obj -> new SimpleFilter(FieldKey.fromParts("ListItemEntityId"), obj.rowId), - auditSchema.getTable("ListAuditEvent", ContainerFilter.getUnsafeEverythingFilter())); - - if (!fixupsNeeded.isEmpty()) - { - listSummaries.put(new ProblemType(table.getName(), fieldName, table.getPkColumnNames().getFirst()), fixupsNeeded); - } - } - - long totalRows = new TableSelector(table).getRowCount(); - long emptyRows = new TableSelector(table, new SimpleFilter(new CompareType.CompareClause(FieldKey.fromParts(fieldName), CompareType.ISBLANK, null)), null).getRowCount(); - problematicFields.put(field, Pair.of(totalRows, emptyRows)); - } - else - { - problematicFields.put(field, Pair.of(null, null)); - } - } - } - - return new HtmlView("Fixups Needed", - DOM.createHtmlFragment( - DOM.H2("Potentially Problematic Fields"), - problematicFields.isEmpty() ? "No problematic fields detected!" : - DOM.TABLE(at(cl("table-condensed", "labkey-data-region", "table-bordered")), - DOM.THEAD(DOM.TH("Domain Name"), DOM.TH("Domain URI"), DOM.TH("Field Name"), DOM.TH("Container"), DOM.TH("Total Rows"), DOM.TH("Rows with Nulls")), - problematicFields.entrySet().stream().map(e -> { - Field f = e.getKey(); - Pair counts = e.getValue(); - return DOM.TR( - DOM.TD(f.domainName), - DOM.TD(f.domainURI), - DOM.TD(f.name), - DOM.TD(f.container.getPath()), - DOM.TD(counts.first), - DOM.TD(counts.second) - ); - } - )), - - DOM.H2("Sample Types"), - sampleTypeSummaries.isEmpty() ? "No problems detected!" : - sampleTypeSummaries.entrySet().stream().map(e -> - e.getKey().toHtml(e.getValue())), - - DOM.H2("Data Classes"), - dataClassSummaries.isEmpty() ? "No problems detected!" : - dataClassSummaries.entrySet().stream().map(e -> - e.getKey().toHtml(e.getValue())), - - DOM.H2("Lists"), - listSummaries.isEmpty() ? "No problems detected!" : - listSummaries.entrySet().stream().map(e -> - e.getKey().toHtml(e.getValue())) - )); - } - - @NotNull - private List checkData(List rowsWithNull, String fieldName, Function filterGenerator, TableInfo auditTable) - { - List fixupsNeeded = new ArrayList<>(); - - // For each sample without a value today, check the audit history - for (MiniExpObject row : rowsWithNull) - { - // Order by RowId to get them in the sequence they happened in - var events = new TableSelector(auditTable, filterGenerator.apply(row), new Sort("RowId")).getArrayList(DetailedAuditTypeEvent.class); - // Remember the most recently set value - String mostRecentValue = null; - for (DetailedAuditTypeEvent event : events) - { - Map newValues = new CaseInsensitiveHashMap<>(AbstractAuditTypeProvider.decodeFromDataMap(event.getNewRecordMap())); - if (newValues.containsKey(fieldName)) - { - // Will be the empty string if the value was intentionally set to blank - mostRecentValue = newValues.get(fieldName); - } - } - // If the value had been set before, and its most recent insert/update wasn't setting it blank, - // it's most likely a lost value - if (mostRecentValue != null && !mostRecentValue.isEmpty()) - { - fixupsNeeded.add(new TimelineSummary(row, mostRecentValue)); - } - } - return fixupsNeeded; - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Accidentally Nulled Field Report"); - } - } - - @RequiresPermission(ReadPermission.class) - public static class CreateHiddenRunGroupAction extends MutatingApiAction - { - @Override - public ApiResponse execute(SimpleApiJsonForm form, BindException errors) throws Exception - { - JSONObject json = form.getJsonObject(); - String selectionKey = json.optString("selectionKey", null); - List runs = new ArrayList<>(); - - // Accept either an explicit list of run IDs - if (json.has("runIds")) - { - JSONArray runIds = json.getJSONArray("runIds"); - for (int i = 0; i < runIds.length(); i++) - { - ExpRunImpl run = ExperimentServiceImpl.get().getExpRun(runIds.getInt(i)); - if (run != null) - { - runs.add(run); - } - } - } - // Or a reference to a DataRegion selection key - else if (selectionKey != null) - { - Set ids = DataRegionSelection.getSelectedIntegers(getViewContext(), selectionKey, false); - for (Long id : ids) - { - ExpRunImpl run = ExperimentServiceImpl.get().getExpRun(id); - if (run != null) - { - runs.add(run); - } - } - } - if (runs.isEmpty()) - { - throw new NotFoundException(); - } - - ExpExperiment group = ExperimentService.get().createHiddenRunGroup(getContainer(), getUser(), runs.toArray(new ExpRun[0])); - if (selectionKey != null) - DataRegionSelection.clearAll(getViewContext(), selectionKey); - - ApiSimpleResponse response = new ApiSimpleResponse(); - response.putBean(group, "rowId", "LSID", "name", "hidden"); - return response; - } - } - - - @RequiresPermission(ReadPermission.class) - public class DetailsAction extends QueryViewAction - { - private ExpExperimentImpl _experiment; - - public DetailsAction() - { - super(ExpObjectForm.class); - } - - private Pair> createViews(ExpObjectForm form, BindException errors) - { - _experiment = ExperimentServiceImpl.get().getExpExperiment(form.getRowId()); - if (_experiment == null) - { - throw new NotFoundException("Could not find an experiment with RowId " + form.getRowId()); - } - - if (!_experiment.getContainer().equals(getContainer())) - { - throw new RedirectException(getViewContext().cloneActionURL().setContainer(_experiment.getContainer())); - } - - List protocols = _experiment.getAllProtocols(); - - Set types = new TreeSet<>(ExperimentService.get().getExperimentRunTypes(getContainer())); - ExperimentRunType selectedType = ExperimentRunType.getSelectedFilter(types, getViewContext().getRequest().getParameter("experimentRunFilter")); - - ChooseExperimentTypeBean bean = new ChooseExperimentTypeBean(types, selectedType, getViewContext().getActionURL().clone(), protocols); - JspView chooserView = new JspView<>("/org/labkey/experiment/experimentRunQueryHeader.jsp", bean, errors); - - ExperimentRunListView runListView = ExperimentRunListView.createView(getViewContext(), bean.getSelectedFilter(), true); - runListView.getRunTable().setExperiment(_experiment); - runListView.setShowRemoveFromExperimentButton(true); - runListView.setShowDeleteButton(true); - runListView.setShowAddToRunGroupButton(true); - runListView.setShowExportButtons(true); - runListView.setShowMoveRunsButton(true); - return new Pair<>(runListView, chooserView); - } - - @Override - protected ModelAndView getHtmlView(ExpObjectForm form, BindException errors) throws Exception - { - Pair> views = createViews(form, errors); - - CustomPropertiesView customPropertiesView = new CustomPropertiesView(_experiment.getLSID(), getContainer()); - - TableInfo runGroupsTable = new ExpSchema(getUser(), getContainer()).getTable(ExpSchema.TableType.RunGroups); - - DetailsView detailsView = new DetailsView(new DataRegion(), _experiment.getRowId()); - detailsView.getDataRegion().setTable(runGroupsTable); - detailsView.getDataRegion().addColumns(runGroupsTable, "RowId,Name,Created,Modified,Contact,ExperimentDescriptionURL,Hypothesis,Comments"); - detailsView.getDataRegion().getDisplayColumn(0).setVisible(false); - detailsView.getDataRegion().getDisplayColumn(2).setWidth("60%"); - - ButtonBar bb = new ButtonBar(); - bb.setStyle(ButtonBar.Style.separateButtons); - ActionButton b = new ActionButton(ExperimentUrlsImpl.get().getShowUpdateURL(_experiment), "Edit"); - b.setDisplayPermission(UpdatePermission.class); - bb.add(b); - detailsView.getDataRegion().setButtonBar(bb); - if (_experiment.getBatchProtocol() != null) - { - detailsView.setTitle("Batch Details"); - detailsView.getDataRegion().addColumns(runGroupsTable, "BatchProtocolId"); - } - else - { - detailsView.setTitle("Run Group Details"); - } - - VBox runsVBox = new VBox(views.second, createInitializedQueryView(form, errors, false, null)); - runsVBox.setTitle("Experiment Runs"); - runsVBox.setFrame(WebPartView.FrameType.PORTAL); - - return new VBox(new StandardAndCustomPropertiesView(detailsView, customPropertiesView), runsVBox); - } - - @Override - protected ExperimentRunListView createQueryView(ExpObjectForm form, BindException errors, boolean forExport, String dataRegion) - { - return createViews(form, errors).first; - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("runGroups"); - addRootNavTrail(root); - root.addChild("Run Groups", ExperimentUrlsImpl.get().getShowExperimentsURL(getContainer())); - root.addChild(_experiment.getName()); - } - } - - @RequiresPermission(ReadPermission.class) - public class ListSampleTypesAction extends SimpleViewAction - { - @Override - public ModelAndView getView(Object o, BindException errors) - { - SampleTypeWebPart view = new SampleTypeWebPart(false, getViewContext()); - view.setFrame(WebPartView.FrameType.NONE); - view.setErrorMessage(getViewContext().getRequest().getParameter("errorMessage")); - - return view; - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("sampleSets"); - addRootNavTrail(root); - root.addChild("Sample Types"); - } - } - - @RequiresPermission(ReadPermission.class) - public class ShowSampleTypeAction extends SimpleViewAction - { - private ExpSampleTypeImpl _sampleType; - - @Override - public ModelAndView getView(ExpObjectForm form, BindException errors) - { - _sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), form.getRowId(), true); - if (_sampleType == null && form.getLsid() != null) - { - if (form.getLsid().equalsIgnoreCase("Material") || form.getLsid().equalsIgnoreCase("Sample")) - { - // Not a real sample type - just show all the materials instead - throw new RedirectException(new ActionURL(ShowAllMaterialsAction.class, getContainer())); - } - // Check if the URL specifies the LSID, and stick the bean back into the form - _sampleType = SampleTypeServiceImpl.get().getSampleType(form.getLsid()); - } - - if (_sampleType == null) - { - throw new NotFoundException("No matching sample type found"); - } - - List allScopedSampleTypes = (List) SampleTypeService.get().getSampleTypes(getContainer(), true); - if (!allScopedSampleTypes.contains(_sampleType)) - { - ensureCorrectContainer(getContainer(), _sampleType, getViewContext()); - } - - SamplesSchema schema = new SamplesSchema(getUser(), getContainer()); - QuerySettings settings = schema.getSettings(getViewContext(), "Material", _sampleType.getName()); - QueryView queryView = new SampleTypeContentsView(_sampleType, schema, settings, errors); - - DetailsView detailsView = new DetailsView(getSampleTypeRegion(getViewContext()), _sampleType.getRowId()); - detailsView.getDataRegion().getDisplayColumn("Name").setURL((ActionURL)null); - detailsView.getDataRegion().getDisplayColumn("LSID").setVisible(false); - detailsView.getDataRegion().getDisplayColumn("MaterialLSIDPrefix").setVisible(false); - detailsView.getDataRegion().getDisplayColumn("LabelColor").setVisible(false); - detailsView.getDataRegion().getDisplayColumn("MetricUnit").setVisible(false); - detailsView.getDataRegion().getDisplayColumn("Category").setVisible(false); - - detailsView.setTitle("Sample Type Properties"); - detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).setStyle(ButtonBar.Style.separateButtons); - - Container autoLinkContainer = _sampleType.getAutoLinkTargetContainer(); - if (null != autoLinkContainer) - { - DisplayColumn autoLinkTargetColumn = detailsView.getDataRegion().getDisplayColumn("autoLinkTargetContainer"); - autoLinkTargetColumn.setVisible(false); - - SimpleDisplayColumn displayAutoLinkTargetColumn = new SimpleDisplayColumn(); - displayAutoLinkTargetColumn.setCaption("Auto Link Target Container:"); - String path = autoLinkContainer.getPath(); - displayAutoLinkTargetColumn.setDisplayHtml(path.equals("/") ? "" : path); - detailsView.getDataRegion().addDisplayColumn(displayAutoLinkTargetColumn); - } - - DisplayColumn autoLinkCategoryColumn = detailsView.getDataRegion().getDisplayColumn("autoLinkCategory"); - autoLinkCategoryColumn.setVisible(false); - SimpleDisplayColumn displayAutoLinkCategoryColumn = new SimpleDisplayColumn(); - displayAutoLinkCategoryColumn.setCaption("Auto Link Category:"); - displayAutoLinkCategoryColumn.setDisplayHtml(_sampleType.getAutoLinkCategory()); - detailsView.getDataRegion().addDisplayColumn(displayAutoLinkCategoryColumn); - - if (_sampleType.hasNameAsIdCol()) - { - SimpleDisplayColumn nameIdCol = new SimpleDisplayColumn(); - nameIdCol.setCaption("Has Name Id Column:"); - nameIdCol.setDisplayHtml("true"); - detailsView.getDataRegion().addDisplayColumn(nameIdCol); - } - - if (_sampleType.hasIdColumns()) - { - SimpleDisplayColumn idCols = new SimpleDisplayColumn(); - idCols.setCaption("Id Column(s):"); - String names = _sampleType.getIdCols().stream() - .filter(Objects::nonNull) - .map(DomainProperty::getName) - .collect(Collectors.joining(", ")); - if (!names.isEmpty()) - { - idCols.setDisplayHtml(PageFlowUtil.filter(names)); - detailsView.getDataRegion().addDisplayColumn(idCols); - } - } - - if (_sampleType.getParentCol() != null) - { - SimpleDisplayColumn parentCol = new SimpleDisplayColumn(PageFlowUtil.filter(_sampleType.getParentCol().getName())); - parentCol.setCaption("Parent Column:"); - detailsView.getDataRegion().addDisplayColumn(parentCol); - } - - try - { - SimpleDisplayColumn importAliasCol = new SimpleDisplayColumn(); - importAliasCol.setCaption("Parent Import Alias(es):"); - if (!_sampleType.getImportAliases().isEmpty()) - importAliasCol.setDisplayHtml(PageFlowUtil.filter(StringUtils.join(_sampleType.getImportAliases().keySet(), ", "))); - detailsView.getDataRegion().addDisplayColumn(importAliasCol); - } - catch (IOException e) - { - // unable to parse import alias map from JSON - } - - if (!getContainer().equals(_sampleType.getContainer())) - { - ActionURL definitionURL = urlProvider(ExperimentUrls.class).getShowSampleTypeURL(_sampleType); - SimpleDisplayColumn definedInCol = new SimpleDisplayColumn("" + - PageFlowUtil.filter(_sampleType.getContainer().getPath()) + - ""); - definedInCol.setCaption("Defined In:"); - detailsView.getDataRegion().addDisplayColumn(definedInCol); - } - - // Not all sample types can be edited - DomainKind domainKind = _sampleType.getDomain().getDomainKind(); - if (domainKind != null && domainKind.canEditDefinition(getUser(), _sampleType.getDomain())) - { - if (domainKind instanceof SampleTypeDomainKind) - { - ActionURL updateURL = new ActionURL(EditSampleTypeAction.class, _sampleType.getContainer()); - updateURL.addParameter("RowId", _sampleType.getRowId()); - updateURL.addReturnUrl(getViewContext().getActionURL()); - - if (!getContainer().equals(_sampleType.getContainer())) - { - String editLink = updateURL.toString(); - ActionButton updateButton = new ActionButton("Edit Type"); - updateButton.setActionType(ActionButton.Action.SCRIPT); - updateButton.setScript("if (window.confirm('This sample type is defined in the " + _sampleType.getContainer().getPath() + " folder. Would you still like to edit it?')) { window.location = '" + editLink + "' }"); - detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(updateButton); - } - else - { - ActionButton updateButton = new ActionButton(updateURL, "Edit Type", ActionButton.Action.LINK); - updateButton.setDisplayPermission(DesignSampleTypePermission.class); - updateButton.setPrimary(true); - detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(updateButton); - } - - ActionURL deleteURL = new ActionURL(DeleteSampleTypesAction.class, _sampleType.getContainer()); - deleteURL.addParameter("singleObjectRowId", _sampleType.getRowId()); - deleteURL.addReturnUrl(ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); - ActionButton deleteButton = new ActionButton(deleteURL, "Delete Type", ActionButton.Action.LINK); - deleteButton.setDisplayPermission(DesignSampleTypePermission.class); - detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(deleteButton); - } - else - { - ActionURL editURL = domainKind.urlEditDefinition(_sampleType.getDomain(), new ViewBackgroundInfo(_sampleType.getContainer(), getUser(), getViewContext().getActionURL())); - if (editURL != null) - { - editURL.addReturnUrl(getViewContext().getActionURL()); - ActionButton editTypeButton = new ActionButton(editURL, "Edit Fields"); - editTypeButton.setDisplayPermission(UpdatePermission.class); - detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(editTypeButton); - } - } - } - - if (_sampleType.canImportMoreSamples()) - { - TableInfo table = queryView.getTable(); - if (table != null) - { - ActionURL importURL = table.getImportDataURL(getContainer()); - if (importURL != null) - { - importURL = importURL.clone(); - importURL.addReturnUrl(getViewContext().getActionURL()); - ActionButton uploadButton = new ActionButton(importURL, "Import More Samples", ActionButton.Action.LINK); - uploadButton.setDisplayPermission(UpdatePermission.class); - detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(uploadButton); - } - } - } - - var publish = StudyPublishService.get(); - if (AuditLogService.get().isViewable() && publish != null) - { - ContainerFilter cf = ContainerFilter.Type.CurrentAndSubfolders.create(getContainer(), getUser()); - ActionURL linkToStudyHistoryURL = publish.getPublishHistory(getContainer(), Dataset.PublishSource.SampleType, _sampleType.getRowId(), cf); - ActionButton linkToStudyHistoryButton = new ActionButton(linkToStudyHistoryURL, "Link to Study History", ActionButton.Action.LINK); - linkToStudyHistoryButton.setDisplayPermission(InsertPermission.class); - detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(linkToStudyHistoryButton); - } - - return new VBox(detailsView, queryView); - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("sampleSets"); - ActionURL url = ExperimentUrls.get().getShowSampleTypeListURL(getContainer()); - addRootNavTrail(root); - root.addChild("Sample Types", url); - root.addChild("Sample Type " + _sampleType.getName()); - } - } - - @RequiresPermission(ReadPermission.class) - public class ShowAllMaterialsAction extends SimpleViewAction - { - @Override - public ModelAndView getView(Object o, BindException errors) - { - ExpSchema schema = new ExpSchema(getUser(), getContainer()); - QuerySettings settings = schema.getSettings(getViewContext(), "Materials", ExpSchema.TableType.Materials.toString()); - QueryView view = new QueryView(schema, settings, errors) - { - @Override - protected void populateButtonBar(DataView view, ButtonBar bar) - { - super.populateButtonBar(view, bar); - bar.add(SampleTypeContentsView.getDeriveSamplesButton(getContainer(),null)); - } - }; - view.setShowDetailsColumn(false); - return view; - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("sampleSets"); - addRootNavTrail(root); - root.addChild("All Materials"); - } - } - - /** - * Only shows standard and custom properties, not parent and child samples. Used for indexing - */ - @RequiresPermission(ReadPermission.class) - public class ShowMaterialSimpleAction extends SimpleViewAction - { - protected ExpMaterialImpl _material; - - @Override - public VBox getView(ExpObjectForm form, BindException errors) throws Exception - { - Container c = getContainer(); - _material = ExperimentServiceImpl.get().getExpMaterial(form.getRowId()); - if (_material == null && form.getLsid() != null) - { - _material = ExperimentServiceImpl.get().getExpMaterial(form.getLsid()); - } - if (_material == null) - { - throw new NotFoundException("Could not find a material with RowId " + form.getRowId()); - } - - ensureCorrectContainer(getContainer(), _material, getViewContext()); - - ExpRunImpl run = _material.getRun(); - ExpProtocol sourceProtocol = _material.getSourceProtocol(); - ExpProtocolApplication sourceProtocolApplication = _material.getSourceApplication(); - - DataRegion dr = new DataRegion(); - dr.addColumns(ExperimentServiceImpl.get().getTinfoMaterial().getUserEditableColumns()); - dr.removeColumns("RowId", "RunId", "LastIndexed", "LSID", "SourceApplicationId", "CpasType"); - - //dr.addColumns(extraProps); - dr.addDisplayColumn(new ExperimentRunDisplayColumn(run, "Source Experiment Run")); - dr.addDisplayColumn(new ProtocolDisplayColumn(sourceProtocol, "Source Protocol")); - dr.addDisplayColumn(new ProtocolApplicationDisplayColumn(sourceProtocolApplication, "Source Protocol Application")); - dr.addDisplayColumn(new LineageGraphDisplayColumn(_material, run)); - dr.addDisplayColumn(new SampleTypeDisplayColumn(_material)); - - //TODO: Can't yet edit materials uploaded from a material source - dr.setButtonBar(new ButtonBar()); - DetailsView detailsView = new DetailsView(dr, _material.getRowId()); - detailsView.setTitle("Standard Properties"); - detailsView.setFrame(WebPartView.FrameType.PORTAL); - - CustomPropertiesView cpv = new CustomPropertiesView(_material, c, getUser()); - - return new VBox(new StandardAndCustomPropertiesView(detailsView, cpv)); - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("sampleSets"); - addRootNavTrail(root); - root.addChild("Sample Types", ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); - ExpSampleType sampleType = _material.getSampleType(); - if (sampleType != null) - { - root.addChild(sampleType.getName(), ExperimentUrlsImpl.get().getShowSampleTypeURL(sampleType)); - } - root.addChild("Sample " + _material.getName()); - } - } - - @RequiresPermission(ReadPermission.class) - public class ShowMaterialAction extends ShowMaterialSimpleAction - { - @Override - public VBox getView(ExpObjectForm form, BindException errors) throws Exception - { - VBox vbox = super.getView(form, errors); - - List materialsToInvestigate = new ArrayList<>(); - final Set successorRuns = new HashSet<>(); - materialsToInvestigate.add(_material); - Set investigatedMaterials = new HashSet<>(); - do - { - // Query for all the next tier of materials at once - issue 45402 - List followupRuns = ExperimentService.get().getRunsUsingMaterials(materialsToInvestigate); - - // Mark this set as investigated and reset for the next cycle - investigatedMaterials.addAll(materialsToInvestigate); - materialsToInvestigate = new ArrayList<>(); - - for (ExpRun r : followupRuns) - { - // Only expand the material outputs of the run if it's our first time visiting it - if (successorRuns.add(r)) - { - materialsToInvestigate.addAll(r.getMaterialOutputs()); - } - } - - if (successorRuns.size() > 1000) - { - // Give up - there may be a cycle or other problematic data - break; - } - - // Cull the ones we've already looked up - materialsToInvestigate.removeAll(investigatedMaterials); - } - while (!materialsToInvestigate.isEmpty()); - - HtmlStringBuilder updateLinks = HtmlStringBuilder.of(); - ExpSampleType st = _material.getSampleType(); - if (st != null && st.getContainer() != null && st.getContainer().hasPermission(getUser(), UpdatePermission.class)) - { - // XXX: ridiculous amount of work to get a update url expression for the sample type's table. - UserSchema samplesSchema = QueryService.get().getUserSchema(getUser(), st.getContainer(), "Samples"); - QueryDefinition queryDef = samplesSchema.getQueryDefForTable(st.getName()); - StringExpression expr = queryDef.urlExpr(QueryAction.updateQueryRow, null); - if (expr != null) - { - // Since we're building a detailsURL outside the context of a "row" need to set the correct - // container context on the generated expr. - ((DetailsURL) expr).setContainerContext(st.getContainer()); - String url = expr.eval(Collections.singletonMap(new FieldKey(null, "RowId"), _material.getRowId())); - updateLinks.append(LinkBuilder.labkeyLink("edit", url)).append(" "); - } - } - - if (getContainer().hasPermission(getUser(), InsertPermission.class)) - { - ActionURL deriveURL = new ActionURL(DeriveSamplesChooseTargetAction.class, getContainer()); - deriveURL.addParameter("rowIds", _material.getRowId()); - if (st != null) - deriveURL.addParameter("targetSampleTypeId", st.getRowId()); - - updateLinks.append(LinkBuilder.labkeyLink("derive samples from this sample", deriveURL)).append(" "); - } - - vbox.addView(new HtmlView(updateLinks)); - - ExperimentRunListView runListView = ExperimentRunListView.createView(getViewContext(), ExperimentRunType.ALL_RUNS_TYPE, true); - runListView.setShowRecordSelectors(false); - runListView.getRunTable().setRuns(successorRuns); - runListView.getRunTable().setContainerFilter(new ContainerFilter.AllFolders(getUser())); - runListView.setAllowableContainerFilterTypes(ContainerFilter.Type.Current, ContainerFilter.Type.CurrentAndSubfolders, ContainerFilter.Type.AllFolders); - runListView.setTitle("Runs associated with this material or a derived material"); - - ParentChildView pv = new ParentChildView(_material, getViewContext()); - vbox.addView(pv); - vbox.addView(runListView); - - return vbox; - } - } - - - // - // DataClass - // - - @RequiresPermission(ReadPermission.class) - public class ListDataClassAction extends SimpleViewAction - { - @Override - public ModelAndView getView(Object o, BindException errors) - { - DataClassWebPart view = new DataClassWebPart(false, getViewContext(), null); - view.setFrame(WebPartView.FrameType.NONE); - view.setErrorMessage(getViewContext().getRequest().getParameter("errorMessage")); - - return view; - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("dataClass"); - addRootNavTrail(root); - root.addChild("Data Classes"); - } - } - - public static class DataClassForm extends ExpObjectForm - { - private String _name; - - public String getName() - { - return _name; - } - - public void setName(String name) - { - _name = name; - } - - public ExpDataClassImpl getDataClass(@Nullable Container container) - { - ExpDataClassImpl dataClass = null; - - if (getName() != null) - { - dataClass = ExperimentServiceImpl.get().getDataClass(getContainer(), getName(), true); - if (dataClass == null) - throw new NotFoundException("No data class found for name '" + getName() + "'."); - } - else if (getRowId() > 0) - { - dataClass = ExperimentServiceImpl.get().getDataClass(getContainer(), getRowId(), true); - } - - if (dataClass == null) - throw new NotFoundException("No data class found."); - else if (container != null && !container.equals(dataClass.getContainer())) - throw new NotFoundException("Data class is not defined in the given container."); - - return dataClass; - } - } - - @RequiresPermission(ReadPermission.class) - public class ShowDataClassAction extends SimpleViewAction - { - private ExpDataClassImpl _dataClass; - - @Override - public ModelAndView getView(DataClassForm form, BindException errors) - { - _dataClass = form.getDataClass(null); - return new VBox(getDataClassPropertiesView(), getDataClassContentsView(errors)); - } - - private DetailsView getDataClassPropertiesView() - { - ExpSchema expSchema = new ExpSchema(getUser(), _dataClass.getContainer()); - - TableInfo table = ExpSchema.TableType.DataClasses.createTable(expSchema, null, null); - QueryUpdateForm tvf = new QueryUpdateForm(table, getViewContext(), null); - tvf.setPkVal(_dataClass.getRowId()); - DetailsView detailsView = new DetailsView(tvf); - detailsView.setTitle("Data Class Properties"); - - ButtonBar bb = new ButtonBar(); - bb.setStyle(ButtonBar.Style.separateButtons); - boolean inDefinitionContainer = getContainer().equals(_dataClass.getContainer()); - - DomainKind domainKind = _dataClass.getDomain().getDomainKind(); - if (domainKind != null && domainKind.canEditDefinition(getUser(), _dataClass.getDomain())) - { - ActionURL updateURL = new ActionURL(EditDataClassAction.class, _dataClass.getContainer()); - updateURL.addParameter("rowId", _dataClass.getRowId()); - updateURL.addReturnUrl(urlProvider(ExperimentUrls.class).getShowDataClassURL(_dataClass.getContainer(), _dataClass.getRowId())); - - if (inDefinitionContainer) - { - ActionButton updateButton = new ActionButton(updateURL, "Edit Data Class", ActionButton.Action.LINK); - updateButton.setDisplayPermission(DesignDataClassPermission.class); - updateButton.setPrimary(true); - bb.add(updateButton); - } - else if (_dataClass.getContainer().hasPermission(getUser(), DesignDataClassPermission.class)) - { - ActionButton updateButton = new ActionButton("Edit Data Class"); - updateButton.setActionType(ActionButton.Action.SCRIPT); - updateButton.setScript("if (window.confirm('This data class is defined in the " + _dataClass.getContainer().getPath() + " folder. Would you still like to edit it?')) { window.location = '" + updateURL + "' }"); - updateButton.setPrimary(true); - bb.add(updateButton); - } - - ActionURL deleteURL = new ActionURL(DeleteDataClassAction.class, _dataClass.getContainer()); - deleteURL.addParameter("singleObjectRowId", _dataClass.getRowId()); - deleteURL.addReturnUrl(ExperimentUrlsImpl.get().getDataClassListURL(getContainer())); - ActionButton deleteButton = new ActionButton(deleteURL, "Delete Data Class", ActionButton.Action.LINK); - - if (inDefinitionContainer) - { - deleteButton.setDisplayPermission(DesignDataClassPermission.class); - bb.add(deleteButton); - } - else if (_dataClass.getContainer().hasPermission(getUser(), DesignDataClassPermission.class)) - { - bb.add(deleteButton); - } - } - detailsView.getDataRegion().setButtonBar(bb); - - if (!inDefinitionContainer) - { - ActionURL definitionURL = urlProvider(ExperimentUrls.class).getShowDataClassURL(_dataClass.getContainer(), _dataClass.getRowId()); - LinkBuilder link = LinkBuilder.simpleLink(_dataClass.getContainer().getPath(), definitionURL); - SimpleDisplayColumn definedInCol = new SimpleDisplayColumn(link.toString()); - definedInCol.setCaption("Defined In:"); - detailsView.getDataRegion().addDisplayColumn(definedInCol); - } - - return detailsView; - } - - private QueryView getDataClassContentsView(BindException errors) - { - UserSchema dataClassSchema = QueryService.get().getUserSchema(getUser(), getContainer(), ExpSchema.SCHEMA_EXP_DATA); - QuerySettings settings = dataClassSchema.getSettings(getViewContext(), QueryView.DATAREGIONNAME_DEFAULT, _dataClass.getName()); - - return new QueryView(dataClassSchema, settings, errors) - { - @Override - public @NotNull LinkedHashSet getClientDependencies() - { - LinkedHashSet resources = super.getClientDependencies(); - resources.add(ClientDependency.fromPath("Ext4")); - resources.add(ClientDependency.fromPath("dataregion/confirmDelete.js")); - return resources; - } - - @Override - public ActionButton createDeleteButton() - { - ActionButton button = super.createDeleteButton(); - if (button != null) - { - String dependencyText = ExperimentService.get() - .getObjectReferencers() - .stream() - .map(referencer -> referencer.getObjectReferenceDescription(ExpData.class)) - .collect(Collectors.joining(" or ")); - - button.setScript("LABKEY.dataregion.confirmDelete(" + - PageFlowUtil.jsString(getDataRegionName()) + ", " + - PageFlowUtil.jsString(ExpSchema.SCHEMA_EXP_DATA.toString()) + ", " + - PageFlowUtil.jsString(getQueryDef().getName()) + ", " + - "'experiment', 'getDataOperationConfirmationData.api', " + - PageFlowUtil.jsString(getSelectionKey()) + ", " + - "'data object', 'data objects', '" + dependencyText + "', {dataOperation: 'Delete'})"); - button.setRequiresSelection(true); - } - return button; - } - }; - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("dataClass"); - addRootNavTrail(root); - root.addChild("Data Classes", ExperimentUrlsImpl.get().getDataClassListURL(getContainer())); - root.addChild(_dataClass.getName()); - } - } - - @RequiresPermission(DesignDataClassPermission.class) - public class DeleteDataClassAction extends AbstractDeleteAction - { - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("dataClass"); - super.addNavTrail(root); - } - - @Override - protected void deleteObjects(DeleteForm deleteForm) - { - List dataClasses = getDataClasses(deleteForm); - if (!ensureCorrectContainer(dataClasses)) - { - throw new UnauthorizedException(); - } - for (ExpRun run : getRuns(dataClasses)) - { - if (!run.getContainer().hasPermission(getUser(), DeletePermission.class)) - { - throw new UnauthorizedException(); - } - } - for (ExpDataClass dataClass : dataClasses) - { - dataClass.delete(getUser(), deleteForm.getUserComment()); - } - } - - @Override - public ModelAndView getView(DeleteForm deleteForm, boolean reshow, BindException errors) - { - List dataClasses = getDataClasses(deleteForm); - - if (!ensureCorrectContainer(dataClasses)) - { - throw new RedirectException(ExperimentUrlsImpl.get().getDataClassListURL(getContainer(), "To delete a data class, you must be in its folder or project.")); - } - - return new ConfirmDeleteView("Data Class", ShowDataClassAction.class, dataClasses, deleteForm, getRuns(dataClasses)); - } - - private List getDataClasses(DeleteForm deleteForm) - { - List dataClasses = new ArrayList<>(); - for (long rowId : deleteForm.getIds(false)) - { - ExpDataClass dataClass = ExperimentServiceImpl.get().getDataClass(getContainer(), rowId, true); - if (dataClass != null) - { - dataClasses.add(dataClass); - } - } - return dataClasses; - } - - private boolean ensureCorrectContainer(List dataClasses) - { - for (ExpDataClass dataClass : dataClasses) - { - Container sourceContainer = dataClass.getContainer(); - if (!sourceContainer.equals(getContainer())) - { - return false; - } - } - return true; - } - - private List getRuns(List dataClasses) - { - if (!dataClasses.isEmpty()) - { - List runArray = ExperimentService.get().getRunsUsingDataClasses(dataClasses); - return ExperimentService.get().runsDeletedWithInput(runArray); - } - else - { - return Collections.emptyList(); - } - } - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(ReadPermission.class) - public static class GetDataClassPropertiesAction extends ReadOnlyApiAction - { - @Override - public Object execute(DataClassForm form, BindException errors) throws Exception - { - ExpDataClass dataClass = form.getDataClass(getContainer()); - if (dataClass != null) - return new DataClassDomainKindProperties(dataClass); - else - throw new NotFoundException("Data class does not exist in this container for rowId " + form.getRowId() + "."); - } - } - - @RequiresPermission(DesignDataClassPermission.class) - public static class EditDataClassAction extends SimpleViewAction - { - private ExpDataClassImpl _dataClass; - - @Override - public ModelAndView getView(DataClassForm form, BindException errors) - { - boolean create = form.getLSID() == null && form.getRowId() == 0 && form.getName() == null; - if (!create) - _dataClass = form.getDataClass(getContainer()); - - return ModuleHtmlView.get(ModuleLoader.getInstance().getModule("core"), ModuleHtmlView.getGeneratedViewPath("dataClassDesigner")); - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("dataClass"); - - root.addChild("Data Classes", ExperimentUrlsImpl.get().getDataClassListURL(getContainer())); - if (_dataClass == null) - { - root.addChild("Create Data Class"); - } - else - { - root.addChild(_dataClass.getName(), ExperimentUrlsImpl.get().getShowDataClassURL(getContainer(), _dataClass.getRowId())); - root.addChild("Update Data Class"); - } - } - } - - @RequiresPermission(DesignDataClassPermission.class) - public static class CreateDataClassFromTemplateAction extends FormViewAction - { - private ActionURL _successUrl; - private Map _domainTemplates; - - @Override - public void validateCommand(CreateDataClassFromTemplateForm form, Errors errors) - { - String name = null; - _domainTemplates = DomainTemplateGroup.getAllTemplates(getContainer()); - - if (!_domainTemplates.containsKey(form.getDomainTemplate())) - { - errors.reject(ERROR_MSG, "Unknown template selected: " + form.getDomainTemplate()); - } - else - { - DomainTemplate template = _domainTemplates.get(form.getDomainTemplate()); - name = template.getTemplateName(); - - // Issue 40230: if template includes sample type option, verify that it exists - if (template.getOptions().containsKey("sampleSet")) - { - String sampleTypeName = template.getOptions().get("sampleSet").toString(); - ExpSampleType sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), sampleTypeName, true); - if (sampleType == null) - errors.reject(ERROR_MSG, "Unable to find a sample type in this container with name: " + sampleTypeName + "."); - } - } - - if (StringUtils.isBlank(name)) - errors.reject(ERROR_MSG, "DataClass template selection is required."); - else if (ExperimentService.get().getDataClass(getContainer(), name, true) != null) - errors.reject(ERROR_MSG, "DataClass '" + name + "' already exists."); - - } - - @Override - public ModelAndView getView(CreateDataClassFromTemplateForm form, boolean reshow, BindException errors) - { - Set templates = DomainTemplateGroup.getTemplatesForDomainKind(getContainer(), DataClassDomainKind.NAME); - form.setAvailableDomainTemplateNames(templates); - - Set messages = new HashSet<>(); - Map groups = DomainTemplateGroup.getAllGroups(getContainer()); - for (DomainTemplateGroup g : groups.values()) - messages.addAll(g.getErrors()); - form.setXmlParseErrors(messages); - - return new JspView<>("/org/labkey/experiment/createDataClassFromTemplate.jsp", form, errors); - } - - @Override - public boolean handlePost(CreateDataClassFromTemplateForm form, BindException errors) throws Exception - { - DomainTemplate template = _domainTemplates.get(form.getDomainTemplate()); - Domain domain = DomainUtil.createDomain(template, getContainer(), getUser(), form.getName()); - - _successUrl = domain.getDomainKind().urlEditDefinition(domain, getViewContext()); - return true; - } - - @Override - public URLHelper getSuccessURL(CreateDataClassFromTemplateForm form) - { - return _successUrl; - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("dataClass"); - root.addChild("Create Data Class from Template"); - } - } - - public static class CreateDataClassFromTemplateForm extends DataClass - { - private String _domainTemplate; - private Set _availableDomainTemplateNames; - private Set _xmlParseErrors; - private final ReturnUrlForm _returnUrlForm = new ReturnUrlForm(); - - public String getDomainTemplate() - { - return _domainTemplate; - } - - public void setDomainTemplate(String domainTemplate) - { - _domainTemplate = domainTemplate; - } - - public Set getAvailableDomainTemplateNames() - { - return _availableDomainTemplateNames; - } - - public void setAvailableDomainTemplateNames(Set availableDomainTemplateNames) - { - _availableDomainTemplateNames = availableDomainTemplateNames; - } - - public Set getXmlParseErrors() - { - return _xmlParseErrors; - } - - public void setXmlParseErrors(Set xmlParseErrors) - { - _xmlParseErrors = xmlParseErrors; - } - - @Nullable - public String getReturnUrl() - { - return _returnUrlForm.getReturnUrl(); - } - - public void setReturnUrl(String s) - { - _returnUrlForm.setReturnUrl(s); - } - } - - public static class ConceptURIForm - { - private String _conceptURI; - - public String getConceptURI() - { - return _conceptURI; - } - - public void setConceptURI(String conceptURI) - { - _conceptURI = conceptURI; - } - } - - @RequiresPermission(AdminPermission.class) - public static class RemoveConceptMappingAction extends MutatingApiAction - { - @Override - public void validateForm(ConceptURIForm form, Errors errors) - { - if (form.getConceptURI() == null || ConceptURIProperties.getLookup(getContainer(), form.getConceptURI()) == null) - errors.reject(ERROR_MSG, "Concept URI not found: " + form.getConceptURI()); - } - - @Override - public Object execute(ConceptURIForm form, BindException errors) - { - ConceptURIProperties.removeLookup(getContainer(), form.getConceptURI()); - return new ApiSimpleResponse("success", true); - } - } - - @RequiresPermission(ReadPermission.class) - public static class RunAttachmentDownloadAction extends BaseDownloadAction - { - @Nullable - @Override - public Pair getAttachment(AttachmentForm form) - { - if (form.getLsid() == null || form.getName() == null) - throw new NotFoundException("Error: missing required param 'lsid' or 'name'."); - - ExpRun run = ExperimentService.get().getExpRun(form.getLsid()); - if (run == null) - throw new NotFoundException("Run not found: " + form.getLsid()); - - if (!run.getContainer().equals(getContainer())) - { - if (run.getContainer().hasPermission(getUser(), ReadPermission.class)) - throw new RedirectException(getViewContext().cloneActionURL().setContainer(run.getContainer())); - else - throw new NotFoundException("Run not found"); - } - - AttachmentParent parent = new ExpRunAttachmentParent(run); - return new Pair<>(parent, form.getName()); - } - } - - @RequiresPermission(ReadPermission.class) - public static class DataClassAttachmentDownloadAction extends BaseDownloadAction - { - @Nullable - @Override - public Pair getAttachment(AttachmentForm form) - { - if (form.getLsid() == null || form.getName() == null) - throw new NotFoundException("Error: missing required param 'lsid' or 'name'."); - - Lsid lsid = new Lsid(form.getLsid()); - ExpData data = ExperimentServiceImpl.get().getExpData(lsid.toString()); - if (data == null) - throw new NotFoundException("Error: Data object not found for the given LSID: " + lsid); - AttachmentParent parent = new ExpDataClassAttachmentParent(data.getContainer(), lsid); - - return new Pair<>(parent, form.getName()); - } - } - - public static class AttachmentForm extends LsidForm implements BaseDownloadAction.InlineDownloader - { - private String _name; - private boolean _inline = true; - - public String getName() - { - return _name; - } - - public void setName(String name) - { - _name = name; - } - - @Override - public boolean isInline() - { - return _inline; - } - - public void setInline(boolean inline) - { - _inline = inline; - } - } - - // - // END DataClass actions - // - - public static ActionURL getRunGraphURL(Container c, long runId) - { - return new ActionURL(ShowRunGraphAction.class, c).addParameter("rowId", runId); - } - - @RequiresPermission(ReadPermission.class) - public class ShowRunGraphAction extends AbstractShowRunAction - { - @Override - protected VBox createLowerView(ExpRunImpl experimentRun, BindException errors) - { - return new VBox( - createRunViewTabs(experimentRun, false, true, true), - new ExperimentRunGraphView(experimentRun, false) - ); - } - } - - private abstract class AbstractShowRunAction extends SimpleViewAction - { - private ExpRunImpl _experimentRun; - - @Override - public ModelAndView getView(ExperimentRunForm form, BindException errors) - { - _experimentRun = (ExpRunImpl) form.lookupRun(); - ensureCorrectContainer(getContainer(), _experimentRun, getViewContext()); - - VBox vbox = new VBox(); - - JspView detailsView = new JspView<>("/org/labkey/experiment/ExperimentRunDetails.jsp", _experimentRun); - detailsView.setTitle("Standard Properties"); - - var attachmentParent = new ExpRunAttachmentParent(_experimentRun); - var attachments = AttachmentService.get().getAttachments(attachmentParent) - .stream() - .map(att -> Pair.of(att.getName(), new ActionURL(RunAttachmentDownloadAction.class, _experimentRun.getContainer()).addParameter("name", att.getName()).addParameter("lsid", _experimentRun.getLSID()))) - .collect(toList()); - CustomPropertiesView cpv = new CustomPropertiesView(_experimentRun.getLSID(), getContainer(), attachments); - - vbox.addView(new StandardAndCustomPropertiesView(detailsView, cpv)); - - HtmlStringBuilder updateLinks = HtmlStringBuilder.of(); - List runEditors = ExperimentService.get().getRunEditors(); - for (ExpRunEditor editor : runEditors) - { - if (editor.isProtocolEditor(form.lookupRun().getProtocol())) - { - updateLinks.append(LinkBuilder.labkeyLink("edit " + editor.getDisplayName() + " run", editor.getEditUrl(getContainer()).addParameter("rowId", form.getRowId()))); - } - } - - if (!updateLinks.isEmpty()) - { - HtmlView view = new HtmlView(updateLinks); - vbox.addView(view); - } - - VBox lowerView = createLowerView(_experimentRun, errors); - lowerView.setFrame(WebPartView.FrameType.PORTAL); - lowerView.setTitle("Run Details"); - NavTree tree = new NavTree(""); - File runRoot = _experimentRun.getFilePathRoot(); - if (NetworkDrive.exists(runRoot)) - { - if (!runRoot.isDirectory()) - { - runRoot = runRoot.getParentFile(); - } - PipeRoot pipelineRoot = PipelineService.get().findPipelineRoot(_experimentRun.getContainer()); - if (pipelineRoot != null) - { - if (pipelineRoot.isUnderRoot(runRoot)) - { - String path = pipelineRoot.relativePath(runRoot); - tree.addChild("View Files", urlProvider(PipelineUrls.class).urlBrowse(_experimentRun.getContainer(), null, path)); - } - } - } - - final String exportFilesFormId = "exportFilesForm"; - NavTree downloadFiles = new NavTree("Download all files"); - downloadFiles.setScript("document.getElementById('" + exportFilesFormId + "').submit();"); - tree.addChild(downloadFiles); - - // CONSIDER: Show modal dialog using ExperimentService.get().createRunExportView() - NavTree exportXarFiles = new NavTree("Export XAR"); - exportXarFiles.setScript("LABKEY.Experiment.exportRuns({runIds: [" + _experimentRun.getRowId() + "] });"); - tree.addChild(exportXarFiles); - - lowerView.setNavMenu(tree); - lowerView.setIsWebPart(false); - - vbox.addView(lowerView); - vbox.addView(new ExperimentRunGroupsView(getUser(), getContainer(), _experimentRun, getViewContext().getActionURL(), errors)); - - DOM.Renderable exportFilesForm = LK.FORM(at( - id, exportFilesFormId, - method, "POST", - action, new ActionURL(ExportRunFilesAction.class, _experimentRun.getContainer())), - INPUT(at(type, "hidden", - name, DataRegionSelection.DATA_REGION_SELECTION_KEY, - value, "ExportSingleRun")), - INPUT(at(type, "hidden", - name, DataRegion.SELECT_CHECKBOX_NAME, - value, _experimentRun.getRowId())), - INPUT(at(type, "hidden", - name, "zipFileName", - value, _experimentRun.getName() + ".zip"))); - - HtmlView hiddenFormView = new HtmlView(exportFilesForm); - vbox.addView(hiddenFormView); - - return vbox; - } - - protected abstract VBox createLowerView(ExpRunImpl experimentRun, BindException errors); - - @Override - public void addNavTrail(NavTree root) - { - root.addChild(_experimentRun.getName()); - } - } - - public static class ToggleRunExperimentMembershipForm - { - private int _runId; - private int _experimentId; - private boolean _included; - - public int getRunId() - { - return _runId; - } - - public void setRunId(int runId) - { - _runId = runId; - } - - public int getExperimentId() - { - return _experimentId; - } - - public void setExperimentId(int experimentId) - { - _experimentId = experimentId; - } - - public boolean isIncluded() - { - return _included; - } - - public void setIncluded(boolean included) - { - _included = included; - } - } - - @RequiresPermission(UpdatePermission.class) - public static class ToggleRunExperimentMembershipAction extends FormHandlerAction - { - @Override - public boolean handlePost(ToggleRunExperimentMembershipForm form, BindException errors) - { - ExpRun run = ExperimentService.get().getExpRun(form.getRunId()); - // Check if the user has permission to update this run - if (run == null || !run.getContainer().hasPermission(getUser(), UpdatePermission.class)) - { - throw new NotFoundException(); - } - - ExpExperiment exp = ExperimentService.get().getExpExperiment(form.getExperimentId()); - if (exp == null) - { - throw new NotFoundException(); - } - // Check if this - if (!ExperimentService.get().getExperiments(run.getContainer(), getUser(), true, false).contains(exp)) - { - throw new NotFoundException(); - } - // Users must have permission to view, but not necessarily update, the container the holds the run group - if (!exp.getContainer().hasPermission(getUser(), ReadPermission.class)) - { - throw new UnauthorizedException(); - } - - if (form.isIncluded()) - { - exp.addRuns(getUser(), run); - } - else - { - exp.removeRun(getUser(), run); - } - - return true; - } - - @Override - public URLHelper getSuccessURL(ToggleRunExperimentMembershipForm form) - { - return null; - } - - @Override - public void validateCommand(ToggleRunExperimentMembershipForm target, Errors errors) - { - } - } - - private HtmlView createRunViewTabs(ExpRun expRun, boolean showGraphSummary, boolean showGraphDetail, boolean showText) - { - return new HtmlView( - TABLE(cl("labkey-tab-strip"), - TR( - createTabSpacer(false), - createTab("Graph Summary View", ExperimentUrlsImpl.get().getRunGraphURL(expRun), !showGraphSummary), - createTabSpacer(false), - createTab("Graph Detail View", ExperimentUrlsImpl.get().getRunGraphDetailURL(expRun), !showGraphDetail), - createTabSpacer(false), - createTab("Text View", ExperimentUrlsImpl.get().getRunTextURL(expRun), !showText), - createTabSpacer(true)))); - } - - private DOM.Renderable createTab(String text, ActionURL url, boolean selected) - { - return TD(cl(selected,"labkey-tab-selected", "labkey-tab"), - A(at(href, url), text)); - } - - private DOM.Renderable createTabSpacer(boolean fullWidth) - { - return TD(cl("labkey-tab-space").at(fullWidth, width, "100%"), - IMG(at(src, AppProps.getInstance().getContextPath() + "/_.gif", width, "5"))); - } - - @RequiresPermission(ReadPermission.class) - public class ShowRunTextAction extends AbstractShowRunAction - { - @Override - protected VBox createLowerView(ExpRunImpl expRun, BindException errors) - { - JspView applicationsView = new JspView<>("/org/labkey/experiment/ProtocolApplications.jsp", expRun); - applicationsView.setFrame(WebPartView.FrameType.TITLE); - applicationsView.setTitle("Protocol Applications"); - - HtmlView toggleView = createRunViewTabs(expRun, true, true, false); - - QuerySettings runDataInputsSettings = new QuerySettings(getViewContext(), "RunDataInputs", DataInputs.name()); - UsageQueryView runDataInputsView = new UsageQueryView("Data Inputs", getViewContext(), expRun, ExpProtocol.ApplicationType.ExperimentRun, runDataInputsSettings, errors); - runDataInputsView.setButtonBarPosition(DataRegion.ButtonBarPosition.TOP); - - QuerySettings runDataOutputsSettings = new QuerySettings(getViewContext(), "RunDataOutputs", DataInputs.name()); - UsageQueryView runDataOutputsView = new UsageQueryView("Data Outputs", getViewContext(), expRun, ExpProtocol.ApplicationType.ExperimentRunOutput, runDataOutputsSettings, errors); - runDataOutputsView.setButtonBarPosition(DataRegion.ButtonBarPosition.NONE); - - QuerySettings runMaterialInputsSetting = new QuerySettings(getViewContext(), "RunMaterialInputs", ExpSchema.TableType.MaterialInputs.name()); - UsageQueryView runMaterialInputsView = new UsageQueryView("Material Inputs", getViewContext(), expRun, ExpProtocol.ApplicationType.ExperimentRun, runMaterialInputsSetting, errors); - runMaterialInputsView.setButtonBarPosition(DataRegion.ButtonBarPosition.TOP); - - QuerySettings runMaterialOutputsSettings = new QuerySettings(getViewContext(), "RunMaterialOutputs", ExpSchema.TableType.MaterialInputs.name()); - UsageQueryView runMaterialOutputsView = new UsageQueryView("Material Outputs", getViewContext(), expRun, ExpProtocol.ApplicationType.ExperimentRunOutput, runMaterialOutputsSettings, errors); - runMaterialOutputsView.setButtonBarPosition(DataRegion.ButtonBarPosition.NONE); - - HBox inputsView = new HBox(runDataInputsView, runMaterialInputsView); - HBox registeredInputsView = new HBox(); - - var expService = ExperimentService.get(); - expService.getRunInputsViewProviders().forEach(provider -> - { - var queryView = provider.createView(getViewContext(), expRun, errors); - if (queryView != null) - { - registeredInputsView.addView(queryView); - } - }); - HBox outputsView = new HBox(runDataOutputsView, runMaterialOutputsView); - HBox registeredOutputsView = new HBox(); - expService.getRunOutputsViewProviders().forEach(provider -> - { - var queryView = provider.createView(getViewContext(), expRun, errors); - if (queryView != null) - { - registeredOutputsView.addView(queryView); - } - }); - - var vBox = new VBox(); - vBox.addView(toggleView); - vBox.addView(inputsView); - if (!registeredInputsView.isEmpty()) - vBox.addView(registeredInputsView); - vBox.addView(outputsView); - if (!registeredOutputsView.isEmpty()) - vBox.addView(registeredOutputsView); - vBox.addView(applicationsView); - - return vBox; - } - } - - private static class UsageQueryView extends QueryView - { - private final ExpRun _run; - private final ExpProtocol.ApplicationType _type; - - public UsageQueryView(String title, ViewContext context, ExpRun run, ExpProtocol.ApplicationType type, - QuerySettings settings, BindException errors) - { - super(new ExpSchema(context.getUser(), context.getContainer()), settings, errors); - setTitle(title); - setFrame(FrameType.TITLE); - _run = run; - _type = type; - setShowBorders(true); - setShadeAlternatingRows(true); - setShowExportButtons(false); - setShowPagination(false); - disableContainerFilterSelection(); - } - - @Override - protected TableInfo createTable() - { - String tableName = getSettings().getQueryName(); - ExpInputTable tableInfo = (ExpInputTable) getSchema().getTable(tableName, new ContainerFilter.AllFolders(getUser()), true, true); - tableInfo.setRun(_run, _type); - tableInfo.setLocked(true); - return tableInfo; - } - } - - - public static ActionURL getShowRunGraphDetailURL(Container c, long rowId) - { - ActionURL url = new ActionURL(ShowRunGraphDetailAction.class, c); - url.addParameter("rowId", rowId); - return url; - } - - - @RequiresPermission(ReadPermission.class) - public class ShowRunGraphDetailAction extends AbstractShowRunAction - { - @Override - protected VBox createLowerView(ExpRunImpl run, BindException errors) - { - ExperimentRunGraphView gw = new ExperimentRunGraphView(run, true); - if (null != getViewContext().getActionURL().getParameter("focus")) - gw.setFocus(getViewContext().getActionURL().getParameter("focus")); - if (null != getViewContext().getActionURL().getParameter("focusType")) - gw.setFocusType(getViewContext().getActionURL().getParameter("focusType")); - return new VBox(createRunViewTabs(run, true, false, true), gw); - } - } - - private abstract class AbstractDataAction extends SimpleViewAction - { - protected ExpDataImpl _data; - - @Override - public final ModelAndView getView(DataForm form, BindException errors) throws Exception - { - _data = form.lookupData(); - if (_data == null) - { - throw new NotFoundException("Could not find a data with RowId " + form.getRowId()); - } - - ensureCorrectContainer(getContainer(), _data, getViewContext()); - return getDataView(form, errors); - } - - protected abstract ModelAndView getDataView(DataForm form, BindException errors) throws Exception; - - @Override - public void addNavTrail(NavTree root) - { - addRootNavTrail(root); - root.addChild("Data " + _data.getName()); - } - } - - @RequiresPermission(ReadPermission.class) - public class ShowDataAction extends AbstractDataAction - { - @Override - public ModelAndView getDataView(DataForm form, BindException errors) - { - ExpRun run = _data.getRun(); - ExpProtocol sourceProtocol = _data.getSourceProtocol(); - ExpProtocolApplication sourceProtocolApplication = _data.getSourceApplication(); - ExpDataClass dataClass = _data.getDataClass(getUser()); - - ExpSchema schema = new ExpSchema(getUser(), getContainer()); - TableInfo table; - long pk; - if (dataClass == null) - { - table = schema.getDatasTable(); - pk = _data.getRowId(); - } - else - { - table = schema.getSchema(ExpSchema.NestedSchemas.data).getTable(dataClass.getName()); - pk = new TableSelector(table, Collections.singleton("rowId"), new SimpleFilter(FieldKey.fromParts("lsid"), _data.getLSID()), null).getObject(Integer.class); - } - - DataRegion dr = new DataRegion(); - dr.setTable(table); - List cols = table.getColumns().stream().filter(ColumnInfo::isShownInDetailsView).collect(toList()); - dr.addColumns(cols); - dr.removeColumns("RowId", "Created", "CreatedBy", "Modified", "ModifiedBy", "DataFileUrl", "Run", "LSID", "CpasType", "SourceApplicationId", "Folder", "Generated"); - dr.addDisplayColumn(new ExperimentRunDisplayColumn(run, "Source Experiment Run")); - dr.addDisplayColumn(new ProtocolDisplayColumn(sourceProtocol, "Source Protocol")); - dr.addDisplayColumn(new ProtocolApplicationDisplayColumn(sourceProtocolApplication, "Source Protocol Application")); - dr.addDisplayColumn(new LineageGraphDisplayColumn(_data, run)); - DetailsView detailsView = new DetailsView(dr, pk); - detailsView.setTitle("Standard Properties"); - detailsView.setFrame(WebPartView.FrameType.PORTAL); - ButtonBar bb = new ButtonBar(); - bb.setStyle(ButtonBar.Style.separateButtons); - - ExperimentDataHandler handler = _data.findDataHandler(); - ActionURL viewDataURL = handler == null ? null : handler.getContentURL(_data); - if (viewDataURL != null) - { - bb.add(new ActionButton("View data", viewDataURL)); - } - - if (_data.isPathAccessible()) - { - bb.add(new ActionButton("View file", ExperimentUrlsImpl.get().getShowFileURL(_data, true))); - bb.add(new ActionButton("Download file", ExperimentUrlsImpl.get().getShowFileURL(_data, false))); - - if (getContainer().hasPermission(getUser(), InsertPermission.class)) - { - String relativePath = null; - PipeRoot root = PipelineService.get().findPipelineRoot(getContainer()); - if (root != null) - { - Path rootFile = root.getRootNioPath(); - Path dataFile = _data.getFilePath(); - if (dataFile != null) - { - Path pathRelative; - try - { - pathRelative = rootFile.relativize(dataFile); - if (null != pathRelative) - relativePath = pathRelative.toString(); - } - catch (IllegalArgumentException e) - { - // dataFile not relative to root - } - } - } - ActionURL browseURL = urlProvider(PipelineUrls.class).urlBrowse(getContainer(), getViewContext().getActionURL(), relativePath); - bb.add(new ActionButton("Browse in pipeline", browseURL)); - } - } - - // add links to any other exp.data that share the same dataFileUrl path - var altDataList = ExperimentService.get().getAllExpDataByURL(_data.getDataFileUrl(), getContainer()); - altDataList.removeIf(_data::equals); - if (!altDataList.isEmpty()) - { - MenuButton menu = new MenuButton("Alternate Data"); - for (ExpData altData : altDataList) - { - ExpRun altDataRun = altData.getRun(); - StringBuilder sb = new StringBuilder(altData.getName()); - if (altDataRun != null) - sb.append(" created by run '").append(altDataRun.getName()).append("' (").append(altDataRun.getProtocol().getName()).append(")"); - menu.addMenuItem(sb.toString(), altData.detailsURL()); - } - bb.add(menu); - } - - dr.setButtonBarPosition(DataRegion.ButtonBarPosition.TOP); - dr.setButtonBar(bb); - - CustomPropertiesView cpv = new CustomPropertiesView(_data.getLSID(), getContainer()); - HBox hbox = new StandardAndCustomPropertiesView(detailsView, cpv); - - VBox vbox = new VBox(hbox); - - ParentChildView pv = new ParentChildView(_data, getViewContext()); - vbox.addView(pv); - - ExperimentRunListView runListView = ExperimentRunListView.createView(getViewContext(), ExperimentRunType.ALL_RUNS_TYPE, true); - runListView.getRunTable().setInputData(_data); - runListView.getRunTable().setContainerFilter(new ContainerFilter.AllFolders(getUser())); - runListView.getRunTable().setLocked(true); - runListView.setTitle("Runs using this data as an input"); - vbox.addView(runListView); - - if (_data.isInlineImage() && _data.isFileOnDisk()) - { - ActionURL showFileURL = new ActionURL(ShowFileAction.class, getContainer()).addParameter("rowId", _data.getRowId()); - HtmlView imageView = new HtmlView(IMG(at(src, showFileURL))); - return new VBox(vbox, imageView); - } - return vbox; - } - } - - @RequiresPermission(AdminPermission.class) - public static class CheckDataFileAction extends MutatingApiAction - { - private ExpDataImpl _data; - - @Override - public void validateForm(DataFileForm form, Errors errors) - { - _data = form.lookupData(); - if (_data == null) - { - errors.reject(ERROR_MSG, "No ExpData found for id: " + form.getRowId()); - } - } - - @Override - public ApiResponse execute(DataFileForm form, BindException errors) - { - File dataFile = _data.getFile(); - Container dataContainer = _data.getContainer(); - boolean fileExists = _data.isFileOnDisk(); - boolean fileExistsAtCurrent = false; - File newDataFile = null; - - ApiSimpleResponse response = new ApiSimpleResponse(); - response.put("dataFileUrl", _data.getDataFileUrl()); - response.put("fileExists", fileExists); - response.put("containerPath", dataContainer.getPath()); - - if (!fileExists) - { - PipeRoot pipelineRoot = PipelineService.get().findPipelineRoot(dataContainer); - if (pipelineRoot != null && pipelineRoot.isValid() && dataFile != null) - { - newDataFile = pipelineRoot.resolvePath("/" + AssayFileWriter.DIR_NAME + "/" + dataFile.getName()); - fileExistsAtCurrent = NetworkDrive.exists(newDataFile); - response.put("fileExistsAtCurrent", fileExistsAtCurrent); - } - } - - // if the current dataFileUrl does not exist on disk and we have the file at the current - // pipeline root /assaydata dir, fix the dataFileUrl value - if (form.isAttemptFilePathFix()) - { - if (fileExistsAtCurrent) - { - ExpDataFileListener fileListener = new ExpDataFileListener(); - fileListener.fileMoved(dataFile, newDataFile, getUser(), dataContainer); - response.put("filePathFixed", true); - - // update the ExpData object so that we can get the new dataFileUrl - _data = form.lookupData(); - response.put("newDataFileUrl", _data.getDataFileUrl()); - } - else - { - response.put("filePathFixed", false); - } - } - - response.put("success", true); - return response; - } - } - - public static class DataFileForm extends DataForm - { - private boolean _attemptFilePathFix; - - public boolean isAttemptFilePathFix() - { - return _attemptFilePathFix; - } - - public void setAttemptFilePathFix(boolean attemptFilePathFix) - { - _attemptFilePathFix = attemptFilePathFix; - } - } - - @RequiresPermission(ReadPermission.class) - public class ShowFileAction extends AbstractDataAction - { - @Override - protected ModelAndView getDataView(DataForm form, BindException errors) throws IOException - { - if (!_data.isPathAccessible()) - { - throw new NotFoundException("Data file " + _data.getDataFileUrl() + " does not exist on disk"); - } - - PipeRoot root = PipelineService.get().findPipelineRoot(getContainer()); - if (root != null && !root.isUnderRoot(_data.getFileLike())) - { - // Issue 35649: ImmPort module "publish" creates exp.data object in this container for paths that originate in a different container - FileContentService fileSvc = FileContentService.get(); - if (fileSvc == null) - throw new UnauthorizedException("Data file is not under the pipeline root for this folder"); - - List containers = fileSvc.getContainersForFilePath(_data.getFilePath()); - if (containers.isEmpty() || containers.stream().noneMatch(c -> c.hasPermission(getUser(), ReadPermission.class))) - throw new UnauthorizedException("Data file is not under the pipeline root for this folder"); - } - - //Issues 25667 and 31152 - if (form.isInline()) - { - ExperimentDataHandler h = _data.findDataHandler(); - if (h != null) - { - URLHelper url = h.getShowFileURL(_data); - if (url != null) - { - throw new RedirectException(url); - } - } - } - - try - { - Path realContent = _data.getFilePath(); - if (null == realContent) - throw new IllegalStateException("Path not found."); - - boolean inline = _data.isInlineImage() || form.isInline() || "inlineImage".equalsIgnoreCase(form.getFormat()); - if (_data.isInlineImage() && form.getMaxDimension() != null) - { - try (InputStream inputStream = Files.newInputStream(realContent)) - { - BufferedImage image = ImageIO.read(inputStream); - // If image, create a thumbnail, otherwise fall through as a regular download attempt - if (image != null) - { - int imageMax = Math.max(image.getHeight(), image.getWidth()); - if (imageMax > form.getMaxDimension().intValue()) - { - double scale = (double) form.getMaxDimension().intValue() / (double) imageMax; - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - ImageUtil.resizeImage(image, bOut, scale, 1); - PageFlowUtil.streamFileBytes(getViewContext().getResponse(), FileUtil.getFileName(realContent) + ".png", bOut.toByteArray(), !inline); - return null; - } - } - } - } - - boolean extended = "jsonTSVExtended".equalsIgnoreCase(form.getFormat()); - boolean ignoreTypes = "jsonTSVIgnoreTypes".equalsIgnoreCase(form.getFormat()); - if ("jsonTSV".equalsIgnoreCase(form.getFormat()) || extended || ignoreTypes) - { - if (!FileUtil.hasCloudScheme(realContent)) // TODO: handle streaming from S3 to JSON - streamToJSON(FileSystemLike.wrapFile(realContent), form.getFormat(), -1, null); - return null; - } - - try (InputStream inputStream = Files.newInputStream(realContent)) - { - PageFlowUtil.streamFile(getViewContext().getResponse(), Collections.emptyMap(), FileUtil.getFileName(realContent), inputStream, !inline); - } - } - catch (IOException e) - { - try - { - // Try to write the exception back to the caller if we haven't already flushed the buffer - ApiJsonWriter writer = new ApiJsonWriter(getViewContext().getResponse()); - writer.writeResponse(e); - } - catch (IllegalStateException ise) - { - // Most likely that a disconnected client caused the IOException writing back the response - } - } - - return null; - } - } - - - public static class ParseForm - { - String format = "jsonTSV"; - int maxRows = -1; - - public String getFormat() - { - return format; - } - - public void setFormat(String format) - { - this.format = format; - } - - public int getMaxRows() - { - return maxRows; - } - - public void setMaxRows(int maxRow) - { - this.maxRows = maxRow; - } - } - - @RequiresNoPermission - public class ParseFileAction extends MutatingApiAction - { - @Override - public Object execute(ParseForm form, BindException errors) throws Exception - { - if (!(getViewContext().getRequest() instanceof MultipartHttpServletRequest)) - throw new BadRequestException("Expected MultipartHttpServletRequest when posting files."); - - MultipartFile formFile = getFileMap().get("file"); - if (formFile == null) - { - return true; - } - - FileLike tempFile = null; - try - { - tempFile = FileUtil.createTempFileLike("parse", formFile.getOriginalFilename()); - FileUtil.copyData(formFile.getInputStream(), tempFile.openOutputStream()); - streamToJSON(tempFile, form.getFormat(), form.getMaxRows(), formFile.getOriginalFilename()); - } - finally - { - if (null != tempFile) - tempFile.delete(); - } - return null; - } - } - - - // SampleTypeTest - private void streamToJSON(FileLike realContent, String format, int maxRow, String originalFileName) throws IOException - { - String lowerCaseFileName = realContent.getName().toLowerCase(); - boolean extended = "jsonTSVExtended".equalsIgnoreCase(format); - boolean ignoreTypes = "jsonTSVIgnoreTypes".equalsIgnoreCase(format); - - JSONArray sheetsArray; - if (lowerCaseFileName.endsWith(".xls") || lowerCaseFileName.endsWith(".xlsx")) - { - try (InputStream in = realContent.openInputStream()) - { - sheetsArray = ExcelFactory.convertExcelToJSON(in, extended, maxRow); - } - } - else - { - DataLoaderFactory dlf = DataLoader.get().findFactory(realContent, null); - if (null == dlf) - { - throw new ApiUsageException("Unable to parse file " + realContent + ", it is likely of an unsupported file type"); - } - - try (InputStream in = realContent.openInputStream(); - DataLoader tabLoader = dlf.createLoader(in, true)) - { - tabLoader.setScanAheadLineCount(5000); - ColumnDescriptor[] cols = tabLoader.getColumns(); - - if (ignoreTypes) - for (ColumnDescriptor col : cols) - col.clazz = String.class; - - JSONArray rowsArray = new JSONArray(); - JSONArray headerArray = new JSONArray(); - for (ColumnDescriptor col : cols) - { - if (extended) - { - JSONObject valueObject = new JSONObject(); - valueObject.put("value", col.name); - headerArray.put(valueObject); - } - else - { - headerArray.put(col.name); - } - } - rowsArray.put(headerArray); - for (Map rowMap : tabLoader) - { - // headers count as a row to be consistent - if (maxRow > -1 && maxRow <= rowsArray.length() + 1) - break; - - JSONArray rowArray = new JSONArray(); - for (ColumnDescriptor col : cols) - { - Object value = rowMap.get(col.name); - if (extended) - { - JSONObject valueObject = new JSONObject(); - valueObject.put("value", value); - rowArray.put(valueObject); - } - else - { - rowArray.put(value); - } - } - rowsArray.put(rowArray); - } - - JSONObject sheetJSON = new JSONObject(); - sheetJSON.put("name", "flat"); - sheetJSON.put("data", rowsArray); - sheetsArray = new JSONArray(); - sheetsArray.put(sheetJSON); - } - } - - try (ApiJsonWriter writer = new ApiJsonWriter(getViewContext().getResponse())) - { - JSONObject workbookJSON = new JSONObject(); - workbookJSON.put("fileName", realContent.getName()); - workbookJSON.put("sheets", sheetsArray); - if (originalFileName != null) - workbookJSON.put("originalFileName", originalFileName); - writer.writeResponse(new ApiSimpleResponse(workbookJSON)); - } - } - - - public static class ConvertArraysToExcelForm - { - private String _json; - - public String getJson() - { - return _json; - } - - public void setJson(String json) - { - _json = json; - } - } - - @RequiresPermission(ReadPermission.class) - public static class ConvertArraysToExcelAction extends ExportAction - { - @Override - public void validate(ConvertArraysToExcelForm form, BindException errors) - { - if (form.getJson() == null) - { - errors.reject(ERROR_MSG, "Unable to convert to Excel - no spreadsheet data given"); - } - } - - @Override - public void export(ConvertArraysToExcelForm form, HttpServletResponse response, BindException errors) throws Exception - { - try - { - JSONObject rootObject; - JSONArray sheetsArray; - if (form.getJson() == null || form.getJson().trim().isEmpty()) - { - // Create JSON so that we return an empty file - rootObject = new JSONObject(); - sheetsArray = new JSONArray(); - JSONObject sheetObject = new JSONObject(); - sheetsArray.put(sheetObject); - } - else - { - rootObject = new JSONObject(form.getJson()); - sheetsArray = rootObject.getJSONArray("sheets"); - } - String filename = rootObject.has("fileName") ? rootObject.getString("fileName") : "ExcelExport.xls"; - ExcelWriter.ExcelDocumentType docType = filename.toLowerCase().endsWith(".xlsx") ? ExcelWriter.ExcelDocumentType.xlsx : ExcelWriter.ExcelDocumentType.xls; - - try (Workbook workbook = ExcelFactory.createFromArray(sheetsArray, docType)) - { - response.setContentType(docType.getMimeType()); - ResponseHelper.setContentDisposition(response, ResponseHelper.ContentDispositionType.attachment, filename); - ResponseHelper.setPrivate(response); - workbook.write(response.getOutputStream()); - - JSONObject qInfo = rootObject.has("queryinfo") ? rootObject.getJSONObject("queryinfo") : null; - if (qInfo != null) - { - QueryService.get().addAuditEvent(getUser(), getContainer(), qInfo.getString("schema"), - qInfo.getString("query"), getViewContext().getActionURL(), - rootObject.getString("auditMessage") + filename, - null); - SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "convertTable", "asExcel"); - } - } - } - catch (JSONException | ClassCastException e) - { - // We can get a ClassCastException if we expect an array and get a simple String, for example - ExceptionUtil.renderErrorView(getViewContext(), getPageConfig(), ErrorRenderer.ErrorType.notFound, HttpServletResponse.SC_BAD_REQUEST, "Failed to convert to Excel - invalid input", e, false, false); - } - } - } - - @RequiresPermission(ReadPermission.class) - public static class ConvertArraysToTableAction extends ExportAction - { - @Override - public void validate(ConvertArraysToExcelForm form, BindException errors) - { - if (form.getJson() == null) - { - errors.reject(ERROR_MSG, "Unable to convert to table - no data given"); - } - } - - @Override - public void export(ConvertArraysToExcelForm form, HttpServletResponse response, BindException errors) throws Exception - { - try - { - JSONObject rootObject; - JSONArray rowsArray; - if (form.getJson() == null || form.getJson().trim().isEmpty()) - { - // Create JSON so that we return an empty file - rootObject = new JSONObject(); - rowsArray = new JSONArray(); - } - else - { - rootObject = new JSONObject(form.getJson()); - rowsArray = rootObject.getJSONArray("rows"); - } - - TSVWriter.DELIM delimType = (!rootObject.isNull("delim") ? TSVWriter.DELIM.valueOf(rootObject.getString("delim")) : TSVWriter.DELIM.TAB); - TSVWriter.QUOTE quoteType = (!rootObject.isNull("quoteChar") ? TSVWriter.QUOTE.valueOf(rootObject.getString("quoteChar")) : TSVWriter.QUOTE.NONE); - String filenamePrefix = (!rootObject.isNull("fileNamePrefix") ? rootObject.getString("fileNamePrefix") : "Export"); - String filename = filenamePrefix + "." + delimType.extension; - String newlineChar = !rootObject.isNull("newlineChar") ? rootObject.getString("newlineChar") : "\n"; - - response.setCharacterEncoding(StringUtilsLabKey.DEFAULT_CHARSET.name()); - - try(var tsvWriter = new TSVJSONWriter(filenamePrefix, rowsArray)) - { - tsvWriter.setRowSeparator(newlineChar); - tsvWriter.setDelimiterCharacter(delimType); - tsvWriter.setQuoteCharacter(quoteType); - tsvWriter.write(response); - } - - JSONObject qInfo = rootObject.optJSONObject("queryinfo"); - if (qInfo != null) - { - QueryService.get().addAuditEvent(getUser(), getContainer(), qInfo.getString("schema"), qInfo.getString("query"), - getViewContext().getActionURL(), - rootObject.getString("auditMessage") + filename, - rowsArray.length()); - SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "convertTable", "asDelimited"); - } - } - catch (JSONException e) - { - ExceptionUtil.renderErrorView(getViewContext(), getPageConfig(), ErrorRenderer.ErrorType.notFound, HttpServletResponse.SC_BAD_REQUEST, "Failed to convert to table - invalid input", e, false, false); - } - } - } - - - public static class ConvertHtmlToExcelForm - { - private String _baseUrl; - private String _htmlFragment; - private String _name = "workbook.xls"; - - - public String getName() - { - return _name; - } - - public void setName(String name) - { - _name = name; - } - - public String getBaseUrl() - { - return _baseUrl; - } - - public void setBaseUrl(String baseUrl) - { - _baseUrl = baseUrl; - } - - public String getHtmlFragment() - { - return _htmlFragment; - } - - public void setHtmlFragment(String htmlFragment) - { - _htmlFragment = htmlFragment; - } - } - - - @RequiresPermission(ReadPermission.class) - public static class ConvertHtmlToExcelAction extends FormViewAction - { - String _responseHtml = null; - - @Override - public void validateCommand(ConvertHtmlToExcelForm target, Errors errors) - { - } - - @Override - public ModelAndView getView(ConvertHtmlToExcelForm form, boolean reshow, BindException errors) - { - String html = - "

" + - "" + - new CsrfInput(getViewContext()) + - "
"; - return HtmlView.unsafe(html); - } - - @Override - public boolean handlePost(ConvertHtmlToExcelForm form, BindException errors) - { - ActionURL url = getViewContext().getActionURL(); - String base = url.getBaseServerURI(); - if (!base.endsWith("/")) base += "/"; - - String baseTag = ""; - SafeToRender css = PageFlowUtil.getStylesheetIncludes(getContainer()); - String htmlFragment = StringUtils.trimToEmpty(form.getHtmlFragment()); - String html = "" + baseTag + css + "" + htmlFragment + ""; - - // UNDONE: strip script - List tidyErrors = new ArrayList<>(); - String tidy = JSoupUtil.tidyHTML(html, false, tidyErrors); - - if (!tidyErrors.isEmpty()) - { - for (String err : tidyErrors) - { - errors.reject(ERROR_MSG, err); - } - return false; - } - - _responseHtml = tidy; - return true; - } - - @Override - public ModelAndView getSuccessView(ConvertHtmlToExcelForm form) - { - ResponseHelper.setContentDisposition(getViewContext().getResponse(), ResponseHelper.ContentDispositionType.attachment, form.getName()); - getPageConfig().setTemplate(PageConfig.Template.None); - HtmlView v = HtmlView.unsafe(_responseHtml); - v.setContentType("application/vnd.ms-excel"); - v.setFrame(WebPartView.FrameType.NONE); - return v; - } - - @Override - public URLHelper getSuccessURL(ConvertHtmlToExcelForm convertHtmlToExcelForm) - { - return null; - } - - @Override - public void addNavTrail(NavTree root) - { - } - } - - - public static ActionURL getShowApplicationURL(Container c, long rowId) - { - ActionURL url = new ActionURL(ShowApplicationAction.class, c); - url.addParameter("rowId", rowId); - - return url; - } - - - @RequiresPermission(ReadPermission.class) - public class ShowApplicationAction extends SimpleViewAction - { - private ExpProtocolApplicationImpl _app; - private ExpRun _run; - - @Override - public ModelAndView getView(ExpObjectForm form, BindException errors) - { - _app = ExperimentServiceImpl.get().getExpProtocolApplication(form.getRowId()); - if (_app == null) - { - throw new NotFoundException("Could not find Protocol Application"); - } - _run = _app.getRun(); - if (_run == null) - { - throw new NotFoundException("No experiment run associated with Protocol Application"); - } - ensureCorrectContainer(getContainer(), _app, getViewContext()); - - ExpProtocol protocol = _app.getProtocol(); - - DataRegion dr = new DataRegion(); - dr.addColumns(ExperimentServiceImpl.get().getTinfoProtocolApplication().getUserEditableColumns()); - DetailsView detailsView = new DetailsView(dr, form.getRowId()); - dr.removeColumns("RunId", "ProtocolLSID", "RowId", "LSID"); - dr.addDisplayColumn(new ExperimentRunDisplayColumn(_run)); - dr.addDisplayColumn(new ProtocolDisplayColumn(protocol)); - dr.addDisplayColumn(new LineageGraphDisplayColumn(_app, _run)); - detailsView.setTitle("Protocol Application"); - - Container c = getContainer(); - ApplicationOutputGrid outMGrid = new ApplicationOutputGrid(c, _app.getRowId(), ExperimentServiceImpl.get().getTinfoMaterial()); - ApplicationOutputGrid outDGrid = new ApplicationOutputGrid(c, _app.getRowId(), ExperimentServiceImpl.get().getTinfoData()); - Map map = new HashMap<>(); - for (ProtocolApplicationParameter param : ExperimentService.get().getProtocolApplicationParameters(_app.getRowId())) - { - map.put(param.getOntologyEntryURI(), param); - } - - JspView> paramsView = new JspView<>("/org/labkey/experiment/Parameters.jsp", map); - paramsView.setTitle("Protocol Application Parameters"); - CustomPropertiesView cpv = new CustomPropertiesView(_app.getLSID(), c); - return new VBox(new StandardAndCustomPropertiesView(detailsView, cpv), paramsView, outMGrid, outDGrid); - } - - @Override - public void addNavTrail(NavTree root) - { - addRootNavTrail(root); - root.addChild("Experiment Run", ExperimentUrlsImpl.get().getRunGraphDetailURL(_run)); - root.addChild("Protocol Application " + _app.getName()); - } - } - - @RequiresPermission(ReadPermission.class) - public class ShowProtocolGridAction extends SimpleViewAction - { - @Override - public ModelAndView getView(Object o, BindException errors) - { - return new ProtocolWebPart(false, getViewContext()); - } - - @Override - public void addNavTrail(NavTree root) - { - addRootNavTrail(root); - root.addChild("Protocols"); - } - } - - @RequiresPermission(ReadPermission.class) - public class ProtocolDetailsAction extends SimpleViewAction - { - private ExpProtocolImpl _protocol; - - @Override - public ModelAndView getView(ExpObjectForm form, BindException errors) - { - _protocol = ExperimentServiceImpl.get().getExpProtocol(form.getRowId()); - if (_protocol == null) - { - _protocol = ExperimentServiceImpl.get().getExpProtocol(form.getLSID()); - } - - if (_protocol == null) - { - throw new NotFoundException("Unable to find a matching protocol"); - } - ensureCorrectContainer(getContainer(), _protocol, getViewContext()); - - JspView detailsView = new JspView<>("/org/labkey/experiment/ProtocolDetails.jsp", _protocol); - detailsView.setTitle("Standard Properties"); - - CustomPropertiesView cpv = new CustomPropertiesView(_protocol.getLSID(), getContainer()); - ProtocolParametersView parametersView = new ProtocolParametersView(_protocol); - - VBox protocolDetails = new VBox(); - protocolDetails.setFrame(WebPartView.FrameType.PORTAL); - protocolDetails.setTitle("Protocol Details"); - protocolDetails.addView(new ProtocolInputOutputsView(_protocol, errors)); - - JspView stepsView = new JspView<>("/org/labkey/experiment/ProtocolSteps.jsp", _protocol); - stepsView.setTitle("Protocol Steps"); - stepsView.setFrame(WebPartView.FrameType.TITLE); - protocolDetails.addView(stepsView); - - ExpSchema schema = new ExpSchema(getUser(), getContainer()); - ExperimentRunListView runView = new ExperimentRunListView(schema, ExperimentRunListView.getRunListQuerySettings(schema, getViewContext(), ExpSchema.TableType.Runs.name(), true), ExperimentRunType.ALL_RUNS_TYPE) - { - @Override - public DataView createDataView() - { - DataView result = super.createDataView(); - result.getRenderContext().setBaseFilter(new SimpleFilter(FieldKey.fromParts("Protocol", "LSID"), _protocol.getLSID())); - return result; - } - }; - - runView.setTitle("Runs Using This Protocol"); - - return new VBox(new StandardAndCustomPropertiesView(detailsView, cpv), parametersView, protocolDetails, runView); - } - - @Override - public void addNavTrail(NavTree root) - { - addRootNavTrail(root); - root.addChild("Protocols", ExperimentUrlsImpl.get().getProtocolGridURL(getContainer())); - root.addChild("Protocol: " + _protocol.getName()); - } - } - - public class ProtocolInputOutputsView extends VBox - { - ProtocolInputOutputsView(ExpProtocol protocol, Errors errors) - { - HBox inputsView = new HBox(); - addView(inputsView); - - HBox outputsView = new HBox(); - addView(outputsView); - - UserSchema expSchema = QueryService.get().getUserSchema(getUser(), getContainer(), ExpSchema.SCHEMA_NAME); - - class ProtocolInputGrid extends QueryView - { - public ProtocolInputGrid(String title, QuerySettings settings, @Nullable Errors errors) - { - super(expSchema, settings, errors); - - setFrame(FrameType.TITLE); - setTitle(title); - setButtonBarPosition(DataRegion.ButtonBarPosition.NONE); - setShowBorders(true); - setShadeAlternatingRows(true); - setShowExportButtons(false); - setShowPagination(false); - disableContainerFilterSelection(); - } - } - - // INPUTS - - QuerySettings materialInputsSettings = expSchema.getSettings("mpi", ExpSchema.TableType.MaterialProtocolInputs.toString()); - materialInputsSettings.getBaseFilter().addCondition(FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.Protocol.toString()), protocol.getRowId()); - materialInputsSettings.setFieldKeys(Arrays.asList( - FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.Name.toString()), - FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.SampleSet.toString()) - )); - QueryView materialInputsView = new ProtocolInputGrid("Material Inputs", materialInputsSettings, errors); - inputsView.addView(materialInputsView); - - QuerySettings dataInputsSettings = expSchema.getSettings("dpi", ExpSchema.TableType.DataProtocolInputs.toString()); - dataInputsSettings.getBaseFilter().addCondition(FieldKey.fromParts(ExpDataProtocolInputTable.Column.Protocol.toString()), protocol.getRowId()); - dataInputsSettings.setFieldKeys(Arrays.asList( - FieldKey.fromParts(ExpDataProtocolInputTable.Column.Name.toString()), - FieldKey.fromParts(ExpDataProtocolInputTable.Column.DataClass.toString()) - )); - QueryView dataInputsView = new ProtocolInputGrid("Data Inputs", dataInputsSettings, errors); - inputsView.addView(dataInputsView); - - // OUTPUTS - - QuerySettings materialOutputsSettings = expSchema.getSettings("mpo", ExpSchema.TableType.MaterialProtocolInputs.toString()); - materialOutputsSettings.getBaseFilter().addCondition(FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.Protocol.toString()), protocol.getRowId()); - materialOutputsSettings.setFieldKeys(Arrays.asList( - FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.Name.toString()), - FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.SampleSet.toString()) - )); - QueryView materialOutputsView = new ProtocolInputGrid("Material Outputs", materialOutputsSettings, errors); - outputsView.addView(materialOutputsView); - - QuerySettings dataOutputsSettings = expSchema.getSettings("dpo", ExpSchema.TableType.DataProtocolInputs.toString()); - dataOutputsSettings.getBaseFilter().addCondition(FieldKey.fromParts(ExpDataProtocolInputTable.Column.Protocol.toString()), protocol.getRowId()); - dataOutputsSettings.setFieldKeys(Arrays.asList( - FieldKey.fromParts(ExpDataProtocolInputTable.Column.Name.toString()), - FieldKey.fromParts(ExpDataProtocolInputTable.Column.DataClass.toString()) - )); - QueryView dataOutputsView = new ProtocolInputGrid("Data Outputs", dataOutputsSettings, errors); - outputsView.addView(dataOutputsView); - } - } - - - @RequiresPermission(ReadPermission.class) - public class ProtocolPredecessorsAction extends SimpleViewAction - { - private ExpProtocol _parentProtocol; - private ProtocolActionStepDetail _actionStep; - - @Override - public ModelAndView getView(Object o, BindException errors) - { - ActionURL url = getViewContext().getActionURL(); - - String parentProtocolLSID = url.getParameter("ParentLSID"); - int actionSequence; - try - { - actionSequence = Integer.parseInt(url.getParameter("Sequence")); - } - catch (NumberFormatException e) - { - throw new NotFoundException("Could not find SequenceId " + url.getParameter("Sequence")); - } - - _parentProtocol = ExperimentService.get().getExpProtocol(parentProtocolLSID); - if (_parentProtocol == null) - { - throw new NotFoundException("Unable to find a matching protocol"); - } - - ensureCorrectContainer(getContainer(), _parentProtocol, getViewContext()); - - _actionStep = ExperimentServiceImpl.get().getProtocolActionStepDetail(parentProtocolLSID, actionSequence); - - if (_actionStep == null) - { - throw new NotFoundException("Unable to find a matching protocol action step"); - } - - ExpProtocol childProtocol = ExperimentService.get().getExpProtocol(_actionStep.getChildProtocolLSID()); - - JspView detailsView = new JspView<>("/org/labkey/experiment/ProtocolDetails.jsp", childProtocol); - detailsView.setTitle("Standard Properties"); - - CustomPropertiesView cpv = new CustomPropertiesView(childProtocol.getLSID(), getContainer()); - - ProtocolParametersView parametersView = new ProtocolParametersView(childProtocol); - - VBox protocolDetails = new VBox(); - protocolDetails.setFrame(WebPartView.FrameType.PORTAL); - protocolDetails.setTitle("Protocol Details"); - protocolDetails.addView(new ProtocolInputOutputsView(childProtocol, errors)); - protocolDetails.addView(new ProtocolSuccessorPredecessorView(parentProtocolLSID, actionSequence, getContainer(), "PredecessorChildLSID", "PredecessorSequence", "ActionSequence", "Protocol Predecessors")); - protocolDetails.addView(new ProtocolSuccessorPredecessorView(parentProtocolLSID, actionSequence, getContainer(), "ChildProtocolLSID", "ActionSequence", "PredecessorSequence", "Protocol Successors")); - - return new VBox(new StandardAndCustomPropertiesView(detailsView, cpv), parametersView, protocolDetails); - } - - @Override - public void addNavTrail(NavTree root) - { - addRootNavTrail(root); - root.addChild("Protocols", ExperimentUrlsImpl.get().getProtocolGridURL(getContainer())); - root.addChild("Parent Protocol '" + _parentProtocol.getName() + "'", ExperimentUrlsImpl.get().getProtocolDetailsURL(_parentProtocol)); - root.addChild("Protocol Step: " + _actionStep.getName()); - } - } - - public static class DataForm - { - private boolean _inline; - private long _rowId; - private String _lsid; - private Integer _maxDimension; - private String _format; - - public boolean isInline() - { - return _inline; - } - - public void setInline(boolean inline) - { - _inline = inline; - } - - public long getRowId() - { - return _rowId; - } - - public void setRowId(long rowId) - { - _rowId = rowId; - } - - public String getLsid() - { - return _lsid; - } - - public void setLsid(String lsid) - { - _lsid = lsid; - } - - public ExpDataImpl lookupData() - { - ExpDataImpl result = ExperimentServiceImpl.get().getExpData(getRowId()); - if (result == null && getLsid() != null) - { - result = ExperimentServiceImpl.get().getExpData(getLsid()); - } - return result; - } - - public Integer getMaxDimension() - { - return _maxDimension; - } - - public void setMaxDimension(Integer maxDimension) - { - _maxDimension = maxDimension; - } - - public String getFormat() - { - return _format; - } - - public void setFormat(String format) - { - _format = format; - } - } - - public static class ExpObjectForm extends QueryViewAction.QueryExportForm - { - private long _rowId; - private String _lsid; - - public String getLsid() - { - return _lsid; - } - - public void setLsid(String lsid) - { - _lsid = lsid; - } - - public String getLSID() - { - return getLsid(); - } - - public void setLSID(String lsid) - { - setLsid(lsid); - } - - public long getRowId() - { - return _rowId; - } - - public void setRowId(long rowId) - { - _rowId = rowId; - } - } - - @RequiresAnyOf({DeletePermission.class, SampleWorkflowDeletePermission.class}) - public class DeleteSelectedExpRunsAction extends AbstractDeleteAction - { - @Override - public void addNavTrail(NavTree root) - { - // UNDONE: Need help topic on Runs - setHelpTopic("experiment"); - super.addNavTrail(root); - } - - @Override - public ModelAndView getView(DeleteForm deleteForm, boolean reshow, BindException errors) - { - List runs = new ArrayList<>(); - - Map idToRunMap = new LongHashMap<>(); - for (long runId : deleteForm.getIds(false)) - { - ExpRun run = ExperimentService.get().getExpRun(runId); - if (run != null) - { - if (!run.canDelete(getUser())) - throw new UnauthorizedException("You do not have permission to delete runs in " - + run.getContainer()); - - runs.add(run); - idToRunMap.put(run.getRowId(), run); - } - } - - Map referencedItems = new LongHashMap<>(); - List referenceDescriptions = new ArrayList<>(); - AssayService assayService = AssayService.get(); - if (!idToRunMap.isEmpty() && assayService != null ) - { - // using the first run as a representative, since all interactions here are (I believe) using the same protocol. - ExpProtocol protocol = runs.getFirst().getProtocol(); - AssayProvider provider = assayService.getProvider(protocol); - if (provider != null) - { - SchemaKey key = AssayProtocolSchema.schemaName(provider, protocol); - ExperimentService.get().getObjectReferencers() - .forEach(referencer -> { - Collection referenced = referencer.getItemsWithReferences( - idToRunMap.keySet(), - key.toString(), - "Runs" - ); - referenced.forEach(id -> referencedItems.put(id, idToRunMap.get(id))); - referenceDescriptions.add(referencer.getObjectReferenceDescription(ExpRun.class)); - } - ); - } - - } - - List> permissionDatasetRows = new ArrayList<>(); - List> noPermissionDatasetRows = new ArrayList<>(); - if (StudyPublishService.get() != null) - { - for (Dataset dataset : StudyPublishService.get().getDatasetsForAssayRuns(runs, getUser())) - { - ActionURL url = urlProvider(StudyUrls.class).getDatasetURL(dataset.getContainer(), dataset.getDatasetId()); - TableInfo t = dataset.getTableInfo(getUser()); - if (null != t && t.hasPermission(getUser(),DeletePermission.class)) - { - permissionDatasetRows.add(new Pair<>(dataset, url)); - } - else - { - noPermissionDatasetRows.add(new Pair<>(dataset, url)); - } - } - } - - return new ConfirmDeleteView( - "run", - ShowRunGraphAction.class, - runs.stream().filter(run -> !referencedItems.containsKey(run.getRowId())).toList(), - deleteForm, - Collections.emptyList(), - "dataset(s) have one or more rows which", - permissionDatasetRows, - noPermissionDatasetRows, - referencedItems.values().stream().toList(), - referenceDescriptions.stream().filter(Objects::nonNull).collect(Collectors.joining(", or "))); - } - - @Override - protected void deleteObjects(DeleteForm deleteForm) - { - ExperimentServiceImpl.get().deleteExperimentRunsByRowIds(getContainer(), getUser(), deleteForm.getUserComment(), deleteForm.getIds(false), getTransactionAuditDetails()); - } - } - - public static class DeleteRunForm - { - private int _runId; - - public int getRunId() - { - return _runId; - } - - public void setRunId(int runId) - { - _runId = runId; - } - } - - /** - * Separate delete action from the client API - */ - @RequiresAnyOf({DeletePermission.class, SampleWorkflowDeletePermission.class}) - public static class DeleteRunAction extends MutatingApiAction - { - @Override - public ApiResponse execute(DeleteRunForm form, BindException errors) - { - ExpRun run = ExperimentService.get().getExpRun(form.getRunId()); - if (run == null) - { - throw new NotFoundException("Could not find run with ID " + form.getRunId()); - } - if (!run.canDelete(getUser())) - throw new UnauthorizedException("You do not have permission to delete runs in this container."); - - run.delete(getUser()); - return new ApiSimpleResponse("success", true); - } - } - - - @RequiresAnyOf({DeletePermission.class, SampleWorkflowDeletePermission.class}) - public static class DeleteRunsAction extends AbstractDeleteAPIAction - { - @Override - protected ApiSimpleResponse deleteObjects(CascadeDeleteForm form) - { - Set runIdsToDelete = new HashSet<>(form.getIds(true)); - Set runIdsCascadeDeleted = new HashSet<>(); - - if (form.isCascade()) - { - for (long runId : runIdsToDelete) - { - ExpRun run = ExperimentService.get().getExpRun(runId); - if (run != null) - addReplacesRuns(run, runIdsCascadeDeleted); - } - - if (!runIdsCascadeDeleted.isEmpty()) - runIdsToDelete.addAll(runIdsCascadeDeleted); - } - - Map transactionAuditDetails = getTransactionAuditDetails(); - if (form.getRequestSource() != null) - transactionAuditDetails.put(TransactionAuditProvider.TransactionDetail.RequestSource, form.getRequestSource()); - if (form.getEditMethod() != null) - transactionAuditDetails.put(TransactionAuditProvider.TransactionDetail.EditMethod, form.getEditMethod()); - ExperimentService.get().deleteExperimentRunsByRowIds(getContainer(), getUser(), form.getUserComment(), runIdsToDelete, transactionAuditDetails); - - ApiSimpleResponse response = new ApiSimpleResponse("success", true); - response.put("runIdsDeleted", runIdsToDelete); - if (!runIdsCascadeDeleted.isEmpty()) - response.put("runIdsCascadeDeleted", runIdsCascadeDeleted); - return response; - } - - private void addReplacesRuns(ExpRun run, Set runIds) - { - for (ExpRun replacedRun : run.getReplacesRuns()) - { - runIds.add(replacedRun.getRowId()); - addReplacesRuns(replacedRun, runIds); - } - } - } - - private abstract static class AbstractDeleteAPIAction extends MutatingApiAction - { - @Override - public void validateForm(CascadeDeleteForm form, Errors errors) - { - if (form.getSingleObjectRowId() == null && form.getDataRegionSelectionKey() == null && form.getRowIds() == null) - errors.reject(ERROR_REQUIRED, "Either singleObjectRowId, dataRegionSelectionKey, or rowIds is required"); - } - - @Override - public ApiResponse execute(CascadeDeleteForm form, BindException errors) throws Exception - { - ApiSimpleResponse response; - - try (DbScope.Transaction tx = ExperimentService.get().ensureTransaction()) - { - tx.addCommitTask(form::clearSelected, POSTCOMMIT); - - response = deleteObjects(form); - tx.commit(); - } - - if (null != response.get("success")) - response.put("success", !errors.hasErrors()); - - return response; - } - - protected abstract ApiSimpleResponse deleteObjects(CascadeDeleteForm form) throws Exception; - } - - public static class CascadeDeleteForm extends DeleteForm - { - private boolean _cascade; - - public boolean isCascade() - { - return _cascade; - } - - public void setCascade(boolean cascade) - { - _cascade = cascade; - } - } - - private abstract static class AbstractDeleteAction extends FormViewAction - { - @Override - public void validateCommand(DeleteForm target, Errors errors) - { - } - - @Override - public boolean handlePost(DeleteForm deleteForm, BindException errors) throws Exception - { - if (!deleteForm.isForceDelete()) - { - return false; - } - else - { - try (DbScope.Transaction tx = ExperimentService.get().ensureTransaction()) - { - tx.addCommitTask(deleteForm::clearSelected, POSTCOMMIT); - - deleteObjects(deleteForm); - tx.commit(); - } - catch (BatchValidationException v) - { - v.addToErrors(errors); - } - - return !errors.hasErrors(); - } - } - - @Override - public ActionURL getSuccessURL(DeleteForm form) - { - return form.getSuccessActionURL(ExperimentUrlsImpl.get().getOverviewURL(getContainer())); - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Confirm Deletion"); - } - - protected abstract void deleteObjects(DeleteForm form) throws Exception; - } - - @RequiresPermission(DesignAssayPermission.class) - public static class DeleteProtocolByRowIdsAPIAction extends AbstractDeleteAPIAction - { - @Override - protected ApiSimpleResponse deleteObjects(CascadeDeleteForm form) - { - for (ExpProtocol protocol : getProtocolsForDeletion(form)) - { - if (!protocol.getContainer().hasPermission(getUser(), DesignAssayPermission.class)) - throw new UnauthorizedException("You do not have sufficient permissions to delete this assay design."); - - protocol.delete(getUser(), form.getUserComment()); - } - - return new ApiSimpleResponse(); - } - } - - public static List getProtocolsForDeletion(DeleteForm form) - { - List protocols = new ArrayList<>(); - for (long protocolId : form.getIds(false)) - { - ExpProtocol protocol = ExperimentService.get().getExpProtocol(protocolId); - if (protocol != null) - { - protocols.add(protocol); - } - } - return protocols; - } - - @RequiresPermission(DesignAssayPermission.class) - public class DeleteProtocolByRowIdsAction extends AbstractDeleteAction - { - @Override - public void addNavTrail(NavTree root) - { - // UNDONE: Need help topic on protocols - setHelpTopic("experiment"); - super.addNavTrail(root); - } - - @Override - public ModelAndView getView(DeleteForm form, boolean reshow, BindException errors) - { - List runs = ExperimentService.get().getExpRunsForProtocolIds(false, form.getIds(false)); - List protocols = getProtocolsForDeletion(form); - String noun = "Assay Design"; - List> deleteableDatasets = new ArrayList<>(); - List> noPermissionDatasets = new ArrayList<>(); - if (AssayService.get() != null && StudyService.get() != null) - { - for (ExpProtocol protocol : protocols) - { - if (!protocol.getContainer().hasPermission(getUser(), DesignAssayPermission.class)) - throw new UnauthorizedException("You do not have sufficient permissions to delete this assay design."); - - if (AssayService.get().getProvider(protocol) == null) - { - noun = "Protocol"; - } - for (Dataset dataset : StudyPublishService.get().getDatasetsForPublishSource(protocol.getRowId(), Dataset.PublishSource.Assay)) - { - Pair entry = new Pair<>(dataset, urlProvider(StudyUrls.class).getDatasetURL(dataset.getContainer(), dataset.getDatasetId())); - if (dataset.canDeleteDefinition(getUser())) - { - deleteableDatasets.add(entry); - } - else - { - noPermissionDatasets.add(entry); - } - } - } - } - - return new ConfirmDeleteView(noun, ProtocolDetailsAction.class, protocols, form, runs, "Dataset", deleteableDatasets, noPermissionDatasets, Collections.emptyList(), null); - } - - @Override - protected void deleteObjects(DeleteForm form) - { - for (ExpProtocol protocol : getProtocolsForDeletion(form)) - { - protocol.delete(getUser(), form.getUserComment()); - } - } - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(ReadPermission.class) - public static class GetDataOperationConfirmationDataAction extends ReadOnlyApiAction - { - @Override - public void validateForm(DataOperationConfirmationForm form, Errors errors) - { - if (form.getDataRegionSelectionKey() == null && form.getRowIds() == null) - errors.reject(ERROR_REQUIRED, "You must provide either a set of rowIds or a dataRegionSelectionKey"); - if (form.getDataOperation() == null) - errors.reject(ERROR_REQUIRED, "An operation type must be provided."); - } - - @Override - public Object execute(DataOperationConfirmationForm form, BindException errors) - { - Collection requestIds = form.getIds(false); - ExperimentServiceImpl service = ExperimentServiceImpl.get(); - List allData = service.getExpDatas(requestIds); - - Set notAllowedIds = new HashSet<>(); - if (form.getDataOperation() == ExpDataImpl.DataOperations.Delete) - service.getObjectReferencers().forEach(referencer -> - notAllowedIds.addAll(referencer.getItemsWithReferences(requestIds, "exp.data"))); - - Map>> response = ExperimentServiceImpl.partitionRequestedOperationObjects(getUser(), requestIds, notAllowedIds, allData); - - Collection containers = new HashSet<>(); - Collection notPermittedIds = new ArrayList<>(); - Class permClass = form.getDataOperation().getPermissionClass(); - for (ExpDataImpl expData : allData) - { - Container c = expData.getContainer(); - if (c.hasPermission(getUser(), ReadPermission.class)) - containers.add(c); - if (permClass != null && !c.hasPermission(getUser(), permClass)) - notPermittedIds.add(expData.getRowId()); - } - - NameExpressionOptionService svc = NameExpressionOptionService.get(); - response.put("containers", containers.stream().map(c -> Map.of( - "id", c.getEntityId(), - "path", (Object) c.getPath(), - "permitted", permClass == null || c.hasPermission(getUser(), permClass), - "canEditName", svc.getAllowUserSpecificNamesValue(c) - )).toList()); - - response.put("notPermitted", notPermittedIds.stream().map(id -> Map.of("RowId", (Object) id)).toList()); - - return success(response); - } - } - - - public static class DataOperationConfirmationForm extends DataViewSnapshotSelectionForm - { - private ExpDataImpl.DataOperations _dataOperation; - - public ExpDataImpl.DataOperations getDataOperation() - { - return _dataOperation; - } - - public void setDataOperation(ExpDataImpl.DataOperations dataOperation) - { - _dataOperation = dataOperation; - } - - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(ReadPermission.class) - public static class GetMaterialOperationConfirmationDataAction extends ReadOnlyApiAction - { - @Override - public void validateForm(MaterialOperationConfirmationForm form, Errors errors) - { - if (form.getDataRegionSelectionKey() == null && form.getRowIds() == null) - errors.reject(ERROR_REQUIRED, "You must provide either a set of rowIds or a dataRegionSelectionKey."); - if (form.getSampleOperation() == null) - errors.reject(ERROR_REQUIRED, "An operation type must be provided."); - } - - @Override - public Object execute(MaterialOperationConfirmationForm form, BindException errors) - { - Set requestIds = form.getIds(false); - ExperimentServiceImpl service = ExperimentServiceImpl.get(); - List allMaterials = service.getExpMaterials(requestIds); - - Set notAllowedIds = new HashSet<>(); - // We prevent deletion if a sample is used as a parent, has assay data, is used in a job, etc. - if (form.getSampleOperation() == SampleTypeService.SampleOperations.Delete) - service.getObjectReferencers().forEach(referencer -> - notAllowedIds.addAll(referencer.getItemsWithReferences(requestIds, "samples"))); - - if (SampleStatusService.get().supportsSampleStatus()) - notAllowedIds.addAll(service.findIdsNotPermittedForOperation(allMaterials, form.getSampleOperation())); - - Map>> response = ExperimentServiceImpl.partitionRequestedOperationObjects(getUser(), requestIds, notAllowedIds, allMaterials); - - Collection containers = new HashSet<>(); - Collection notPermittedIds = new ArrayList<>(); - Class permClass = form.getSampleOperation().getPermissionClass(); - for (ExpMaterial material : allMaterials) - { - Container c = material.getContainer(); - if (c.hasPermission(getUser(), ReadPermission.class)) - containers.add(c); - if (permClass != null && !c.hasPermission(getUser(), permClass)) - notPermittedIds.add(material.getRowId()); - } - - NameExpressionOptionService svc = NameExpressionOptionService.get(); - - response.put("containers", containers.stream().map(c -> Map.of( - "id", c.getEntityId(), - "path", (Object) c.getPath(), - "permitted", permClass == null || c.hasPermission(getUser(), permClass), - "canEditName", svc.getAllowUserSpecificNamesValue(c) - )).toList()); - - response.put("notPermitted", notPermittedIds.stream().map(id -> Map.of("RowId", (Object) id)).toList()); - - if (form.getSampleOperation() == SampleTypeService.SampleOperations.Delete) - // String 'associatedDatasets' must be synced to its handling in confirmDelete.js, confirmDelete() - response.put("associatedDatasets", ExperimentServiceImpl.includeLinkedToStudyText(allMaterials, requestIds, getUser(), getContainer())); - - return success(response); - } - } - - public static class MaterialOperationConfirmationForm extends DataViewSnapshotSelectionForm - { - private SampleTypeService.SampleOperations _sampleOperation; - - public SampleTypeService.SampleOperations getSampleOperation() - { - return _sampleOperation; - } - - public void setSampleOperation(SampleTypeService.SampleOperations sampleOperation) - { - _sampleOperation = sampleOperation; - } - } - - @RequiresPermission(DeletePermission.class) - public class DeleteSelectedDataAction extends AbstractDeleteAction - { - @Override - public void addNavTrail(NavTree root) - { - // UNDONE: Need help topic on Datas - setHelpTopic("experiment"); - super.addNavTrail(root); - } - - @Override - protected void deleteObjects(DeleteForm deleteForm) throws Exception - { - List datas = getDatas(deleteForm, false); - - for (ExpRun run : getRuns(datas)) - { - if (!run.getContainer().hasPermission(getUser(), DeletePermission.class)) - throw new UnauthorizedException(); - } - - // Issue 32076: Delete the exp.Data objects using QueryUpdateService so trigger scripts will be executed - Map, List> byDataClass = datas.stream().collect(Collectors.groupingBy(d -> Optional.ofNullable(d.getDataClass(null)))); - for (Optional opt : byDataClass.keySet()) - { - SchemaKey schemaKey; - String queryName; - ExpDataClass dc = opt.orElse(null); - List ds = byDataClass.get(opt); - if (dc == null) - { - // Reference to exp.Data table - schemaKey = ExpSchema.SCHEMA_EXP; - queryName = ExpSchema.TableType.Data.name(); - } - else - { - // Reference to exp.data. table - schemaKey = ExpSchema.SCHEMA_EXP_DATA; - queryName = dc.getName(); - } - - UserSchema schema = QueryService.get().getUserSchema(getUser(), getContainer(), schemaKey); - if (schema == null) - throw new IllegalStateException("Failed to get schema '" + schemaKey + "'"); - - TableInfo table = schema.getTable(queryName); - if (table == null) - throw new IllegalStateException("Failed to get table '" + queryName + "' in schema '" + schemaKey + "'"); - - QueryUpdateService qus = table.getUpdateService(); - if (qus == null) - throw new IllegalStateException(); - - qus.deleteRows(getUser(), getContainer(), toKeys(ds), null, null); - } - } - - protected List> toKeys(List datas) - { - return datas.stream().map(d -> CaseInsensitiveHashMap.of("rowId", d.getRowId())).collect(toList()); - } - - @Override - public ModelAndView getView(DeleteForm deleteForm, boolean reshow, BindException errors) - { - if (errors.hasErrors()) - return new SimpleErrorView(errors, false); - - List datas = getDatas(deleteForm, false); - List runs = getRuns(datas); - - return new ConfirmDeleteView("Data", ShowDataAction.class, datas, deleteForm, runs); - } - - private List getRuns(List datas) - { - List runArray = ExperimentService.get().getRunsUsingDatas(datas); - return new ArrayList<>(ExperimentService.get().runsDeletedWithInput(runArray)); - } - - private List getDatas(DeleteForm deleteForm, boolean clear) - { - List datas = new ArrayList<>(); - for (long dataId : deleteForm.getIds(clear)) - { - ExpData data = ExperimentService.get().getExpData(dataId); - if (data != null) - { - datas.add(data); - } - } - return datas; - } - } - - @RequiresPermission(DeletePermission.class) - public class DeleteSelectedExperimentsAction extends AbstractDeleteAction - { - @Override - protected void deleteObjects(DeleteForm deleteForm) - { - for (ExpExperiment exp : lookupExperiments(deleteForm)) - { - exp.delete(getUser()); - } - } - - @Override - public ModelAndView getView(DeleteForm deleteForm, boolean reshow, BindException errors) - { - List experiments = lookupExperiments(deleteForm); - - List runs = new ArrayList<>(); - boolean allBatches = true; - for (ExpExperiment experiment : experiments) - { - // Deleting a batch also deletes all of its runs - if (experiment.getBatchProtocol() != null) - { - runs.addAll(experiment.getRuns()); - } - else - { - allBatches = false; - } - } - - return new ConfirmDeleteView(allBatches ? "batch" : "run group", DetailsAction.class, experiments, deleteForm, runs); - } - - private List lookupExperiments(DeleteForm deleteForm) - { - List experiments = new ArrayList<>(); - for (long experimentId : deleteForm.getIds(false)) - { - ExpExperiment experiment = ExperimentService.get().getExpExperiment(experimentId); - if (experiment != null) - { - experiments.add(experiment); - } - } - return experiments; - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("runGroups"); - super.addNavTrail(root); - } - } - - @RequiresPermission(DesignSampleTypePermission.class) - public class DeleteSampleTypesAction extends AbstractDeleteAction - { - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("sampleSets"); - super.addNavTrail(root); - } - - @Override - protected void deleteObjects(DeleteForm deleteForm) - { - List sampleTypes = getSampleTypes(deleteForm); - if (sampleTypes.isEmpty()) - { - throw new NotFoundException("No sample types found for ids provided."); - } - if (!ensureCorrectContainer(sampleTypes)) - { - throw new UnauthorizedException(); - } - - for (ExpRun run : getRuns(sampleTypes)) - { - if (!run.getContainer().hasPermission(getUser(), DeletePermission.class)) - { - throw new UnauthorizedException(); - } - } - - for (ExpSampleType source : sampleTypes) - { - Domain domain = source.getDomain(); - if (!domain.getDomainKind().canDeleteDefinition(getUser(), domain)) - { - throw new UnauthorizedException(); - } - - source.delete(getUser(), deleteForm.getUserComment()); - } - } - - @Override - public ModelAndView getView(DeleteForm deleteForm, boolean reshow, BindException errors) - { - List sampleTypes = getSampleTypes(deleteForm); - if (!ensureCorrectContainer(sampleTypes)) - { - throw new RedirectException(ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer(), "To delete a sample type, you must be in its folder or project.")); - } - - List> deleteableDatasets = new ArrayList<>(); - List> noPermissionDatasets = new ArrayList<>(); - if (StudyService.get() != null && StudyPublishService.get() != null) - { - for (ExpSampleType sampleType: sampleTypes) - { - for (Dataset dataset : StudyPublishService.get().getDatasetsForPublishSource(sampleType.getRowId(), Dataset.PublishSource.SampleType)) - { - ActionURL datasetURL = StudyService.get().getDatasetURL(getContainer(), dataset.getDatasetId()); - Pair entry = new Pair<>(dataset, datasetURL); - if (dataset.canDeleteDefinition(getUser())) - { - deleteableDatasets.add(entry); - } - else - { - noPermissionDatasets.add(entry); - } - } - } - } - return new ConfirmDeleteView("Sample Type", ShowSampleTypeAction.class, sampleTypes, deleteForm, getRuns(sampleTypes), "Dataset", deleteableDatasets, noPermissionDatasets, Collections.emptyList(), null); - } - - private List getSampleTypes(DeleteForm deleteForm) - { - List sources = new ArrayList<>(); - for (long rowId : deleteForm.getIds(false)) - { - ExpSampleType sampleType = SampleTypeService.get().getSampleType(getContainer(), rowId, true); - if (sampleType != null) - { - sources.add(sampleType); - } - } - return sources; - } - - private boolean ensureCorrectContainer(List sampleTypes) - { - for (ExpSampleType source : sampleTypes) - { - Container sourceContainer = source.getContainer(); - if (!sourceContainer.equals(getContainer())) - { - return false; - } - } - return true; - } - - private List getRuns(List sampleTypes) - { - if (!sampleTypes.isEmpty()) - { - List runArray = ExperimentService.get().getRunsUsingSampleTypes(sampleTypes.toArray(new ExpSampleType[0])); - return ExperimentService.get().runsDeletedWithInput(runArray); - } - else - { - return Collections.emptyList(); - } - } - } - - private DataRegion getSampleTypeRegion(ViewContext model) - { - TableInfo tableInfo = ExperimentServiceImpl.get().getTinfoSampleType(); - - QuerySettings settings = new QuerySettings(model, "SampleType"); - settings.setSelectionKey(DataRegionSelection.getSelectionKey(tableInfo.getSchema().getName(), tableInfo.getName(), "SampleType", settings.getDataRegionName())); - - DataRegion dr = new DataRegion(); - dr.setSettings(settings); - dr.addColumns(tableInfo.getUserEditableColumns()); - dr.removeColumns("lastindexed"); - dr.getDisplayColumn(0).setVisible(false); - - dr.getDisplayColumn("idcol1").setVisible(false); - dr.getDisplayColumn("idcol2").setVisible(false); - dr.getDisplayColumn("idcol3").setVisible(false); - dr.getDisplayColumn("lsid").setVisible(false); - dr.getDisplayColumn("materiallsidprefix").setVisible(false); - dr.getDisplayColumn("parentcol").setVisible(false); - - ActionURL url = new ActionURL(ShowSampleTypeAction.class, model.getContainer()); - dr.getDisplayColumn(1).setURL(url.addParameter("rowId", "${RowId}")); - dr.setShowRecordSelectors(getContainer().hasOneOf(getUser(), DeletePermission.class, UpdatePermission.class)); - - return dr; - } - - @RequiresPermission(ReadPermission.class) - @ActionNames("getSampleType,getSampleTypeApi") // Referenced in labkey-ui-components components/samples/actions.ts TODO: migrate getSampleTypeApi -> getSampleType - public static class GetSampleTypeAction extends ReadOnlyApiAction - { - @Override - public void validateForm(SampleTypeForm form, Errors errors) - { - if (form.getRowId() == null && form.getLSID() == null) - errors.reject(ERROR_REQUIRED, "RowId or LSID must be provided"); - } - - @Override - public Object execute(SampleTypeForm form, BindException errors) throws Exception - { - ExpSampleTypeImpl st = form.getSampleType(getContainer()); - - return getSampleTypeResponse(st); - } - } - - @NotNull - private static ApiSimpleResponse getSampleTypeResponse(ExpSampleType st) throws IOException - { - Map sampleType = new HashMap<>(); - sampleType.put("name", st.getName()); - sampleType.put("nameExpression", st.getNameExpression()); - sampleType.put("labelColor", st.getLabelColor()); - sampleType.put("metricUnit", st.getMetricUnit()); - sampleType.put("description", st.getDescription()); - sampleType.put("importAliases", st.getImportAliasMap()); - sampleType.put("lsid", st.getLSID()); - sampleType.put("rowId", st.getRowId()); - sampleType.put("domainId", st.getDomain().getTypeId()); - sampleType.put("category", st.getCategory()); - - return new ApiSimpleResponse(Map.of("sampleSet", sampleType, "success", true)); - } - - public static class DataTypesWithRequiredLineageForm - { - private Integer _parentDataTypeRowId; - private boolean _sampleParent; - - public Integer getParentDataTypeRowId() - { - return _parentDataTypeRowId; - } - - public void setParentDataTypeRowId(Integer parentDataTypeRowId) - { - this._parentDataTypeRowId = parentDataTypeRowId; - } - - public boolean isSampleParent() - { - return _sampleParent; - } - - public void setSampleParent(boolean sampleParent) - { - _sampleParent = sampleParent; - } - } - - @RequiresPermission(ReadPermission.class) - public static class GetDataTypesWithRequiredLineageAction extends ReadOnlyApiAction - { - @Override - public void validateForm(DataTypesWithRequiredLineageForm form, Errors errors) - { - if (form.getParentDataTypeRowId() == null) - errors.reject(ERROR_REQUIRED, "ParentDataTypeRowId must be provided"); - } - - @Override - public Object execute(DataTypesWithRequiredLineageForm form, BindException errors) throws Exception - { - return getDataTypesWithRequiredLineageResponse(form.getParentDataTypeRowId(), form.isSampleParent(), getContainer(), getUser()); - } - } - @NotNull - private static ApiSimpleResponse getDataTypesWithRequiredLineageResponse(Integer parentDataType, boolean isSampleParent, Container container, User user) - { - Pair, Set> requiredLineages = ExperimentServiceImpl.get().getDataTypesWithRequiredLineage(parentDataType, isSampleParent, container, user); - return new ApiSimpleResponse(Map.of("sampleTypes", requiredLineages.first, "dataClasses", requiredLineages.second,"success", true)); - } - - @RequiresPermission(DesignSampleTypePermission.class) - public static class EditSampleTypeAction extends SimpleViewAction - { - private ExpSampleTypeImpl _sampleType; - - @Override - public ModelAndView getView(SampleTypeForm form, BindException errors) - { - boolean create = form.getLSID() == null && form.getRowId() == null; - if (!create) - _sampleType = form.getSampleType(getContainer()); - - return ModuleHtmlView.get(ModuleLoader.getInstance().getModule("core"), ModuleHtmlView.getGeneratedViewPath("sampleTypeDesigner")); - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("sampleSets"); - - root.addChild("Sample Types", ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); - if (_sampleType == null) - { - root.addChild("Create Sample Type"); - } - else - { - root.addChild(_sampleType.getName(), ExperimentUrlsImpl.get().getShowSampleTypeURL(_sampleType)); - root.addChild("Update Sample Type"); - } - } - } - - public static class SampleTypeForm extends ReturnUrlForm - { - private Integer rowId; - private String lsid; - - public Integer getRowId() - { - return rowId; - } - - public void setRowId(Integer rowId) - { - this.rowId = rowId; - } - - public String getLSID() - { - return this.lsid; - } - - public void setLSID(String lsid) - { - this.lsid = lsid; - } - - public ExpSampleTypeImpl getSampleType(Container container) throws NotFoundException - { - ExpSampleTypeImpl sampleType = SampleTypeServiceImpl.get().getSampleType(getLSID()); - if (sampleType == null) - sampleType = SampleTypeServiceImpl.get().getSampleType(getRowId()); - - if (sampleType == null) - { - throw new NotFoundException("Sample type not found: " + (getLSID() != null ? getLSID() : getRowId())); - } - - if (!container.equals(sampleType.getContainer())) - { - throw new NotFoundException("Sample type is not defined in the given container."); - } - - return sampleType; - } - } - - @RequiresPermission(InsertPermission.class) - public static class ImportSamplesAction extends AbstractExpDataImportAction - { - ExpSampleTypeImpl _sampleType; - boolean _isCrossTypeImport = false; - - @Override - public void validateForm(QueryForm queryForm, Errors errors) - { - _form = queryForm; - _insertOption = queryForm.getInsertOption(); - _isCrossTypeImport = getOptionParamValue(Params.crossTypeImport); - _form.setSchemaName(getTargetSchemaName()); - if (_isCrossTypeImport) - { - _form.setQueryName(getPipelineTargetQueryName()); - } - super.validateForm(queryForm, errors); - if (queryForm.getQueryName() == null) - errors.reject(ERROR_REQUIRED, "Sample type name is required"); - else - { - if (!_isCrossTypeImport) - { - _sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), queryForm.getQueryName(), true); - if (_sampleType == null) - { - errors.reject(ERROR_GENERIC, "Sample type '" + queryForm.getQueryName() + " not found."); - } - } - } - } - - private String getTargetSchemaName() - { - return getOptionParamValue(Params.crossTypeImport) ? ExpSchema.SCHEMA_NAME : "samples"; - } - - @Override - protected UserSchema getTargetSchema() - { - return getOptionParamValue(Params.crossTypeImport) ? QueryService.get().getUserSchema(getUser(), getContainer(), getTargetSchemaName()) : super.getTargetSchema(); - } - - @Override - protected String getPipelineTargetQueryName() - { - return getOptionParamValue(Params.crossTypeImport) ? "materials" : super.getPipelineTargetQueryName(); - } - - @Override - protected Map getRenamedColumns() - { - Map renamedColumns = super.getRenamedColumns(); - renamedColumns.putAll(SampleTypeUpdateServiceDI.SAMPLE_ALT_IMPORT_NAME_COLS); - return renamedColumns; - } - - @Override - protected @Nullable Set getLineageImportAliases() throws IOException - { - Set aliases = new CaseInsensitiveHashSet(); - // Issue 53419: Aliquot parent with number like names that starts with leading zeroes aren't resolved during import - aliases.add(ExpMaterial.ALIQUOTED_FROM_INPUT); - aliases.add(ExpMaterial.ALIQUOTED_FROM_INPUT_LABEL); - boolean crossTypeImport = getOptionParamValue(AbstractQueryImportAction.Params.crossTypeImport); - // Issue 51894: We need to stop conversion to numbers for alias fields for all type - // If there are aliases defined for one type that are number fields in another type, this will prevent - // conversion to numbers during the initial partitioning, but the conversion will happen when the partition - // file is loaded. - if (crossTypeImport) - { - List sampleTypes = SampleTypeServiceImpl.get().getSampleTypes(getContainer(), true); - for (ExpSampleTypeImpl sampleType : sampleTypes) - aliases.addAll(sampleType.getImportAliases().keySet()); - } - else - { - ExpSampleTypeImpl sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), _form.getQueryName(), true); - aliases.addAll(sampleType.getImportAliases().keySet()); - } - return aliases; - } - - @Override - protected int importData( - DataLoader dl, - FileStream file, - String originalName, - BatchValidationException errors, - @Nullable AuditBehaviorType auditBehaviorType, - TransactionAuditProvider.@Nullable TransactionAuditEvent auditEvent, - @Nullable String auditUserComment - ) throws IOException - { - initContext(dl, errors, auditBehaviorType, auditUserComment); - - TableInfo tInfo = _target; - QueryUpdateService updateService = _updateService; - if (getOptionParamValue(Params.crossTypeImport)) - { - tInfo = ExperimentService.get().createMaterialTable(new SamplesSchema(getUser(), getContainer()), ContainerFilter.current(this), null); - updateService = tInfo.getUpdateService(); - } - if (WorkflowService.get() != null) - { - try - { - WorkflowService.get().populateConfigParams(getViewContext().getRequest(), _context.getConfigParameters()); - } - catch (ValidationException e) - { - errors.addRowError(e); - } - } - - int count = importData(dl, tInfo, updateService, _context, auditEvent, getUser(), getContainer()); - - if (getOptionParamValue(Params.crossTypeImport)) - { - SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "sampleImport", "crossTypeImport"); - if (_context.getInsertOption() == QueryUpdateService.InsertOption.UPDATE) - SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "sampleImport", "crossTypeUpdate"); - else if (_context.getInsertOption() == QueryUpdateService.InsertOption.MERGE) - SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "sampleImport", "crossTypeMerge"); - } - - return count; - } - - @Override - public ModelAndView getView(QueryForm form, BindException errors) throws Exception - { - initRequest(form); - setHelpTopic("importSampleSets"); // page-wide help topic - setImportHelpTopic("importSampleSets"); // importOptions help topic - setTypeName("samples"); - return getDefaultImportView(form, errors); - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Sample Types", ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); - ActionURL url = _form.urlFor(QueryAction.executeQuery); - if (_form.getQueryName() != null && url != null) - root.addChild(_form.getQueryName(), url); - root.addChild("Import Data"); - } - - @Override - protected JSONObject createSuccessResponse(int rowCount) - { - JSONObject json = super.createSuccessResponse(rowCount); - if (!_context.getResponseInfo().isEmpty()) - { - for (String key : _context.getResponseInfo().keySet()) - json.put(key, _context.getResponseInfo().get(key)); - } - return json; - } - - } - - public abstract static class AbstractExpDataImportAction extends AbstractQueryImportAction - { - protected QueryForm _form; - protected DataIteratorContext _context; - - @Override - public void validateForm(QueryForm form, Errors errors) - { - QueryDefinition query = form.getQueryDef(); - if (query.getContainerFilter() != null && query.getContainerFilter().getType() != null) - { - // cross folder import not supported - if (query.getContainerFilter().getType() != ContainerFilter.Type.Current) - errors.reject(ERROR_GENERIC, "ContainerFilter is not supported for import actions."); - } - } - - @Override - protected void initRequest(QueryForm form) throws ServletException - { - QueryDefinition query = form.getQueryDef(); - setContainerFilterForImport(query, getContainer(), getUser()); - List qpe = new ArrayList<>(); - TableInfo t = query.getTable(form.getSchema(), qpe, true); - - if (!qpe.isEmpty()) - throw qpe.getFirst(); - if (!getOptionParamValue(Params.crossTypeImport) && null != t) - { - setTarget(t); - setShowMergeOption(t.supportsInsertOption(QueryUpdateService.InsertOption.MERGE)); - setShowUpdateOption(t.supportsInsertOption(QueryUpdateService.InsertOption.UPDATE)); - } - - _auditBehaviorType = form.getAuditBehavior(); - _auditUserComment = form.getAuditUserComment(); - } - - @Override - protected Map getRenamedColumns() - { - final String renameParamPrefix = "importAlias."; - Map renameColumns = new CaseInsensitiveHashMap<>(); - PropertyValue[] pvs = _form.getInitParameters().getPropertyValues(); - for (PropertyValue pv : pvs) - { - String paramName = pv.getName(); - if (!paramName.startsWith(renameParamPrefix) || pv.getValue() == null) - continue; - - renameColumns.put(paramName.substring(renameParamPrefix.length()), (String) pv.getValue()); - } - return renameColumns; - } - - @Override - protected Set getLineageImportAliases() throws IOException - { - ExpDataClass dataClass = ExperimentServiceImpl.get().getDataClass(getContainer(), _form.getQueryName(), true); - return new CaseInsensitiveHashSet(dataClass.getImportAliases().keySet()); - } - - protected void initContext(DataLoader dl, BatchValidationException errors, @Nullable AuditBehaviorType auditBehaviorType, @Nullable String auditUserComment) - { - _context = createDataIteratorContext(_insertOption, getOptionParamsMap(), getLookupResolutionType(), auditBehaviorType, auditUserComment, errors, null, getContainer()); - - if (_context.isCrossFolderImport() && !getContainer().hasProductFolders()) - _context.setCrossFolderImport(false); - } - - @Override - protected int importData(DataLoader dl, FileStream file, String originalName, BatchValidationException errors, @Nullable AuditBehaviorType auditBehaviorType, TransactionAuditProvider.@Nullable TransactionAuditEvent auditEvent, @Nullable String auditUserComment) throws IOException - { - initContext(dl, errors, auditBehaviorType, auditUserComment); - return importData(dl, _target, _updateService, _context, auditEvent, getUser(), getContainer()); - } - - @Override - protected String getQueryImportProviderName() - { - PropertyValue pv = _form.getInitParameters().getPropertyValue(QUERY_IMPORT_PIPELINE_PROVIDER_PARAM); - return pv == null ? null : (String) pv.getValue(); - } - - @Override - protected String getQueryImportDescription() - { - PropertyValue pv = _form.getInitParameters().getPropertyValue(QUERY_IMPORT_PIPELINE_DESCRIPTION_PARAM); - return pv == null ? null : (String) pv.getValue(); - } - - @Override - protected String getQueryImportJobNotificationProviderName() - { - PropertyValue pv = _form.getInitParameters().getPropertyValue(QUERY_IMPORT_NOTIFICATION_PROVIDER_PARAM); - return pv == null ? null : (String) pv.getValue(); - } - - @Override - protected boolean isBackgroundImportSupported(@NotNull String fileName) - { - return true; - } - - @Override - protected boolean allowLineageColumns() - { - return true; - } - - } - - @RequiresPermission(InsertPermission.class) - public static class ImportDataAction extends AbstractExpDataImportAction - { - @Override - public void validateForm(QueryForm queryForm, Errors errors) - { - _form = queryForm; - _form.setSchemaName("exp.data"); - _insertOption = queryForm.getInsertOption(); - super.validateForm(queryForm, errors); - if (queryForm.getQueryName() == null) - errors.reject(ERROR_REQUIRED, "Data class name is required"); - else - { - ExpDataClass dataClass = ExperimentService.get().getDataClass(getContainer(), queryForm.getQueryName(), true); - if (dataClass == null) - { - errors.reject(ERROR_GENERIC, "Data class '" + queryForm.getQueryName() + " not found."); - } - } - } - - @Override - public ModelAndView getView(QueryForm form, BindException errors) throws Exception - { - initRequest(form); - setHelpTopic("dataClass"); // page wide help topic - setImportHelpTopic("dataClass#ui"); // importOptions help topic - setTypeName("data"); - return getDefaultImportView(form, errors); - } - - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Data Classes", ExperimentUrlsImpl.get().getDataClassListURL(getContainer())); - ActionURL url = _form.urlFor(QueryAction.executeQuery); - if (_form.getQueryName() != null && url != null) - root.addChild(_form.getQueryName(), url); - root.addChild("Import Data"); - } - - } - - @RequiresPermission(UpdatePermission.class) - public class ShowUpdateAction extends SimpleViewAction - { - @Override - public ModelAndView getView(ExperimentForm form, BindException errors) - { - form.refreshFromDb(); - Experiment exp = form.getBean(); - if (exp == null) - { - throw new NotFoundException(); - } - ensureCorrectContainer(getContainer(), ExperimentService.get().getExpExperiment(exp.getRowId()), getViewContext()); - - return new ExperimentUpdateView(new DataRegion(), form, errors); - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("runGroups"); - addRootNavTrail(root); - root.addChild("Update Run Group"); - } - } - - @RequiresPermission(UpdatePermission.class) - public static class UpdateAction extends FormHandlerAction - { - private Experiment _exp; - - @Override - public void validateCommand(ExperimentForm target, Errors errors) - { - } - - @Override - public boolean handlePost(ExperimentForm form, BindException errors) throws Exception - { - form.doUpdate(); - form.refreshFromDb(); - _exp = form.getBean(); - return true; - } - - @Override - public ActionURL getSuccessURL(ExperimentForm experimentForm) - { - return ExperimentUrlsImpl.get().getExperimentDetailsURL(getContainer(), ExperimentService.get().getExpExperiment(_exp.getRowId())); - } - } - - public static class ExportBean - { - private final LSIDRelativizer _selectedRelativizer; - private final XarExportType _selectedExportType; - private final String _fileName; - private final String _dataRegionSelectionKey; - private final String _error; - private final Long _expRowId; - private final Long _protocolId; - private final ActionURL _postURL; - private final Set _roles; - - public ExportBean(LSIDRelativizer selectedRelativizer, XarExportType selectedExportType, String fileName, ExportOptionsForm form, Set roles, ActionURL postURL) - { - _selectedRelativizer = selectedRelativizer; - _selectedExportType = selectedExportType; - _fileName = fileName; - _dataRegionSelectionKey = form.getDataRegionSelectionKey(); - _error = form.getError(); - _expRowId = form.getExpRowId(); - _postURL = postURL; - _roles = roles; - _protocolId = form.getProtocolId(); - } - - public LSIDRelativizer getSelectedRelativizer() - { - return _selectedRelativizer; - } - - public XarExportType getSelectedExportType() - { - return _selectedExportType; - } - - public String getError() - { - return _error; - } - - public String getFileName() - { - return _fileName; - } - - public Set getRoles() - { - return _roles; - } - - public String getDataRegionSelectionKey() - { - return _dataRegionSelectionKey; - } - - public ActionURL getPostURL() - { - return _postURL; - } - - public Long getProtocolId() - { - return _protocolId; - } - - public Long getExpRowId() - { - return _expRowId; - } - } - - - private String fixupExportName(String runName) - { - runName = runName.replace('/', '-'); - runName = runName.replace('\\', '-'); - return runName; - } - - public static class ExportOptionsForm extends ExperimentRunListForm - { - private String _error; - private XarExportType _exportType; - private LSIDRelativizer _lsidOutputType; - private String _xarFileName; - private String _zipFileName; - private String _fileExportType; - private Long _protocolId; - private Integer _sampleTypeId; - private long[] _dataIds; - private String[] _roles = new String[0]; - - public String getError() - { - return _error; - } - - public void setError(String error) - { - _error = error; - } - - public XarExportType getExportType() - { - return _exportType; - } - - public LSIDRelativizer getLsidOutputType() - { - return _lsidOutputType; - } - - public String getFileExportType() - { - return _fileExportType; - } - - public void setFileExportType(String fileExportType) - { - _fileExportType = fileExportType; - } - - public String getXarFileName() - { - return _xarFileName; - } - - public void setXarFileName(String xarFileName) - { - _xarFileName = xarFileName; - } - - public String getZipFileName() - { - return _zipFileName; - } - - public void setZipFileName(String zipFileName) - { - _zipFileName = zipFileName; - } - - public void setExportType(XarExportType exportType) - { - _exportType = exportType; - } - - public void setLsidOutputType(LSIDRelativizer lsidOutputType) - { - _lsidOutputType = lsidOutputType; - } - - public Long getProtocolId() - { - return _protocolId; - } - - public void setProtocolId(Long protocolId) - { - _protocolId = protocolId; - } - - public String[] getRoles() - { - return _roles; - } - - public void setRoles(String[] roles) - { - _roles = roles; - } - - public Integer getSampleTypeId() - { - return _sampleTypeId; - } - - public void setSampleTypeId(Integer sampleTypeId) - { - _sampleTypeId = sampleTypeId; - } - - public long[] getDataIds() - { - return _dataIds; - } - - public void setDataIds(long[] dataIds) - { - _dataIds = dataIds; - } - - public List lookupProtocols(ViewContext context, boolean clearSelection) - { - List protocols = new ArrayList<>(); - - if (_protocolId != null) - { - ExpProtocol protocol = ExperimentService.get().getExpProtocol(_protocolId.intValue()); - if (protocol == null || !protocol.getContainer().equals(context.getContainer())) - { - throw new NotFoundException(); - } - protocols.add(protocol); - return protocols; - } - - for (Long protocolId : DataRegionSelection.getSelectedIntegers(context, clearSelection)) - { - try - { - ExpProtocol protocol = ExperimentService.get().getExpProtocol(protocolId); - if (protocol == null || !protocol.getContainer().equals(context.getContainer())) - { - throw new NotFoundException(); - } - protocols.add(protocol); - } - catch (NumberFormatException e) - { - throw new NotFoundException("Invalid protocol id: " + protocolId); - } - } - if (protocols.isEmpty()) - { - throw new NotFoundException("No protocols selected"); - } - return protocols; - } - } - - private ActionURL exportXAR(@NotNull XarExportSelection selection, @Nullable String fileName) - throws ExperimentException, IOException, PipelineValidationException - { - return exportXAR(selection, null, null, fileName); - } - - private ActionURL exportXAR(@NotNull XarExportSelection selection, @Nullable LSIDRelativizer lsidRelativizer, @Nullable XarExportType exportType, @Nullable String fileName) - throws ExperimentException, IOException, PipelineValidationException - { - if (lsidRelativizer == null) - lsidRelativizer = LSIDRelativizer.FOLDER_RELATIVE; - - if (exportType == null) - exportType = XarExportType.BROWSER_DOWNLOAD; - - if (fileName == null || fileName.isEmpty()) - fileName = "export.xar"; - - fileName = fixupExportName(fileName); - String xarXmlFileName = null; - if (Strings.CI.endsWith(fileName, ".xar")) - xarXmlFileName = fileName + ".xml"; - - switch (exportType) - { - case BROWSER_DOWNLOAD: - XarExporter exporter = new XarExporter(lsidRelativizer, selection, getUser(), xarXmlFileName, null, getContainer()); - - getViewContext().getResponse().setContentType("application/zip"); - ResponseHelper.setContentDisposition(getViewContext().getResponse(), ResponseHelper.ContentDispositionType.attachment, fileName); - ResponseHelper.setPrivate(getViewContext().getResponse()); - - exporter.writeAsArchive(getViewContext().getResponse().getOutputStream()); - return null; - case PIPELINE_FILE: - if (!PipelineService.get().hasValidPipelineRoot(getContainer())) - { - throw new IllegalStateException("You must set a valid pipeline root before you can export a XAR to it."); - } - PipeRoot pipeRoot = PipelineService.get().findPipelineRoot(getContainer()); - XarExportPipelineJob job = new XarExportPipelineJob(getViewBackgroundInfo(), pipeRoot, fileName, lsidRelativizer, selection, xarXmlFileName); - PipelineService.get().queueJob(job); - PipelineStatusFile status = PipelineService.get().getStatusFile(job.getJobGUID()); - return PageFlowUtil.urlProvider(PipelineUrls.class).statusDetails(getContainer(), status.getRowId()); - default: - throw new IllegalArgumentException("Unknown export type: " + exportType); - } - } - - @RequiresPermission(ReadPermission.class) - public class ExportProtocolsAction extends AbstractExportAction - { - @Override - public boolean handlePost(ExportOptionsForm form, BindException errors) throws Exception - { - List protocols = form.lookupProtocols(getViewContext(), false); - - long[] ids = new long[protocols.size()]; - for (int i = 0; i < ids.length; i++) - { - ids[i] = protocols.get(i).getRowId(); - } - XarExportSelection selection = new XarExportSelection(); - selection.addProtocolIds(ids); - - exportXAR(selection, form.getLsidOutputType(), form.getExportType(), form.getXarFileName()); - - if (form.getDataRegionSelectionKey() != null) - { - // Clear the selection - form.lookupProtocols(getViewContext(), true); - } - return true; - } - } - - public abstract static class AbstractExportAction extends FormViewAction - { - protected ActionURL _resultURL; - - @Override - public void validateCommand(ExportOptionsForm target, Errors errors) - { - } - - @Override - public ActionURL getSuccessURL(ExportOptionsForm exportOptionsForm) - { - return _resultURL; - } - - @Override - public ModelAndView getSuccessView(ExportOptionsForm exportOptionsForm) - { - return null; - } - - @Override - public ModelAndView getView(ExportOptionsForm form, boolean reshow, BindException errors) throws Exception - { - // FormViewAction can reinvoke getView() in response to a POST if we're not redirecting the browser, - // so avoid double-creating the export - if ("get".equalsIgnoreCase(getViewContext().getRequest().getMethod())) - handlePost(form, errors); - return null; - } - - @Override - public void addNavTrail(NavTree root) - { - } - - public List lookupRuns(ExportOptionsForm form) - { - Set runIds; - if (form.getRunIds() != null && form.getRunIds().length > 0) - runIds = new HashSet<>(Arrays.asList(form.getRunIds())); - else - runIds = DataRegionSelection.getSelectedIntegers(getViewContext(), form.getDataRegionSelectionKey(), false); - - if (runIds.isEmpty()) - { - throw new NotFoundException(); - } - List result = new ArrayList<>(); - - for (long id : runIds) - { - ExpRun run = ExperimentService.get().getExpRun(id); - if (run == null || !run.getContainer().hasPermission(getUser(), ReadPermission.class)) - { - throw new NotFoundException("Could not find run " + id); - } - result.add(run); - } - return result; - } - } - - @RequiresPermission(ReadPermission.class) - public class ExportRunsAction extends AbstractExportAction - { - @Override - public boolean handlePost(ExportOptionsForm form, BindException errors) throws Exception - { - XarExportSelection selection = new XarExportSelection(); - if (form.getExpRowId() != null) - { - ExpExperiment experiment = ExperimentService.get().getExpExperiment(form.getExpRowId()); - if (experiment != null && !experiment.getContainer().hasPermission(getUser(), ReadPermission.class)) - { - throw new NotFoundException("Run group " + form.getExpRowId()); - } - selection.addExperimentIds(experiment.getRowId()); - } - selection.addRuns(lookupRuns(form)); - - _resultURL = exportXAR(selection, form.getLsidOutputType(), form.getExportType(), form.getXarFileName()); - if (form.getDataRegionSelectionKey() != null) - DataRegionSelection.clearAll(getViewContext(), form.getDataRegionSelectionKey()); - return true; - } - } - - @RequiresPermission(ReadPermission.class) - public class ExportSampleTypeAction extends AbstractExportAction - { - @Override - public boolean handlePost(ExportOptionsForm form, BindException errors) throws Exception - { - Integer rowId = form.getSampleTypeId(); - if (rowId == null) - { - throw new NotFoundException("No sampleTypeId parameter specified"); - } - ExpSampleType sampleType = SampleTypeService.get().getSampleType(getContainer(), rowId.intValue(), true); - if (sampleType == null) - { - throw new NotFoundException("No such sample type with RowId " + rowId); - } - if (!sampleType.getContainer().hasPermission(getUser(), ReadPermission.class)) - { - throw new UnauthorizedException(); - } - - XarExportSelection selection = new XarExportSelection(); - selection.addSampleType(sampleType); - - _resultURL = exportXAR(selection, form.getLsidOutputType(), form.getExportType(), FileUtil.makeLegalName(sampleType.getName() + ".xar")); - return true; - } - } - - @RequiresPermission(ReadPermission.class) - public class ExportRunFilesAction extends AbstractExportAction - { - @Override - public boolean handlePost(ExportOptionsForm form, BindException errors) throws Exception - { - XarExportSelection selection = new XarExportSelection(); - selection.setIncludeXarXml(false); - if ("role".equalsIgnoreCase(form.getFileExportType())) - { - selection.addRoles(form.getRoles()); - } - selection.addRuns(lookupRuns(form)); - - _resultURL = exportXAR(selection, form.getZipFileName()); - if (form.getDataRegionSelectionKey() != null) - DataRegionSelection.clearAll(getViewContext(), form.getDataRegionSelectionKey()); - return true; - } - } - - @RequiresPermission(ReadPermission.class) - public class ExportFilesAction extends AbstractExportAction - { - @Override - public boolean handlePost(ExportOptionsForm form, BindException errors) throws Exception - { - long[] dataIds = form.getDataIds(); - if (dataIds == null || dataIds.length == 0) - { - throw new NotFoundException(); - } - - try - { - for (long id : dataIds) - { - ExpData data = ExperimentService.get().getExpData(id); - if (data == null || !data.getContainer().hasPermission(getUser(), ReadPermission.class)) - { - throw new NotFoundException("Could not find file " + id); - } - } - - XarExportSelection selection = new XarExportSelection(); - selection.setIncludeXarXml(false); - selection.addDataIds(dataIds); - - _resultURL = exportXAR(selection, form.getZipFileName()); - return true; - } - catch (NumberFormatException e) - { - throw new NotFoundException(Arrays.toString(dataIds)); - } - } - } - - public static class ExperimentRunListForm implements DataRegionSelection.DataSelectionKeyForm - { - private String _dataRegionSelectionKey; - private Long _expRowId; - private Long[] _runIds; - - @Override - public String getDataRegionSelectionKey() - { - return _dataRegionSelectionKey; - } - - @Override - public void setDataRegionSelectionKey(String key) - { - _dataRegionSelectionKey = key; - } - - public Long getExpRowId() - { - return _expRowId; - } - - public void setExpRowId(Long expRowId) - { - _expRowId = expRowId; - } - - public Long[] getRunIds() - { - return _runIds; - } - - public void setRunIds(Long[] runIds) - { - _runIds = runIds; - } - - public ExpExperiment lookupExperiment() - { - return getExpRowId() == null ? null : ExperimentService.get().getExpExperiment(getExpRowId().intValue()); - } - } - - private void addSelectedRunsToExperiment(ExpExperiment exp, String dataRegionSelectionKey) - { - Collection runIds = DataRegionSelection.getSelectedIntegers(getViewContext(), dataRegionSelectionKey, true); - List runs = new ArrayList<>(); - for (long runId : runIds) - { - ExpRun run = ExperimentServiceImpl.get().getExpRun(runId); - if (run != null) - { - runs.add(run); - } - } - exp.addRuns(getUser(), runs.toArray(new ExpRun[0])); - } - - - @RequiresPermission(InsertPermission.class) - public class AddRunsToExperimentAction extends FormHandlerAction - { - @Override - public void validateCommand(ExperimentRunListForm target, Errors errors) - { - } - - @Override - public boolean handlePost(ExperimentRunListForm form, BindException errors) - { - addSelectedRunsToExperiment(form.lookupExperiment(), form.getDataRegionSelectionKey()); - return true; - } - - @Override - public ActionURL getSuccessURL(ExperimentRunListForm form) - { - return ExperimentUrlsImpl.get().getExperimentDetailsURL(getContainer(), form.lookupExperiment()); - } - } - - @RequiresPermission(DeletePermission.class) - public static class RemoveSelectedExpRunsAction extends FormHandlerAction - { - @Override - public void validateCommand(ExperimentRunListForm target, Errors errors) - { - } - - @Override - public boolean handlePost(ExperimentRunListForm form, BindException errors) - { - ExpExperiment exp = form.lookupExperiment(); - if (exp == null || !exp.getContainer().hasPermission(getUser(), DeletePermission.class)) - { - throw new NotFoundException("Could not find run group with RowId " + form.getExpRowId()); - } - - for (long runId : DataRegionSelection.getSelectedIntegers(getViewContext(), form.getDataRegionSelectionKey(), false)) - { - ExpRun run = ExperimentService.get().getExpRun(runId); - if (run == null || !run.getContainer().hasPermission(getUser(), DeletePermission.class)) - { - throw new NotFoundException("Could not find run with RowId " + runId); - } - exp.removeRun(getUser(), run); - } - if (form.getDataRegionSelectionKey() != null) - DataRegionSelection.clearAll(getViewContext(), form.getDataRegionSelectionKey()); - return true; - } - - @Override - public ActionURL getSuccessURL(ExperimentRunListForm form) - { - return ExperimentUrlsImpl.get().getExperimentDetailsURL(getContainer(), form.lookupExperiment()); - } - } - - public static ActionURL getResolveLsidURL(Container c, @NotNull String type, @NotNull String lsid) - { - ActionURL url = new ActionURL(ResolveLSIDAction.class, c); - url.addParameter("type", type); - url.addParameter("lsid", lsid); - - return url; - } - - - @RequiresPermission(ReadPermission.class) - public static class ResolveLSIDAction extends SimpleViewAction - { - @Override - public ModelAndView getView(LsidForm form, BindException errors) - { - String message = ""; - if (!PageFlowUtil.empty(form.getLsid())) - { - try - { - String lsid = Lsid.canonical(form.getLsid().trim()); - ActionURL url = LsidManager.get().getDisplayURL(lsid); - if (url == null && form.getType() != null) - { - url = switch (form.getType().toLowerCase()) - { - case "data" -> LsidType.Data.getDisplayURL(new Lsid(lsid)); - case "material" -> LsidType.Material.getDisplayURL(new Lsid(lsid)); - default -> url; - }; - } - if (null != url) - { - throw new RedirectException(url); - } - message = "Could not map LSID to URL"; - } - catch (IllegalArgumentException e) - { - message = "Invalid LSID"; - } - } - - return new HtmlView("Enter LSID", - DOM.createHtmlFragment( - message, - DOM.FORM(at(action, getViewContext().cloneActionURL().setAction(ResolveLSIDAction.class)), - "LSID: ", - DOM.INPUT(at(type, "text", name, "lsid", size, "80", value, form.getLsid())), - PageFlowUtil.button("Go").submit(true)))); - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Resolve LSID"); - } - } - - public static class LsidForm - { - private String _lsid; - - public String getType() - { - return _type; - } - - public void setType(String type) - { - _type = type; - } - - private String _type; - - public void setLsid(String lsid) - { - _lsid = lsid; - } - - public String getLsid() - { - return _lsid; - } - } - - public static class SetFlagForm extends LsidForm - { - private String _comment; - private boolean _redirect = true; - - public String getComment() - { - return _comment; - } - - public void setComment(String comment) - { - _comment = comment; - } - - public boolean isRedirect() - { - return _redirect; - } - - public void setRedirect(boolean redirect) - { - _redirect = redirect; - } - } - - /** - * Check for update on the object itself - */ - @RequiresNoPermission - public static class SetFlagAction extends FormHandlerAction - { - @Override - public void validateCommand(SetFlagForm target, Errors errors) - { - } - - @Override - public boolean handlePost(SetFlagForm form, BindException errors) throws Exception - { - String lsid = form.getLsid(); - if (lsid == null) - throw new NotFoundException(); - ExpObject obj = ExperimentService.get().findObjectFromLSID(lsid); - if (obj == null) - throw new NotFoundException(); - Container container = obj.getContainer(); - if (!container.hasPermission(getUser(), UpdatePermission.class)) - { - throw new UnauthorizedException(); - } - - obj.setComment(getUser(), form.getComment()); - return true; - } - - @Override - public URLHelper getSuccessURL(SetFlagForm form) - { - return null; - } - } - - @RequiresPermission(InsertPermission.class) - public class DeriveSamplesChooseTargetAction extends SimpleViewAction - { - private List _materials; - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("sampleSets"); - addRootNavTrail(root); - root.addChild("Sample Types", ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); - ExpSampleType sampleType = _materials != null && !_materials.isEmpty() ? _materials.getFirst().getSampleType() : null; - if (sampleType != null) - { - root.addChild(sampleType.getName(), ExperimentUrlsImpl.get().getShowSampleTypeURL(sampleType)); - } - root.addChild("Derive Samples"); - } - - @Override - public void validate(DeriveMaterialForm form, BindException errors) - { - _materials = form.lookupMaterials(); - if (_materials.isEmpty()) - { - throw new NotFoundException("Could not find any matching materials"); - } - } - - @Override - public ModelAndView getView(DeriveMaterialForm form, BindException errors) - { - Container c = getContainer(); - PipeRoot root = PipelineService.get().findPipelineRoot(c); - - if (root == null || !root.isValid()) - { - ActionURL pipelineURL = urlProvider(PipelineUrls.class).urlSetup(c); - return new HtmlView(DIV("You must ", - DOM.A(DOM.at(href, pipelineURL), "configure a valid pipeline root for this folder"), - " before deriving samples.")); - } - else - { - Set materialInputRoles = new TreeSet<>(ExperimentService.get().getMaterialInputRoles(getContainer(), getUser())); - Map materialsWithRoles = new LinkedHashMap<>(); - for (ExpMaterial material : _materials) - { - materialsWithRoles.put(material, null); - } - - List sampleTypes = getUploadableSampleTypes(); - - DeriveSamplesChooseTargetBean bean = new DeriveSamplesChooseTargetBean(form.getDataRegionSelectionKey(), form.getTargetSampleTypeId(), sampleTypes, materialsWithRoles, form.getOutputCount(), materialInputRoles, null); - return new JspView<>("/org/labkey/experiment/deriveSamplesChooseTarget.jsp", bean); - } - } - } - - public static class DeriveSamplesChooseTargetBean implements DataRegionSelection.DataSelectionKeyForm - { - private String _dataRegionSelectionKey; - - private final Integer _targetSampleTypeId; - private final List _sampleTypes; - private final Map _sourceMaterials; - private final int _sampleCount; - private final Collection _inputRoles; - private final DerivedSamplePropertyHelper _propertyHelper; - - public static final String CUSTOM_ROLE = "--CUSTOM--"; - - public DeriveSamplesChooseTargetBean(String dataRegionSelectionKey, Integer targetSampleTypeId, List sampleTypes, Map sourceMaterials, int sampleCount, Collection inputRoles, DerivedSamplePropertyHelper helper) - { - _dataRegionSelectionKey = dataRegionSelectionKey; - _targetSampleTypeId = targetSampleTypeId; - _sampleTypes = sampleTypes; - _sourceMaterials = sourceMaterials; - _sampleCount = sampleCount; - _inputRoles = inputRoles; - _propertyHelper = helper; - } - - public Integer getTargetSampleTypeId() - { - return _targetSampleTypeId; - } - - public DerivedSamplePropertyHelper getPropertyHelper() - { - return _propertyHelper; - } - - public int getSampleCount() - { - return _sampleCount; - } - - public Map getSourceMaterials() - { - return _sourceMaterials; - } - - public List getSampleTypes() - { - return _sampleTypes; - } - - public Collection getInputRoles() - { - return _inputRoles; - } - - @Override - public String getDataRegionSelectionKey() - { - return _dataRegionSelectionKey; - } - - @Override - public void setDataRegionSelectionKey(String key) - { - _dataRegionSelectionKey = key; - } - } - - private List getUploadableSampleTypes() - { - // Make a copy so we can modify it - List sampleTypes = new ArrayList<>(SampleTypeService.get().getSampleTypes(getContainer(), true)); - sampleTypes.removeIf(sampleType -> !sampleType.canImportMoreSamples()); - return sampleTypes; - } - - @RequiresPermission(InsertPermission.class) - public class DeriveSamplesAction extends FormViewAction - { - private List _materials; - private ActionURL _successUrl; - private final Map _inputMaterials = new LinkedHashMap<>(); - - @Override - public ModelAndView getView(DeriveMaterialForm form, boolean reshow, BindException errors) - { - _materials = form.lookupMaterials(); - if (_materials.isEmpty()) - { - throw new NotFoundException("Could not find any matching materials"); - } - - Container c = getContainer(); - - if (form.getOutputCount() <= 0) - { - form.setOutputCount(1); - } - - if (form.getTargetSampleTypeId() == 0) - throw new NotFoundException("Target sample type required for the derived samples"); - - ExpSampleTypeImpl sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), form.getTargetSampleTypeId(), true); - if (sampleType == null) - throw new NotFoundException("Could not find sample type with rowId " + form.getTargetSampleTypeId()); - - InsertView insertView = new InsertView(new DataRegion(), errors); - - DerivedSamplePropertyHelper helper = new DerivedSamplePropertyHelper(sampleType, form.getOutputCount(), c, getUser()); - helper.addSampleColumns(insertView, getUser()); - - int[] rowIds = form.getRowIds(); - for (int i = 0; i < rowIds.length; i++) - { - insertView.getDataRegion().addHiddenFormField("rowIds", Integer.toString(rowIds[i])); - insertView.getDataRegion().addHiddenFormField("inputRole" + i, form.getInputRole(i) == null ? "" : form.getInputRole(i)); - insertView.getDataRegion().addHiddenFormField("customRole" + i, form.getCustomRole(i) == null ? "" : form.getCustomRole(i)); - } - - insertView.getDataRegion().addHiddenFormField("targetSampleTypeId", Integer.toString(form.getTargetSampleTypeId())); - insertView.getDataRegion().addHiddenFormField("outputCount", Integer.toString(form.getOutputCount())); - if (form.getDataRegionSelectionKey() != null) - insertView.getDataRegion().addHiddenFormField(DataRegionSelection.DATA_REGION_SELECTION_KEY, form.getDataRegionSelectionKey()); - insertView.setInitialValues(ViewServlet.adaptParameterMap(getViewContext().getRequest().getParameterMap())); - ButtonBar bar = new ButtonBar(); - bar.setStyle(ButtonBar.Style.separateButtons); - ActionButton submitButton = new ActionButton(DeriveSamplesAction.class, "Submit"); - submitButton.setActionType(ActionButton.Action.POST); - bar.add(submitButton); - insertView.getDataRegion().setButtonBar(bar); - insertView.setTitle("Output Samples"); - - Map materialsWithRoles = new LinkedHashMap<>(); - List materials = form.lookupMaterials(); - for (int i = 0; i < materials.size(); i++) - { - materialsWithRoles.put(materials.get(i), form.determineLabel(i)); - } - - DeriveSamplesChooseTargetBean bean = new DeriveSamplesChooseTargetBean(form.getDataRegionSelectionKey(), form.getTargetSampleTypeId(), getUploadableSampleTypes(), materialsWithRoles, form.getOutputCount(), Collections.emptyList(), helper); - JspView view = new JspView<>("/org/labkey/experiment/summarizeMaterialInputs.jsp", bean); - view.setTitle("Input Samples"); - - return new VBox(view, insertView); - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("sampleSets"); - addRootNavTrail(root); - root.addChild("Sample Types", ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); - ExpSampleType sampleType = _materials != null && !_materials.isEmpty() ? _materials.getFirst().getSampleType() : null; - if (sampleType != null) - { - root.addChild(sampleType.getName(), ExperimentUrlsImpl.get().getShowSampleTypeURL(sampleType)); - } - root.addChild("Derive Samples"); - } - - @Override - public void validateCommand(DeriveMaterialForm form, Errors errors) - { - List materials = form.lookupMaterials(); - - List lockedSamples = new ArrayList<>(); - for (int i = 0; i < materials.size(); i++) - { - ExpMaterial m = materials.get(i); - if (!m.isOperationPermitted(SampleTypeService.SampleOperations.EditLineage)) - { - lockedSamples.add(m); - } - String inputRole = form.determineLabel(i); - if (inputRole == null || inputRole.isEmpty()) - { - ExpSampleType st = m.getSampleType(); - inputRole = st != null ? st.getName() : ExpMaterialRunInput.DEFAULT_ROLE; - } - _inputMaterials.put(materials.get(i), inputRole); - } - - if (!lockedSamples.isEmpty()) - { - errors.reject(ERROR_MSG, SampleTypeService.get().getOperationNotPermittedMessage(lockedSamples, SampleTypeService.SampleOperations.EditLineage)); - } - } - - @Override - public boolean handlePost(DeriveMaterialForm form, BindException errors) - { - ExpSampleTypeImpl sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), form.getTargetSampleTypeId(), true); - - DerivedSamplePropertyHelper helper = new DerivedSamplePropertyHelper(sampleType, form.getOutputCount(), getContainer(), getUser()); - - Map, Map> allProperties; - try - { - boolean valid = true; - for (Map.Entry> entry : helper.getPostedPropertyValues(getViewContext().getRequest()).entrySet()) - valid = UploadWizardAction.validatePostedProperties(getViewContext(), entry.getValue(), errors) && valid; - if (!valid) - return false; - - allProperties = helper.getSampleProperties(getViewContext().getRequest(), _inputMaterials.keySet()); - } - catch (DuplicateMaterialException e) - { - errors.addError(new ObjectError(e.getColName(), null, null, e.getMessage())); - return false; - } - catch (ExperimentException e) - { - errors.reject(SpringActionController.ERROR_MSG, e.getMessage()); - return false; - } - - try (DbScope.Transaction tx = ExperimentService.get().ensureTransaction()) - { - Map outputMaterials = new HashMap<>(); - int i = 0; - for (Map.Entry, Map> entry : allProperties.entrySet()) - { - Lsid lsid = entry.getKey().first; - String name = entry.getKey().second; - assert name != null; - - ExpMaterialImpl outputMaterial = ExperimentServiceImpl.get().createExpMaterial(getContainer(), lsid.toString(), name); - if (sampleType != null) - { - outputMaterial.setCpasType(sampleType.getLSID()); - } - outputMaterial.save(getUser()); - - if (sampleType != null) - { - Map pvs = new HashMap<>(); - for (Map.Entry propertyEntry : entry.getValue().entrySet()) - pvs.put(propertyEntry.getKey().getName(), propertyEntry.getValue()); - outputMaterial.setProperties(getUser(), pvs, false); - } - - outputMaterials.put(outputMaterial, helper.getSampleNames().get(i++)); - } - - ExperimentService.get().deriveSamples(_inputMaterials, outputMaterials, getViewBackgroundInfo(), _log); - - tx.commit(); - - // automatically link samples to study, if configured - StudyPublishService.get().autoLinkDerivedSamples(sampleType, outputMaterials.keySet().stream().map(ExpObject::getRowId).collect(toList()), getContainer(), getUser()); - - _successUrl = ExperimentUrlsImpl.get().getShowSampleURL(getContainer(), outputMaterials.keySet().iterator().next()); - - if (form.getDataRegionSelectionKey() != null) - DataRegionSelection.clearAll(getViewContext(), form.getDataRegionSelectionKey()); - } - catch (Exception e) - { - errors.reject(SpringActionController.ERROR_MSG, e.getMessage()); - return false; - } - - return true; - } - - @Override - public URLHelper getSuccessURL(DeriveMaterialForm deriveMaterialForm) - { - return _successUrl; - } - } - - public static class DeriveMaterialForm implements HasViewContext, DataRegionSelection.DataSelectionKeyForm - { - private String _dataRegionSelectionKey; - private int _outputCount = 1; - private int _targetSampleTypeId; - private int[] _rowIds; - private String _name; - - private ViewContext _context; - - @Override - public void setViewContext(ViewContext context) - { - _context = context; - } - - @Override - public ViewContext getViewContext() - { - return _context; - } - - public List lookupMaterials() - { - List result = new ArrayList<>(); - for (int rowId : getRowIds()) - { - ExpMaterial material = ExperimentService.get().getExpMaterial(rowId); - if (material != null) - { - if (material.getContainer().hasPermission(_context.getUser(), ReadPermission.class)) - { - result.add(material); - } - else - { - throw new UnauthorizedException(); - } - } - else - { - throw new NotFoundException("No material with RowId " + rowId); - } - } - result.sort(Comparator.comparing(Identifiable::getName)); - return result; - } - - public String getName() - { - return _name; - } - - public void setName(String name) - { - _name = name; - } - - @Override - public String getDataRegionSelectionKey() - { - return _dataRegionSelectionKey; - } - - @Override - public void setDataRegionSelectionKey(String dataRegionSelectionKey) - { - _dataRegionSelectionKey = dataRegionSelectionKey; - } - - public int[] getRowIds() - { - if (_rowIds == null) - { - _rowIds = PageFlowUtil.toInts(DataRegionSelection.getSelected(getViewContext(), getDataRegionSelectionKey(), false)); - } - return _rowIds; - } - - public void setRowIds(int[] rowIds) - { - _rowIds = rowIds; - } - - public int getOutputCount() - { - return _outputCount; - } - - public void setOutputCount(int outputCount) - { - _outputCount = outputCount; - } - - public int getTargetSampleTypeId() - { - return _targetSampleTypeId; - } - - public void setTargetSampleTypeId(int targetSampleTypeId) - { - _targetSampleTypeId = targetSampleTypeId; - } - - public String getInputRole(int i) - { - return _context.getRequest().getParameter("inputRole" + i); - } - - public String getCustomRole(int i) - { - return _context.getRequest().getParameter("customRole" + i); - } - - public String determineLabel(int index) - { - String result = getInputRole(index); - if (DeriveSamplesChooseTargetBean.CUSTOM_ROLE.equals(result)) - { - result = getCustomRole(index); - } - if (result != null) - { - result = result.trim(); - } - return result; - } - } - - - public static class ExpInput - { - public String role; - public int rowId; - public Lsid lsid; - } - - public static class DerivationSpec - { - public String role; - public Map values; - } - - public static class DerivationForm - { - public List dataInputs; - public List materialInputs; - - public int dataOutputCount; - public Lsid targetDataClass; - public Map dataDefault; - public List dataOutputs; - - public int materialOutputCount; - public Lsid targetSampleType; - public Map materialDefault; - public List materialOutputs; - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(InsertPermission.class) - public static class DeriveAction extends MutatingApiAction - { - @Override - public void validateForm(DerivationForm form, Errors errors) - { - if (errors.hasErrors()) - return; - - if (form.materialOutputCount > 0 && form.materialOutputs != null && !form.materialOutputs.isEmpty()) - errors.reject(ERROR_MSG, "Either 'materialOutputCount' or 'materialOutputs' property can be specified, but not both."); - - if (form.dataOutputCount > 0 && form.dataOutputs != null && !form.dataOutputs.isEmpty()) - errors.reject(ERROR_MSG, "Either 'dataOutputCount' or 'dataOutputs' property can be specified, but not both."); - - boolean hasMaterialOutputs = form.materialOutputCount > 0 || form.materialOutputs != null && !form.materialOutputs.isEmpty(); - boolean hasDataOutputs = form.dataOutputCount > 0 || form.dataOutputs != null && !form.dataOutputs.isEmpty(); - - if (!hasMaterialOutputs && !hasDataOutputs) - errors.reject(ERROR_MSG, "At least one data output or material output is required"); - - if (hasMaterialOutputs && form.targetSampleType == null) - errors.reject(ERROR_MSG, "targetSampleType lsid required for material outputs"); - - if (hasDataOutputs && form.targetDataClass == null) - errors.reject(ERROR_MSG, "targetDataClass lsid required for data outputs"); - } - - @Override - public Object execute(DerivationForm form, BindException errors) throws Exception - { - // Find material inputs - Map materialInputs = new LinkedHashMap<>(); - if (form.materialInputs != null) - { - for (ExpInput in : form.materialInputs) - { - ExpMaterial m = null; - if (in.lsid != null) - { - m = ExperimentService.get().getExpMaterial(in.lsid.toString()); - if (m == null) - errors.reject(ERROR_MSG, "Can't resolve sample '" + in.lsid + "'"); - } - else if (in.rowId > 0) - { - m = ExperimentService.get().getExpMaterial(in.rowId); - if (m == null) - errors.reject(ERROR_MSG, "Can't resolve sample '" + in.rowId + "'"); - } - - if (m == null) - { - errors.reject(ERROR_MSG, "Material input lsid or rowId required"); - continue; - } - - ExpSampleType st = m.getSampleType(); - if (st == null) - { - errors.reject(ERROR_MSG, "Material input is not a member of a SampleType"); - continue; - } - - String role = in.role; - if (role == null || role.isEmpty()) - { - role = st.getName(); - } - materialInputs.put(m, role); - } - } - - // Find input data - Map dataInputs = new LinkedHashMap<>(); - if (form.dataInputs != null) - { - for (ExpInput in : form.dataInputs) - { - ExpData d = null; - if (in.lsid != null) - { - d = ExperimentService.get().getExpData(in.lsid.toString()); - if (d == null) - errors.reject(ERROR_MSG, "Can't resolve data '" + in.lsid + "'"); - } - else if (in.rowId > 0) - { - d = ExperimentService.get().getExpData(in.rowId); - if (d == null) - errors.reject(ERROR_MSG, "Can't resolve data '" + in.rowId + "'"); - } - - if (d == null) - { - errors.reject(ERROR_MSG, "Data input lsid or rowId required"); - continue; - } - - ExpDataClass dc = d.getDataClass(getUser()); - if (dc == null) - { - errors.reject(ERROR_MSG, "Data input is not a member of a DataClass"); - continue; - } - - String role = in.role; - if (role == null || role.isEmpty()) - { - role = dc.getName(); - } - dataInputs.put(d, role); - } - } - - ExpSampleType outSampleType; - if (form.targetSampleType != null) - { - // TODO: check in scope and has permission - outSampleType = SampleTypeService.get().getSampleType(form.targetSampleType.toString()); - if (outSampleType == null) - errors.reject(ERROR_MSG, "Sample type not found: " + form.targetSampleType.toString()); - } - else - { - outSampleType = null; - } - - ExpDataClass outDataClass; - if (form.targetDataClass != null) - { - // TODO: check in scope and has permission - outDataClass = ExperimentServiceImpl.get().getDataClass(form.targetDataClass.toString()); - if (outDataClass == null) - errors.reject(ERROR_MSG, "DataClass not found: " + form.targetDataClass.toString()); - } - else - { - outDataClass = null; - } - - if (errors.hasErrors()) - return null; - - // TODO: support list of resolved ExpData or ExpMaterial instead of string concatenated names - // Create "MaterialInputs/" columns with a value containing a comma-separated list of Material names - final Map> parentInputNames = new HashMap<>(); - Set inputTypes = new CaseInsensitiveHashSet(); - for (ExpMaterial material : materialInputs.keySet()) - { - ExpSampleType st = material.getSampleType(); - String keyName = ExpMaterial.MATERIAL_INPUT_PARENT + "/" + st.getName(); - inputTypes.add(keyName); - parentInputNames.computeIfAbsent(keyName, (x) -> new LinkedHashSet<>()).add(material.getName()); - } - - // TODO: support list of resolved ExpData or ExpMaterial instead of string concatenated names - // Create "DataInputs/" columns with a value containing a comma-separated list of ExpData names - for (ExpData d : dataInputs.keySet()) - { - ExpDataClass dc = d.getDataClass(getUser()); - String keyName = ExpData.DATA_INPUT_PARENT + "/" + dc.getName(); - inputTypes.add(keyName); - parentInputNames.computeIfAbsent(keyName, (x) -> new LinkedHashSet<>()).add(d.getName()); - } - - - try (DbScope.Transaction tx = ExperimentService.get().ensureTransaction()) - { - Set requiredParentTypes = new CaseInsensitiveHashSet(); - - // output materials - Map outputMaterials = new HashMap<>(); - int materialOutputCount = Math.max(form.materialOutputCount, form.materialOutputs != null ? form.materialOutputs.size() : 0); - if (materialOutputCount > 0 && outSampleType != null) - { - requiredParentTypes.addAll(outSampleType.getRequiredImportAliases().values()); - DerivedOutputs derived = new DerivedOutputs<>(parentInputNames, form.materialDefault, form.materialOutputs, materialOutputCount, ExpMaterial.DEFAULT_CPAS_TYPE) - { - @Override - protected TableInfo createTable() - { - SamplesSchema schema = new SamplesSchema(getUser(), getContainer()); - return schema.getTable(outSampleType.getName()); - } - - @Override - protected List getExpObject(List> insertedRows) - { - List rowIds = insertedRows.stream().map(r -> MapUtils.getLong(r,"rowid")).collect(toList()); - return ExperimentService.get().getExpMaterials(rowIds); - } - }; - - outputMaterials = derived.createOutputs(); - } - - - // create output data - Map outputData = new HashMap<>(); - int dataOutputCount = Math.max(form.dataOutputCount, form.dataOutputs != null ? form.dataOutputs.size() : 0); - if (dataOutputCount > 0 && outDataClass != null) - { - requiredParentTypes.addAll(outDataClass.getRequiredImportAliases().values()); - DerivedOutputs derived = new DerivedOutputs<>(parentInputNames, form.dataDefault, form.dataOutputs, dataOutputCount, ExpData.DEFAULT_CPAS_TYPE) - { - @Override - protected TableInfo createTable() - { - ExpSchema expSchema = new ExpSchema(getUser(), getContainer()); - UserSchema dataSchema = expSchema.getUserSchema(ExpSchema.NestedSchemas.data.name()); - return dataSchema.getTable(outDataClass.getName()); - } - - @Override - protected List getExpObject(List> insertedRows) - { - List lsids = insertedRows.stream().map(r -> (String) r.get("lsid")).collect(toList()); - return ExperimentService.get().getExpDatasByLSID(lsids); - } - }; - - outputData = derived.createOutputs(); - } - - if (outputMaterials.isEmpty() && outputData.isEmpty()) - throw new IllegalStateException("Expected to create " + materialOutputCount + " materials and " + dataOutputCount + " datas"); - - boolean hasMissingRequiredParent = false; - for (String required : requiredParentTypes) - { - if (!inputTypes.contains(required)) - { - hasMissingRequiredParent = true; - break; - } - } - if (hasMissingRequiredParent) - throw new IllegalStateException("Inputs are required: " + String.join(",", requiredParentTypes)); - - // finally, create the derived run if there are any parents - ExpRun run = null; - if (!materialInputs.isEmpty() || !dataInputs.isEmpty()) - run = ExperimentService.get().derive(materialInputs, dataInputs, outputMaterials, outputData, new ViewBackgroundInfo(getContainer(), getUser(), null), _log); - tx.commit(); - - StringBuilder successMessage = new StringBuilder("Created "); - if (!outputMaterials.isEmpty()) - successMessage.append(outputMaterials.size()).append(" materials"); - if (!outputData.isEmpty()) - successMessage.append(outputData.size()).append(" data"); - - JSONObject ret; - if (run != null) - ret = ExperimentJSONConverter.serializeRun(run, null, getUser(), ExperimentJSONConverter.DEFAULT_SETTINGS); - else - ret = ExperimentJSONConverter.serializeRunOutputs(outputData.keySet(), outputMaterials.keySet(), getUser(), ExperimentJSONConverter.DEFAULT_SETTINGS); - - return success(successMessage.toString(), ret); - } - } - - // Helper class that prepares and executes the QueryUpdateService.insertRows() on the data or material table. - private abstract class DerivedOutputs - { - private final @NotNull Map> _parentInputNames; - private final @Nullable Map _defaultValues; - private final @Nullable List _values; - private final int _outputCount; - private final String _rolePrefix; - - - public DerivedOutputs(@NotNull Map> parentInputNames, @Nullable Map defaultValues, @Nullable List values, int outputCount, String rolePrefix) - { - _parentInputNames = parentInputNames; - _defaultValues = defaultValues; - _values = values; - _outputCount = outputCount; - _rolePrefix = rolePrefix; - } - - public Pair>, List> prepareRows() - { - List> rows = new ArrayList<>(); - List roles = new ArrayList<>(); - int unknownOutputDataCount = 0; - - for (int i = 0; i < _outputCount; i++) - { - Map row = new CaseInsensitiveHashMap<>(); - if (_defaultValues != null) - row.putAll(_defaultValues); - DerivationSpec spec = _values != null && i < _values.size() ? _values.get(i) : null; - String role = null; - if (spec != null) - { - row.putAll(spec.values); - role = spec.role; - } - - // NOTE: Input parents are added to each row, but are only used for name generation and not for derivation. - // NOTE: We will derive the inserted samples in a single derivation run after the sample/date have been inserted. - row.putAll(_parentInputNames); - - rows.add(row); - - if (StringUtils.trimToNull(role) == null) - { - role = _rolePrefix + (unknownOutputDataCount == 0 ? "" : Integer.toString(unknownOutputDataCount + 1)); - unknownOutputDataCount++; - } - roles.add(role); - } - return Pair.of(rows, roles); - } - - protected abstract TableInfo createTable(); - - protected abstract List getExpObject(List> insertedRows); - - public Map createOutputs() throws BatchValidationException, DuplicateKeyException, SQLException, QueryUpdateServiceException - { - Pair>, List> pair = prepareRows(); - List> rows = pair.first; - List roles = pair.second; - - TableInfo table = createTable(); - QueryUpdateService qus = table.getUpdateService(); - if (qus == null) - throw new IllegalStateException(); - - Map configParams = new HashMap<>(); - // Skip derivation during insert -- DeriveAction will call ExperimentService.get().derive() after samples are inserted - configParams.put(SampleTypeUpdateServiceDI.Options.SkipDerivation, true); - - BatchValidationException qusErrors = new BatchValidationException(); - List> insertedRows = qus.insertRows(getUser(), getContainer(), rows, qusErrors, configParams, null); - if (qusErrors.hasErrors()) - throw qusErrors; - - if (insertedRows.size() != roles.size()) - throw new IllegalStateException("Expected to create " + roles.size() + " new exp objects for derivation"); - - List outputs = getExpObject(insertedRows); - if (outputs.size() != roles.size()) - throw new IllegalStateException("Expected to create " + roles.size() + " new exp objects for derivation"); - - Map outputMap = new HashMap<>(); - for (int i = 0; i < outputs.size(); i++) - { - String role = roles.get(i); - T data = outputs.get(i); - outputMap.put(data, role); - } - - return outputMap; - } - } - } - - public static class CreateExperimentForm extends ExperimentForm implements DataRegionSelection.DataSelectionKeyForm - { - private boolean _addSelectedRuns; - private String _dataRegionSelectionKey; - - public boolean isAddSelectedRuns() - { - return _addSelectedRuns; - } - - public void setAddSelectedRuns(boolean addSelectedRuns) - { - _addSelectedRuns = addSelectedRuns; - } - - @Override - public String getDataRegionSelectionKey() - { - return _dataRegionSelectionKey; - } - - @Override - public void setDataRegionSelectionKey(String dataRegionSelectionKey) - { - _dataRegionSelectionKey = dataRegionSelectionKey; - } - } - - @RequiresPermission(InsertPermission.class) - @ActionNames("createRunGroup, createExperiment") - public class CreateRunGroupAction extends FormViewAction - { - @Override - public ModelAndView getView(CreateExperimentForm form, boolean reshow, BindException errors) - { - // HACK - convert ExperimentForm to not be a BeanViewForm - form.setAddSelectedRuns("true".equals(getViewContext().getRequest().getParameter("addSelectedRuns"))); - form.setDataRegionSelectionKey(getViewContext().getRequest().getParameter(DataRegionSelection.DATA_REGION_SELECTION_KEY)); - - DataRegion drg = new DataRegion(); - - drg.addHiddenFormField(ActionURL.Param.returnUrl, getViewContext().getRequest().getParameter(ActionURL.Param.returnUrl.name())); - drg.addHiddenFormField("addSelectedRuns", Boolean.toString("true".equals(getViewContext().getRequest().getParameter("addSelectedRuns")))); - form.setDataRegionSelectionKey(getViewContext().getRequest().getParameter(DataRegionSelection.DATA_REGION_SELECTION_KEY)); - // Fix issue 27562 - include session-stored selection - if (form.getDataRegionSelectionKey() != null) - { - for (String rowId : DataRegionSelection.getSelected(getViewContext(), form.getDataRegionSelectionKey(), false)) - { - drg.addHiddenFormField(DataRegion.SELECT_CHECKBOX_NAME, rowId); - } - } - drg.addHiddenFormField(DataRegionSelection.DATA_REGION_SELECTION_KEY, getViewContext().getRequest().getParameter(DataRegionSelection.DATA_REGION_SELECTION_KEY)); - - drg.addColumns(ExperimentServiceImpl.get().getTinfoExperiment(), "RowId,Name,LSID,ContactId,ExperimentDescriptionURL,Hypothesis,Comments,Created"); - - DisplayColumn col = drg.getDisplayColumn("RowId"); - col.setVisible(false); - drg.getDisplayColumn("LSID").setVisible(false); - drg.getDisplayColumn("Created").setVisible(false); - - ButtonBar bb = new ButtonBar(); - bb.setStyle(ButtonBar.Style.separateButtons); - ActionButton insertButton = new ActionButton(new ActionURL(CreateRunGroupAction.class, getContainer()), "Submit", ActionButton.Action.POST); - bb.add(insertButton); - - drg.setButtonBar(bb); - - return new InsertView(drg, errors); - } - - - @Override - public boolean handlePost(CreateExperimentForm form, BindException errors) throws Exception - { - // This is strange... but the "Create new run group..." menu item on the run grid always POSTs, probably to - // allow for long lists of run IDs. This "noPost" parameter on the initial POST is used to inform the action - // that it wants to display the form, not try to save anything yet. - if (!"true".equals(getViewContext().getRequest().getParameter("noPost"))) - { - form.setAddSelectedRuns("true".equals(getViewContext().getRequest().getParameter("addSelectedRuns"))); - form.setDataRegionSelectionKey(getViewContext().getRequest().getParameter(DataRegionSelection.DATA_REGION_SELECTION_KEY)); - - Experiment exp = form.getBean(); - if (exp.getName() == null || exp.getName().trim().isEmpty()) - { - errors.reject(ERROR_MSG, "You must specify a name for the experiment"); - } - else - { - int maxNameLength = ExperimentService.get().getTinfoExperimentRun().getColumn("Name").getScale(); - if (exp.getName().length() > maxNameLength) - { - errors.reject(ERROR_MSG, "Name of the experiment must be " + maxNameLength + " characters or less."); - } - } - - String lsid; - int suffix = 1; - do - { - String template = "urn:lsid:" + XarContext.LSID_AUTHORITY_SUBSTITUTION + ":Experiment.Folder-" + XarContext.CONTAINER_ID_SUBSTITUTION + ":" + exp.getName(); - if (suffix > 1) - { - template = template + suffix; - } - suffix++; - lsid = LsidUtils.resolveLsidFromTemplate(template, new XarContext("Experiment Creation", getContainer(), getUser()), ExpExperiment.DEFAULT_CPAS_TYPE); - } - while (ExperimentService.get().getExpExperiment(lsid) != null); - exp.setLSID(lsid); - exp.setContainer(getContainer()); - - if (errors.getErrorCount() == 0) - { - ExpExperimentImpl wrapper = new ExpExperimentImpl(exp); - wrapper.save(getUser()); - - if (form.isAddSelectedRuns()) - { - addSelectedRunsToExperiment(wrapper, form.getDataRegionSelectionKey()); - } - - if (form.getReturnUrl() != null) - { - throw new RedirectException(form.getReturnActionURL()); - } - throw new RedirectException(ExperimentUrlsImpl.get().getShowExperimentsURL(getContainer())); - } - } - return true; - } - - @Override - public void addNavTrail(NavTree root) - { - setHelpTopic("runGroups"); - root.addChild("Create Run Group"); - } - - @Override - public URLHelper getSuccessURL(CreateExperimentForm createExperimentForm) - { - return null; // null is used to show the form in the case where IDs are POSTed from the grid - } - - @Override - public void validateCommand(CreateExperimentForm target, Errors errors) { } - } - - public static class MoveRunsForm implements DataRegionSelection.DataSelectionKeyForm - { - private String _targetContainerId; - private String _dataRegionSelectionKey; - - @Override - public String getDataRegionSelectionKey() - { - return _dataRegionSelectionKey; - } - - @Override - public void setDataRegionSelectionKey(String key) - { - _dataRegionSelectionKey = key; - } - - public String getTargetContainerId() - { - return _targetContainerId; - } - - public void setTargetContainerId(String targetContainerId) - { - _targetContainerId = targetContainerId; - } - } - - @RequiresPermission(DeletePermission.class) - public class MoveRunsLocationAction extends SimpleViewAction - { - @Override - public ModelAndView getView(MoveRunsForm form, BindException errors) - { - ActionURL moveURL = new ActionURL(MoveRunsAction.class, getContainer()); - PipelineRootContainerTree ct = new PipelineRootContainerTree(getUser(), moveURL) - { - private boolean _clickHandlerRegistered = false; - - @Override - protected void renderCellContents(StringBuilder html, Container c, ActionURL url, boolean hasRoot) - { - boolean renderLink = hasRoot && !c.equals(getContainer()); - - if (renderLink) - { - html.append(""); - } - html.append(PageFlowUtil.filter(c.getName())); - if (renderLink) - { - html.append(""); - } - - if (!_clickHandlerRegistered) - { - HttpView.currentPageConfig().addHandlerForQuerySelector("a.move-target-container", "click", "moveTo(this.attributes.getNamedItem('data-objectid').value);" ); - _clickHandlerRegistered = true; - } - } - }; - ct.setInitialLevel(1); - - MoveRunsBean bean = new MoveRunsBean(ct, form.getDataRegionSelectionKey()); - JspView result = new JspView<>("/org/labkey/experiment/moveRunsLocation.jsp", bean); - result.setTitle("Choose Destination Folder"); - result.setFrame(WebPartView.FrameType.PORTAL); - return result; - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild("Move Runs"); - } - } - - - @RequiresPermission(DeletePermission.class) - public class MoveRunsAction extends FormHandlerAction - { - private Container _targetContainer; - - @Override - public void validateCommand(MoveRunsForm target, Errors errors) - { - } - - @Override - public boolean handlePost(MoveRunsForm form, BindException errors) - { - _targetContainer = ContainerManager.getForId(form.getTargetContainerId()); - if (_targetContainer == null || !_targetContainer.hasPermission(getUser(), InsertPermission.class)) - { - throw new UnauthorizedException(); - } - - Set runIds = DataRegionSelection.getSelectedIntegers(getViewContext(), form.getDataRegionSelectionKey(), false); - List runs = new ArrayList<>(); - for (Long runId : runIds) - { - ExpRun run = ExperimentService.get().getExpRun(runId); - if (run != null) - { - runs.add(run); - } - } - - ViewBackgroundInfo info = getViewBackgroundInfo(); - info.setContainer(_targetContainer); - - try - { - ExperimentService.get().moveRuns(info, getContainer(), runs); - if (form.getDataRegionSelectionKey() != null) - DataRegionSelection.clearAll(getViewContext(), form.getDataRegionSelectionKey()); - } - catch (IOException e) - { - throw new NotFoundException("Failed to initialize move. Check that the pipeline root is configured correctly. " + e); - } - return true; - } - - @Override - public ActionURL getSuccessURL(MoveRunsForm form) - { - return urlProvider(PipelineUrls.class).urlBegin(_targetContainer); - } - } - - public static class ShowExternalDocsForm - { - private String _objectURI; - private String _propertyURI; - - public String getObjectURI() - { - return _objectURI; - } - - public void setObjectURI(String objectURI) - { - _objectURI = objectURI; - } - - public String getPropertyURI() - { - return _propertyURI; - } - - public void setPropertyURI(String propertyURI) - { - _propertyURI = propertyURI; - } - } - - @RequiresPermission(ReadPermission.class) - public static class ShowExternalDocsAction extends SimpleViewAction - { - @Override - public ModelAndView getView(ShowExternalDocsForm form, BindException errors) throws Exception - { - Map props = OntologyManager.getPropertyObjects(getContainer(), form.getObjectURI()); - ObjectProperty prop = props.get(form.getPropertyURI()); - if (prop == null || !getContainer().equals(prop.getContainer())) - { - throw new NotFoundException(); - } - URI uri = new URI(prop.getStringValue()); - File f = new File(uri); - if (!f.exists()) - { - throw new NotFoundException(); - } - - PageFlowUtil.streamFile(getViewContext().getResponse(), f.toPath(), false); - return null; - } - - @Override - public void addNavTrail(NavTree root) - { - } - } - - - // TODO: DotGraph has been adding a "runId" parameter, but ShowGraphMoreListAction - public static ActionURL getShowGraphMoreListURL(Container c, @Nullable Long runId, @NotNull String objtype) - { - ActionURL url = new ActionURL(ShowGraphMoreListAction.class, c); - - if (null != runId) - url.addParameter("runId", runId); - - url.addParameter("objtype", objtype); - - return url; - } - - - @RequiresPermission(ReadPermission.class) - public static class ShowGraphMoreListAction extends SimpleViewAction - { - private ExperimentRunForm _form; - - @Override - public ModelAndView getView(ExperimentRunForm form, BindException errors) - { - _form = form; - return new GraphMoreGrid(getContainer(), errors, getViewContext().getActionURL()); - } - - @Override - public void addNavTrail(NavTree root) - { - root.addChild(new NavTree("Experiments", ExperimentUrlsImpl.get().getShowExperimentsURL(getContainer()))); - ExpRun run = ExperimentService.get().getExpRun(_form.getRowId()); - if (run != null) - { - root.addChild(new NavTree("Experiment Run", ExperimentUrlsImpl.get().getRunGraphURL(_form.lookupRun()))); - } - root.addChild(new NavTree("Selected Protocol Applications")); - } - } - - @RequiresPermission(DesignAssayPermission.class) - public class AssayXarFileAction extends MutatingApiAction - { - - @Override - public Object execute(Object o, BindException errors) throws Exception - { - ApiSimpleResponse response = new ApiSimpleResponse(); - - if (!(getViewContext().getRequest() instanceof MultipartHttpServletRequest)) - throw new BadRequestException("Expected MultipartHttpServletRequest when posting files."); - - if (!PipelineService.get().hasValidPipelineRoot(getContainer())) - { - return false; - } - - MultipartFile formFile = getFileMap().get("file"); - if (formFile == null) - { - errors.reject(ERROR_MSG, "No file was posted by the browser."); - return false; - } - - byte[] bytes = formFile.getBytes(); - if (bytes.length == 0) - { - errors.reject(ERROR_MSG, "No file was posted by the browser."); - return false; - } - - PipeRoot pipeRoot = PipelineService.get().findPipelineRoot(getContainer()); - FileLike systemDir = pipeRoot.ensureSystemDirectory(); - FileLike uploadDir = systemDir.resolveChild("UploadedXARs"); - FileUtil.createDirectories(uploadDir); - if (!uploadDir.isDirectory()) - { - errors.reject(ERROR_MSG, "Unable to create a 'system/UploadedXARs' directory under the pipeline root"); - return false; - } - String userDirName = getUser().getEmail(); - if (userDirName == null || userDirName.isEmpty()) - { - userDirName = GUEST_DIRECTORY_NAME; - } - FileLike userDir = uploadDir.resolveChild(userDirName); - FileUtil.createDirectories(userDir); - if (!userDir.isDirectory()) - { - errors.reject(ERROR_MSG, "Unable to create an 'UploadedXARs/" + userDirName + "' directory under the pipeline root"); - return false; - } - - FileLike xarFile = userDir.resolveChild(formFile.getOriginalFilename()); - - // As this is multi-part will need to use finally to close, to prevent a stream closure exception - try (OutputStream out = new BufferedOutputStream(xarFile.openOutputStream())) - { - out.write(bytes); - } - catch (IOException e) - { - errors.reject(ERROR_MSG, "Unable to write uploaded XAR file to " + xarFile); - return false; - } - //noinspection EmptyCatchBlock - - ExperimentPipelineJob job = new ExperimentPipelineJob(getViewBackgroundInfo(), xarFile, - "Uploaded file", true, pipeRoot); - PipelineService.get().queueJob(job); - - response.put("success", true); - return response; - } - } - - @RequiresPermission(InsertPermission.class) - public class ImportXarFileAction extends FormHandlerAction - { - @Override - public void validateCommand(ImportXarForm target, Errors errors) - { - } - - @Override - public boolean handlePost(ImportXarForm form, BindException errors) throws Exception - { - for (FileLike f : form.getValidatedFiles(getContainer())) - { - if (f.isFile()) - { - ExperimentPipelineJob job = new ExperimentPipelineJob(getViewBackgroundInfo(), f, "Experiment Import", false, form.getPipeRoot(getContainer())); - - // TODO: Configure module resources with the appropriate log location per container - if (form.getModule() != null) - { - FileLike logFile = form.getPipeRoot(getContainer()).getLogDirectory(true).resolveChild("module-resource-xar.log"); - job.setLogFile(logFile); - } - - PipelineService.get().queueJob(job); - } - else - { - throw new NotFoundException("Expected a file but found a directory: " + f.getName()); - } - } - - return true; - } - - @Override - public URLHelper getSuccessURL(ImportXarForm importXarForm) - { - return getContainer().getStartURL(getUser()); - } - } - - - @RequiresPermission(InsertPermission.class) - public class ImportXarAction extends MutatingApiAction - { - @Override - public Object execute(ImportXarForm form, BindException errors) throws Exception - { - ApiSimpleResponse response = new ApiSimpleResponse(); - - List> archives = new ArrayList<>(); - for (FileLike f : form.getValidatedFiles(getContainer())) - { - Map archive = new HashMap<>(); - ExperimentPipelineJob job = new ExperimentPipelineJob(getViewBackgroundInfo(), f, "Experiment Import", false, form.getPipeRoot(getContainer())); - - // TODO: Configure module resources with the appropriate log location per container - if (form.getModule() != null) - { - FileLike logFile = form.getPipeRoot(getContainer()).getLogDirectory(true).resolveChild("module-resource-xar.log"); - job.setLogFile(logFile); - } - - PipelineService.get().queueJob(job); - - archive.put("file", f.getName()); - archive.put("job", job.getJobGUID()); - archive.put("path", form.getPath()); // echo back the public path - - archives.add(archive); - } - - response.put("success", true); - response.put("archives", archives); - - return response; - } - } - - - /** - * User: jeckels - * Date: Jan 27, 2008 - */ - public static class ExperimentUrlsImpl implements ExperimentUrls - { - public ActionURL getOverviewURL(Container c) - { - return new ActionURL(BeginAction.class, c); - } - - @Override - public ActionURL getExperimentDetailsURL(Container c, ExpExperiment expExperiment) - { - return new ActionURL(DetailsAction.class, c).addParameter("rowId", expExperiment.getRowId()); - } - - public ActionURL getShowSampleURL(Container c, ExpMaterial material) - { - return getMaterialDetailsBaseURL(c, null).addParameter("rowId", material.getRowId()); - } - - @Override - public ActionURL getExportProtocolURL(Container container, ExpProtocol protocol) - { - return new ActionURL(ExperimentController.ExportProtocolsAction.class, container). - addParameter("protocolId", protocol.getRowId()). - addParameter("xarFileName", protocol.getName() + ".xar"); - } - - @Override - public ActionURL getMoveRunsLocationURL(Container container) - { - return new ActionURL(ExperimentController.MoveRunsLocationAction.class, container); - } - - @Override - public ActionURL getProtocolDetailsURL(ExpProtocol protocol) - { - return new ActionURL(ProtocolDetailsAction.class, protocol.getContainer()).addParameter("rowId", protocol.getRowId()); - } - - @Override - public ActionURL getProtocolApplicationDetailsURL(ExpProtocolApplication app) - { - return getShowApplicationURL(app.getContainer(), app.getRowId()); - } - - public ActionURL getProtocolGridURL(Container c) - { - return new ActionURL(ShowProtocolGridAction.class, c); - } - - public ActionURL getRunGraphDetailURL(ExpRun run) - { - return getShowRunGraphDetailURL(run.getContainer(), run.getRowId()); - } - - @Override - public ActionURL getRunGraphDetailURL(ExpRun run, @Nullable ExpData focus) - { - return getRunGraphDetailURL(run, focus, DotGraph.TYPECODE_DATA); - } - - public ActionURL getRunGraphDetailURL(ExpRun run, @Nullable ExpMaterial focus) - { - return getRunGraphDetailURL(run, focus, DotGraph.TYPECODE_MATERIAL); - } - - public ActionURL getRunGraphDetailURL(ExpRun run, @Nullable ExpProtocolApplication focus) - { - return getRunGraphDetailURL(run, focus, DotGraph.TYPECODE_PROT_APP); - } - - private ActionURL getRunGraphDetailURL(ExpRun run, @Nullable ExpObject focus, String typeCode) - { - ActionURL result = getShowRunGraphDetailURL(run.getContainer(), run.getRowId()); - result.addParameter("detail", "true"); - if (focus != null) - { - result.addParameter("focus", typeCode + focus.getRowId()); - } - return result; - } - - @Override - public ActionURL getRunGraphURL(Container container, long runId) - { - return ExperimentController.getRunGraphURL(container, runId); - } - - @Override - public ActionURL getRunGraphURL(ExpRun run) - { - return getRunGraphURL(run.getContainer(), run.getRowId()); - } - - @Override - public ActionURL getRunTextURL(Container c, long runId) - { - return new ActionURL(ShowRunTextAction.class, c).addParameter("rowId", runId); - } - - @Override - public ActionURL getRunTextURL(ExpRun run) - { - return getRunTextURL(run.getContainer(), run.getRowId()); - } - - @Override - public ActionURL getDeleteExperimentsURL(Container container, URLHelper returnUrl) - { - return new ActionURL(DeleteSelectedExperimentsAction.class, container).addReturnUrl(returnUrl); - } - - @Override - public ActionURL getDeleteProtocolURL(@NotNull ExpProtocol protocol, URLHelper returnUrl) - { - ActionURL result = new ActionURL(DeleteProtocolByRowIdsAction.class, protocol.getContainer()); - result.addParameter("singleObjectRowId", protocol.getRowId()); - if (returnUrl != null) - { - result.addReturnUrl(returnUrl); - } - return result; - } - - @Override - public ActionURL getAddRunsToExperimentURL(Container c, ExpExperiment exp) - { - return new ActionURL(AddRunsToExperimentAction.class, c).addParameter("expRowId", exp.getRowId()); - } - - @Override - public ActionURL getShowRunsURL(Container c, ExperimentRunType type) - { - ActionURL result = new ActionURL(ShowRunsAction.class, c); - result.addParameter("experimentRunFilter", type.getDescription()); - return result; - } - - public ActionURL getShowExperimentsURL(Container c) - { - return new ActionURL(ShowRunGroupsAction.class, c); - } - - @Override - public ActionURL getShowSampleTypeListURL(Container c) - { - return getShowSampleTypeListURL(c, null); - } - - @Override - public ActionURL getShowSampleTypeURL(ExpSampleType sampleType) - { - return getShowSampleTypeURL(sampleType, sampleType.getContainer()); - } - - @Override - public ActionURL getShowSampleTypeURL(ExpSampleType sampleType, Container container) - { - return new ActionURL(ShowSampleTypeAction.class, container).addParameter("rowId", sampleType.getRowId()); - } - - public ActionURL getExperimentListURL(Container container) - { - return new ActionURL(ShowRunGroupsAction.class, container); - } - - public ActionURL getShowSampleTypeListURL(Container c, String errorMessage) - { - ActionURL url = new ActionURL(ListSampleTypesAction.class, c); - if (errorMessage != null) - { - url.addParameter("errorMessage", errorMessage); - } - return url; - } - - @Override - public ActionURL getDataClassListURL(Container c) - { - return getDataClassListURL(c, null); - } - - public ActionURL getDataClassListURL(Container c, String errorMessage) - { - ActionURL url = new ActionURL(ListDataClassAction.class, c); - if (errorMessage != null) - { - url.addParameter("errorMessage", errorMessage); - } - return url; - } - - @Override - public ActionURL getDeleteDatasURL(Container c, URLHelper returnUrl) - { - ActionURL url = new ActionURL(DeleteSelectedDataAction.class, c); - if (returnUrl != null) - url.addReturnUrl(returnUrl); - return url; - } - - public ActionURL getDeleteSelectedExperimentsURL(Container c, URLHelper returnUrl) - { - ActionURL result = new ActionURL(DeleteSelectedExperimentsAction.class, c); - if (returnUrl != null) - result.addReturnUrl(returnUrl); - return result; - } - - @Override - public ActionURL getDeleteSelectedExpRunsURL(Container container, URLHelper returnUrl) - { - return new ActionURL(DeleteSelectedExpRunsAction.class, container).addReturnUrl(returnUrl); - } - - public ActionURL getShowUpdateURL(ExpExperiment experiment) - { - return new ActionURL(ShowUpdateAction.class, experiment.getContainer()).addParameter("rowId", experiment.getRowId()); - } - - @Override - public ActionURL getRemoveSelectedExpRunsURL(Container container, URLHelper returnUrl, ExpExperiment exp) - { - return new ActionURL(RemoveSelectedExpRunsAction.class, container).addReturnUrl(returnUrl).addParameter("expRowId", exp.getRowId()); - } - - @Override - public ActionURL getCreateRunGroupURL(Container container, URLHelper returnUrl, boolean addSelectedRuns) - { - ActionURL result = new ActionURL(CreateRunGroupAction.class, container); - if (returnUrl != null) - { - result.addReturnUrl(returnUrl); - } - if (addSelectedRuns) - { - result.addParameter("addSelectedRuns", "true"); - } - return result; - } - - - public static ExperimentUrlsImpl get() - { - return (ExperimentUrlsImpl) urlProvider(ExperimentUrls.class); - } - - public ActionURL getBeginURL(Container container) - { - return new ActionURL(BeginAction.class, container); - } - - @Override - public ActionURL getDomainEditorURL(Container container, String domainURI, boolean createOrEdit) - { - Domain domain = PropertyService.get().getDomain(container, domainURI); - if (domain != null) - return getDomainEditorURL(container, domain); - - ActionURL url = new ActionURL(PropertyController.EditDomainAction.class, container); - url.addParameter("domainURI", domainURI); - if (createOrEdit) - url.addParameter("createOrEdit", true); - return url; - } - - @Override - public ActionURL getDomainEditorURL(Container container, Domain domain) - { - ActionURL url = new ActionURL(PropertyController.EditDomainAction.class, container); - url.addParameter("domainId", domain.getTypeId()); - return url; - } - - @Override - public ActionURL getCreateDataClassURL(Container container) - { - return new ActionURL(EditDataClassAction.class, container); - } - - @Override - public ActionURL getShowDataClassURL(Container container, long rowId) - { - ActionURL url = new ActionURL(ShowDataClassAction.class, container); - url.addParameter("rowId", rowId); - return url; - } - - @Override - public ActionURL getShowFileURL(ExpData data, boolean inline) - { - ActionURL result = getShowFileURL(data.getContainer()).addParameter("rowId", data.getRowId()); - if (inline) - { - result.addParameter("inline", inline); - } - return result; - } - - @Override - public ActionURL getMaterialDetailsURL(ExpMaterial material) - { - return getMaterialDetailsURL(material.getContainer(), material.getRowId()); - } - - @Override - public ActionURL getMaterialDetailsURL(Container c, long materialRowId) - { - return getMaterialDetailsBaseURL(c, null).addParameter("rowId", materialRowId); - } - - @Override - public ActionURL getMaterialDetailsBaseURL(Container c, @Nullable String materialIdFieldKey) - { - return new ActionURL(ShowMaterialAction.class, c); - } - - @Override - public ActionURL getCreateSampleTypeURL(Container container) - { - return new ActionURL(EditSampleTypeAction.class, container); - } - - @Override - public ActionURL getImportSamplesURL(Container container, String sampleTypeName) - { - ActionURL url = new ActionURL(ImportSamplesAction.class, container); - url.addParameter("query.queryName", sampleTypeName); - url.addParameter("schemaName", "exp.materials"); - return url; - } - - @Override - public ActionURL getImportDataURL(Container container, String dataClassName) - { - ActionURL url = new ActionURL(ImportDataAction.class, container); - url.addParameter("query.queryName", dataClassName); - url.addParameter("schemaName", "exp.data"); - return url; - } - - @Override - public ActionURL getDataDetailsURL(ExpData data) - { - return new ActionURL(ShowDataAction.class, data.getContainer()).addParameter("rowId", data.getRowId()); - } - - @Override - public ActionURL getShowFileURL(Container c) - { - return new ActionURL(ShowFileAction.class, c); - } - - @Override - public ActionURL getSetFlagURL(Container container) - { - return new ActionURL(SetFlagAction.class, container); - } - - @Override - public ActionURL getShowRunGraphURL(ExpRun run) - { - return ExperimentController.getRunGraphURL(run.getContainer(), run.getRowId()); - } - - @Override - public ActionURL getRepairTypeURL(Container container) - { - return new ActionURL(TypesController.RepairAction.class, container); - } - - @Override - public ActionURL getUpdateMaterialQueryRowAction(Container c, TableInfo table) - { - ActionURL url = new ActionURL(UpdateMaterialQueryRowAction.class, c); - url.addParameter("schemaName", "samples"); - url.addParameter(QueryView.DATAREGIONNAME_DEFAULT + "." + QueryParam.queryName, table.getName()); - - return url; - } - - @Override - public ActionURL getInsertMaterialQueryRowAction(Container c, TableInfo table) - { - ActionURL url = new ActionURL(InsertMaterialQueryRowAction.class, c); - url.addParameter(QueryView.DATAREGIONNAME_DEFAULT + "." + QueryParam.queryName, table.getName()); - - return url; - } - - @Override - public ActionURL getDataClassAttachmentDownloadAction(Container c) - { - return new ActionURL(ExperimentController.DataClassAttachmentDownloadAction.class, c); - } - - } - - private static abstract class BaseResolveLsidApiAction extends ReadOnlyApiAction - { - protected Set _seeds; - - @Override - public void validateForm(F form, Errors errors) - { - if (null != form.getLsids()) - { - _seeds = new LinkedHashSet<>(form.getLsids().size()); - for (String lsid : form.getLsids()) - { - Identifiable id = LsidManager.get().getObject(lsid); - if (id == null) - throw new NotFoundException("Unable to resolve object: " + lsid); - - // ensure the user has read permission in the seed container - if (!getContainer().equals(id.getContainer())) - { - if (!id.getContainer().hasPermission(getUser(), ReadPermission.class)) - throw new UnauthorizedException("User does not have permission to read object: " + lsid); - } - - _seeds.add(id); - } - } - else - { - throw new ApiUsageException("Starting lsids required"); - } - } - } - - @RequiresPermission(ReadPermission.class) - public static class ResolveAction extends BaseResolveLsidApiAction - { - @Override - public Object execute(ResolveLsidsForm form, BindException errors) - { - var settings = new ExperimentJSONConverter.Settings(form); - var data = _seeds.stream().map(n -> ExperimentJSONConverter.serialize(n, getUser(), settings)).collect(toList()); - return new ApiSimpleResponse("data", data); - } - } - - @RequiresPermission(ReadPermission.class) - public static class LineageAction extends BaseResolveLsidApiAction - { - @Override - public Object execute(ExpLineageOptions options, BindException errors) throws Exception - { - ExpLineageServiceImpl.get().streamLineage(getContainer(), getUser(), getViewContext().getResponse(), _seeds, options); - return null; - } - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(AdminPermission.class) - public static class RebuildEdgesAction extends MutatingApiAction - { - @Override - public Object execute(ExperimentRunForm form, BindException errors) - { - if (form.getRowId() != 0 || form.getLsid() != null) - { - ExpRun run = form.lookupRun(); - if (!run.getContainer().hasPermission(getUser(), ReadPermission.class)) - throw new UnauthorizedException("Not permitted"); - - ExperimentServiceImpl.get().syncRunEdges(run); - } - else - { - // should this require site admin permissions? - ExperimentServiceImpl.get().rebuildAllRunEdges(); - } - return success(); - } - } - - private static class VerifyEdgesForm extends ExperimentRunForm - { - private Integer _limit; - - public Integer getLimit() - { - return _limit; - } - - public void setLimit(Integer limit) - { - _limit = limit; - } - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(AdminPermission.class) - public static class VerifyEdgesAction extends ReadOnlyApiAction - { - @Override - public Object execute(VerifyEdgesForm form, BindException errors) - { - if (form.getRowId() != 0 || form.getLsid() != null) - { - ExpRun run = form.lookupRun(); - if (!run.getContainer().hasPermission(getUser(), ReadPermission.class)) - throw new UnauthorizedException("Not permitted"); - - ExperimentServiceImpl.get().verifyRunEdges(run); - } - else - { - ExperimentServiceImpl.get().verifyAllEdges(getContainer(), form.getLimit()); - } - return success(); - } - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(AdminPermission.class) - public static class RebuildAncestorsAction extends MutatingApiAction - { - @Override - public Object execute(Object form, BindException errors) - { - ClosureQueryHelper.truncateAndRecreate(); - return success(); - } - } - - - @Marshal(Marshaller.Jackson) - @RequiresPermission(AdminPermission.class) - public static class CheckDataClassesIndexedAction extends ReadOnlyApiAction - { - @Override - public Object execute(Object o, BindException errors) throws Exception - { - List> notInIndex = new ArrayList<>(100); - - List list = ExperimentService.get().getDataClasses(getContainer(), false); - for (ExpDataClass dc : list) - { - for (ExpData d : dc.getDatas()) - { - String docId = d.getDocumentId(); - if (docId != null) - { - SearchService.SearchHit hit = SearchService.get().find(docId); - if (hit == null) - { - JSONObject props = ExperimentJSONConverter.serializeData(d, getUser(), ExperimentJSONConverter.DEFAULT_SETTINGS); - props.put("docid", docId); - notInIndex.add(props.toMap()); - } - } - } - } - - return success(notInIndex); - } - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(AdminPermission.class) - public static class CheckEdgesAction extends ReadOnlyApiAction - { - @Override - public Object execute(Object o, BindException errors) throws Exception - { - List result; - DbSchema schema = ExperimentService.get().getSchema(); - TableInfo edgeTable = schema.getTable("Edge"); - - if (null != edgeTable.getColumn("fromObjectId")) - { - var edges = new SqlSelector(ExperimentService.get().getSchema(), "SELECT fromObjectId, toObjectId FROM exp.Edge") - .resultSetStream() - .map(r -> { try { return new Pair<>(r.getInt(1), r.getInt(2)); } catch (SQLException x) { throw new RuntimeException(x); } }) - .collect(toList()); - var cycles = (new GraphAlgorithms()).detectCycleInDirectedGraph(edges); - result = cycles.stream().map(e -> new Integer[]{e.first, e.second}).collect(toList()); - } - else - { - var edges = new SqlSelector(ExperimentService.get().getSchema(), "SELECT fromLsid, toLsid FROM exp.Edge") - .resultSetStream() - .map(r -> { try { return new Pair<>(r.getString(1), r.getString(2)); } catch (SQLException x) { throw new RuntimeException(x); } }) - .collect(toList()); - var cycles = (new GraphAlgorithms()).detectCycleInDirectedGraph(edges); - result = cycles.stream().map(e -> new String[]{e.first, e.second}).collect(toList()); - } - - JSONObject ret = new JSONObject(); - ret.put("result", result); - ret.put("success", true); - return ret; - } - } - - @RequiresPermission(UpdatePermission.class) - public static class UpdateMaterialQueryRowAction extends UserSchemaAction - { - @Override - protected QueryForm createQueryForm(ViewContext context) - { - QueryForm form = new QueryForm("samples", null); - form.setViewContext(getViewContext()); - form.bindParameters(getViewContext().getBindPropertyValues()); - return form; - } - - @Override - public BindException bindParameters(PropertyValues m) throws Exception - { - BindException bind = super.bindParameters(m); - - QueryUpdateForm tableForm = (QueryUpdateForm)bind.getTarget(); - - long sampleId = getSampleId(tableForm); - - ExpMaterial material = ExperimentService.get().getExpMaterial(sampleId); - if (material == null) - throw new NotFoundException("Invalid material: " + tableForm.getPkVal()); - - return bind; - } - - private static long getSampleId(QueryUpdateForm tableForm) - { - Long sampleId = null; - try - { - sampleId = ConvertHelper.convert(tableForm.getPkVal(), Long.class); - } - catch (ConversionException e) - { - } - if (null == sampleId) - throw new NotFoundException("Invalid RowId: " + tableForm.getPkVal()); - return sampleId; - } - - @Override - public ModelAndView getView(QueryUpdateForm tableForm, boolean reshow, BindException errors) - { - long sampleId = getSampleId(tableForm); - - ExpMaterial material = ExperimentService.get().getExpMaterial(sampleId); - if (material == null) - throw new NotFoundException("Invalid material: " + tableForm.getPkVal()); - - boolean isAliquot = !StringUtils.isEmpty(material.getAliquotedFromLSID()); - - TableInfo tableInfo = tableForm.getTable(); - Map scopedFields = new CaseInsensitiveHashMap<>(); - for (DomainProperty dp : tableInfo.getDomain().getProperties()) - { - if (!ExpSchema.DerivationDataScopeType.All.name().equalsIgnoreCase(dp.getDerivationDataScope())) - scopedFields.put(dp.getName(), ExpSchema.DerivationDataScopeType.ChildOnly.name().equalsIgnoreCase(dp.getDerivationDataScope())); - } - - for (var column : tableInfo.getColumns()) - { - String columnName = column.getName(); - if (scopedFields.containsKey(columnName)) - { - boolean isAliquotField = scopedFields.get(columnName); - boolean show = (isAliquot && isAliquotField) || (!isAliquot && !isAliquotField); - ((BaseColumnInfo)column).setUserEditable(show); - ((BaseColumnInfo)column).setHidden(!show); - } - } - - ButtonBar bb = createSubmitCancelButtonBar(tableForm); - UpdateView view = new UpdateView(tableForm, errors); - view.getDataRegion().setButtonBar(bb); - return view; - } - - @Override - public boolean handlePost(QueryUpdateForm tableForm, BindException errors) - { - doInsertUpdate(tableForm, errors, false); - return 0 == errors.getErrorCount(); - } - - @Override - public void addNavTrail(NavTree root) - { - super.addNavTrail(root); - root.addChild("Edit " + _form.getQueryName()); - } - } - - @RequiresPermission(InsertPermission.class) - public static class InsertMaterialQueryRowAction extends UserSchemaAction - { - @Override - protected QueryForm createQueryForm(ViewContext context) - { - QueryForm form = new QueryForm("samples", null); - form.setViewContext(getViewContext()); - form.bindParameters(getViewContext().getBindPropertyValues()); - - return form; - } - - @Override - public ModelAndView getView(QueryUpdateForm tableForm, boolean reshow, BindException errors) - { - TableInfo tableInfo = tableForm.getTable(); - Map propertyFields = new CaseInsensitiveHashMap<>(); - for (DomainProperty dp : tableInfo.getDomain().getProperties()) - { - propertyFields.put(dp.getName(), ExpSchema.DerivationDataScopeType.ChildOnly.name().equalsIgnoreCase(dp.getDerivationDataScope())); - } - - for (var column : tableInfo.getColumns()) - { - String columnName = column.getName(); - if (propertyFields.containsKey(columnName)) - { - boolean isAliquotField = propertyFields.get(columnName); - ((BaseColumnInfo)column).setUserEditable(!isAliquotField); - ((BaseColumnInfo)column).setHidden(isAliquotField); - } - } - - InsertView view = new InsertView(tableForm, errors); - view.getDataRegion().setButtonBar(createSubmitCancelButtonBar(tableForm)); - return view; - } - - @Override - public boolean handlePost(QueryUpdateForm tableForm, BindException errors) - { - doInsertUpdate(tableForm, errors, true); - return 0 == errors.getErrorCount(); - } - - @Override - public void addNavTrail(NavTree root) - { - super.addNavTrail(root); - root.addChild("Insert " + _form.getQueryName()); - } - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(ReadPermission.class) - public static class SaveFindIdsAction extends ReadOnlyApiAction - { - - public static final String FIND_BY_IDS_SESSION_KEY_PREFIX = "findByIds"; - - @Override - public Object execute(FindByIdsForm form, BindException errors) throws Exception - { - HttpServletRequest request = getViewContext().getRequest(); - String key = form.getSessionKey(); - boolean removePrevious = false; - - if (key == null) - { - removePrevious = true; - key = FIND_BY_IDS_SESSION_KEY_PREFIX + "_" + UniqueID.getServerSessionScopedUID(); - } - - if (request != null) - { - if (removePrevious) - SessionHelper.clearAttributesWithPrefix(request, FIND_BY_IDS_SESSION_KEY_PREFIX); - HttpSession session = request.getSession(false); - if (session != null) - { - @SuppressWarnings("unchecked") - List existingIds = (List) session.getAttribute(key); - - // deduplicate from existing ids - if (existingIds != null && form.getSessionKey() != null) - { - existingIds.addAll(form.getIds().stream().filter(id -> !existingIds.contains(id)).toList()); - session.setAttribute(key, existingIds); - } - else - { - session.setAttribute(key, form.getIds()); - } - return success("Saved ids to session key", key); - } - } - - return new SimpleResponse<>(false, "Unable to save to session. Session or request may be null."); - } - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(ReadPermission.class) - public static class SaveOrderedSamplesQueryAction extends ReadOnlyApiAction - { - private static final String SAMPLE_ID_PREFIX = "s:"; - private static final String UNIQUE_ID_PREFIX = "u:"; - - private List _ids; - private Map> _uniqueIdLsids; - - @Override - public void validateForm(FindByIdsForm form, Errors errors) - { - if (form.getSessionKey() == null) - errors.reject(ERROR_REQUIRED, "sessionKey must be provided"); - else - { - _ids = getFindIdsFromSession(form.getSessionKey()); - if (_ids == null || _ids.isEmpty()) - errors.reject(ERROR_REQUIRED, "No ids found corresponding to session key " + form.getSessionKey()); - } - } - - private void ensureUniqueIdLsids() - { - boolean hasUniqueId = _ids.stream().anyMatch(s -> s.startsWith(UNIQUE_ID_PREFIX)); - if (hasUniqueId && _uniqueIdLsids == null) - { - List uniqueIds = _ids.stream().map(s -> s.substring(UNIQUE_ID_PREFIX.length())).toList(); - _uniqueIdLsids = ExperimentService.get().getUniqueIdLsids(uniqueIds, getUser(), getContainer()); - } - } - - @Override - public Object execute(FindByIdsForm form, BindException errors) throws Exception - { - ensureUniqueIdLsids(); - - SQLFragment select = getOrderedRowsSql(); - // need to set the key field so selections are possible - // need the SampleTypeUnits so we will display using that unit - String metadata = - """ - - - - - true - true - - - true - - - true - - -
-
"""; - QueryDefinition def = QueryService.get().saveSessionQuery(getViewContext(), getContainer(), ExperimentServiceImpl.getExpSchema().getName(), select.getSQL(), metadata); - return success("Session query created", Map.of("queryName", def.getName(), "ids", _ids)); - } - - - private List getFindIdsFromSession(String sessionKey) - { - HttpServletRequest request = getViewContext().getRequest(); - List ids = new ArrayList<>(); - if (request != null) - { - HttpSession session = request.getSession(false); - if (session != null) - { - ids = (List) session.getAttribute(sessionKey); - } - } - return ids; - } - - private SQLFragment getOrderedRowsSql() - { - boolean isFMEnabled = InventoryService.isFreezerManagementEnabled(getContainer()); - String samplesTable = isFMEnabled ? "inventory.SampleItems" : "exp.materials"; - List orderedIdCols = new ArrayList<>(Arrays.asList("Id AS ProvidedID", "RowId", "Ordinal")); - List sampleColumns = new ArrayList<>(); - if (!isFMEnabled) - { - sampleColumns.addAll(Arrays.asList( - "S.Name AS SampleID", - "S.MaterialExpDate AS ExpirationDate", - "S.SampleSet as SampleType", - "S.SampleState", - "S.isAliquot", - "S.Created", - "S.CreatedBy" - )); - } - else - { - sampleColumns.addAll(Arrays.asList( - "S.Name AS SampleID", - "S.MaterialExpDate AS ExpirationDate", - "S.LabelColor", - "S.SampleSet", - "S.SampleState", - "S.StoredAmount", - "S.Units", - "S.SampleTypeUnits", - "S.FreezeThawCount", - "S.StorageStatus", - "S.CheckedOutBy", - "S.StorageLocation", - "S.StorageRow", - "S.StorageCol", - "S.StoragePositionNumber", - "S.IsAliquot", - "S.Created", - "S.CreatedBy" - )); - } - - - String sampleIdComma = ""; - String uniqueIdComma = ""; - int index = 1; - SQLFragment sampleIdValuesSql = new SQLFragment(); - SQLFragment uniqueIdValuesSql = new SQLFragment(); - for (String id : _ids) - { - if (id.startsWith(SAMPLE_ID_PREFIX)) - { - sampleIdValuesSql.append(sampleIdComma).append("\t(").appendValue(index); - sampleIdValuesSql.append(", "); - sampleIdValuesSql.append(LabKeySql.quoteString(id.substring(SAMPLE_ID_PREFIX.length()))); - sampleIdValuesSql.append(", "); - sampleIdValuesSql.append(LabKeySql.quoteString("null")); - sampleIdValuesSql.append(")"); - sampleIdComma = "\n,"; - } - else if (id.startsWith(UNIQUE_ID_PREFIX)) - { - String idClean = id.substring(UNIQUE_ID_PREFIX.length()); - - List lsids = _uniqueIdLsids.get(idClean); - if (lsids != null) - { - for (String lsid : lsids) - { - uniqueIdValuesSql.append(uniqueIdComma).append("\t(").appendValue(index); - uniqueIdValuesSql.append(", "); - uniqueIdValuesSql.append(LabKeySql.quoteString(idClean)); - uniqueIdValuesSql.append(", "); - uniqueIdValuesSql.append(LabKeySql.quoteString(lsid)); - uniqueIdValuesSql.append(")"); - uniqueIdComma = "\n,"; - } - } - } - index++; - } - - boolean haveData = !sampleIdValuesSql.isEmpty() || !_uniqueIdLsids.isEmpty(); - SQLFragment sql = new SQLFragment(); - if (!sampleIdValuesSql.isEmpty()) - { - sql.append("WITH _ordered_ids_ AS (\nSELECT * FROM (VALUES\n"); - sql.append(sampleIdValuesSql); - sql.append("\n) AS _values_ )\n"); // name of the alias here doesn't matter - } - if (!uniqueIdValuesSql.isEmpty()) - { - if (!sampleIdValuesSql.isEmpty()) - sql.append(",\n"); - else - sql.append("WITH "); - - sql.append("_ordered_unique_ids_ AS (\nSELECT * FROM (VALUES\n"); - sql.append(uniqueIdValuesSql); - sql.append("\n) AS _values_ )\n"); // name of the alias here doesn't matter - } - - sql.append("SELECT "); - sql.append("\n\tOID.").append(StringUtils.join(orderedIdCols, ",\n\tOID.")); - sql.append(",\n\t").append(StringUtils.join( sampleColumns, ",\n\t")); - sql.append("\nFROM\n("); - if (!sampleIdValuesSql.isEmpty()) - { - sql.append("SELECT\n\tM.RowId,\n\t_ordered_ids_.column1 as Ordinal,\n\t_ordered_ids_.column2 as Id,\n\t_ordered_ids_.column2 as lsid"); - sql.append("\nFROM _ordered_ids_\n"); - sql.append("INNER JOIN exp.materials M ON _ordered_ids_.column2 = M.Name"); - sql.append("\n"); - } - if (!uniqueIdValuesSql.isEmpty()) - { - if (!sampleIdValuesSql.isEmpty()) - sql.append("\nUNION ALL\n\n"); - - sql.append("SELECT\n\tM.RowId,\n\t_ordered_unique_ids_.column1 as Ordinal,\n\t_ordered_unique_ids_.column2 as Id,\n\t_ordered_unique_ids_.column3 as lsid"); - sql.append("\nFROM _ordered_unique_ids_\n"); - sql.append("INNER JOIN exp.materials M ON _ordered_unique_ids_.column3 = M.lsid"); - sql.append("\n"); - } - if (!haveData) // no data to return but return data in the expected shape. - { - sql = new SQLFragment("SELECT\n"); - sql.append(orderedIdCols.stream() - .map(col -> { - int asIndex = col.indexOf("AS"); - if (asIndex > 0) - return "NULL AS " + col.substring(asIndex+ 3); - else - return "NULL AS " + col; - }) - .collect(Collectors.joining(",\t\n"))); - sql.append(",\t\n").append(StringUtils.join(sampleColumns, ",\t\n")); - sql.append("\nFROM ").append(samplesTable).append(" S WHERE 1 = 2"); - return sql; - } - else - { - sql.append(") OID"); - if (isFMEnabled) - sql.append("\nLEFT JOIN inventory.SampleItems S on S.RowId = OID.RowId"); - else - sql.append("\nINNER JOIN exp.materials S on S.RowId = OID.RowId"); - sql.append("\n\nORDER BY Ordinal"); - return sql; - } - } - } - - public static class FindByIdsForm extends FindSessionKeyForm - { - List _ids; - - public List getIds() - { - return _ids; - } - - public void setIds(List ids) - { - _ids = ids; - } - } - - - public static class FindSessionKeyForm - { - private String _sessionKey; - - public String getSessionKey() - { - return _sessionKey; - } - - public void setSessionKey(String sessionKey) - { - _sessionKey = sessionKey; - } - } - - static void validateEntitySequenceForm(EntitySequenceForm form, Errors errors) - { - String kindName = form.getKindName(); - if (StringUtils.isEmpty(kindName) || form.getSeqType() == null) - { - errors.reject(ERROR_REQUIRED, "KindName and SeqType must be provided"); - return; - } - - if (form.getSeqType() == NameGenerator.EntityCounter.genId) - { - if (form.getRowId() == null) - errors.reject(ERROR_REQUIRED, "Data type RowId must be provided for genId"); - } - else if (!SampleTypeDomainKind.NAME.equalsIgnoreCase(kindName)) - { - errors.reject(ERROR_MSG, form.getSeqType() + " is not supported for " + kindName); - } - - if (!SampleTypeDomainKind.NAME.equalsIgnoreCase(kindName) && !DataClassDomainKind.NAME.equalsIgnoreCase(kindName)) - errors.reject(ERROR_MSG, "Invalid KindName. Should be either " + SampleTypeDomainKind.NAME + " or " + DataClassDomainKind.NAME + "."); - - } - - @RequiresPermission(ReadPermission.class) - public static class GetEntitySequenceAction extends ReadOnlyApiAction - { - @Override - public void validateForm(EntitySequenceForm form, Errors errors) - { - validateEntitySequenceForm(form, errors); - } - - @Override - public Object execute(EntitySequenceForm form, BindException errors) throws Exception - { - long value = -1; - if (SampleTypeDomainKind.NAME.equalsIgnoreCase(form.getKindName())) - { - if (form.getSeqType() == NameGenerator.EntityCounter.genId) - { - ExpSampleType sampleType = SampleTypeService.get().getSampleType(form.getRowId()); - if (sampleType != null) - value = sampleType.getCurrentGenId(); - } - else - { - value = SampleTypeService.get().getCurrentCount(form.getSeqType(), getContainer()); - } - - } - else if (DataClassDomainKind.NAME.equalsIgnoreCase(form.getKindName())) - { - ExpDataClass dataClass = ExperimentService.get().getDataClass(form.getRowId()); - if (dataClass != null) - value = dataClass.getCurrentGenId(); - } - - ApiSimpleResponse resp = new ApiSimpleResponse(); - resp.put("success", value > -1); - resp.put("value", value); - return resp; - } - } - - @RequiresPermission(ReadPermission.class) // actual permission checked later - public static class SetEntitySequenceAction extends MutatingApiAction - { - @Override - public void validateForm(EntitySequenceForm form, Errors errors) - { - validateEntitySequenceForm(form, errors); - - if (form.getNewValue() == null || form.getNewValue() < 0) - errors.reject(ERROR_MSG, "Invalid newValue."); - } - - @Override - public Object execute(EntitySequenceForm form, BindException errors) - { - ApiSimpleResponse resp = new ApiSimpleResponse(); - resp.put("success", true); - - try - { - Domain domain = null; - if (SampleTypeDomainKind.NAME.equalsIgnoreCase(form.getKindName())) - { - if (form.getSeqType() == NameGenerator.EntityCounter.genId) - { - if (!getContainer().hasPermission(getUser(), DesignSampleTypePermission.class)) - throw new UnauthorizedException("Insufficient permissions."); - - ExpSampleType sampleType = SampleTypeService.get().getSampleType(form.getRowId()); - if (sampleType != null) - { - sampleType.ensureMinGenId(form.getNewValue()); - domain = sampleType.getDomain(); - } - else - { - resp.put("success", false); - resp.put("error", "Sample type does not exist."); - } - } - else - { - if (!getContainer().hasPermission(getUser(), AdminPermission.class)) - throw new UnauthorizedException("Insufficient permissions."); - - SampleTypeService.get().ensureMinSampleCount(form.getNewValue(), form.getSeqType(), getContainer()); - } - } - else if (DataClassDomainKind.NAME.equalsIgnoreCase(form.getKindName())) - { - if (!getContainer().hasPermission(getUser(), DesignDataClassPermission.class)) - throw new BadRequestException("Insufficient permissions."); - - ExpDataClass dataClass = ExperimentService.get().getDataClass(form.getRowId()); - if (dataClass != null) - { - dataClass.ensureMinGenId(form.getNewValue(), getContainer()); - domain = dataClass.getDomain(); - } - else - { - resp.put("success", false); - resp.put("error", "DataClass does not exist."); - } - } - - if (domain != null) - { - DomainAuditProvider.DomainAuditEvent event = new DomainAuditProvider.DomainAuditEvent(getContainer(), "The genId for domain " + domain.getName() + " has been updated to " + form.getNewValue() + "."); - event.setDomainUri(domain.getTypeURI()); - event.setDomainName(domain.getName()); - AuditLogService.get().addEvent(getUser(), event); - } - } - catch (ExperimentException e) - { - resp.put("success", false); - resp.put("error", e.getMessage()); - } - - return resp; - } - } - - public static class EntitySequenceForm - { - private String _kindName; - private NameGenerator.EntityCounter _seqType; - private Integer _rowId; - private Long _newValue; - - public Integer getRowId() - { - return _rowId; - } - - public void setRowId(Integer rowId) - { - _rowId = rowId; - } - - public String getKindName() - { - return _kindName; - } - - public void setKindName(String kindName) - { - _kindName = kindName; - } - - public Long getNewValue() - { - return _newValue; - } - - public void setNewValue(Long newValue) - { - this._newValue = newValue; - } - - public NameGenerator.EntityCounter getSeqType() - { - return _seqType; - } - - public void setSeqType(String seqType) - { - _seqType = NameGenerator.EntityCounter.valueOf(seqType); - } - - } - - @Marshal(Marshaller.Jackson) - @RequiresPermission(ReadPermission.class) - public static class GetCrossFolderDataSelectionAction extends ReadOnlyApiAction - { - @Override - public void validateForm(CrossFolderSelectionForm form, Errors errors) - { - if (form.getDataRegionSelectionKey() == null && form.getRowIds() == null) - errors.reject(ERROR_REQUIRED, "You must provide either a set of rowIds or a dataRegionSelectionKey."); - if (!"samples".equalsIgnoreCase(form.getDataType()) && !"exp.data".equalsIgnoreCase(form.getDataType())&& !"assay".equalsIgnoreCase(form.getDataType())) - errors.reject(ERROR_REQUIRED, "Data type (sample, data or assayrun) must be specified."); - } - - @Override - public Object execute(CrossFolderSelectionForm form, BindException errors) - { - Pair result = ExperimentServiceImpl.get().getCurrentAndCrossFolderDataCount(form.getIds(false), form.getDataType(), getContainer()); - - ApiSimpleResponse resp = new ApiSimpleResponse(); - resp.put("success", true); - resp.put("currentFolderSelectionCount", result.first); - resp.put("crossFolderSelectionCount", result.second); - - return success(resp); - } - } - - public static class CrossFolderSelectionForm extends DataViewSnapshotSelectionForm - { - private String _dataType; - private String _picklistName; - - public String getDataType() - { - return _dataType; - } - - public void setDataType(String dataType) - { - _dataType = dataType; - } - - public String getPicklistName() - { - return _picklistName; - } - - public void setPicklistName(String picklistName) - { - _picklistName = picklistName; - } - - @Override - public Set getIds(boolean clear) - { - Set selectedIds; - - if (_rowIds != null) - selectedIds = _rowIds; - else if (isUseSnapshotSelection()) - selectedIds = new HashSet<>(DataRegionSelection.getSnapshotSelectedIntegers(getViewContext(), getDataRegionSelectionKey())); - else - selectedIds = DataRegionSelection.getSelectedIntegers(getViewContext(), getDataRegionSelectionKey(), clear); - - if (_picklistName != null) - { - User user = getViewContext().getUser(); - Container container = getViewContext().getContainer(); - UserSchema schema = ListService.get().getUserSchema(user, container); - TableInfo tInfo = schema.getTable(_picklistName); - if (tInfo != null) - { - SimpleFilter filter = SimpleFilter.createContainerFilter(container); - filter.addInClause(FieldKey.fromParts("id"), selectedIds); - TableSelector selector = new TableSelector(tInfo, Collections.singleton("SampleID"), filter, null); - return new HashSet<>(selector.getArrayList(Long.class)); - } - } - return selectedIds; - } - } - - @RequiresPermission(AdminPermission.class) - public static class RecomputeAliquotRollup extends SimpleViewAction - { - @Override - public void addNavTrail(NavTree root) - { - } - - @Override - public ModelAndView getView(Object o, BindException errors) throws SQLException - { - try (var ignore = SpringActionController.ignoreSqlUpdates()) - { - Container container = getContainer(); - - List sampleTypes = SampleTypeService.get() - .getSampleTypes(container, true); - - HtmlStringBuilder builder = HtmlStringBuilder.of(); - builder.unsafeAppend(""); - - SampleTypeService service = SampleTypeService.get(); - for (ExpSampleType sampleType : sampleTypes) - { - int updatedCount; - updatedCount = service.recomputeSampleTypeRollup(sampleType, container); - // we could check "if (0 < updatedCount) refresh(rollup)", but since this is a "manual" usage lets just always refresh - SampleTypeServiceImpl.get().refreshSampleTypeMaterializedView(sampleType, update); - builder.unsafeAppend(""); - } - - builder.unsafeAppend("
Sample Type#Recomputed
") - .append(sampleType.getName()) - .unsafeAppend("") - .append(updatedCount) - .unsafeAppend("
"); - return new HtmlView("Aliquot Rollup Recalculation Result", builder); - } - } - } - - /* Also see API CheckEdgesAction */ - @RequiresPermission(TroubleshooterPermission.class) - public static class CycleCheckAction extends FormViewAction - { - List cycleObjectIds = null; - - @Override - public void validateCommand(Object target, Errors errors) - { - - } - - @Override - public ModelAndView getView(Object o, boolean reshow, BindException errors) - { - if (!reshow) - { - return new HtmlView( - DIV("This operation can use a lot of memory.", - LK.FORM(at(method,"POST"), - PageFlowUtil.button("Continue").submit(true))) - ); - } - - if (null == cycleObjectIds) - return new HtmlView(HtmlString.of("No cycles found")); - - Map map = new LongHashMap<>(); - var cf = new ContainerFilter.AllFolders(getUser()); - var materials = ExperimentServiceImpl.get().getExpMaterialsByObjectId(cf, cycleObjectIds); - materials.forEach( (m) -> map.put(m.getObjectId(), m)); - var datas = ExperimentServiceImpl.get().getExpDatasByObjectId(cf, cycleObjectIds); - datas.forEach( (d) -> map.put(d.getObjectId(), d)); - var runs = ExperimentServiceImpl.get().getRunsByObjectId(cf, cycleObjectIds); - runs.forEach( (r) -> map.put(r.getObjectId(), r)); - - ExperimentUrls urls = ExperimentUrls.get(); - return new HtmlView( - DIV("Cycle found involving these objects.", - UL(cycleObjectIds.stream().map((objectid) -> - { - ExpObject exp = map.get(objectid); - if (exp instanceof ExpMaterial mat) - return LI(A(at(target, "_blank", href, urls.getMaterialDetailsURL(mat)), objectid + " : material - " + mat.getName())); - else if (exp instanceof ExpRun run) - return LI(A(at(target, "_blank", href, urls.getRunTextURL(run)), objectid + " : run - " + run.getName())); - else if (exp instanceof ExpData data) - return LI(A(at(target, "_blank", href, urls.getDataDetailsURL(data)), objectid + " : run - " + data.getName())); - else - return LI(String.valueOf(objectid)); - })) - ) - ); - } - - @Override - public boolean handlePost(Object o, BindException errors) - { - var edges = new SqlSelector(ExperimentService.get().getSchema(), "SELECT fromObjectId, toObjectId FROM exp.Edge") - .resultSetStream() - .map(r -> { try { return new Pair<>(r.getInt(1), r.getInt(2)); } catch (SQLException x) { throw new RuntimeException(x); } }) - .collect(toList()); - var cyclesEdges = (new GraphAlgorithms()).detectCycleInDirectedGraph(edges); - - var set = new LinkedHashSet(); - cyclesEdges.forEach( (edge) -> { - set.add(edge.first); - set.add(edge.second); - }); - cycleObjectIds = set.stream().toList(); - return false; - } - - @Override - public URLHelper getSuccessURL(Object o) - { - return null; - } - - @Override - public void addNavTrail(NavTree root) - { - - } - } - - @RequiresPermission(AdminPermission.class) - public static class MissingFilesCheckAction extends ReadOnlyApiAction - { - @Override - public Object execute(Object o, BindException errors) throws Exception - { - Map> info = ExperimentServiceImpl.get().doMissingFilesCheck(getUser(), getContainer(), true); - JSONObject results = new JSONObject(); - for (String containerId : info.keySet()) - { - JSONObject containerResults = new JSONObject(); - for (String sourceName : info.get(containerId).keySet()) - containerResults.put(sourceName, info.get(containerId).get(sourceName).toJSON()); - results.put(containerId, containerResults); - } - - ApiSimpleResponse response = new ApiSimpleResponse(); - response.put("success", true); - response.put("result", results); - return response; - } - } - -} +/* + * Copyright (c) 2008-2019 LabKey Corporation + * + * 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.labkey.experiment.controllers.exp; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.apache.commons.beanutils.ConversionException; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.poi.ss.usermodel.Workbook; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.labkey.api.action.ApiJsonWriter; +import org.labkey.api.action.ApiResponse; +import org.labkey.api.action.ApiSimpleResponse; +import org.labkey.api.action.ApiUsageException; +import org.labkey.api.action.ExportAction; +import org.labkey.api.action.FormHandlerAction; +import org.labkey.api.action.FormViewAction; +import org.labkey.api.action.HasViewContext; +import org.labkey.api.action.Marshal; +import org.labkey.api.action.Marshaller; +import org.labkey.api.action.MutatingApiAction; +import org.labkey.api.action.QueryViewAction; +import org.labkey.api.action.ReadOnlyApiAction; +import org.labkey.api.action.ReturnUrlForm; +import org.labkey.api.action.SimpleApiJsonForm; +import org.labkey.api.action.SimpleErrorView; +import org.labkey.api.action.SimpleResponse; +import org.labkey.api.action.SimpleViewAction; +import org.labkey.api.action.SpringActionController; +import org.labkey.api.assay.AssayFileWriter; +import org.labkey.api.assay.AssayProtocolSchema; +import org.labkey.api.assay.AssayProvider; +import org.labkey.api.assay.AssayService; +import org.labkey.api.assay.actions.UploadWizardAction; +import org.labkey.api.assay.security.DesignAssayPermission; +import org.labkey.api.attachments.AttachmentParent; +import org.labkey.api.attachments.AttachmentService; +import org.labkey.api.attachments.BaseDownloadAction; +import org.labkey.api.audit.AbstractAuditTypeProvider; +import org.labkey.api.audit.AuditLogService; +import org.labkey.api.audit.DetailedAuditTypeEvent; +import org.labkey.api.audit.SampleTimelineAuditEvent; +import org.labkey.api.audit.TransactionAuditProvider; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.collections.CaseInsensitiveHashSet; +import org.labkey.api.collections.LongHashMap; +import org.labkey.api.data.ActionButton; +import org.labkey.api.data.BaseColumnInfo; +import org.labkey.api.data.ButtonBar; +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.ConvertHelper; +import org.labkey.api.data.DataRegion; +import org.labkey.api.data.DataRegionSelection; +import org.labkey.api.data.DbSchema; +import org.labkey.api.data.DbScope; +import org.labkey.api.data.DisplayColumn; +import org.labkey.api.data.ExcelWriter; +import org.labkey.api.data.MenuButton; +import org.labkey.api.data.NameGenerator; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.ShowRows; +import org.labkey.api.data.SimpleDisplayColumn; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Sort; +import org.labkey.api.data.SqlSelector; +import org.labkey.api.data.TSVJSONWriter; +import org.labkey.api.data.TSVWriter; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.TableSelector; +import org.labkey.api.dataiterator.DataIteratorContext; +import org.labkey.api.exp.AbstractParameter; +import org.labkey.api.exp.DeleteForm; +import org.labkey.api.exp.DuplicateMaterialException; +import org.labkey.api.exp.ExperimentDataHandler; +import org.labkey.api.exp.ExperimentException; +import org.labkey.api.exp.ExperimentRunForm; +import org.labkey.api.exp.ExperimentRunListView; +import org.labkey.api.exp.ExperimentRunType; +import org.labkey.api.exp.Identifiable; +import org.labkey.api.exp.Lsid; +import org.labkey.api.exp.LsidManager; +import org.labkey.api.exp.LsidType; +import org.labkey.api.exp.ObjectProperty; +import org.labkey.api.exp.OntologyManager; +import org.labkey.api.exp.ProtocolApplicationParameter; +import org.labkey.api.exp.XarContext; +import org.labkey.api.exp.api.DataClassDomainKindProperties; +import org.labkey.api.exp.api.ExpData; +import org.labkey.api.exp.api.ExpDataClass; +import org.labkey.api.exp.api.ExpExperiment; +import org.labkey.api.exp.api.ExpLineageOptions; +import org.labkey.api.exp.api.ExpMaterial; +import org.labkey.api.exp.api.ExpMaterialRunInput; +import org.labkey.api.exp.api.ExpObject; +import org.labkey.api.exp.api.ExpProtocol; +import org.labkey.api.exp.api.ExpProtocolApplication; +import org.labkey.api.exp.api.ExpRun; +import org.labkey.api.exp.api.ExpRunAttachmentParent; +import org.labkey.api.exp.api.ExpRunEditor; +import org.labkey.api.exp.api.ExpRunItem; +import org.labkey.api.exp.api.ExpSampleType; +import org.labkey.api.exp.api.ExperimentJSONConverter; +import org.labkey.api.exp.api.ExperimentService; +import org.labkey.api.exp.api.ExperimentUrls; +import org.labkey.api.exp.api.NameExpressionOptionService; +import org.labkey.api.exp.api.ResolveLsidsForm; +import org.labkey.api.exp.api.SampleTypeDomainKind; +import org.labkey.api.exp.api.SampleTypeService; +import org.labkey.api.exp.list.ListService; +import org.labkey.api.exp.property.Domain; +import org.labkey.api.exp.property.DomainAuditProvider; +import org.labkey.api.exp.property.DomainKind; +import org.labkey.api.exp.property.DomainProperty; +import org.labkey.api.exp.property.DomainTemplate; +import org.labkey.api.exp.property.DomainTemplateGroup; +import org.labkey.api.exp.property.DomainUtil; +import org.labkey.api.exp.property.PropertyService; +import org.labkey.api.exp.query.ExpDataProtocolInputTable; +import org.labkey.api.exp.query.ExpInputTable; +import org.labkey.api.exp.query.ExpMaterialProtocolInputTable; +import org.labkey.api.exp.query.ExpSchema; +import org.labkey.api.exp.query.SamplesSchema; +import org.labkey.api.exp.xar.LSIDRelativizer; +import org.labkey.api.exp.xar.LsidUtils; +import org.labkey.api.files.FileContentService; +import org.labkey.api.gwt.client.AuditBehaviorType; +import org.labkey.api.inventory.InventoryService; +import org.labkey.api.module.ModuleHtmlView; +import org.labkey.api.module.ModuleLoader; +import org.labkey.api.pipeline.PipeRoot; +import org.labkey.api.pipeline.PipelineService; +import org.labkey.api.pipeline.PipelineStatusFile; +import org.labkey.api.pipeline.PipelineUrls; +import org.labkey.api.pipeline.PipelineValidationException; +import org.labkey.api.qc.SampleStatusService; +import org.labkey.api.query.AbstractQueryImportAction; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DetailsURL; +import org.labkey.api.query.DuplicateKeyException; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.QueryAction; +import org.labkey.api.query.QueryDefinition; +import org.labkey.api.query.QueryException; +import org.labkey.api.query.QueryForm; +import org.labkey.api.query.QueryParam; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.QuerySettings; +import org.labkey.api.query.QueryUpdateForm; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.QueryUpdateServiceException; +import org.labkey.api.query.QueryView; +import org.labkey.api.query.SchemaKey; +import org.labkey.api.query.UserSchema; +import org.labkey.api.query.UserSchemaAction; +import org.labkey.api.query.ValidationException; +import org.labkey.api.reader.ColumnDescriptor; +import org.labkey.api.reader.DataLoader; +import org.labkey.api.reader.DataLoaderFactory; +import org.labkey.api.reader.ExcelFactory; +import org.labkey.api.search.SearchService; +import org.labkey.api.security.ActionNames; +import org.labkey.api.security.RequiresAnyOf; +import org.labkey.api.security.RequiresNoPermission; +import org.labkey.api.security.RequiresPermission; +import org.labkey.api.security.SecurableResource; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.security.permissions.DeletePermission; +import org.labkey.api.security.permissions.DesignDataClassPermission; +import org.labkey.api.security.permissions.DesignSampleTypePermission; +import org.labkey.api.security.permissions.InsertPermission; +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.security.permissions.SampleWorkflowDeletePermission; +import org.labkey.api.security.permissions.SiteAdminPermission; +import org.labkey.api.security.permissions.TroubleshooterPermission; +import org.labkey.api.security.permissions.UpdatePermission; +import org.labkey.api.settings.AppProps; +import org.labkey.api.settings.ConceptURIProperties; +import org.labkey.api.sql.LabKeySql; +import org.labkey.api.study.Dataset; +import org.labkey.api.study.StudyService; +import org.labkey.api.study.StudyUrls; +import org.labkey.api.study.publish.StudyPublishService; +import org.labkey.api.usageMetrics.SimpleMetricsService; +import org.labkey.api.util.CsrfInput; +import org.labkey.api.util.DOM; +import org.labkey.api.util.DOM.LK; +import org.labkey.api.util.ErrorRenderer; +import org.labkey.api.util.ExceptionUtil; +import org.labkey.api.util.FileStream; +import org.labkey.api.util.FileUtil; +import org.labkey.api.util.HtmlString; +import org.labkey.api.util.HtmlStringBuilder; +import org.labkey.api.util.ImageUtil; +import org.labkey.api.util.JSoupUtil; +import org.labkey.api.util.LinkBuilder; +import org.labkey.api.util.NetworkDrive; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.Pair; +import org.labkey.api.util.ResponseHelper; +import org.labkey.api.util.SafeToRender; +import org.labkey.api.util.SessionHelper; +import org.labkey.api.util.StringExpression; +import org.labkey.api.util.StringUtilsLabKey; +import org.labkey.api.util.URLHelper; +import org.labkey.api.util.UniqueID; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.BadRequestException; +import org.labkey.api.view.DataView; +import org.labkey.api.view.DataViewSnapshotSelectionForm; +import org.labkey.api.view.DetailsView; +import org.labkey.api.view.HBox; +import org.labkey.api.view.HtmlView; +import org.labkey.api.view.HttpView; +import org.labkey.api.view.InsertView; +import org.labkey.api.view.JspView; +import org.labkey.api.view.NavTree; +import org.labkey.api.view.NotFoundException; +import org.labkey.api.view.RedirectException; +import org.labkey.api.view.UnauthorizedException; +import org.labkey.api.view.UpdateView; +import org.labkey.api.view.VBox; +import org.labkey.api.view.ViewBackgroundInfo; +import org.labkey.api.view.ViewContext; +import org.labkey.api.view.ViewServlet; +import org.labkey.api.view.WebPartView; +import org.labkey.api.view.template.ClientDependency; +import org.labkey.api.view.template.PageConfig; +import org.labkey.api.workflow.WorkflowService; +import org.labkey.experiment.ChooseExperimentTypeBean; +import org.labkey.experiment.ConfirmDeleteView; +import org.labkey.experiment.CustomPropertiesView; +import org.labkey.experiment.DataClassWebPart; +import org.labkey.experiment.DerivedSamplePropertyHelper; +import org.labkey.experiment.DotGraph; +import org.labkey.experiment.ExpDataFileListener; +import org.labkey.experiment.ExperimentRunDisplayColumn; +import org.labkey.experiment.LineageGraphDisplayColumn; +import org.labkey.experiment.MissingFilesCheckInfo; +import org.labkey.experiment.MoveRunsBean; +import org.labkey.experiment.ParentChildView; +import org.labkey.experiment.ProtocolApplicationDisplayColumn; +import org.labkey.experiment.ProtocolDisplayColumn; +import org.labkey.experiment.ProtocolWebPart; +import org.labkey.experiment.RunGroupWebPart; +import org.labkey.experiment.SampleTypeDisplayColumn; +import org.labkey.experiment.SampleTypeWebPart; +import org.labkey.experiment.StandardAndCustomPropertiesView; +import org.labkey.experiment.XarExportPipelineJob; +import org.labkey.experiment.XarExportType; +import org.labkey.experiment.XarExporter; +import org.labkey.experiment.api.ClosureQueryHelper; +import org.labkey.experiment.api.DataClass; +import org.labkey.experiment.api.DataClassDomainKind; +import org.labkey.experiment.api.ExpDataClassAttachmentParent; +import org.labkey.experiment.api.ExpDataClassImpl; +import org.labkey.experiment.api.ExpDataImpl; +import org.labkey.experiment.api.ExpExperimentImpl; +import org.labkey.experiment.api.ExpMaterialImpl; +import org.labkey.experiment.api.ExpProtocolApplicationImpl; +import org.labkey.experiment.api.ExpProtocolImpl; +import org.labkey.experiment.api.ExpRunImpl; +import org.labkey.experiment.api.ExpSampleTypeImpl; +import org.labkey.experiment.api.Experiment; +import org.labkey.experiment.api.ExperimentServiceImpl; +import org.labkey.experiment.api.GraphAlgorithms; +import org.labkey.experiment.api.ProtocolActionStepDetail; +import org.labkey.experiment.api.SampleTypeServiceImpl; +import org.labkey.experiment.api.SampleTypeUpdateServiceDI; +import org.labkey.experiment.controllers.property.PropertyController; +import org.labkey.experiment.lineage.ExpLineageServiceImpl; +import org.labkey.experiment.pipeline.ExperimentPipelineJob; +import org.labkey.experiment.types.TypesController; +import org.labkey.experiment.xar.XarExportSelection; +import org.labkey.vfs.FileLike; +import org.labkey.vfs.FileSystemLike; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.validation.ObjectError; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; +import static org.labkey.api.data.DbScope.CommitTaskOption.POSTCOMMIT; +import static org.labkey.api.exp.query.ExpSchema.TableType.DataInputs; +import static org.labkey.api.query.QueryImportPipelineJob.QUERY_IMPORT_NOTIFICATION_PROVIDER_PARAM; +import static org.labkey.api.query.QueryImportPipelineJob.QUERY_IMPORT_PIPELINE_DESCRIPTION_PARAM; +import static org.labkey.api.query.QueryImportPipelineJob.QUERY_IMPORT_PIPELINE_PROVIDER_PARAM; +import static org.labkey.api.util.DOM.A; +import static org.labkey.api.util.DOM.Attribute.action; +import static org.labkey.api.util.DOM.Attribute.href; +import static org.labkey.api.util.DOM.Attribute.id; +import static org.labkey.api.util.DOM.Attribute.method; +import static org.labkey.api.util.DOM.Attribute.name; +import static org.labkey.api.util.DOM.Attribute.size; +import static org.labkey.api.util.DOM.Attribute.src; +import static org.labkey.api.util.DOM.Attribute.target; +import static org.labkey.api.util.DOM.Attribute.type; +import static org.labkey.api.util.DOM.Attribute.value; +import static org.labkey.api.util.DOM.Attribute.width; +import static org.labkey.api.util.DOM.DIV; +import static org.labkey.api.util.DOM.IMG; +import static org.labkey.api.util.DOM.INPUT; +import static org.labkey.api.util.DOM.LI; +import static org.labkey.api.util.DOM.TABLE; +import static org.labkey.api.util.DOM.TD; +import static org.labkey.api.util.DOM.TR; +import static org.labkey.api.util.DOM.UL; +import static org.labkey.api.util.DOM.at; +import static org.labkey.api.util.DOM.cl; +import static org.labkey.experiment.ExpDataIterators.setContainerFilterForImport; +import static org.labkey.experiment.api.SampleTypeServiceImpl.SampleChangeType.update; + +public class ExperimentController extends SpringActionController +{ + private static final Logger _log = LogManager.getLogger(ExperimentController.class); + private static final DefaultActionResolver _actionResolver = new DefaultActionResolver( + ExperimentController.class + ); + private static final String GUEST_DIRECTORY_NAME = "guest"; + + public ExperimentController() + { + setActionResolver(_actionResolver); + } + + public static void ensureCorrectContainer(Container requestContainer, ExpObject object, ViewContext viewContext) + { + Container objectContainer = object.getContainer(); + if (!requestContainer.equals(objectContainer)) + { + ActionURL url = viewContext.cloneActionURL(); + url.setContainer(objectContainer); + throw new RedirectException(url); + } + } + + // Complete no-op, but leave in place in case we decide to adjust the base nav trail + private void addRootNavTrail(NavTree root) + { + // Intentionally don't add an "Experiment" node to the list because it's too overloaded. All content on the + // default action can be added to a portal page if desired. + } + + @Override + public PageConfig defaultPageConfig() + { + // set default help topic for controller + PageConfig config = super.defaultPageConfig(); + config.setHelpTopic("experiment"); + return config; + } + + @ActionNames("begin,gridView") + @RequiresPermission(ReadPermission.class) + public class BeginAction extends SimpleViewAction + { + @Override + public VBox getView(Object o, BindException errors) + { + VBox result = new VBox(); + + VBox runListView = createRunListView(20); + result.addView(runListView); + + RunGroupWebPart runGroups = new RunGroupWebPart(getViewContext(), false); + runGroups.showHeader(); + result.addView(runGroups); + + result.addView(new ProtocolWebPart(false, getViewContext())); + result.addView(new SampleTypeWebPart(false, getViewContext())); + result.addView(new DataClassWebPart(false, getViewContext(), null)); + + return result; + } + + @Override + public void addNavTrail(NavTree root) + { + addRootNavTrail(root); + root.addChild("Experiment"); + } + } + + @RequiresPermission(ReadPermission.class) + public class ShowRunsAction extends SimpleViewAction + { + @Override + public VBox getView(Object o, BindException errors) + { + return createRunListView(100); + } + + @Override + public void addNavTrail(NavTree root) + { + addRootNavTrail(root); + root.addChild("Experiment Runs"); + } + } + + private VBox createRunListView(int defaultMaxRows) + { + Set types = ExperimentService.get().getExperimentRunTypes(getContainer()); + ChooseExperimentTypeBean bean = new ChooseExperimentTypeBean(types, ExperimentRunType.getSelectedFilter(types, getViewContext().getRequest().getParameter("experimentRunFilter")), getViewContext().getActionURL().clone(), Collections.emptyList()); + JspView chooserView = new JspView<>("/org/labkey/experiment/experimentRunQueryHeader.jsp", bean); + + ExperimentRunListView view = ExperimentService.get().createExperimentRunWebPart(getViewContext(), bean.getSelectedFilter()); + view.setFrame(WebPartView.FrameType.NONE); + + // When paginated and the user hasn't explicitly set a maxRows, use the default maxRows size. + QuerySettings settings = view.getSettings(); + if (!settings.isMaxRowsSet() && settings.getShowRows() == ShowRows.PAGINATED) + { + settings.setMaxRows(defaultMaxRows); + } + + VBox result = new VBox(chooserView, view); + result.setFrame(WebPartView.FrameType.PORTAL); + return result; + } + + @RequiresPermission(ReadPermission.class) + @ActionNames("showRunGroups, showExperiments") + public class ShowRunGroupsAction extends SimpleViewAction + { + @Override + public ModelAndView getView(Object o, BindException errors) + { + RunGroupWebPart webPart = new RunGroupWebPart(getViewContext(), false); + webPart.setFrame(WebPartView.FrameType.NONE); + return webPart; + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("runGroups"); + addRootNavTrail(root); + root.addChild("Run Groups"); + } + } + + public record Field(String domainURI, String domainName, String name, Container container) {} + public record MiniExpObject(Object rowId, String name) {} + public record TimelineSummary(MiniExpObject miniExpObject, String mostRecentValue) {} + public record ProblemType(String tableName, String fieldName, String pkName) { + public Object toHtml(List summaries) + { + return DOM.DIV( + DOM.H4(tableName), + DOM.TABLE(at(cl("table-condensed", "labkey-data-region", "table-bordered")), + DOM.THEAD(DOM.TH(pkName), DOM.TH(fieldName)), + summaries.stream().map(summary -> + DOM.TR(DOM.TD(summary.miniExpObject.name), DOM.TD(summary.mostRecentValue))) + )); + } + } + + @RequiresPermission(SiteAdminPermission.class) + public static class ReportLostFieldValuesAction extends SimpleViewAction + { + @Override + public ModelAndView getView(Object o, BindException errors) + { + // Find all the fields that could have lost data due to issue 52666 + TableInfo t = new ExpSchema(getUser(), ContainerManager.getRoot()).getTable(ExpSchema.TableType.Fields.name(), ContainerFilter.getUnsafeEverythingFilter()); + List fields = new TableSelector(t, + new SimpleFilter(FieldKey.fromParts("StorageColumnNameMatch"), false). + addCondition(FieldKey.fromParts("DomainURI"), ":AssayDomain-Data.", CompareType.DOES_NOT_CONTAIN), + null). + getArrayList(Field.class); + + // Prep audit table for querying + UserSchema auditSchema = AuditLogService.get().createSchema(getUser(), ContainerManager.getRoot()); + + Map> sampleTypeSummaries = new HashMap<>(); + Map> dataClassSummaries = new HashMap<>(); + Map> listSummaries = new HashMap<>(); + + Map> problematicFields = new LinkedHashMap<>(); + + for (Field field : fields) + { + String domainURI = field.domainURI; + String fieldName = field.name; + Container container = field.container; + Domain domain = PropertyService.get().getDomain(container, domainURI); + if (domain != null && domain.getDomainKind() != null) + { + TableInfo table = domain.getDomainKind().getTableInfo(getUser(), container, domain, ContainerFilter.getUnsafeEverythingFilter()); + + if (table != null) + { + // Drill into sample types + if (domain.getDomainKind().getClass().equals(SampleTypeDomainKind.class)) + { + // rows that currently have no value for the field with potential for data loss + List rowsWithNull = new TableSelector(table, + new HashSet<>(List.of("RowId", "Name")), + new SimpleFilter(new CompareType.CompareClause(FieldKey.fromParts(fieldName), CompareType.ISBLANK, null)), + null). + getArrayList(MiniExpObject.class); + + List fixupsNeeded = checkData( + rowsWithNull, + fieldName, + obj -> new SimpleFilter(FieldKey.fromParts("SampleId"), obj.rowId), + auditSchema.getTable(SampleTimelineAuditEvent.EVENT_TYPE, ContainerFilter.getUnsafeEverythingFilter())); + if (!fixupsNeeded.isEmpty()) + { + sampleTypeSummaries.put(new ProblemType(table.getName(), fieldName, "SampleID"), fixupsNeeded); + } + } + // and data classes/sample sources + if (domain.getDomainKind().getClass().equals(DataClassDomainKind.class)) + { + // rows samples that current have no value for the field with potential for data loss + List rowsWithNull = new TableSelector(table, + new HashSet<>(List.of("RowId", "Name")), + new SimpleFilter(new CompareType.CompareClause(FieldKey.fromParts(fieldName), CompareType.ISBLANK, null)), + null). + getArrayList(MiniExpObject.class); + + List fixupsNeeded = checkData( + rowsWithNull, + fieldName, + obj -> new SimpleFilter(FieldKey.fromParts("RowPk"), Objects.toString(obj.rowId)). + addCondition(FieldKey.fromParts("SchemaName"), "exp.data"). + addCondition(FieldKey.fromParts("QueryName"), domain.getName()), + auditSchema.getTable("QueryUpdateAuditEvent", ContainerFilter.getUnsafeEverythingFilter())); + + if (!fixupsNeeded.isEmpty()) + { + dataClassSummaries.put(new ProblemType(table.getName(), fieldName, "SourceID"), fixupsNeeded); + } + } + // and lists + if ("lists".equals(table.getUserSchema().getName())) + { + // rows samples that current have no value for the field with potential for data loss + List rowsWithNull = new ArrayList<>(); + + ColumnInfo entityIdCol = table.getColumn("EntityId"); + ColumnInfo pkCol = table.getPkColumns().getFirst(); + + new TableSelector(table, + List.of(entityIdCol, pkCol), + new SimpleFilter(new CompareType.CompareClause(FieldKey.fromParts(fieldName), CompareType.ISBLANK, null)), + null). + forEachResults(r -> + { + Object entityId = entityIdCol.getValue(r); + Object pk = pkCol.getValue(r); + rowsWithNull.add(new MiniExpObject(entityId, pk.toString())); + }); + + + List fixupsNeeded = checkData( + rowsWithNull, + fieldName, + obj -> new SimpleFilter(FieldKey.fromParts("ListItemEntityId"), obj.rowId), + auditSchema.getTable("ListAuditEvent", ContainerFilter.getUnsafeEverythingFilter())); + + if (!fixupsNeeded.isEmpty()) + { + listSummaries.put(new ProblemType(table.getName(), fieldName, table.getPkColumnNames().getFirst()), fixupsNeeded); + } + } + + long totalRows = new TableSelector(table).getRowCount(); + long emptyRows = new TableSelector(table, new SimpleFilter(new CompareType.CompareClause(FieldKey.fromParts(fieldName), CompareType.ISBLANK, null)), null).getRowCount(); + problematicFields.put(field, Pair.of(totalRows, emptyRows)); + } + else + { + problematicFields.put(field, Pair.of(null, null)); + } + } + } + + return new HtmlView("Fixups Needed", + DOM.createHtmlFragment( + DOM.H2("Potentially Problematic Fields"), + problematicFields.isEmpty() ? "No problematic fields detected!" : + DOM.TABLE(at(cl("table-condensed", "labkey-data-region", "table-bordered")), + DOM.THEAD(DOM.TH("Domain Name"), DOM.TH("Domain URI"), DOM.TH("Field Name"), DOM.TH("Container"), DOM.TH("Total Rows"), DOM.TH("Rows with Nulls")), + problematicFields.entrySet().stream().map(e -> { + Field f = e.getKey(); + Pair counts = e.getValue(); + return DOM.TR( + DOM.TD(f.domainName), + DOM.TD(f.domainURI), + DOM.TD(f.name), + DOM.TD(f.container.getPath()), + DOM.TD(counts.first), + DOM.TD(counts.second) + ); + } + )), + + DOM.H2("Sample Types"), + sampleTypeSummaries.isEmpty() ? "No problems detected!" : + sampleTypeSummaries.entrySet().stream().map(e -> + e.getKey().toHtml(e.getValue())), + + DOM.H2("Data Classes"), + dataClassSummaries.isEmpty() ? "No problems detected!" : + dataClassSummaries.entrySet().stream().map(e -> + e.getKey().toHtml(e.getValue())), + + DOM.H2("Lists"), + listSummaries.isEmpty() ? "No problems detected!" : + listSummaries.entrySet().stream().map(e -> + e.getKey().toHtml(e.getValue())) + )); + } + + @NotNull + private List checkData(List rowsWithNull, String fieldName, Function filterGenerator, TableInfo auditTable) + { + List fixupsNeeded = new ArrayList<>(); + + // For each sample without a value today, check the audit history + for (MiniExpObject row : rowsWithNull) + { + // Order by RowId to get them in the sequence they happened in + var events = new TableSelector(auditTable, filterGenerator.apply(row), new Sort("RowId")).getArrayList(DetailedAuditTypeEvent.class); + // Remember the most recently set value + String mostRecentValue = null; + for (DetailedAuditTypeEvent event : events) + { + Map newValues = new CaseInsensitiveHashMap<>(AbstractAuditTypeProvider.decodeFromDataMap(event.getNewRecordMap())); + if (newValues.containsKey(fieldName)) + { + // Will be the empty string if the value was intentionally set to blank + mostRecentValue = newValues.get(fieldName); + } + } + // If the value had been set before, and its most recent insert/update wasn't setting it blank, + // it's most likely a lost value + if (mostRecentValue != null && !mostRecentValue.isEmpty()) + { + fixupsNeeded.add(new TimelineSummary(row, mostRecentValue)); + } + } + return fixupsNeeded; + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Accidentally Nulled Field Report"); + } + } + + @RequiresPermission(ReadPermission.class) + public static class CreateHiddenRunGroupAction extends MutatingApiAction + { + @Override + public ApiResponse execute(SimpleApiJsonForm form, BindException errors) throws Exception + { + JSONObject json = form.getJsonObject(); + String selectionKey = json.optString("selectionKey", null); + List runs = new ArrayList<>(); + + // Accept either an explicit list of run IDs + if (json.has("runIds")) + { + JSONArray runIds = json.getJSONArray("runIds"); + for (int i = 0; i < runIds.length(); i++) + { + ExpRunImpl run = ExperimentServiceImpl.get().getExpRun(runIds.getInt(i)); + if (run != null) + { + runs.add(run); + } + } + } + // Or a reference to a DataRegion selection key + else if (selectionKey != null) + { + Set ids = DataRegionSelection.getSelectedIntegers(getViewContext(), selectionKey, false); + for (Long id : ids) + { + ExpRunImpl run = ExperimentServiceImpl.get().getExpRun(id); + if (run != null) + { + runs.add(run); + } + } + } + if (runs.isEmpty()) + { + throw new NotFoundException(); + } + + ExpExperiment group = ExperimentService.get().createHiddenRunGroup(getContainer(), getUser(), runs.toArray(new ExpRun[0])); + if (selectionKey != null) + DataRegionSelection.clearAll(getViewContext(), selectionKey); + + ApiSimpleResponse response = new ApiSimpleResponse(); + response.putBean(group, "rowId", "LSID", "name", "hidden"); + return response; + } + } + + + @RequiresPermission(ReadPermission.class) + public class DetailsAction extends QueryViewAction + { + private ExpExperimentImpl _experiment; + + public DetailsAction() + { + super(ExpObjectForm.class); + } + + private Pair> createViews(ExpObjectForm form, BindException errors) + { + _experiment = ExperimentServiceImpl.get().getExpExperiment(form.getRowId()); + if (_experiment == null) + { + throw new NotFoundException("Could not find an experiment with RowId " + form.getRowId()); + } + + if (!_experiment.getContainer().equals(getContainer())) + { + throw new RedirectException(getViewContext().cloneActionURL().setContainer(_experiment.getContainer())); + } + + List protocols = _experiment.getAllProtocols(); + + Set types = new TreeSet<>(ExperimentService.get().getExperimentRunTypes(getContainer())); + ExperimentRunType selectedType = ExperimentRunType.getSelectedFilter(types, getViewContext().getRequest().getParameter("experimentRunFilter")); + + ChooseExperimentTypeBean bean = new ChooseExperimentTypeBean(types, selectedType, getViewContext().getActionURL().clone(), protocols); + JspView chooserView = new JspView<>("/org/labkey/experiment/experimentRunQueryHeader.jsp", bean, errors); + + ExperimentRunListView runListView = ExperimentRunListView.createView(getViewContext(), bean.getSelectedFilter(), true); + runListView.getRunTable().setExperiment(_experiment); + runListView.setShowRemoveFromExperimentButton(true); + runListView.setShowDeleteButton(true); + runListView.setShowAddToRunGroupButton(true); + runListView.setShowExportButtons(true); + runListView.setShowMoveRunsButton(true); + return new Pair<>(runListView, chooserView); + } + + @Override + protected ModelAndView getHtmlView(ExpObjectForm form, BindException errors) throws Exception + { + Pair> views = createViews(form, errors); + + CustomPropertiesView customPropertiesView = new CustomPropertiesView(_experiment.getLSID(), getContainer()); + + TableInfo runGroupsTable = new ExpSchema(getUser(), getContainer()).getTable(ExpSchema.TableType.RunGroups); + + DetailsView detailsView = new DetailsView(new DataRegion(), _experiment.getRowId()); + detailsView.getDataRegion().setTable(runGroupsTable); + detailsView.getDataRegion().addColumns(runGroupsTable, "RowId,Name,Created,Modified,Contact,ExperimentDescriptionURL,Hypothesis,Comments"); + detailsView.getDataRegion().getDisplayColumn(0).setVisible(false); + detailsView.getDataRegion().getDisplayColumn(2).setWidth("60%"); + + ButtonBar bb = new ButtonBar(); + bb.setStyle(ButtonBar.Style.separateButtons); + ActionButton b = new ActionButton(ExperimentUrlsImpl.get().getShowUpdateURL(_experiment), "Edit"); + b.setDisplayPermission(UpdatePermission.class); + bb.add(b); + detailsView.getDataRegion().setButtonBar(bb); + if (_experiment.getBatchProtocol() != null) + { + detailsView.setTitle("Batch Details"); + detailsView.getDataRegion().addColumns(runGroupsTable, "BatchProtocolId"); + } + else + { + detailsView.setTitle("Run Group Details"); + } + + VBox runsVBox = new VBox(views.second, createInitializedQueryView(form, errors, false, null)); + runsVBox.setTitle("Experiment Runs"); + runsVBox.setFrame(WebPartView.FrameType.PORTAL); + + return new VBox(new StandardAndCustomPropertiesView(detailsView, customPropertiesView), runsVBox); + } + + @Override + protected ExperimentRunListView createQueryView(ExpObjectForm form, BindException errors, boolean forExport, String dataRegion) + { + return createViews(form, errors).first; + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("runGroups"); + addRootNavTrail(root); + root.addChild("Run Groups", ExperimentUrlsImpl.get().getShowExperimentsURL(getContainer())); + root.addChild(_experiment.getName()); + } + } + + @RequiresPermission(ReadPermission.class) + public class ListSampleTypesAction extends SimpleViewAction + { + @Override + public ModelAndView getView(Object o, BindException errors) + { + SampleTypeWebPart view = new SampleTypeWebPart(false, getViewContext()); + view.setFrame(WebPartView.FrameType.NONE); + view.setErrorMessage(getViewContext().getRequest().getParameter("errorMessage")); + + return view; + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("sampleSets"); + addRootNavTrail(root); + root.addChild("Sample Types"); + } + } + + @RequiresPermission(ReadPermission.class) + public class ShowSampleTypeAction extends SimpleViewAction + { + private ExpSampleTypeImpl _sampleType; + + @Override + public ModelAndView getView(ExpObjectForm form, BindException errors) + { + _sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), form.getRowId(), true); + if (_sampleType == null && form.getLsid() != null) + { + if (form.getLsid().equalsIgnoreCase("Material") || form.getLsid().equalsIgnoreCase("Sample")) + { + // Not a real sample type - just show all the materials instead + throw new RedirectException(new ActionURL(ShowAllMaterialsAction.class, getContainer())); + } + // Check if the URL specifies the LSID, and stick the bean back into the form + _sampleType = SampleTypeServiceImpl.get().getSampleType(form.getLsid()); + } + + if (_sampleType == null) + { + throw new NotFoundException("No matching sample type found"); + } + + List allScopedSampleTypes = (List) SampleTypeService.get().getSampleTypes(getContainer(), true); + if (!allScopedSampleTypes.contains(_sampleType)) + { + ensureCorrectContainer(getContainer(), _sampleType, getViewContext()); + } + + SamplesSchema schema = new SamplesSchema(getUser(), getContainer()); + QuerySettings settings = schema.getSettings(getViewContext(), "Material", _sampleType.getName()); + QueryView queryView = new SampleTypeContentsView(_sampleType, schema, settings, errors); + + DetailsView detailsView = new DetailsView(getSampleTypeRegion(getViewContext()), _sampleType.getRowId()); + detailsView.getDataRegion().getDisplayColumn("Name").setURL((ActionURL)null); + detailsView.getDataRegion().getDisplayColumn("LSID").setVisible(false); + detailsView.getDataRegion().getDisplayColumn("MaterialLSIDPrefix").setVisible(false); + detailsView.getDataRegion().getDisplayColumn("LabelColor").setVisible(false); + detailsView.getDataRegion().getDisplayColumn("MetricUnit").setVisible(false); + detailsView.getDataRegion().getDisplayColumn("Category").setVisible(false); + + detailsView.setTitle("Sample Type Properties"); + detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).setStyle(ButtonBar.Style.separateButtons); + + Container autoLinkContainer = _sampleType.getAutoLinkTargetContainer(); + if (null != autoLinkContainer) + { + DisplayColumn autoLinkTargetColumn = detailsView.getDataRegion().getDisplayColumn("autoLinkTargetContainer"); + autoLinkTargetColumn.setVisible(false); + + SimpleDisplayColumn displayAutoLinkTargetColumn = new SimpleDisplayColumn(); + displayAutoLinkTargetColumn.setCaption("Auto Link Target Container:"); + String path = autoLinkContainer.getPath(); + displayAutoLinkTargetColumn.setDisplayHtml(path.equals("/") ? "" : path); + detailsView.getDataRegion().addDisplayColumn(displayAutoLinkTargetColumn); + } + + DisplayColumn autoLinkCategoryColumn = detailsView.getDataRegion().getDisplayColumn("autoLinkCategory"); + autoLinkCategoryColumn.setVisible(false); + SimpleDisplayColumn displayAutoLinkCategoryColumn = new SimpleDisplayColumn(); + displayAutoLinkCategoryColumn.setCaption("Auto Link Category:"); + displayAutoLinkCategoryColumn.setDisplayHtml(_sampleType.getAutoLinkCategory()); + detailsView.getDataRegion().addDisplayColumn(displayAutoLinkCategoryColumn); + + if (_sampleType.hasNameAsIdCol()) + { + SimpleDisplayColumn nameIdCol = new SimpleDisplayColumn(); + nameIdCol.setCaption("Has Name Id Column:"); + nameIdCol.setDisplayHtml("true"); + detailsView.getDataRegion().addDisplayColumn(nameIdCol); + } + + if (_sampleType.hasIdColumns()) + { + SimpleDisplayColumn idCols = new SimpleDisplayColumn(); + idCols.setCaption("Id Column(s):"); + String names = _sampleType.getIdCols().stream() + .filter(Objects::nonNull) + .map(DomainProperty::getName) + .collect(Collectors.joining(", ")); + if (!names.isEmpty()) + { + idCols.setDisplayHtml(PageFlowUtil.filter(names)); + detailsView.getDataRegion().addDisplayColumn(idCols); + } + } + + if (_sampleType.getParentCol() != null) + { + SimpleDisplayColumn parentCol = new SimpleDisplayColumn(PageFlowUtil.filter(_sampleType.getParentCol().getName())); + parentCol.setCaption("Parent Column:"); + detailsView.getDataRegion().addDisplayColumn(parentCol); + } + + try + { + SimpleDisplayColumn importAliasCol = new SimpleDisplayColumn(); + importAliasCol.setCaption("Parent Import Alias(es):"); + if (!_sampleType.getImportAliases().isEmpty()) + importAliasCol.setDisplayHtml(PageFlowUtil.filter(StringUtils.join(_sampleType.getImportAliases().keySet(), ", "))); + detailsView.getDataRegion().addDisplayColumn(importAliasCol); + } + catch (IOException e) + { + // unable to parse import alias map from JSON + } + + if (!getContainer().equals(_sampleType.getContainer())) + { + ActionURL definitionURL = urlProvider(ExperimentUrls.class).getShowSampleTypeURL(_sampleType); + SimpleDisplayColumn definedInCol = new SimpleDisplayColumn("" + + PageFlowUtil.filter(_sampleType.getContainer().getPath()) + + ""); + definedInCol.setCaption("Defined In:"); + detailsView.getDataRegion().addDisplayColumn(definedInCol); + } + + // Not all sample types can be edited + DomainKind domainKind = _sampleType.getDomain().getDomainKind(); + if (domainKind != null && domainKind.canEditDefinition(getUser(), _sampleType.getDomain())) + { + if (domainKind instanceof SampleTypeDomainKind) + { + ActionURL updateURL = new ActionURL(EditSampleTypeAction.class, _sampleType.getContainer()); + updateURL.addParameter("RowId", _sampleType.getRowId()); + updateURL.addReturnUrl(getViewContext().getActionURL()); + + if (!getContainer().equals(_sampleType.getContainer())) + { + String editLink = updateURL.toString(); + ActionButton updateButton = new ActionButton("Edit Type"); + updateButton.setActionType(ActionButton.Action.SCRIPT); + updateButton.setScript("if (window.confirm('This sample type is defined in the " + _sampleType.getContainer().getPath() + " folder. Would you still like to edit it?')) { window.location = '" + editLink + "' }"); + detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(updateButton); + } + else + { + ActionButton updateButton = new ActionButton(updateURL, "Edit Type", ActionButton.Action.LINK); + updateButton.setDisplayPermission(DesignSampleTypePermission.class); + updateButton.setPrimary(true); + detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(updateButton); + } + + ActionURL deleteURL = new ActionURL(DeleteSampleTypesAction.class, _sampleType.getContainer()); + deleteURL.addParameter("singleObjectRowId", _sampleType.getRowId()); + deleteURL.addReturnUrl(ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); + ActionButton deleteButton = new ActionButton(deleteURL, "Delete Type", ActionButton.Action.LINK); + deleteButton.setDisplayPermission(DesignSampleTypePermission.class); + detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(deleteButton); + } + else + { + ActionURL editURL = domainKind.urlEditDefinition(_sampleType.getDomain(), new ViewBackgroundInfo(_sampleType.getContainer(), getUser(), getViewContext().getActionURL())); + if (editURL != null) + { + editURL.addReturnUrl(getViewContext().getActionURL()); + ActionButton editTypeButton = new ActionButton(editURL, "Edit Fields"); + editTypeButton.setDisplayPermission(UpdatePermission.class); + detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(editTypeButton); + } + } + } + + if (_sampleType.canImportMoreSamples()) + { + TableInfo table = queryView.getTable(); + if (table != null) + { + ActionURL importURL = table.getImportDataURL(getContainer()); + if (importURL != null) + { + importURL = importURL.clone(); + importURL.addReturnUrl(getViewContext().getActionURL()); + ActionButton uploadButton = new ActionButton(importURL, "Import More Samples", ActionButton.Action.LINK); + uploadButton.setDisplayPermission(UpdatePermission.class); + detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(uploadButton); + } + } + } + + var publish = StudyPublishService.get(); + if (AuditLogService.get().isViewable() && publish != null) + { + ContainerFilter cf = ContainerFilter.Type.CurrentAndSubfolders.create(getContainer(), getUser()); + ActionURL linkToStudyHistoryURL = publish.getPublishHistory(getContainer(), Dataset.PublishSource.SampleType, _sampleType.getRowId(), cf); + ActionButton linkToStudyHistoryButton = new ActionButton(linkToStudyHistoryURL, "Link to Study History", ActionButton.Action.LINK); + linkToStudyHistoryButton.setDisplayPermission(InsertPermission.class); + detailsView.getDataRegion().getButtonBar(DataRegion.MODE_DETAILS).add(linkToStudyHistoryButton); + } + + return new VBox(detailsView, queryView); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("sampleSets"); + ActionURL url = ExperimentUrls.get().getShowSampleTypeListURL(getContainer()); + addRootNavTrail(root); + root.addChild("Sample Types", url); + root.addChild("Sample Type " + _sampleType.getName()); + } + } + + @RequiresPermission(ReadPermission.class) + public class ShowAllMaterialsAction extends SimpleViewAction + { + @Override + public ModelAndView getView(Object o, BindException errors) + { + ExpSchema schema = new ExpSchema(getUser(), getContainer()); + QuerySettings settings = schema.getSettings(getViewContext(), "Materials", ExpSchema.TableType.Materials.toString()); + QueryView view = new QueryView(schema, settings, errors) + { + @Override + protected void populateButtonBar(DataView view, ButtonBar bar) + { + super.populateButtonBar(view, bar); + bar.add(SampleTypeContentsView.getDeriveSamplesButton(getContainer(),null)); + } + }; + view.setShowDetailsColumn(false); + return view; + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("sampleSets"); + addRootNavTrail(root); + root.addChild("All Materials"); + } + } + + /** + * Only shows standard and custom properties, not parent and child samples. Used for indexing + */ + @RequiresPermission(ReadPermission.class) + public class ShowMaterialSimpleAction extends SimpleViewAction + { + protected ExpMaterialImpl _material; + + @Override + public VBox getView(ExpObjectForm form, BindException errors) throws Exception + { + Container c = getContainer(); + _material = ExperimentServiceImpl.get().getExpMaterial(form.getRowId()); + if (_material == null && form.getLsid() != null) + { + _material = ExperimentServiceImpl.get().getExpMaterial(form.getLsid()); + } + if (_material == null) + { + throw new NotFoundException("Could not find a material with RowId " + form.getRowId()); + } + + ensureCorrectContainer(getContainer(), _material, getViewContext()); + + ExpRunImpl run = _material.getRun(); + ExpProtocol sourceProtocol = _material.getSourceProtocol(); + ExpProtocolApplication sourceProtocolApplication = _material.getSourceApplication(); + + DataRegion dr = new DataRegion(); + dr.addColumns(ExperimentServiceImpl.get().getTinfoMaterial().getUserEditableColumns()); + dr.removeColumns("RowId", "RunId", "LastIndexed", "LSID", "SourceApplicationId", "CpasType"); + + //dr.addColumns(extraProps); + dr.addDisplayColumn(new ExperimentRunDisplayColumn(run, "Source Experiment Run")); + dr.addDisplayColumn(new ProtocolDisplayColumn(sourceProtocol, "Source Protocol")); + dr.addDisplayColumn(new ProtocolApplicationDisplayColumn(sourceProtocolApplication, "Source Protocol Application")); + dr.addDisplayColumn(new LineageGraphDisplayColumn(_material, run)); + dr.addDisplayColumn(new SampleTypeDisplayColumn(_material)); + + //TODO: Can't yet edit materials uploaded from a material source + dr.setButtonBar(new ButtonBar()); + DetailsView detailsView = new DetailsView(dr, _material.getRowId()); + detailsView.setTitle("Standard Properties"); + detailsView.setFrame(WebPartView.FrameType.PORTAL); + + CustomPropertiesView cpv = new CustomPropertiesView(_material, c, getUser()); + + return new VBox(new StandardAndCustomPropertiesView(detailsView, cpv)); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("sampleSets"); + addRootNavTrail(root); + root.addChild("Sample Types", ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); + ExpSampleType sampleType = _material.getSampleType(); + if (sampleType != null) + { + root.addChild(sampleType.getName(), ExperimentUrlsImpl.get().getShowSampleTypeURL(sampleType)); + } + root.addChild("Sample " + _material.getName()); + } + } + + @RequiresPermission(ReadPermission.class) + public class ShowMaterialAction extends ShowMaterialSimpleAction + { + @Override + public VBox getView(ExpObjectForm form, BindException errors) throws Exception + { + VBox vbox = super.getView(form, errors); + + List materialsToInvestigate = new ArrayList<>(); + final Set successorRuns = new HashSet<>(); + materialsToInvestigate.add(_material); + Set investigatedMaterials = new HashSet<>(); + do + { + // Query for all the next tier of materials at once - issue 45402 + List followupRuns = ExperimentService.get().getRunsUsingMaterials(materialsToInvestigate); + + // Mark this set as investigated and reset for the next cycle + investigatedMaterials.addAll(materialsToInvestigate); + materialsToInvestigate = new ArrayList<>(); + + for (ExpRun r : followupRuns) + { + // Only expand the material outputs of the run if it's our first time visiting it + if (successorRuns.add(r)) + { + materialsToInvestigate.addAll(r.getMaterialOutputs()); + } + } + + if (successorRuns.size() > 1000) + { + // Give up - there may be a cycle or other problematic data + break; + } + + // Cull the ones we've already looked up + materialsToInvestigate.removeAll(investigatedMaterials); + } + while (!materialsToInvestigate.isEmpty()); + + HtmlStringBuilder updateLinks = HtmlStringBuilder.of(); + ExpSampleType st = _material.getSampleType(); + if (st != null && st.getContainer() != null && st.getContainer().hasPermission(getUser(), UpdatePermission.class)) + { + // XXX: ridiculous amount of work to get a update url expression for the sample type's table. + UserSchema samplesSchema = QueryService.get().getUserSchema(getUser(), st.getContainer(), "Samples"); + QueryDefinition queryDef = samplesSchema.getQueryDefForTable(st.getName()); + StringExpression expr = queryDef.urlExpr(QueryAction.updateQueryRow, null); + if (expr != null) + { + // Since we're building a detailsURL outside the context of a "row" need to set the correct + // container context on the generated expr. + ((DetailsURL) expr).setContainerContext(st.getContainer()); + String url = expr.eval(Collections.singletonMap(new FieldKey(null, "RowId"), _material.getRowId())); + updateLinks.append(LinkBuilder.labkeyLink("edit", url)).append(" "); + } + } + + if (getContainer().hasPermission(getUser(), InsertPermission.class)) + { + ActionURL deriveURL = new ActionURL(DeriveSamplesChooseTargetAction.class, getContainer()); + deriveURL.addParameter("rowIds", _material.getRowId()); + if (st != null) + deriveURL.addParameter("targetSampleTypeId", st.getRowId()); + + updateLinks.append(LinkBuilder.labkeyLink("derive samples from this sample", deriveURL)).append(" "); + } + + vbox.addView(new HtmlView(updateLinks)); + + ExperimentRunListView runListView = ExperimentRunListView.createView(getViewContext(), ExperimentRunType.ALL_RUNS_TYPE, true); + runListView.setShowRecordSelectors(false); + runListView.getRunTable().setRuns(successorRuns); + runListView.getRunTable().setContainerFilter(new ContainerFilter.AllFolders(getUser())); + runListView.setAllowableContainerFilterTypes(ContainerFilter.Type.Current, ContainerFilter.Type.CurrentAndSubfolders, ContainerFilter.Type.AllFolders); + runListView.setTitle("Runs associated with this material or a derived material"); + + ParentChildView pv = new ParentChildView(_material, getViewContext()); + vbox.addView(pv); + vbox.addView(runListView); + + return vbox; + } + } + + + // + // DataClass + // + + @RequiresPermission(ReadPermission.class) + public class ListDataClassAction extends SimpleViewAction + { + @Override + public ModelAndView getView(Object o, BindException errors) + { + DataClassWebPart view = new DataClassWebPart(false, getViewContext(), null); + view.setFrame(WebPartView.FrameType.NONE); + view.setErrorMessage(getViewContext().getRequest().getParameter("errorMessage")); + + return view; + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("dataClass"); + addRootNavTrail(root); + root.addChild("Data Classes"); + } + } + + public static class DataClassForm extends ExpObjectForm + { + private String _name; + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public ExpDataClassImpl getDataClass(@Nullable Container container) + { + ExpDataClassImpl dataClass = null; + + if (getName() != null) + { + dataClass = ExperimentServiceImpl.get().getDataClass(getContainer(), getName(), true); + if (dataClass == null) + throw new NotFoundException("No data class found for name '" + getName() + "'."); + } + else if (getRowId() > 0) + { + dataClass = ExperimentServiceImpl.get().getDataClass(getContainer(), getRowId(), true); + } + + if (dataClass == null) + throw new NotFoundException("No data class found."); + else if (container != null && !container.equals(dataClass.getContainer())) + throw new NotFoundException("Data class is not defined in the given container."); + + return dataClass; + } + } + + @RequiresPermission(ReadPermission.class) + public class ShowDataClassAction extends SimpleViewAction + { + private ExpDataClassImpl _dataClass; + + @Override + public ModelAndView getView(DataClassForm form, BindException errors) + { + _dataClass = form.getDataClass(null); + return new VBox(getDataClassPropertiesView(), getDataClassContentsView(errors)); + } + + private DetailsView getDataClassPropertiesView() + { + ExpSchema expSchema = new ExpSchema(getUser(), _dataClass.getContainer()); + + TableInfo table = ExpSchema.TableType.DataClasses.createTable(expSchema, null, null); + QueryUpdateForm tvf = new QueryUpdateForm(table, getViewContext(), null); + tvf.setPkVal(_dataClass.getRowId()); + DetailsView detailsView = new DetailsView(tvf); + detailsView.setTitle("Data Class Properties"); + + ButtonBar bb = new ButtonBar(); + bb.setStyle(ButtonBar.Style.separateButtons); + boolean inDefinitionContainer = getContainer().equals(_dataClass.getContainer()); + + DomainKind domainKind = _dataClass.getDomain().getDomainKind(); + if (domainKind != null && domainKind.canEditDefinition(getUser(), _dataClass.getDomain())) + { + ActionURL updateURL = new ActionURL(EditDataClassAction.class, _dataClass.getContainer()); + updateURL.addParameter("rowId", _dataClass.getRowId()); + updateURL.addReturnUrl(urlProvider(ExperimentUrls.class).getShowDataClassURL(_dataClass.getContainer(), _dataClass.getRowId())); + + if (inDefinitionContainer) + { + ActionButton updateButton = new ActionButton(updateURL, "Edit Data Class", ActionButton.Action.LINK); + updateButton.setDisplayPermission(DesignDataClassPermission.class); + updateButton.setPrimary(true); + bb.add(updateButton); + } + else if (_dataClass.getContainer().hasPermission(getUser(), DesignDataClassPermission.class)) + { + ActionButton updateButton = new ActionButton("Edit Data Class"); + updateButton.setActionType(ActionButton.Action.SCRIPT); + updateButton.setScript("if (window.confirm('This data class is defined in the " + _dataClass.getContainer().getPath() + " folder. Would you still like to edit it?')) { window.location = '" + updateURL + "' }"); + updateButton.setPrimary(true); + bb.add(updateButton); + } + + ActionURL deleteURL = new ActionURL(DeleteDataClassAction.class, _dataClass.getContainer()); + deleteURL.addParameter("singleObjectRowId", _dataClass.getRowId()); + deleteURL.addReturnUrl(ExperimentUrlsImpl.get().getDataClassListURL(getContainer())); + ActionButton deleteButton = new ActionButton(deleteURL, "Delete Data Class", ActionButton.Action.LINK); + + if (inDefinitionContainer) + { + deleteButton.setDisplayPermission(DesignDataClassPermission.class); + bb.add(deleteButton); + } + else if (_dataClass.getContainer().hasPermission(getUser(), DesignDataClassPermission.class)) + { + bb.add(deleteButton); + } + } + detailsView.getDataRegion().setButtonBar(bb); + + if (!inDefinitionContainer) + { + ActionURL definitionURL = urlProvider(ExperimentUrls.class).getShowDataClassURL(_dataClass.getContainer(), _dataClass.getRowId()); + LinkBuilder link = LinkBuilder.simpleLink(_dataClass.getContainer().getPath(), definitionURL); + SimpleDisplayColumn definedInCol = new SimpleDisplayColumn(link.toString()); + definedInCol.setCaption("Defined In:"); + detailsView.getDataRegion().addDisplayColumn(definedInCol); + } + + return detailsView; + } + + private QueryView getDataClassContentsView(BindException errors) + { + UserSchema dataClassSchema = QueryService.get().getUserSchema(getUser(), getContainer(), ExpSchema.SCHEMA_EXP_DATA); + QuerySettings settings = dataClassSchema.getSettings(getViewContext(), QueryView.DATAREGIONNAME_DEFAULT, _dataClass.getName()); + + return new QueryView(dataClassSchema, settings, errors) + { + @Override + public @NotNull LinkedHashSet getClientDependencies() + { + LinkedHashSet resources = super.getClientDependencies(); + resources.add(ClientDependency.fromPath("Ext4")); + resources.add(ClientDependency.fromPath("dataregion/confirmDelete.js")); + return resources; + } + + @Override + public ActionButton createDeleteButton() + { + ActionButton button = super.createDeleteButton(); + if (button != null) + { + String dependencyText = ExperimentService.get() + .getObjectReferencers() + .stream() + .map(referencer -> referencer.getObjectReferenceDescription(ExpData.class)) + .collect(Collectors.joining(" or ")); + + button.setScript("LABKEY.dataregion.confirmDelete(" + + PageFlowUtil.jsString(getDataRegionName()) + ", " + + PageFlowUtil.jsString(ExpSchema.SCHEMA_EXP_DATA.toString()) + ", " + + PageFlowUtil.jsString(getQueryDef().getName()) + ", " + + "'experiment', 'getDataOperationConfirmationData.api', " + + PageFlowUtil.jsString(getSelectionKey()) + ", " + + "'data object', 'data objects', '" + dependencyText + "', {dataOperation: 'Delete'})"); + button.setRequiresSelection(true); + } + return button; + } + }; + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("dataClass"); + addRootNavTrail(root); + root.addChild("Data Classes", ExperimentUrlsImpl.get().getDataClassListURL(getContainer())); + root.addChild(_dataClass.getName()); + } + } + + @RequiresPermission(DesignDataClassPermission.class) + public class DeleteDataClassAction extends AbstractDeleteAction + { + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("dataClass"); + super.addNavTrail(root); + } + + @Override + protected void deleteObjects(DeleteForm deleteForm) + { + List dataClasses = getDataClasses(deleteForm); + if (!ensureCorrectContainer(dataClasses)) + { + throw new UnauthorizedException(); + } + for (ExpRun run : getRuns(dataClasses)) + { + if (!run.getContainer().hasPermission(getUser(), DeletePermission.class)) + { + throw new UnauthorizedException(); + } + } + for (ExpDataClass dataClass : dataClasses) + { + dataClass.delete(getUser(), deleteForm.getUserComment()); + } + } + + @Override + public ModelAndView getView(DeleteForm deleteForm, boolean reshow, BindException errors) + { + List dataClasses = getDataClasses(deleteForm); + + if (!ensureCorrectContainer(dataClasses)) + { + throw new RedirectException(ExperimentUrlsImpl.get().getDataClassListURL(getContainer(), "To delete a data class, you must be in its folder or project.")); + } + + return new ConfirmDeleteView("Data Class", ShowDataClassAction.class, dataClasses, deleteForm, getRuns(dataClasses)); + } + + private List getDataClasses(DeleteForm deleteForm) + { + List dataClasses = new ArrayList<>(); + for (long rowId : deleteForm.getIds(false)) + { + ExpDataClass dataClass = ExperimentServiceImpl.get().getDataClass(getContainer(), rowId, true); + if (dataClass != null) + { + dataClasses.add(dataClass); + } + } + return dataClasses; + } + + private boolean ensureCorrectContainer(List dataClasses) + { + for (ExpDataClass dataClass : dataClasses) + { + Container sourceContainer = dataClass.getContainer(); + if (!sourceContainer.equals(getContainer())) + { + return false; + } + } + return true; + } + + private List getRuns(List dataClasses) + { + if (!dataClasses.isEmpty()) + { + List runArray = ExperimentService.get().getRunsUsingDataClasses(dataClasses); + return ExperimentService.get().runsDeletedWithInput(runArray); + } + else + { + return Collections.emptyList(); + } + } + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(ReadPermission.class) + public static class GetDataClassPropertiesAction extends ReadOnlyApiAction + { + @Override + public Object execute(DataClassForm form, BindException errors) throws Exception + { + ExpDataClass dataClass = form.getDataClass(getContainer()); + if (dataClass != null) + return new DataClassDomainKindProperties(dataClass); + else + throw new NotFoundException("Data class does not exist in this container for rowId " + form.getRowId() + "."); + } + } + + @RequiresPermission(DesignDataClassPermission.class) + public static class EditDataClassAction extends SimpleViewAction + { + private ExpDataClassImpl _dataClass; + + @Override + public ModelAndView getView(DataClassForm form, BindException errors) + { + boolean create = form.getLSID() == null && form.getRowId() == 0 && form.getName() == null; + if (!create) + _dataClass = form.getDataClass(getContainer()); + + return ModuleHtmlView.get(ModuleLoader.getInstance().getModule("core"), ModuleHtmlView.getGeneratedViewPath("dataClassDesigner")); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("dataClass"); + + root.addChild("Data Classes", ExperimentUrlsImpl.get().getDataClassListURL(getContainer())); + if (_dataClass == null) + { + root.addChild("Create Data Class"); + } + else + { + root.addChild(_dataClass.getName(), ExperimentUrlsImpl.get().getShowDataClassURL(getContainer(), _dataClass.getRowId())); + root.addChild("Update Data Class"); + } + } + } + + @RequiresPermission(DesignDataClassPermission.class) + public static class CreateDataClassFromTemplateAction extends FormViewAction + { + private ActionURL _successUrl; + private Map _domainTemplates; + + @Override + public void validateCommand(CreateDataClassFromTemplateForm form, Errors errors) + { + String name = null; + _domainTemplates = DomainTemplateGroup.getAllTemplates(getContainer()); + + if (!_domainTemplates.containsKey(form.getDomainTemplate())) + { + errors.reject(ERROR_MSG, "Unknown template selected: " + form.getDomainTemplate()); + } + else + { + DomainTemplate template = _domainTemplates.get(form.getDomainTemplate()); + name = template.getTemplateName(); + + // Issue 40230: if template includes sample type option, verify that it exists + if (template.getOptions().containsKey("sampleSet")) + { + String sampleTypeName = template.getOptions().get("sampleSet").toString(); + ExpSampleType sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), sampleTypeName, true); + if (sampleType == null) + errors.reject(ERROR_MSG, "Unable to find a sample type in this container with name: " + sampleTypeName + "."); + } + } + + if (StringUtils.isBlank(name)) + errors.reject(ERROR_MSG, "DataClass template selection is required."); + else if (ExperimentService.get().getDataClass(getContainer(), name, true) != null) + errors.reject(ERROR_MSG, "DataClass '" + name + "' already exists."); + + } + + @Override + public ModelAndView getView(CreateDataClassFromTemplateForm form, boolean reshow, BindException errors) + { + Set templates = DomainTemplateGroup.getTemplatesForDomainKind(getContainer(), DataClassDomainKind.NAME); + form.setAvailableDomainTemplateNames(templates); + + Set messages = new HashSet<>(); + Map groups = DomainTemplateGroup.getAllGroups(getContainer()); + for (DomainTemplateGroup g : groups.values()) + messages.addAll(g.getErrors()); + form.setXmlParseErrors(messages); + + return new JspView<>("/org/labkey/experiment/createDataClassFromTemplate.jsp", form, errors); + } + + @Override + public boolean handlePost(CreateDataClassFromTemplateForm form, BindException errors) throws Exception + { + DomainTemplate template = _domainTemplates.get(form.getDomainTemplate()); + Domain domain = DomainUtil.createDomain(template, getContainer(), getUser(), form.getName()); + + _successUrl = domain.getDomainKind().urlEditDefinition(domain, getViewContext()); + return true; + } + + @Override + public URLHelper getSuccessURL(CreateDataClassFromTemplateForm form) + { + return _successUrl; + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("dataClass"); + root.addChild("Create Data Class from Template"); + } + } + + public static class CreateDataClassFromTemplateForm extends DataClass + { + private String _domainTemplate; + private Set _availableDomainTemplateNames; + private Set _xmlParseErrors; + private final ReturnUrlForm _returnUrlForm = new ReturnUrlForm(); + + public String getDomainTemplate() + { + return _domainTemplate; + } + + public void setDomainTemplate(String domainTemplate) + { + _domainTemplate = domainTemplate; + } + + public Set getAvailableDomainTemplateNames() + { + return _availableDomainTemplateNames; + } + + public void setAvailableDomainTemplateNames(Set availableDomainTemplateNames) + { + _availableDomainTemplateNames = availableDomainTemplateNames; + } + + public Set getXmlParseErrors() + { + return _xmlParseErrors; + } + + public void setXmlParseErrors(Set xmlParseErrors) + { + _xmlParseErrors = xmlParseErrors; + } + + @Nullable + public String getReturnUrl() + { + return _returnUrlForm.getReturnUrl(); + } + + public void setReturnUrl(String s) + { + _returnUrlForm.setReturnUrl(s); + } + } + + public static class ConceptURIForm + { + private String _conceptURI; + + public String getConceptURI() + { + return _conceptURI; + } + + public void setConceptURI(String conceptURI) + { + _conceptURI = conceptURI; + } + } + + @RequiresPermission(AdminPermission.class) + public static class RemoveConceptMappingAction extends MutatingApiAction + { + @Override + public void validateForm(ConceptURIForm form, Errors errors) + { + if (form.getConceptURI() == null || ConceptURIProperties.getLookup(getContainer(), form.getConceptURI()) == null) + errors.reject(ERROR_MSG, "Concept URI not found: " + form.getConceptURI()); + } + + @Override + public Object execute(ConceptURIForm form, BindException errors) + { + ConceptURIProperties.removeLookup(getContainer(), form.getConceptURI()); + return new ApiSimpleResponse("success", true); + } + } + + @RequiresPermission(ReadPermission.class) + public static class RunAttachmentDownloadAction extends BaseDownloadAction + { + @Nullable + @Override + public Pair getAttachment(AttachmentForm form) + { + if (form.getLsid() == null || form.getName() == null) + throw new NotFoundException("Error: missing required param 'lsid' or 'name'."); + + ExpRun run = ExperimentService.get().getExpRun(form.getLsid()); + if (run == null) + throw new NotFoundException("Run not found: " + form.getLsid()); + + if (!run.getContainer().equals(getContainer())) + { + if (run.getContainer().hasPermission(getUser(), ReadPermission.class)) + throw new RedirectException(getViewContext().cloneActionURL().setContainer(run.getContainer())); + else + throw new NotFoundException("Run not found"); + } + + AttachmentParent parent = new ExpRunAttachmentParent(run); + return new Pair<>(parent, form.getName()); + } + } + + @RequiresPermission(ReadPermission.class) + public static class DataClassAttachmentDownloadAction extends BaseDownloadAction + { + @Nullable + @Override + public Pair getAttachment(AttachmentForm form) + { + if (form.getLsid() == null || form.getName() == null) + throw new NotFoundException("Error: missing required param 'lsid' or 'name'."); + + Lsid lsid = new Lsid(form.getLsid()); + ExpData data = ExperimentServiceImpl.get().getExpData(lsid.toString()); + if (data == null) + throw new NotFoundException("Error: Data object not found for the given LSID: " + lsid); + AttachmentParent parent = new ExpDataClassAttachmentParent(data.getContainer(), lsid); + + return new Pair<>(parent, form.getName()); + } + } + + public static class AttachmentForm extends LsidForm implements BaseDownloadAction.InlineDownloader + { + private String _name; + private boolean _inline = true; + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + @Override + public boolean isInline() + { + return _inline; + } + + public void setInline(boolean inline) + { + _inline = inline; + } + } + + // + // END DataClass actions + // + + public static ActionURL getRunGraphURL(Container c, long runId) + { + return new ActionURL(ShowRunGraphAction.class, c).addParameter("rowId", runId); + } + + @RequiresPermission(ReadPermission.class) + public class ShowRunGraphAction extends AbstractShowRunAction + { + @Override + protected VBox createLowerView(ExpRunImpl experimentRun, BindException errors) + { + return new VBox( + createRunViewTabs(experimentRun, false, true, true), + new ExperimentRunGraphView(experimentRun, false) + ); + } + } + + private abstract class AbstractShowRunAction extends SimpleViewAction + { + private ExpRunImpl _experimentRun; + + @Override + public ModelAndView getView(ExperimentRunForm form, BindException errors) + { + _experimentRun = (ExpRunImpl) form.lookupRun(); + ensureCorrectContainer(getContainer(), _experimentRun, getViewContext()); + + VBox vbox = new VBox(); + + JspView detailsView = new JspView<>("/org/labkey/experiment/ExperimentRunDetails.jsp", _experimentRun); + detailsView.setTitle("Standard Properties"); + + var attachmentParent = new ExpRunAttachmentParent(_experimentRun); + var attachments = AttachmentService.get().getAttachments(attachmentParent) + .stream() + .map(att -> Pair.of(att.getName(), new ActionURL(RunAttachmentDownloadAction.class, _experimentRun.getContainer()).addParameter("name", att.getName()).addParameter("lsid", _experimentRun.getLSID()))) + .collect(toList()); + CustomPropertiesView cpv = new CustomPropertiesView(_experimentRun.getLSID(), getContainer(), attachments); + + vbox.addView(new StandardAndCustomPropertiesView(detailsView, cpv)); + + HtmlStringBuilder updateLinks = HtmlStringBuilder.of(); + List runEditors = ExperimentService.get().getRunEditors(); + for (ExpRunEditor editor : runEditors) + { + if (editor.isProtocolEditor(form.lookupRun().getProtocol())) + { + updateLinks.append(LinkBuilder.labkeyLink("edit " + editor.getDisplayName() + " run", editor.getEditUrl(getContainer()).addParameter("rowId", form.getRowId()))); + } + } + + if (!updateLinks.isEmpty()) + { + HtmlView view = new HtmlView(updateLinks); + vbox.addView(view); + } + + VBox lowerView = createLowerView(_experimentRun, errors); + lowerView.setFrame(WebPartView.FrameType.PORTAL); + lowerView.setTitle("Run Details"); + NavTree tree = new NavTree(""); + File runRoot = _experimentRun.getFilePathRoot(); + if (NetworkDrive.exists(runRoot)) + { + if (!runRoot.isDirectory()) + { + runRoot = runRoot.getParentFile(); + } + PipeRoot pipelineRoot = PipelineService.get().findPipelineRoot(_experimentRun.getContainer()); + if (pipelineRoot != null) + { + if (pipelineRoot.isUnderRoot(runRoot)) + { + String path = pipelineRoot.relativePath(runRoot); + tree.addChild("View Files", urlProvider(PipelineUrls.class).urlBrowse(_experimentRun.getContainer(), null, path)); + } + } + } + + final String exportFilesFormId = "exportFilesForm"; + NavTree downloadFiles = new NavTree("Download all files"); + downloadFiles.setScript("document.getElementById('" + exportFilesFormId + "').submit();"); + tree.addChild(downloadFiles); + + // CONSIDER: Show modal dialog using ExperimentService.get().createRunExportView() + NavTree exportXarFiles = new NavTree("Export XAR"); + exportXarFiles.setScript("LABKEY.Experiment.exportRuns({runIds: [" + _experimentRun.getRowId() + "] });"); + tree.addChild(exportXarFiles); + + lowerView.setNavMenu(tree); + lowerView.setIsWebPart(false); + + vbox.addView(lowerView); + vbox.addView(new ExperimentRunGroupsView(getUser(), getContainer(), _experimentRun, getViewContext().getActionURL(), errors)); + + DOM.Renderable exportFilesForm = LK.FORM(at( + id, exportFilesFormId, + method, "POST", + action, new ActionURL(ExportRunFilesAction.class, _experimentRun.getContainer())), + INPUT(at(type, "hidden", + name, DataRegionSelection.DATA_REGION_SELECTION_KEY, + value, "ExportSingleRun")), + INPUT(at(type, "hidden", + name, DataRegion.SELECT_CHECKBOX_NAME, + value, _experimentRun.getRowId())), + INPUT(at(type, "hidden", + name, "zipFileName", + value, _experimentRun.getName() + ".zip"))); + + HtmlView hiddenFormView = new HtmlView(exportFilesForm); + vbox.addView(hiddenFormView); + + return vbox; + } + + protected abstract VBox createLowerView(ExpRunImpl experimentRun, BindException errors); + + @Override + public void addNavTrail(NavTree root) + { + root.addChild(_experimentRun.getName()); + } + } + + public static class ToggleRunExperimentMembershipForm + { + private int _runId; + private int _experimentId; + private boolean _included; + + public int getRunId() + { + return _runId; + } + + public void setRunId(int runId) + { + _runId = runId; + } + + public int getExperimentId() + { + return _experimentId; + } + + public void setExperimentId(int experimentId) + { + _experimentId = experimentId; + } + + public boolean isIncluded() + { + return _included; + } + + public void setIncluded(boolean included) + { + _included = included; + } + } + + @RequiresPermission(UpdatePermission.class) + public static class ToggleRunExperimentMembershipAction extends FormHandlerAction + { + @Override + public boolean handlePost(ToggleRunExperimentMembershipForm form, BindException errors) + { + ExpRun run = ExperimentService.get().getExpRun(form.getRunId()); + // Check if the user has permission to update this run + if (run == null || !run.getContainer().hasPermission(getUser(), UpdatePermission.class)) + { + throw new NotFoundException(); + } + + ExpExperiment exp = ExperimentService.get().getExpExperiment(form.getExperimentId()); + if (exp == null) + { + throw new NotFoundException(); + } + // Check if this + if (!ExperimentService.get().getExperiments(run.getContainer(), getUser(), true, false).contains(exp)) + { + throw new NotFoundException(); + } + // Users must have permission to view, but not necessarily update, the container the holds the run group + if (!exp.getContainer().hasPermission(getUser(), ReadPermission.class)) + { + throw new UnauthorizedException(); + } + + if (form.isIncluded()) + { + exp.addRuns(getUser(), run); + } + else + { + exp.removeRun(getUser(), run); + } + + return true; + } + + @Override + public URLHelper getSuccessURL(ToggleRunExperimentMembershipForm form) + { + return null; + } + + @Override + public void validateCommand(ToggleRunExperimentMembershipForm target, Errors errors) + { + } + } + + private HtmlView createRunViewTabs(ExpRun expRun, boolean showGraphSummary, boolean showGraphDetail, boolean showText) + { + return new HtmlView( + TABLE(cl("labkey-tab-strip"), + TR( + createTabSpacer(false), + createTab("Graph Summary View", ExperimentUrlsImpl.get().getRunGraphURL(expRun), !showGraphSummary), + createTabSpacer(false), + createTab("Graph Detail View", ExperimentUrlsImpl.get().getRunGraphDetailURL(expRun), !showGraphDetail), + createTabSpacer(false), + createTab("Text View", ExperimentUrlsImpl.get().getRunTextURL(expRun), !showText), + createTabSpacer(true)))); + } + + private DOM.Renderable createTab(String text, ActionURL url, boolean selected) + { + return TD(cl(selected,"labkey-tab-selected", "labkey-tab"), + A(at(href, url), text)); + } + + private DOM.Renderable createTabSpacer(boolean fullWidth) + { + return TD(cl("labkey-tab-space").at(fullWidth, width, "100%"), + IMG(at(src, AppProps.getInstance().getContextPath() + "/_.gif", width, "5"))); + } + + @RequiresPermission(ReadPermission.class) + public class ShowRunTextAction extends AbstractShowRunAction + { + @Override + protected VBox createLowerView(ExpRunImpl expRun, BindException errors) + { + JspView applicationsView = new JspView<>("/org/labkey/experiment/ProtocolApplications.jsp", expRun); + applicationsView.setFrame(WebPartView.FrameType.TITLE); + applicationsView.setTitle("Protocol Applications"); + + HtmlView toggleView = createRunViewTabs(expRun, true, true, false); + + QuerySettings runDataInputsSettings = new QuerySettings(getViewContext(), "RunDataInputs", DataInputs.name()); + UsageQueryView runDataInputsView = new UsageQueryView("Data Inputs", getViewContext(), expRun, ExpProtocol.ApplicationType.ExperimentRun, runDataInputsSettings, errors); + runDataInputsView.setButtonBarPosition(DataRegion.ButtonBarPosition.TOP); + + QuerySettings runDataOutputsSettings = new QuerySettings(getViewContext(), "RunDataOutputs", DataInputs.name()); + UsageQueryView runDataOutputsView = new UsageQueryView("Data Outputs", getViewContext(), expRun, ExpProtocol.ApplicationType.ExperimentRunOutput, runDataOutputsSettings, errors); + runDataOutputsView.setButtonBarPosition(DataRegion.ButtonBarPosition.NONE); + + QuerySettings runMaterialInputsSetting = new QuerySettings(getViewContext(), "RunMaterialInputs", ExpSchema.TableType.MaterialInputs.name()); + UsageQueryView runMaterialInputsView = new UsageQueryView("Material Inputs", getViewContext(), expRun, ExpProtocol.ApplicationType.ExperimentRun, runMaterialInputsSetting, errors); + runMaterialInputsView.setButtonBarPosition(DataRegion.ButtonBarPosition.TOP); + + QuerySettings runMaterialOutputsSettings = new QuerySettings(getViewContext(), "RunMaterialOutputs", ExpSchema.TableType.MaterialInputs.name()); + UsageQueryView runMaterialOutputsView = new UsageQueryView("Material Outputs", getViewContext(), expRun, ExpProtocol.ApplicationType.ExperimentRunOutput, runMaterialOutputsSettings, errors); + runMaterialOutputsView.setButtonBarPosition(DataRegion.ButtonBarPosition.NONE); + + HBox inputsView = new HBox(runDataInputsView, runMaterialInputsView); + HBox registeredInputsView = new HBox(); + + var expService = ExperimentService.get(); + expService.getRunInputsViewProviders().forEach(provider -> + { + var queryView = provider.createView(getViewContext(), expRun, errors); + if (queryView != null) + { + registeredInputsView.addView(queryView); + } + }); + HBox outputsView = new HBox(runDataOutputsView, runMaterialOutputsView); + HBox registeredOutputsView = new HBox(); + expService.getRunOutputsViewProviders().forEach(provider -> + { + var queryView = provider.createView(getViewContext(), expRun, errors); + if (queryView != null) + { + registeredOutputsView.addView(queryView); + } + }); + + var vBox = new VBox(); + vBox.addView(toggleView); + vBox.addView(inputsView); + if (!registeredInputsView.isEmpty()) + vBox.addView(registeredInputsView); + vBox.addView(outputsView); + if (!registeredOutputsView.isEmpty()) + vBox.addView(registeredOutputsView); + vBox.addView(applicationsView); + + return vBox; + } + } + + private static class UsageQueryView extends QueryView + { + private final ExpRun _run; + private final ExpProtocol.ApplicationType _type; + + public UsageQueryView(String title, ViewContext context, ExpRun run, ExpProtocol.ApplicationType type, + QuerySettings settings, BindException errors) + { + super(new ExpSchema(context.getUser(), context.getContainer()), settings, errors); + setTitle(title); + setFrame(FrameType.TITLE); + _run = run; + _type = type; + setShowBorders(true); + setShadeAlternatingRows(true); + setShowExportButtons(false); + setShowPagination(false); + disableContainerFilterSelection(); + } + + @Override + protected TableInfo createTable() + { + String tableName = getSettings().getQueryName(); + ExpInputTable tableInfo = (ExpInputTable) getSchema().getTable(tableName, new ContainerFilter.AllFolders(getUser()), true, true); + tableInfo.setRun(_run, _type); + tableInfo.setLocked(true); + return tableInfo; + } + } + + + public static ActionURL getShowRunGraphDetailURL(Container c, long rowId) + { + ActionURL url = new ActionURL(ShowRunGraphDetailAction.class, c); + url.addParameter("rowId", rowId); + return url; + } + + + @RequiresPermission(ReadPermission.class) + public class ShowRunGraphDetailAction extends AbstractShowRunAction + { + @Override + protected VBox createLowerView(ExpRunImpl run, BindException errors) + { + ExperimentRunGraphView gw = new ExperimentRunGraphView(run, true); + if (null != getViewContext().getActionURL().getParameter("focus")) + gw.setFocus(getViewContext().getActionURL().getParameter("focus")); + if (null != getViewContext().getActionURL().getParameter("focusType")) + gw.setFocusType(getViewContext().getActionURL().getParameter("focusType")); + return new VBox(createRunViewTabs(run, true, false, true), gw); + } + } + + private abstract class AbstractDataAction extends SimpleViewAction + { + protected ExpDataImpl _data; + + @Override + public final ModelAndView getView(DataForm form, BindException errors) throws Exception + { + _data = form.lookupData(); + if (_data == null) + { + throw new NotFoundException("Could not find a data with RowId " + form.getRowId()); + } + + ensureCorrectContainer(getContainer(), _data, getViewContext()); + return getDataView(form, errors); + } + + protected abstract ModelAndView getDataView(DataForm form, BindException errors) throws Exception; + + @Override + public void addNavTrail(NavTree root) + { + addRootNavTrail(root); + root.addChild("Data " + _data.getName()); + } + } + + @RequiresPermission(ReadPermission.class) + public class ShowDataAction extends AbstractDataAction + { + @Override + public ModelAndView getDataView(DataForm form, BindException errors) + { + ExpRun run = _data.getRun(); + ExpProtocol sourceProtocol = _data.getSourceProtocol(); + ExpProtocolApplication sourceProtocolApplication = _data.getSourceApplication(); + ExpDataClass dataClass = _data.getDataClass(getUser()); + + ExpSchema schema = new ExpSchema(getUser(), getContainer()); + TableInfo table; + long pk; + if (dataClass == null) + { + table = schema.getDatasTable(); + pk = _data.getRowId(); + } + else + { + table = schema.getSchema(ExpSchema.NestedSchemas.data).getTable(dataClass.getName()); + pk = new TableSelector(table, Collections.singleton("rowId"), new SimpleFilter(FieldKey.fromParts("lsid"), _data.getLSID()), null).getObject(Integer.class); + } + + DataRegion dr = new DataRegion(); + dr.setTable(table); + List cols = table.getColumns().stream().filter(ColumnInfo::isShownInDetailsView).collect(toList()); + dr.addColumns(cols); + dr.removeColumns("RowId", "Created", "CreatedBy", "Modified", "ModifiedBy", "DataFileUrl", "Run", "LSID", "CpasType", "SourceApplicationId", "Folder", "Generated"); + dr.addDisplayColumn(new ExperimentRunDisplayColumn(run, "Source Experiment Run")); + dr.addDisplayColumn(new ProtocolDisplayColumn(sourceProtocol, "Source Protocol")); + dr.addDisplayColumn(new ProtocolApplicationDisplayColumn(sourceProtocolApplication, "Source Protocol Application")); + dr.addDisplayColumn(new LineageGraphDisplayColumn(_data, run)); + DetailsView detailsView = new DetailsView(dr, pk); + detailsView.setTitle("Standard Properties"); + detailsView.setFrame(WebPartView.FrameType.PORTAL); + ButtonBar bb = new ButtonBar(); + bb.setStyle(ButtonBar.Style.separateButtons); + + ExperimentDataHandler handler = _data.findDataHandler(); + ActionURL viewDataURL = handler == null ? null : handler.getContentURL(_data); + if (viewDataURL != null) + { + bb.add(new ActionButton("View data", viewDataURL)); + } + + if (_data.isPathAccessible()) + { + bb.add(new ActionButton("View file", ExperimentUrlsImpl.get().getShowFileURL(_data, true))); + bb.add(new ActionButton("Download file", ExperimentUrlsImpl.get().getShowFileURL(_data, false))); + + if (getContainer().hasPermission(getUser(), InsertPermission.class)) + { + String relativePath = null; + PipeRoot root = PipelineService.get().findPipelineRoot(getContainer()); + if (root != null) + { + Path rootFile = root.getRootNioPath(); + Path dataFile = _data.getFilePath(); + if (dataFile != null) + { + Path pathRelative; + try + { + pathRelative = rootFile.relativize(dataFile); + if (null != pathRelative) + relativePath = pathRelative.toString(); + } + catch (IllegalArgumentException e) + { + // dataFile not relative to root + } + } + } + ActionURL browseURL = urlProvider(PipelineUrls.class).urlBrowse(getContainer(), getViewContext().getActionURL(), relativePath); + bb.add(new ActionButton("Browse in pipeline", browseURL)); + } + } + + // add links to any other exp.data that share the same dataFileUrl path + var altDataList = ExperimentService.get().getAllExpDataByURL(_data.getDataFileUrl(), getContainer()); + altDataList.removeIf(_data::equals); + if (!altDataList.isEmpty()) + { + MenuButton menu = new MenuButton("Alternate Data"); + for (ExpData altData : altDataList) + { + ExpRun altDataRun = altData.getRun(); + StringBuilder sb = new StringBuilder(altData.getName()); + if (altDataRun != null) + sb.append(" created by run '").append(altDataRun.getName()).append("' (").append(altDataRun.getProtocol().getName()).append(")"); + menu.addMenuItem(sb.toString(), altData.detailsURL()); + } + bb.add(menu); + } + + dr.setButtonBarPosition(DataRegion.ButtonBarPosition.TOP); + dr.setButtonBar(bb); + + CustomPropertiesView cpv = new CustomPropertiesView(_data.getLSID(), getContainer()); + HBox hbox = new StandardAndCustomPropertiesView(detailsView, cpv); + + VBox vbox = new VBox(hbox); + + ParentChildView pv = new ParentChildView(_data, getViewContext()); + vbox.addView(pv); + + ExperimentRunListView runListView = ExperimentRunListView.createView(getViewContext(), ExperimentRunType.ALL_RUNS_TYPE, true); + runListView.getRunTable().setInputData(_data); + runListView.getRunTable().setContainerFilter(new ContainerFilter.AllFolders(getUser())); + runListView.getRunTable().setLocked(true); + runListView.setTitle("Runs using this data as an input"); + vbox.addView(runListView); + + if (_data.isInlineImage() && _data.isFileOnDisk()) + { + ActionURL showFileURL = new ActionURL(ShowFileAction.class, getContainer()).addParameter("rowId", _data.getRowId()); + HtmlView imageView = new HtmlView(IMG(at(src, showFileURL))); + return new VBox(vbox, imageView); + } + return vbox; + } + } + + @RequiresPermission(AdminPermission.class) + public static class CheckDataFileAction extends MutatingApiAction + { + private ExpDataImpl _data; + + @Override + public void validateForm(DataFileForm form, Errors errors) + { + _data = form.lookupData(); + if (_data == null) + { + errors.reject(ERROR_MSG, "No ExpData found for id: " + form.getRowId()); + } + } + + @Override + public ApiResponse execute(DataFileForm form, BindException errors) + { + File dataFile = _data.getFile(); + Container dataContainer = _data.getContainer(); + boolean fileExists = _data.isFileOnDisk(); + boolean fileExistsAtCurrent = false; + File newDataFile = null; + + ApiSimpleResponse response = new ApiSimpleResponse(); + response.put("dataFileUrl", _data.getDataFileUrl()); + response.put("fileExists", fileExists); + response.put("containerPath", dataContainer.getPath()); + + if (!fileExists) + { + PipeRoot pipelineRoot = PipelineService.get().findPipelineRoot(dataContainer); + if (pipelineRoot != null && pipelineRoot.isValid() && dataFile != null) + { + newDataFile = pipelineRoot.resolvePath("/" + AssayFileWriter.DIR_NAME + "/" + dataFile.getName()); + fileExistsAtCurrent = NetworkDrive.exists(newDataFile); + response.put("fileExistsAtCurrent", fileExistsAtCurrent); + } + } + + // if the current dataFileUrl does not exist on disk and we have the file at the current + // pipeline root /assaydata dir, fix the dataFileUrl value + if (form.isAttemptFilePathFix()) + { + if (fileExistsAtCurrent) + { + ExpDataFileListener fileListener = new ExpDataFileListener(); + fileListener.fileMoved(dataFile, newDataFile, getUser(), dataContainer); + response.put("filePathFixed", true); + + // update the ExpData object so that we can get the new dataFileUrl + _data = form.lookupData(); + response.put("newDataFileUrl", _data.getDataFileUrl()); + } + else + { + response.put("filePathFixed", false); + } + } + + response.put("success", true); + return response; + } + } + + public static class DataFileForm extends DataForm + { + private boolean _attemptFilePathFix; + + public boolean isAttemptFilePathFix() + { + return _attemptFilePathFix; + } + + public void setAttemptFilePathFix(boolean attemptFilePathFix) + { + _attemptFilePathFix = attemptFilePathFix; + } + } + + @RequiresPermission(ReadPermission.class) + public class ShowFileAction extends AbstractDataAction + { + @Override + protected ModelAndView getDataView(DataForm form, BindException errors) throws IOException + { + if (!_data.isPathAccessible()) + { + throw new NotFoundException("Data file " + _data.getDataFileUrl() + " does not exist on disk"); + } + + PipeRoot root = PipelineService.get().findPipelineRoot(getContainer()); + if (root != null && !root.isUnderRoot(_data.getFileLike())) + { + // Issue 35649: ImmPort module "publish" creates exp.data object in this container for paths that originate in a different container + FileContentService fileSvc = FileContentService.get(); + if (fileSvc == null) + throw new UnauthorizedException("Data file is not under the pipeline root for this folder"); + + List containers = fileSvc.getContainersForFilePath(_data.getFilePath()); + if (containers.isEmpty() || containers.stream().noneMatch(c -> c.hasPermission(getUser(), ReadPermission.class))) + throw new UnauthorizedException("Data file is not under the pipeline root for this folder"); + } + + //Issues 25667 and 31152 + if (form.isInline()) + { + ExperimentDataHandler h = _data.findDataHandler(); + if (h != null) + { + URLHelper url = h.getShowFileURL(_data); + if (url != null) + { + throw new RedirectException(url); + } + } + } + + try + { + Path realContent = _data.getFilePath(); + if (null == realContent) + throw new IllegalStateException("Path not found."); + + boolean inline = _data.isInlineImage() || form.isInline() || "inlineImage".equalsIgnoreCase(form.getFormat()); + if (_data.isInlineImage() && form.getMaxDimension() != null) + { + try (InputStream inputStream = Files.newInputStream(realContent)) + { + BufferedImage image = ImageIO.read(inputStream); + // If image, create a thumbnail, otherwise fall through as a regular download attempt + if (image != null) + { + int imageMax = Math.max(image.getHeight(), image.getWidth()); + if (imageMax > form.getMaxDimension().intValue()) + { + double scale = (double) form.getMaxDimension().intValue() / (double) imageMax; + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + ImageUtil.resizeImage(image, bOut, scale, 1); + PageFlowUtil.streamFileBytes(getViewContext().getResponse(), FileUtil.getFileName(realContent) + ".png", bOut.toByteArray(), !inline); + return null; + } + } + } + } + + boolean extended = "jsonTSVExtended".equalsIgnoreCase(form.getFormat()); + boolean ignoreTypes = "jsonTSVIgnoreTypes".equalsIgnoreCase(form.getFormat()); + if ("jsonTSV".equalsIgnoreCase(form.getFormat()) || extended || ignoreTypes) + { + if (!FileUtil.hasCloudScheme(realContent)) // TODO: handle streaming from S3 to JSON + streamToJSON(FileSystemLike.wrapFile(realContent), form.getFormat(), -1, null); + return null; + } + + try (InputStream inputStream = Files.newInputStream(realContent)) + { + PageFlowUtil.streamFile(getViewContext().getResponse(), Collections.emptyMap(), FileUtil.getFileName(realContent), inputStream, !inline); + } + } + catch (IOException e) + { + try + { + // Try to write the exception back to the caller if we haven't already flushed the buffer + ApiJsonWriter writer = new ApiJsonWriter(getViewContext().getResponse()); + writer.writeResponse(e); + } + catch (IllegalStateException ise) + { + // Most likely that a disconnected client caused the IOException writing back the response + } + } + + return null; + } + } + + + public static class ParseForm + { + String format = "jsonTSV"; + int maxRows = -1; + + public String getFormat() + { + return format; + } + + public void setFormat(String format) + { + this.format = format; + } + + public int getMaxRows() + { + return maxRows; + } + + public void setMaxRows(int maxRow) + { + this.maxRows = maxRow; + } + } + + @RequiresNoPermission + public class ParseFileAction extends MutatingApiAction + { + @Override + public Object execute(ParseForm form, BindException errors) throws Exception + { + if (!(getViewContext().getRequest() instanceof MultipartHttpServletRequest)) + throw new BadRequestException("Expected MultipartHttpServletRequest when posting files."); + + MultipartFile formFile = getFileMap().get("file"); + if (formFile == null) + { + return true; + } + + FileLike tempFile = null; + try + { + tempFile = FileUtil.createTempFileLike("parse", formFile.getOriginalFilename()); + FileUtil.copyData(formFile.getInputStream(), tempFile.openOutputStream()); + streamToJSON(tempFile, form.getFormat(), form.getMaxRows(), formFile.getOriginalFilename()); + } + finally + { + if (null != tempFile) + tempFile.delete(); + } + return null; + } + } + + + // SampleTypeTest + private void streamToJSON(FileLike realContent, String format, int maxRow, String originalFileName) throws IOException + { + String lowerCaseFileName = realContent.getName().toLowerCase(); + boolean extended = "jsonTSVExtended".equalsIgnoreCase(format); + boolean ignoreTypes = "jsonTSVIgnoreTypes".equalsIgnoreCase(format); + + JSONArray sheetsArray; + if (lowerCaseFileName.endsWith(".xls") || lowerCaseFileName.endsWith(".xlsx")) + { + try (InputStream in = realContent.openInputStream()) + { + sheetsArray = ExcelFactory.convertExcelToJSON(in, extended, maxRow); + } + } + else + { + DataLoaderFactory dlf = DataLoader.get().findFactory(realContent, null); + if (null == dlf) + { + throw new ApiUsageException("Unable to parse file " + realContent + ", it is likely of an unsupported file type"); + } + + try (InputStream in = realContent.openInputStream(); + DataLoader tabLoader = dlf.createLoader(in, true)) + { + tabLoader.setScanAheadLineCount(5000); + ColumnDescriptor[] cols = tabLoader.getColumns(); + + if (ignoreTypes) + for (ColumnDescriptor col : cols) + col.clazz = String.class; + + JSONArray rowsArray = new JSONArray(); + JSONArray headerArray = new JSONArray(); + for (ColumnDescriptor col : cols) + { + if (extended) + { + JSONObject valueObject = new JSONObject(); + valueObject.put("value", col.name); + headerArray.put(valueObject); + } + else + { + headerArray.put(col.name); + } + } + rowsArray.put(headerArray); + for (Map rowMap : tabLoader) + { + // headers count as a row to be consistent + if (maxRow > -1 && maxRow <= rowsArray.length() + 1) + break; + + JSONArray rowArray = new JSONArray(); + for (ColumnDescriptor col : cols) + { + Object value = rowMap.get(col.name); + if (extended) + { + JSONObject valueObject = new JSONObject(); + valueObject.put("value", value); + rowArray.put(valueObject); + } + else + { + rowArray.put(value); + } + } + rowsArray.put(rowArray); + } + + JSONObject sheetJSON = new JSONObject(); + sheetJSON.put("name", "flat"); + sheetJSON.put("data", rowsArray); + sheetsArray = new JSONArray(); + sheetsArray.put(sheetJSON); + } + } + + try (ApiJsonWriter writer = new ApiJsonWriter(getViewContext().getResponse())) + { + JSONObject workbookJSON = new JSONObject(); + workbookJSON.put("fileName", realContent.getName()); + workbookJSON.put("sheets", sheetsArray); + if (originalFileName != null) + workbookJSON.put("originalFileName", originalFileName); + writer.writeResponse(new ApiSimpleResponse(workbookJSON)); + } + } + + + public static class ConvertArraysToExcelForm + { + private String _json; + + public String getJson() + { + return _json; + } + + public void setJson(String json) + { + _json = json; + } + } + + @RequiresPermission(ReadPermission.class) + public static class ConvertArraysToExcelAction extends ExportAction + { + @Override + public void validate(ConvertArraysToExcelForm form, BindException errors) + { + if (form.getJson() == null) + { + errors.reject(ERROR_MSG, "Unable to convert to Excel - no spreadsheet data given"); + } + } + + @Override + public void export(ConvertArraysToExcelForm form, HttpServletResponse response, BindException errors) throws Exception + { + try + { + JSONObject rootObject; + JSONArray sheetsArray; + if (form.getJson() == null || form.getJson().trim().isEmpty()) + { + // Create JSON so that we return an empty file + rootObject = new JSONObject(); + sheetsArray = new JSONArray(); + JSONObject sheetObject = new JSONObject(); + sheetsArray.put(sheetObject); + } + else + { + rootObject = new JSONObject(form.getJson()); + sheetsArray = rootObject.getJSONArray("sheets"); + } + String filename = rootObject.has("fileName") ? rootObject.getString("fileName") : "ExcelExport.xls"; + ExcelWriter.ExcelDocumentType docType = filename.toLowerCase().endsWith(".xlsx") ? ExcelWriter.ExcelDocumentType.xlsx : ExcelWriter.ExcelDocumentType.xls; + + try (Workbook workbook = ExcelFactory.createFromArray(sheetsArray, docType)) + { + response.setContentType(docType.getMimeType()); + ResponseHelper.setContentDisposition(response, ResponseHelper.ContentDispositionType.attachment, filename); + ResponseHelper.setPrivate(response); + workbook.write(response.getOutputStream()); + + JSONObject qInfo = rootObject.has("queryinfo") ? rootObject.getJSONObject("queryinfo") : null; + if (qInfo != null) + { + QueryService.get().addAuditEvent(getUser(), getContainer(), qInfo.getString("schema"), + qInfo.getString("query"), getViewContext().getActionURL(), + rootObject.getString("auditMessage") + filename, + null); + SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "convertTable", "asExcel"); + } + } + } + catch (JSONException | ClassCastException e) + { + // We can get a ClassCastException if we expect an array and get a simple String, for example + ExceptionUtil.renderErrorView(getViewContext(), getPageConfig(), ErrorRenderer.ErrorType.notFound, HttpServletResponse.SC_BAD_REQUEST, "Failed to convert to Excel - invalid input", e, false, false); + } + } + } + + @RequiresPermission(ReadPermission.class) + public static class ConvertArraysToTableAction extends ExportAction + { + @Override + public void validate(ConvertArraysToExcelForm form, BindException errors) + { + if (form.getJson() == null) + { + errors.reject(ERROR_MSG, "Unable to convert to table - no data given"); + } + } + + @Override + public void export(ConvertArraysToExcelForm form, HttpServletResponse response, BindException errors) throws Exception + { + try + { + JSONObject rootObject; + JSONArray rowsArray; + if (form.getJson() == null || form.getJson().trim().isEmpty()) + { + // Create JSON so that we return an empty file + rootObject = new JSONObject(); + rowsArray = new JSONArray(); + } + else + { + rootObject = new JSONObject(form.getJson()); + rowsArray = rootObject.getJSONArray("rows"); + } + + TSVWriter.DELIM delimType = (!rootObject.isNull("delim") ? TSVWriter.DELIM.valueOf(rootObject.getString("delim")) : TSVWriter.DELIM.TAB); + TSVWriter.QUOTE quoteType = (!rootObject.isNull("quoteChar") ? TSVWriter.QUOTE.valueOf(rootObject.getString("quoteChar")) : TSVWriter.QUOTE.NONE); + String filenamePrefix = (!rootObject.isNull("fileNamePrefix") ? rootObject.getString("fileNamePrefix") : "Export"); + String filename = filenamePrefix + "." + delimType.extension; + String newlineChar = !rootObject.isNull("newlineChar") ? rootObject.getString("newlineChar") : "\n"; + + response.setCharacterEncoding(StringUtilsLabKey.DEFAULT_CHARSET.name()); + + try(var tsvWriter = new TSVJSONWriter(filenamePrefix, rowsArray)) + { + tsvWriter.setRowSeparator(newlineChar); + tsvWriter.setDelimiterCharacter(delimType); + tsvWriter.setQuoteCharacter(quoteType); + tsvWriter.write(response); + } + + JSONObject qInfo = rootObject.optJSONObject("queryinfo"); + if (qInfo != null) + { + QueryService.get().addAuditEvent(getUser(), getContainer(), qInfo.getString("schema"), qInfo.getString("query"), + getViewContext().getActionURL(), + rootObject.getString("auditMessage") + filename, + rowsArray.length()); + SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "convertTable", "asDelimited"); + } + } + catch (JSONException e) + { + ExceptionUtil.renderErrorView(getViewContext(), getPageConfig(), ErrorRenderer.ErrorType.notFound, HttpServletResponse.SC_BAD_REQUEST, "Failed to convert to table - invalid input", e, false, false); + } + } + } + + + public static class ConvertHtmlToExcelForm + { + private String _baseUrl; + private String _htmlFragment; + private String _name = "workbook.xls"; + + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public String getBaseUrl() + { + return _baseUrl; + } + + public void setBaseUrl(String baseUrl) + { + _baseUrl = baseUrl; + } + + public String getHtmlFragment() + { + return _htmlFragment; + } + + public void setHtmlFragment(String htmlFragment) + { + _htmlFragment = htmlFragment; + } + } + + + @RequiresPermission(ReadPermission.class) + public static class ConvertHtmlToExcelAction extends FormViewAction + { + String _responseHtml = null; + + @Override + public void validateCommand(ConvertHtmlToExcelForm target, Errors errors) + { + } + + @Override + public ModelAndView getView(ConvertHtmlToExcelForm form, boolean reshow, BindException errors) + { + String html = + "

" + + "" + + new CsrfInput(getViewContext()) + + "
"; + return HtmlView.unsafe(html); + } + + @Override + public boolean handlePost(ConvertHtmlToExcelForm form, BindException errors) + { + ActionURL url = getViewContext().getActionURL(); + String base = url.getBaseServerURI(); + if (!base.endsWith("/")) base += "/"; + + String baseTag = ""; + SafeToRender css = PageFlowUtil.getStylesheetIncludes(getContainer()); + String htmlFragment = StringUtils.trimToEmpty(form.getHtmlFragment()); + String html = "" + baseTag + css + "" + htmlFragment + ""; + + // UNDONE: strip script + List tidyErrors = new ArrayList<>(); + String tidy = JSoupUtil.tidyHTML(html, false, tidyErrors); + + if (!tidyErrors.isEmpty()) + { + for (String err : tidyErrors) + { + errors.reject(ERROR_MSG, err); + } + return false; + } + + _responseHtml = tidy; + return true; + } + + @Override + public ModelAndView getSuccessView(ConvertHtmlToExcelForm form) + { + ResponseHelper.setContentDisposition(getViewContext().getResponse(), ResponseHelper.ContentDispositionType.attachment, form.getName()); + getPageConfig().setTemplate(PageConfig.Template.None); + HtmlView v = HtmlView.unsafe(_responseHtml); + v.setContentType("application/vnd.ms-excel"); + v.setFrame(WebPartView.FrameType.NONE); + return v; + } + + @Override + public URLHelper getSuccessURL(ConvertHtmlToExcelForm convertHtmlToExcelForm) + { + return null; + } + + @Override + public void addNavTrail(NavTree root) + { + } + } + + + public static ActionURL getShowApplicationURL(Container c, long rowId) + { + ActionURL url = new ActionURL(ShowApplicationAction.class, c); + url.addParameter("rowId", rowId); + + return url; + } + + + @RequiresPermission(ReadPermission.class) + public class ShowApplicationAction extends SimpleViewAction + { + private ExpProtocolApplicationImpl _app; + private ExpRun _run; + + @Override + public ModelAndView getView(ExpObjectForm form, BindException errors) + { + _app = ExperimentServiceImpl.get().getExpProtocolApplication(form.getRowId()); + if (_app == null) + { + throw new NotFoundException("Could not find Protocol Application"); + } + _run = _app.getRun(); + if (_run == null) + { + throw new NotFoundException("No experiment run associated with Protocol Application"); + } + ensureCorrectContainer(getContainer(), _app, getViewContext()); + + ExpProtocol protocol = _app.getProtocol(); + + DataRegion dr = new DataRegion(); + dr.addColumns(ExperimentServiceImpl.get().getTinfoProtocolApplication().getUserEditableColumns()); + DetailsView detailsView = new DetailsView(dr, form.getRowId()); + dr.removeColumns("RunId", "ProtocolLSID", "RowId", "LSID"); + dr.addDisplayColumn(new ExperimentRunDisplayColumn(_run)); + dr.addDisplayColumn(new ProtocolDisplayColumn(protocol)); + dr.addDisplayColumn(new LineageGraphDisplayColumn(_app, _run)); + detailsView.setTitle("Protocol Application"); + + Container c = getContainer(); + ApplicationOutputGrid outMGrid = new ApplicationOutputGrid(c, _app.getRowId(), ExperimentServiceImpl.get().getTinfoMaterial()); + ApplicationOutputGrid outDGrid = new ApplicationOutputGrid(c, _app.getRowId(), ExperimentServiceImpl.get().getTinfoData()); + Map map = new HashMap<>(); + for (ProtocolApplicationParameter param : ExperimentService.get().getProtocolApplicationParameters(_app.getRowId())) + { + map.put(param.getOntologyEntryURI(), param); + } + + JspView> paramsView = new JspView<>("/org/labkey/experiment/Parameters.jsp", map); + paramsView.setTitle("Protocol Application Parameters"); + CustomPropertiesView cpv = new CustomPropertiesView(_app.getLSID(), c); + return new VBox(new StandardAndCustomPropertiesView(detailsView, cpv), paramsView, outMGrid, outDGrid); + } + + @Override + public void addNavTrail(NavTree root) + { + addRootNavTrail(root); + root.addChild("Experiment Run", ExperimentUrlsImpl.get().getRunGraphDetailURL(_run)); + root.addChild("Protocol Application " + _app.getName()); + } + } + + @RequiresPermission(ReadPermission.class) + public class ShowProtocolGridAction extends SimpleViewAction + { + @Override + public ModelAndView getView(Object o, BindException errors) + { + return new ProtocolWebPart(false, getViewContext()); + } + + @Override + public void addNavTrail(NavTree root) + { + addRootNavTrail(root); + root.addChild("Protocols"); + } + } + + @RequiresPermission(ReadPermission.class) + public class ProtocolDetailsAction extends SimpleViewAction + { + private ExpProtocolImpl _protocol; + + @Override + public ModelAndView getView(ExpObjectForm form, BindException errors) + { + _protocol = ExperimentServiceImpl.get().getExpProtocol(form.getRowId()); + if (_protocol == null) + { + _protocol = ExperimentServiceImpl.get().getExpProtocol(form.getLSID()); + } + + if (_protocol == null) + { + throw new NotFoundException("Unable to find a matching protocol"); + } + ensureCorrectContainer(getContainer(), _protocol, getViewContext()); + + JspView detailsView = new JspView<>("/org/labkey/experiment/ProtocolDetails.jsp", _protocol); + detailsView.setTitle("Standard Properties"); + + CustomPropertiesView cpv = new CustomPropertiesView(_protocol.getLSID(), getContainer()); + ProtocolParametersView parametersView = new ProtocolParametersView(_protocol); + + VBox protocolDetails = new VBox(); + protocolDetails.setFrame(WebPartView.FrameType.PORTAL); + protocolDetails.setTitle("Protocol Details"); + protocolDetails.addView(new ProtocolInputOutputsView(_protocol, errors)); + + JspView stepsView = new JspView<>("/org/labkey/experiment/ProtocolSteps.jsp", _protocol); + stepsView.setTitle("Protocol Steps"); + stepsView.setFrame(WebPartView.FrameType.TITLE); + protocolDetails.addView(stepsView); + + ExpSchema schema = new ExpSchema(getUser(), getContainer()); + ExperimentRunListView runView = new ExperimentRunListView(schema, ExperimentRunListView.getRunListQuerySettings(schema, getViewContext(), ExpSchema.TableType.Runs.name(), true), ExperimentRunType.ALL_RUNS_TYPE) + { + @Override + public DataView createDataView() + { + DataView result = super.createDataView(); + result.getRenderContext().setBaseFilter(new SimpleFilter(FieldKey.fromParts("Protocol", "LSID"), _protocol.getLSID())); + return result; + } + }; + + runView.setTitle("Runs Using This Protocol"); + + return new VBox(new StandardAndCustomPropertiesView(detailsView, cpv), parametersView, protocolDetails, runView); + } + + @Override + public void addNavTrail(NavTree root) + { + addRootNavTrail(root); + root.addChild("Protocols", ExperimentUrlsImpl.get().getProtocolGridURL(getContainer())); + root.addChild("Protocol: " + _protocol.getName()); + } + } + + public class ProtocolInputOutputsView extends VBox + { + ProtocolInputOutputsView(ExpProtocol protocol, Errors errors) + { + HBox inputsView = new HBox(); + addView(inputsView); + + HBox outputsView = new HBox(); + addView(outputsView); + + UserSchema expSchema = QueryService.get().getUserSchema(getUser(), getContainer(), ExpSchema.SCHEMA_NAME); + + class ProtocolInputGrid extends QueryView + { + public ProtocolInputGrid(String title, QuerySettings settings, @Nullable Errors errors) + { + super(expSchema, settings, errors); + + setFrame(FrameType.TITLE); + setTitle(title); + setButtonBarPosition(DataRegion.ButtonBarPosition.NONE); + setShowBorders(true); + setShadeAlternatingRows(true); + setShowExportButtons(false); + setShowPagination(false); + disableContainerFilterSelection(); + } + } + + // INPUTS + + QuerySettings materialInputsSettings = expSchema.getSettings("mpi", ExpSchema.TableType.MaterialProtocolInputs.toString()); + materialInputsSettings.getBaseFilter().addCondition(FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.Protocol.toString()), protocol.getRowId()); + materialInputsSettings.setFieldKeys(Arrays.asList( + FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.Name.toString()), + FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.SampleSet.toString()) + )); + QueryView materialInputsView = new ProtocolInputGrid("Material Inputs", materialInputsSettings, errors); + inputsView.addView(materialInputsView); + + QuerySettings dataInputsSettings = expSchema.getSettings("dpi", ExpSchema.TableType.DataProtocolInputs.toString()); + dataInputsSettings.getBaseFilter().addCondition(FieldKey.fromParts(ExpDataProtocolInputTable.Column.Protocol.toString()), protocol.getRowId()); + dataInputsSettings.setFieldKeys(Arrays.asList( + FieldKey.fromParts(ExpDataProtocolInputTable.Column.Name.toString()), + FieldKey.fromParts(ExpDataProtocolInputTable.Column.DataClass.toString()) + )); + QueryView dataInputsView = new ProtocolInputGrid("Data Inputs", dataInputsSettings, errors); + inputsView.addView(dataInputsView); + + // OUTPUTS + + QuerySettings materialOutputsSettings = expSchema.getSettings("mpo", ExpSchema.TableType.MaterialProtocolInputs.toString()); + materialOutputsSettings.getBaseFilter().addCondition(FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.Protocol.toString()), protocol.getRowId()); + materialOutputsSettings.setFieldKeys(Arrays.asList( + FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.Name.toString()), + FieldKey.fromParts(ExpMaterialProtocolInputTable.Column.SampleSet.toString()) + )); + QueryView materialOutputsView = new ProtocolInputGrid("Material Outputs", materialOutputsSettings, errors); + outputsView.addView(materialOutputsView); + + QuerySettings dataOutputsSettings = expSchema.getSettings("dpo", ExpSchema.TableType.DataProtocolInputs.toString()); + dataOutputsSettings.getBaseFilter().addCondition(FieldKey.fromParts(ExpDataProtocolInputTable.Column.Protocol.toString()), protocol.getRowId()); + dataOutputsSettings.setFieldKeys(Arrays.asList( + FieldKey.fromParts(ExpDataProtocolInputTable.Column.Name.toString()), + FieldKey.fromParts(ExpDataProtocolInputTable.Column.DataClass.toString()) + )); + QueryView dataOutputsView = new ProtocolInputGrid("Data Outputs", dataOutputsSettings, errors); + outputsView.addView(dataOutputsView); + } + } + + + @RequiresPermission(ReadPermission.class) + public class ProtocolPredecessorsAction extends SimpleViewAction + { + private ExpProtocol _parentProtocol; + private ProtocolActionStepDetail _actionStep; + + @Override + public ModelAndView getView(Object o, BindException errors) + { + ActionURL url = getViewContext().getActionURL(); + + String parentProtocolLSID = url.getParameter("ParentLSID"); + int actionSequence; + try + { + actionSequence = Integer.parseInt(url.getParameter("Sequence")); + } + catch (NumberFormatException e) + { + throw new NotFoundException("Could not find SequenceId " + url.getParameter("Sequence")); + } + + _parentProtocol = ExperimentService.get().getExpProtocol(parentProtocolLSID); + if (_parentProtocol == null) + { + throw new NotFoundException("Unable to find a matching protocol"); + } + + ensureCorrectContainer(getContainer(), _parentProtocol, getViewContext()); + + _actionStep = ExperimentServiceImpl.get().getProtocolActionStepDetail(parentProtocolLSID, actionSequence); + + if (_actionStep == null) + { + throw new NotFoundException("Unable to find a matching protocol action step"); + } + + ExpProtocol childProtocol = ExperimentService.get().getExpProtocol(_actionStep.getChildProtocolLSID()); + + JspView detailsView = new JspView<>("/org/labkey/experiment/ProtocolDetails.jsp", childProtocol); + detailsView.setTitle("Standard Properties"); + + CustomPropertiesView cpv = new CustomPropertiesView(childProtocol.getLSID(), getContainer()); + + ProtocolParametersView parametersView = new ProtocolParametersView(childProtocol); + + VBox protocolDetails = new VBox(); + protocolDetails.setFrame(WebPartView.FrameType.PORTAL); + protocolDetails.setTitle("Protocol Details"); + protocolDetails.addView(new ProtocolInputOutputsView(childProtocol, errors)); + protocolDetails.addView(new ProtocolSuccessorPredecessorView(parentProtocolLSID, actionSequence, getContainer(), "PredecessorChildLSID", "PredecessorSequence", "ActionSequence", "Protocol Predecessors")); + protocolDetails.addView(new ProtocolSuccessorPredecessorView(parentProtocolLSID, actionSequence, getContainer(), "ChildProtocolLSID", "ActionSequence", "PredecessorSequence", "Protocol Successors")); + + return new VBox(new StandardAndCustomPropertiesView(detailsView, cpv), parametersView, protocolDetails); + } + + @Override + public void addNavTrail(NavTree root) + { + addRootNavTrail(root); + root.addChild("Protocols", ExperimentUrlsImpl.get().getProtocolGridURL(getContainer())); + root.addChild("Parent Protocol '" + _parentProtocol.getName() + "'", ExperimentUrlsImpl.get().getProtocolDetailsURL(_parentProtocol)); + root.addChild("Protocol Step: " + _actionStep.getName()); + } + } + + public static class DataForm + { + private boolean _inline; + private long _rowId; + private String _lsid; + private Integer _maxDimension; + private String _format; + + public boolean isInline() + { + return _inline; + } + + public void setInline(boolean inline) + { + _inline = inline; + } + + public long getRowId() + { + return _rowId; + } + + public void setRowId(long rowId) + { + _rowId = rowId; + } + + public String getLsid() + { + return _lsid; + } + + public void setLsid(String lsid) + { + _lsid = lsid; + } + + public ExpDataImpl lookupData() + { + ExpDataImpl result = ExperimentServiceImpl.get().getExpData(getRowId()); + if (result == null && getLsid() != null) + { + result = ExperimentServiceImpl.get().getExpData(getLsid()); + } + return result; + } + + public Integer getMaxDimension() + { + return _maxDimension; + } + + public void setMaxDimension(Integer maxDimension) + { + _maxDimension = maxDimension; + } + + public String getFormat() + { + return _format; + } + + public void setFormat(String format) + { + _format = format; + } + } + + public static class ExpObjectForm extends QueryViewAction.QueryExportForm + { + private long _rowId; + private String _lsid; + + public String getLsid() + { + return _lsid; + } + + public void setLsid(String lsid) + { + _lsid = lsid; + } + + public String getLSID() + { + return getLsid(); + } + + public void setLSID(String lsid) + { + setLsid(lsid); + } + + public long getRowId() + { + return _rowId; + } + + public void setRowId(long rowId) + { + _rowId = rowId; + } + } + + @RequiresAnyOf({DeletePermission.class, SampleWorkflowDeletePermission.class}) + public class DeleteSelectedExpRunsAction extends AbstractDeleteAction + { + @Override + public void addNavTrail(NavTree root) + { + // UNDONE: Need help topic on Runs + setHelpTopic("experiment"); + super.addNavTrail(root); + } + + @Override + public ModelAndView getView(DeleteForm deleteForm, boolean reshow, BindException errors) + { + List runs = new ArrayList<>(); + + Map idToRunMap = new LongHashMap<>(); + for (long runId : deleteForm.getIds(false)) + { + ExpRun run = ExperimentService.get().getExpRun(runId); + if (run != null) + { + if (!run.canDelete(getUser())) + throw new UnauthorizedException("You do not have permission to delete runs in " + + run.getContainer()); + + runs.add(run); + idToRunMap.put(run.getRowId(), run); + } + } + + Map referencedItems = new LongHashMap<>(); + List referenceDescriptions = new ArrayList<>(); + AssayService assayService = AssayService.get(); + if (!idToRunMap.isEmpty() && assayService != null ) + { + // using the first run as a representative, since all interactions here are (I believe) using the same protocol. + ExpProtocol protocol = runs.getFirst().getProtocol(); + AssayProvider provider = assayService.getProvider(protocol); + if (provider != null) + { + SchemaKey key = AssayProtocolSchema.schemaName(provider, protocol); + ExperimentService.get().getObjectReferencers() + .forEach(referencer -> { + Collection referenced = referencer.getItemsWithReferences( + idToRunMap.keySet(), + key.toString(), + "Runs" + ); + referenced.forEach(id -> referencedItems.put(id, idToRunMap.get(id))); + referenceDescriptions.add(referencer.getObjectReferenceDescription(ExpRun.class)); + } + ); + } + + } + + List> permissionDatasetRows = new ArrayList<>(); + List> noPermissionDatasetRows = new ArrayList<>(); + if (StudyPublishService.get() != null) + { + for (Dataset dataset : StudyPublishService.get().getDatasetsForAssayRuns(runs, getUser())) + { + ActionURL url = urlProvider(StudyUrls.class).getDatasetURL(dataset.getContainer(), dataset.getDatasetId()); + TableInfo t = dataset.getTableInfo(getUser()); + if (null != t && t.hasPermission(getUser(),DeletePermission.class)) + { + permissionDatasetRows.add(new Pair<>(dataset, url)); + } + else + { + noPermissionDatasetRows.add(new Pair<>(dataset, url)); + } + } + } + + return new ConfirmDeleteView( + "run", + ShowRunGraphAction.class, + runs.stream().filter(run -> !referencedItems.containsKey(run.getRowId())).toList(), + deleteForm, + Collections.emptyList(), + "dataset(s) have one or more rows which", + permissionDatasetRows, + noPermissionDatasetRows, + referencedItems.values().stream().toList(), + referenceDescriptions.stream().filter(Objects::nonNull).collect(Collectors.joining(", or "))); + } + + @Override + protected void deleteObjects(DeleteForm deleteForm) + { + ExperimentServiceImpl.get().deleteExperimentRunsByRowIds(getContainer(), getUser(), deleteForm.getUserComment(), deleteForm.getIds(false), getTransactionAuditDetails()); + } + } + + public static class DeleteRunForm + { + private int _runId; + + public int getRunId() + { + return _runId; + } + + public void setRunId(int runId) + { + _runId = runId; + } + } + + /** + * Separate delete action from the client API + */ + @RequiresAnyOf({DeletePermission.class, SampleWorkflowDeletePermission.class}) + public static class DeleteRunAction extends MutatingApiAction + { + @Override + public ApiResponse execute(DeleteRunForm form, BindException errors) + { + ExpRun run = ExperimentService.get().getExpRun(form.getRunId()); + if (run == null) + { + throw new NotFoundException("Could not find run with ID " + form.getRunId()); + } + if (!run.canDelete(getUser())) + throw new UnauthorizedException("You do not have permission to delete runs in this container."); + + run.delete(getUser()); + return new ApiSimpleResponse("success", true); + } + } + + + @RequiresAnyOf({DeletePermission.class, SampleWorkflowDeletePermission.class}) + public static class DeleteRunsAction extends AbstractDeleteAPIAction + { + @Override + protected ApiSimpleResponse deleteObjects(CascadeDeleteForm form) + { + Set runIdsToDelete = new HashSet<>(form.getIds(true)); + Set runIdsCascadeDeleted = new HashSet<>(); + + if (form.isCascade()) + { + for (long runId : runIdsToDelete) + { + ExpRun run = ExperimentService.get().getExpRun(runId); + if (run != null) + addReplacesRuns(run, runIdsCascadeDeleted); + } + + if (!runIdsCascadeDeleted.isEmpty()) + runIdsToDelete.addAll(runIdsCascadeDeleted); + } + + Map transactionAuditDetails = getTransactionAuditDetails(); + if (form.getRequestSource() != null) + transactionAuditDetails.put(TransactionAuditProvider.TransactionDetail.RequestSource, form.getRequestSource()); + if (form.getEditMethod() != null) + transactionAuditDetails.put(TransactionAuditProvider.TransactionDetail.EditMethod, form.getEditMethod()); + ExperimentService.get().deleteExperimentRunsByRowIds(getContainer(), getUser(), form.getUserComment(), runIdsToDelete, transactionAuditDetails); + + ApiSimpleResponse response = new ApiSimpleResponse("success", true); + response.put("runIdsDeleted", runIdsToDelete); + if (!runIdsCascadeDeleted.isEmpty()) + response.put("runIdsCascadeDeleted", runIdsCascadeDeleted); + return response; + } + + private void addReplacesRuns(ExpRun run, Set runIds) + { + for (ExpRun replacedRun : run.getReplacesRuns()) + { + runIds.add(replacedRun.getRowId()); + addReplacesRuns(replacedRun, runIds); + } + } + } + + private abstract static class AbstractDeleteAPIAction extends MutatingApiAction + { + @Override + public void validateForm(CascadeDeleteForm form, Errors errors) + { + if (form.getSingleObjectRowId() == null && form.getDataRegionSelectionKey() == null && form.getRowIds() == null) + errors.reject(ERROR_REQUIRED, "Either singleObjectRowId, dataRegionSelectionKey, or rowIds is required"); + } + + @Override + public ApiResponse execute(CascadeDeleteForm form, BindException errors) throws Exception + { + ApiSimpleResponse response; + + try (DbScope.Transaction tx = ExperimentService.get().ensureTransaction()) + { + tx.addCommitTask(form::clearSelected, POSTCOMMIT); + + response = deleteObjects(form); + tx.commit(); + } + + if (null != response.get("success")) + response.put("success", !errors.hasErrors()); + + return response; + } + + protected abstract ApiSimpleResponse deleteObjects(CascadeDeleteForm form) throws Exception; + } + + public static class CascadeDeleteForm extends DeleteForm + { + private boolean _cascade; + + public boolean isCascade() + { + return _cascade; + } + + public void setCascade(boolean cascade) + { + _cascade = cascade; + } + } + + private abstract static class AbstractDeleteAction extends FormViewAction + { + @Override + public void validateCommand(DeleteForm target, Errors errors) + { + } + + @Override + public boolean handlePost(DeleteForm deleteForm, BindException errors) throws Exception + { + if (!deleteForm.isForceDelete()) + { + return false; + } + else + { + try (DbScope.Transaction tx = ExperimentService.get().ensureTransaction()) + { + tx.addCommitTask(deleteForm::clearSelected, POSTCOMMIT); + + deleteObjects(deleteForm); + tx.commit(); + } + catch (BatchValidationException v) + { + v.addToErrors(errors); + } + + return !errors.hasErrors(); + } + } + + @Override + public ActionURL getSuccessURL(DeleteForm form) + { + return form.getSuccessActionURL(ExperimentUrlsImpl.get().getOverviewURL(getContainer())); + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Confirm Deletion"); + } + + protected abstract void deleteObjects(DeleteForm form) throws Exception; + } + + @RequiresPermission(DesignAssayPermission.class) + public static class DeleteProtocolByRowIdsAPIAction extends AbstractDeleteAPIAction + { + @Override + protected ApiSimpleResponse deleteObjects(CascadeDeleteForm form) + { + for (ExpProtocol protocol : getProtocolsForDeletion(form)) + { + if (!protocol.getContainer().hasPermission(getUser(), DesignAssayPermission.class)) + throw new UnauthorizedException("You do not have sufficient permissions to delete this assay design."); + + protocol.delete(getUser(), form.getUserComment()); + } + + return new ApiSimpleResponse(); + } + } + + public static List getProtocolsForDeletion(DeleteForm form) + { + List protocols = new ArrayList<>(); + for (long protocolId : form.getIds(false)) + { + ExpProtocol protocol = ExperimentService.get().getExpProtocol(protocolId); + if (protocol != null) + { + protocols.add(protocol); + } + } + return protocols; + } + + @RequiresPermission(DesignAssayPermission.class) + public class DeleteProtocolByRowIdsAction extends AbstractDeleteAction + { + @Override + public void addNavTrail(NavTree root) + { + // UNDONE: Need help topic on protocols + setHelpTopic("experiment"); + super.addNavTrail(root); + } + + @Override + public ModelAndView getView(DeleteForm form, boolean reshow, BindException errors) + { + List runs = ExperimentService.get().getExpRunsForProtocolIds(false, form.getIds(false)); + List protocols = getProtocolsForDeletion(form); + String noun = "Assay Design"; + List> deleteableDatasets = new ArrayList<>(); + List> noPermissionDatasets = new ArrayList<>(); + if (AssayService.get() != null && StudyService.get() != null) + { + for (ExpProtocol protocol : protocols) + { + if (!protocol.getContainer().hasPermission(getUser(), DesignAssayPermission.class)) + throw new UnauthorizedException("You do not have sufficient permissions to delete this assay design."); + + if (AssayService.get().getProvider(protocol) == null) + { + noun = "Protocol"; + } + for (Dataset dataset : StudyPublishService.get().getDatasetsForPublishSource(protocol.getRowId(), Dataset.PublishSource.Assay)) + { + Pair entry = new Pair<>(dataset, urlProvider(StudyUrls.class).getDatasetURL(dataset.getContainer(), dataset.getDatasetId())); + if (dataset.canDeleteDefinition(getUser())) + { + deleteableDatasets.add(entry); + } + else + { + noPermissionDatasets.add(entry); + } + } + } + } + + return new ConfirmDeleteView(noun, ProtocolDetailsAction.class, protocols, form, runs, "Dataset", deleteableDatasets, noPermissionDatasets, Collections.emptyList(), null); + } + + @Override + protected void deleteObjects(DeleteForm form) + { + for (ExpProtocol protocol : getProtocolsForDeletion(form)) + { + protocol.delete(getUser(), form.getUserComment()); + } + } + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(ReadPermission.class) + public static class GetDataOperationConfirmationDataAction extends ReadOnlyApiAction + { + @Override + public void validateForm(DataOperationConfirmationForm form, Errors errors) + { + if (form.getDataRegionSelectionKey() == null && form.getRowIds() == null) + errors.reject(ERROR_REQUIRED, "You must provide either a set of rowIds or a dataRegionSelectionKey"); + if (form.getDataOperation() == null) + errors.reject(ERROR_REQUIRED, "An operation type must be provided."); + } + + @Override + public Object execute(DataOperationConfirmationForm form, BindException errors) + { + Collection requestIds = form.getIds(false); + ExperimentServiceImpl service = ExperimentServiceImpl.get(); + List allData = service.getExpDatas(requestIds); + + Set notAllowedIds = new HashSet<>(); + if (form.getDataOperation() == ExpDataImpl.DataOperations.Delete) + service.getObjectReferencers().forEach(referencer -> + notAllowedIds.addAll(referencer.getItemsWithReferences(requestIds, "exp.data"))); + + Map>> response = ExperimentServiceImpl.partitionRequestedOperationObjects(getUser(), requestIds, notAllowedIds, allData); + + Collection containers = new HashSet<>(); + Collection notPermittedIds = new ArrayList<>(); + Class permClass = form.getDataOperation().getPermissionClass(); + for (ExpDataImpl expData : allData) + { + Container c = expData.getContainer(); + if (c.hasPermission(getUser(), ReadPermission.class)) + containers.add(c); + if (permClass != null && !c.hasPermission(getUser(), permClass)) + notPermittedIds.add(expData.getRowId()); + } + + NameExpressionOptionService svc = NameExpressionOptionService.get(); + response.put("containers", containers.stream().map(c -> Map.of( + "id", c.getEntityId(), + "path", (Object) c.getPath(), + "permitted", permClass == null || c.hasPermission(getUser(), permClass), + "canEditName", svc.getAllowUserSpecificNamesValue(c) + )).toList()); + + response.put("notPermitted", notPermittedIds.stream().map(id -> Map.of("RowId", (Object) id)).toList()); + + return success(response); + } + } + + + public static class DataOperationConfirmationForm extends DataViewSnapshotSelectionForm + { + private ExpDataImpl.DataOperations _dataOperation; + + public ExpDataImpl.DataOperations getDataOperation() + { + return _dataOperation; + } + + public void setDataOperation(ExpDataImpl.DataOperations dataOperation) + { + _dataOperation = dataOperation; + } + + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(ReadPermission.class) + public static class GetMaterialOperationConfirmationDataAction extends ReadOnlyApiAction + { + @Override + public void validateForm(MaterialOperationConfirmationForm form, Errors errors) + { + if (form.getDataRegionSelectionKey() == null && form.getRowIds() == null) + errors.reject(ERROR_REQUIRED, "You must provide either a set of rowIds or a dataRegionSelectionKey."); + if (form.getSampleOperation() == null) + errors.reject(ERROR_REQUIRED, "An operation type must be provided."); + } + + @Override + public Object execute(MaterialOperationConfirmationForm form, BindException errors) + { + Set requestIds = form.getIds(false); + ExperimentServiceImpl service = ExperimentServiceImpl.get(); + List allMaterials = service.getExpMaterials(requestIds); + + Set notAllowedIds = new HashSet<>(); + // We prevent deletion if a sample is used as a parent, has assay data, is used in a job, etc. + if (form.getSampleOperation() == SampleTypeService.SampleOperations.Delete) + service.getObjectReferencers().forEach(referencer -> + notAllowedIds.addAll(referencer.getItemsWithReferences(requestIds, "samples"))); + + if (SampleStatusService.get().supportsSampleStatus()) + notAllowedIds.addAll(service.findIdsNotPermittedForOperation(allMaterials, form.getSampleOperation())); + + Map>> response = ExperimentServiceImpl.partitionRequestedOperationObjects(getUser(), requestIds, notAllowedIds, allMaterials); + + Collection containers = new HashSet<>(); + Collection notPermittedIds = new ArrayList<>(); + Class permClass = form.getSampleOperation().getPermissionClass(); + for (ExpMaterial material : allMaterials) + { + Container c = material.getContainer(); + if (c.hasPermission(getUser(), ReadPermission.class)) + containers.add(c); + if (permClass != null && !c.hasPermission(getUser(), permClass)) + notPermittedIds.add(material.getRowId()); + } + + NameExpressionOptionService svc = NameExpressionOptionService.get(); + + response.put("containers", containers.stream().map(c -> Map.of( + "id", c.getEntityId(), + "path", (Object) c.getPath(), + "permitted", permClass == null || c.hasPermission(getUser(), permClass), + "canEditName", svc.getAllowUserSpecificNamesValue(c) + )).toList()); + + response.put("notPermitted", notPermittedIds.stream().map(id -> Map.of("RowId", (Object) id)).toList()); + + if (form.getSampleOperation() == SampleTypeService.SampleOperations.Delete) + // String 'associatedDatasets' must be synced to its handling in confirmDelete.js, confirmDelete() + response.put("associatedDatasets", ExperimentServiceImpl.includeLinkedToStudyText(allMaterials, requestIds, getUser(), getContainer())); + + return success(response); + } + } + + public static class MaterialOperationConfirmationForm extends DataViewSnapshotSelectionForm + { + private SampleTypeService.SampleOperations _sampleOperation; + + public SampleTypeService.SampleOperations getSampleOperation() + { + return _sampleOperation; + } + + public void setSampleOperation(SampleTypeService.SampleOperations sampleOperation) + { + _sampleOperation = sampleOperation; + } + } + + @RequiresPermission(DeletePermission.class) + public class DeleteSelectedDataAction extends AbstractDeleteAction + { + @Override + public void addNavTrail(NavTree root) + { + // UNDONE: Need help topic on Datas + setHelpTopic("experiment"); + super.addNavTrail(root); + } + + @Override + protected void deleteObjects(DeleteForm deleteForm) throws Exception + { + List datas = getDatas(deleteForm, false); + + for (ExpRun run : getRuns(datas)) + { + if (!run.getContainer().hasPermission(getUser(), DeletePermission.class)) + throw new UnauthorizedException(); + } + + // Issue 32076: Delete the exp.Data objects using QueryUpdateService so trigger scripts will be executed + Map, List> byDataClass = datas.stream().collect(Collectors.groupingBy(d -> Optional.ofNullable(d.getDataClass(null)))); + for (Optional opt : byDataClass.keySet()) + { + SchemaKey schemaKey; + String queryName; + ExpDataClass dc = opt.orElse(null); + List ds = byDataClass.get(opt); + if (dc == null) + { + // Reference to exp.Data table + schemaKey = ExpSchema.SCHEMA_EXP; + queryName = ExpSchema.TableType.Data.name(); + } + else + { + // Reference to exp.data. table + schemaKey = ExpSchema.SCHEMA_EXP_DATA; + queryName = dc.getName(); + } + + UserSchema schema = QueryService.get().getUserSchema(getUser(), getContainer(), schemaKey); + if (schema == null) + throw new IllegalStateException("Failed to get schema '" + schemaKey + "'"); + + TableInfo table = schema.getTable(queryName); + if (table == null) + throw new IllegalStateException("Failed to get table '" + queryName + "' in schema '" + schemaKey + "'"); + + QueryUpdateService qus = table.getUpdateService(); + if (qus == null) + throw new IllegalStateException(); + + qus.deleteRows(getUser(), getContainer(), toKeys(ds), null, null); + } + } + + protected List> toKeys(List datas) + { + return datas.stream().map(d -> CaseInsensitiveHashMap.of("rowId", d.getRowId())).collect(toList()); + } + + @Override + public ModelAndView getView(DeleteForm deleteForm, boolean reshow, BindException errors) + { + if (errors.hasErrors()) + return new SimpleErrorView(errors, false); + + List datas = getDatas(deleteForm, false); + List runs = getRuns(datas); + + return new ConfirmDeleteView("Data", ShowDataAction.class, datas, deleteForm, runs); + } + + private List getRuns(List datas) + { + List runArray = ExperimentService.get().getRunsUsingDatas(datas); + return new ArrayList<>(ExperimentService.get().runsDeletedWithInput(runArray)); + } + + private List getDatas(DeleteForm deleteForm, boolean clear) + { + List datas = new ArrayList<>(); + for (long dataId : deleteForm.getIds(clear)) + { + ExpData data = ExperimentService.get().getExpData(dataId); + if (data != null) + { + datas.add(data); + } + } + return datas; + } + } + + @RequiresPermission(DeletePermission.class) + public class DeleteSelectedExperimentsAction extends AbstractDeleteAction + { + @Override + protected void deleteObjects(DeleteForm deleteForm) + { + for (ExpExperiment exp : lookupExperiments(deleteForm)) + { + exp.delete(getUser()); + } + } + + @Override + public ModelAndView getView(DeleteForm deleteForm, boolean reshow, BindException errors) + { + List experiments = lookupExperiments(deleteForm); + + List runs = new ArrayList<>(); + boolean allBatches = true; + for (ExpExperiment experiment : experiments) + { + // Deleting a batch also deletes all of its runs + if (experiment.getBatchProtocol() != null) + { + runs.addAll(experiment.getRuns()); + } + else + { + allBatches = false; + } + } + + return new ConfirmDeleteView(allBatches ? "batch" : "run group", DetailsAction.class, experiments, deleteForm, runs); + } + + private List lookupExperiments(DeleteForm deleteForm) + { + List experiments = new ArrayList<>(); + for (long experimentId : deleteForm.getIds(false)) + { + ExpExperiment experiment = ExperimentService.get().getExpExperiment(experimentId); + if (experiment != null) + { + experiments.add(experiment); + } + } + return experiments; + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("runGroups"); + super.addNavTrail(root); + } + } + + @RequiresPermission(DesignSampleTypePermission.class) + public class DeleteSampleTypesAction extends AbstractDeleteAction + { + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("sampleSets"); + super.addNavTrail(root); + } + + @Override + protected void deleteObjects(DeleteForm deleteForm) + { + List sampleTypes = getSampleTypes(deleteForm); + if (sampleTypes.isEmpty()) + { + throw new NotFoundException("No sample types found for ids provided."); + } + if (!ensureCorrectContainer(sampleTypes)) + { + throw new UnauthorizedException(); + } + + for (ExpRun run : getRuns(sampleTypes)) + { + if (!run.getContainer().hasPermission(getUser(), DeletePermission.class)) + { + throw new UnauthorizedException(); + } + } + + for (ExpSampleType source : sampleTypes) + { + Domain domain = source.getDomain(); + if (!domain.getDomainKind().canDeleteDefinition(getUser(), domain)) + { + throw new UnauthorizedException(); + } + + source.delete(getUser(), deleteForm.getUserComment()); + } + } + + @Override + public ModelAndView getView(DeleteForm deleteForm, boolean reshow, BindException errors) + { + List sampleTypes = getSampleTypes(deleteForm); + if (!ensureCorrectContainer(sampleTypes)) + { + throw new RedirectException(ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer(), "To delete a sample type, you must be in its folder or project.")); + } + + List> deleteableDatasets = new ArrayList<>(); + List> noPermissionDatasets = new ArrayList<>(); + if (StudyService.get() != null && StudyPublishService.get() != null) + { + for (ExpSampleType sampleType: sampleTypes) + { + for (Dataset dataset : StudyPublishService.get().getDatasetsForPublishSource(sampleType.getRowId(), Dataset.PublishSource.SampleType)) + { + ActionURL datasetURL = StudyService.get().getDatasetURL(getContainer(), dataset.getDatasetId()); + Pair entry = new Pair<>(dataset, datasetURL); + if (dataset.canDeleteDefinition(getUser())) + { + deleteableDatasets.add(entry); + } + else + { + noPermissionDatasets.add(entry); + } + } + } + } + return new ConfirmDeleteView("Sample Type", ShowSampleTypeAction.class, sampleTypes, deleteForm, getRuns(sampleTypes), "Dataset", deleteableDatasets, noPermissionDatasets, Collections.emptyList(), null); + } + + private List getSampleTypes(DeleteForm deleteForm) + { + List sources = new ArrayList<>(); + for (long rowId : deleteForm.getIds(false)) + { + ExpSampleType sampleType = SampleTypeService.get().getSampleType(getContainer(), rowId, true); + if (sampleType != null) + { + sources.add(sampleType); + } + } + return sources; + } + + private boolean ensureCorrectContainer(List sampleTypes) + { + for (ExpSampleType source : sampleTypes) + { + Container sourceContainer = source.getContainer(); + if (!sourceContainer.equals(getContainer())) + { + return false; + } + } + return true; + } + + private List getRuns(List sampleTypes) + { + if (!sampleTypes.isEmpty()) + { + List runArray = ExperimentService.get().getRunsUsingSampleTypes(sampleTypes.toArray(new ExpSampleType[0])); + return ExperimentService.get().runsDeletedWithInput(runArray); + } + else + { + return Collections.emptyList(); + } + } + } + + private DataRegion getSampleTypeRegion(ViewContext model) + { + TableInfo tableInfo = ExperimentServiceImpl.get().getTinfoSampleType(); + + QuerySettings settings = new QuerySettings(model, "SampleType"); + settings.setSelectionKey(DataRegionSelection.getSelectionKey(tableInfo.getSchema().getName(), tableInfo.getName(), "SampleType", settings.getDataRegionName())); + + DataRegion dr = new DataRegion(); + dr.setSettings(settings); + dr.addColumns(tableInfo.getUserEditableColumns()); + dr.removeColumns("lastindexed"); + dr.getDisplayColumn(0).setVisible(false); + + dr.getDisplayColumn("idcol1").setVisible(false); + dr.getDisplayColumn("idcol2").setVisible(false); + dr.getDisplayColumn("idcol3").setVisible(false); + dr.getDisplayColumn("lsid").setVisible(false); + dr.getDisplayColumn("materiallsidprefix").setVisible(false); + dr.getDisplayColumn("parentcol").setVisible(false); + + ActionURL url = new ActionURL(ShowSampleTypeAction.class, model.getContainer()); + dr.getDisplayColumn(1).setURL(url.addParameter("rowId", "${RowId}")); + dr.setShowRecordSelectors(getContainer().hasOneOf(getUser(), DeletePermission.class, UpdatePermission.class)); + + return dr; + } + + @RequiresPermission(ReadPermission.class) + @ActionNames("getSampleType,getSampleTypeApi") // Referenced in labkey-ui-components components/samples/actions.ts TODO: migrate getSampleTypeApi -> getSampleType + public static class GetSampleTypeAction extends ReadOnlyApiAction + { + @Override + public void validateForm(SampleTypeForm form, Errors errors) + { + if (form.getRowId() == null && form.getLSID() == null) + errors.reject(ERROR_REQUIRED, "RowId or LSID must be provided"); + } + + @Override + public Object execute(SampleTypeForm form, BindException errors) throws Exception + { + ExpSampleTypeImpl st = form.getSampleType(getContainer()); + + return getSampleTypeResponse(st); + } + } + + @NotNull + private static ApiSimpleResponse getSampleTypeResponse(ExpSampleType st) throws IOException + { + Map sampleType = new HashMap<>(); + sampleType.put("name", st.getName()); + sampleType.put("nameExpression", st.getNameExpression()); + sampleType.put("labelColor", st.getLabelColor()); + sampleType.put("metricUnit", st.getMetricUnit()); + sampleType.put("description", st.getDescription()); + sampleType.put("importAliases", st.getImportAliasMap()); + sampleType.put("lsid", st.getLSID()); + sampleType.put("rowId", st.getRowId()); + sampleType.put("domainId", st.getDomain().getTypeId()); + sampleType.put("category", st.getCategory()); + + return new ApiSimpleResponse(Map.of("sampleSet", sampleType, "success", true)); + } + + public static class DataTypesWithRequiredLineageForm + { + private Integer _parentDataTypeRowId; + private boolean _sampleParent; + + public Integer getParentDataTypeRowId() + { + return _parentDataTypeRowId; + } + + public void setParentDataTypeRowId(Integer parentDataTypeRowId) + { + this._parentDataTypeRowId = parentDataTypeRowId; + } + + public boolean isSampleParent() + { + return _sampleParent; + } + + public void setSampleParent(boolean sampleParent) + { + _sampleParent = sampleParent; + } + } + + @RequiresPermission(ReadPermission.class) + public static class GetDataTypesWithRequiredLineageAction extends ReadOnlyApiAction + { + @Override + public void validateForm(DataTypesWithRequiredLineageForm form, Errors errors) + { + if (form.getParentDataTypeRowId() == null) + errors.reject(ERROR_REQUIRED, "ParentDataTypeRowId must be provided"); + } + + @Override + public Object execute(DataTypesWithRequiredLineageForm form, BindException errors) throws Exception + { + return getDataTypesWithRequiredLineageResponse(form.getParentDataTypeRowId(), form.isSampleParent(), getContainer(), getUser()); + } + } + @NotNull + private static ApiSimpleResponse getDataTypesWithRequiredLineageResponse(Integer parentDataType, boolean isSampleParent, Container container, User user) + { + Pair, Set> requiredLineages = ExperimentServiceImpl.get().getDataTypesWithRequiredLineage(parentDataType, isSampleParent, container, user); + return new ApiSimpleResponse(Map.of("sampleTypes", requiredLineages.first, "dataClasses", requiredLineages.second,"success", true)); + } + + @RequiresPermission(DesignSampleTypePermission.class) + public static class EditSampleTypeAction extends SimpleViewAction + { + private ExpSampleTypeImpl _sampleType; + + @Override + public ModelAndView getView(SampleTypeForm form, BindException errors) + { + boolean create = form.getLSID() == null && form.getRowId() == null; + if (!create) + _sampleType = form.getSampleType(getContainer()); + + return ModuleHtmlView.get(ModuleLoader.getInstance().getModule("core"), ModuleHtmlView.getGeneratedViewPath("sampleTypeDesigner")); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("sampleSets"); + + root.addChild("Sample Types", ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); + if (_sampleType == null) + { + root.addChild("Create Sample Type"); + } + else + { + root.addChild(_sampleType.getName(), ExperimentUrlsImpl.get().getShowSampleTypeURL(_sampleType)); + root.addChild("Update Sample Type"); + } + } + } + + public static class SampleTypeForm extends ReturnUrlForm + { + private Integer rowId; + private String lsid; + + public Integer getRowId() + { + return rowId; + } + + public void setRowId(Integer rowId) + { + this.rowId = rowId; + } + + public String getLSID() + { + return this.lsid; + } + + public void setLSID(String lsid) + { + this.lsid = lsid; + } + + public ExpSampleTypeImpl getSampleType(Container container) throws NotFoundException + { + ExpSampleTypeImpl sampleType = SampleTypeServiceImpl.get().getSampleType(getLSID()); + if (sampleType == null) + sampleType = SampleTypeServiceImpl.get().getSampleType(getRowId()); + + if (sampleType == null) + { + throw new NotFoundException("Sample type not found: " + (getLSID() != null ? getLSID() : getRowId())); + } + + if (!container.equals(sampleType.getContainer())) + { + throw new NotFoundException("Sample type is not defined in the given container."); + } + + return sampleType; + } + } + + @RequiresPermission(InsertPermission.class) + public static class ImportSamplesAction extends AbstractExpDataImportAction + { + ExpSampleTypeImpl _sampleType; + boolean _isCrossTypeImport = false; + + @Override + public void validateForm(QueryForm queryForm, Errors errors) + { + _form = queryForm; + _insertOption = queryForm.getInsertOption(); + _isCrossTypeImport = getOptionParamValue(Params.crossTypeImport); + _form.setSchemaName(getTargetSchemaName()); + if (_isCrossTypeImport) + { + _form.setQueryName(getPipelineTargetQueryName()); + } + super.validateForm(queryForm, errors); + if (queryForm.getQueryName() == null) + errors.reject(ERROR_REQUIRED, "Sample type name is required"); + else + { + if (!_isCrossTypeImport) + { + _sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), queryForm.getQueryName(), true); + if (_sampleType == null) + { + errors.reject(ERROR_GENERIC, "Sample type '" + queryForm.getQueryName() + " not found."); + } + } + } + } + + private String getTargetSchemaName() + { + return getOptionParamValue(Params.crossTypeImport) ? ExpSchema.SCHEMA_NAME : "samples"; + } + + @Override + protected UserSchema getTargetSchema() + { + return getOptionParamValue(Params.crossTypeImport) ? QueryService.get().getUserSchema(getUser(), getContainer(), getTargetSchemaName()) : super.getTargetSchema(); + } + + @Override + protected String getPipelineTargetQueryName() + { + return getOptionParamValue(Params.crossTypeImport) ? "materials" : super.getPipelineTargetQueryName(); + } + + @Override + protected Map getRenamedColumns() + { + Map renamedColumns = super.getRenamedColumns(); + renamedColumns.putAll(SampleTypeUpdateServiceDI.SAMPLE_ALT_IMPORT_NAME_COLS); + return renamedColumns; + } + + @Override + protected @Nullable Set getLineageImportAliases() throws IOException + { + Set aliases = new CaseInsensitiveHashSet(); + // Issue 53419: Aliquot parent with number like names that starts with leading zeroes aren't resolved during import + aliases.add(ExpMaterial.ALIQUOTED_FROM_INPUT); + aliases.add(ExpMaterial.ALIQUOTED_FROM_INPUT_LABEL); + boolean crossTypeImport = getOptionParamValue(AbstractQueryImportAction.Params.crossTypeImport); + // Issue 51894: We need to stop conversion to numbers for alias fields for all type + // If there are aliases defined for one type that are number fields in another type, this will prevent + // conversion to numbers during the initial partitioning, but the conversion will happen when the partition + // file is loaded. + if (crossTypeImport) + { + List sampleTypes = SampleTypeServiceImpl.get().getSampleTypes(getContainer(), true); + for (ExpSampleTypeImpl sampleType : sampleTypes) + aliases.addAll(sampleType.getImportAliases().keySet()); + } + else + { + ExpSampleTypeImpl sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), _form.getQueryName(), true); + aliases.addAll(sampleType.getImportAliases().keySet()); + } + return aliases; + } + + @Override + protected int importData( + DataLoader dl, + FileStream file, + String originalName, + BatchValidationException errors, + @Nullable AuditBehaviorType auditBehaviorType, + TransactionAuditProvider.@Nullable TransactionAuditEvent auditEvent, + @Nullable String auditUserComment + ) throws IOException + { + initContext(dl, errors, auditBehaviorType, auditUserComment); + + TableInfo tInfo = _target; + QueryUpdateService updateService = _updateService; + if (getOptionParamValue(Params.crossTypeImport)) + { + tInfo = ExperimentService.get().createMaterialTable(new SamplesSchema(getUser(), getContainer()), ContainerFilter.current(this), null); + updateService = tInfo.getUpdateService(); + } + if (WorkflowService.get() != null) + { + try + { + WorkflowService.get().populateConfigParams(getViewContext().getRequest(), _context.getConfigParameters()); + } + catch (ValidationException e) + { + errors.addRowError(e); + } + } + + int count = importData(dl, tInfo, updateService, _context, auditEvent, getUser(), getContainer()); + + if (getOptionParamValue(Params.crossTypeImport)) + { + SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "sampleImport", "crossTypeImport"); + if (_context.getInsertOption() == QueryUpdateService.InsertOption.UPDATE) + SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "sampleImport", "crossTypeUpdate"); + else if (_context.getInsertOption() == QueryUpdateService.InsertOption.MERGE) + SimpleMetricsService.get().increment(ExperimentService.MODULE_NAME, "sampleImport", "crossTypeMerge"); + } + + return count; + } + + @Override + public ModelAndView getView(QueryForm form, BindException errors) throws Exception + { + initRequest(form); + setHelpTopic("importSampleSets"); // page-wide help topic + setImportHelpTopic("importSampleSets"); // importOptions help topic + setTypeName("samples"); + return getDefaultImportView(form, errors); + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Sample Types", ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); + ActionURL url = _form.urlFor(QueryAction.executeQuery); + if (_form.getQueryName() != null && url != null) + root.addChild(_form.getQueryName(), url); + root.addChild("Import Data"); + } + + @Override + protected JSONObject createSuccessResponse(int rowCount) + { + JSONObject json = super.createSuccessResponse(rowCount); + if (!_context.getResponseInfo().isEmpty()) + { + for (String key : _context.getResponseInfo().keySet()) + json.put(key, _context.getResponseInfo().get(key)); + } + return json; + } + + } + + public abstract static class AbstractExpDataImportAction extends AbstractQueryImportAction + { + protected QueryForm _form; + protected DataIteratorContext _context; + + @Override + public void validateForm(QueryForm form, Errors errors) + { + QueryDefinition query = form.getQueryDef(); + if (query.getContainerFilter() != null && query.getContainerFilter().getType() != null) + { + // cross folder import not supported + if (query.getContainerFilter().getType() != ContainerFilter.Type.Current) + errors.reject(ERROR_GENERIC, "ContainerFilter is not supported for import actions."); + } + } + + @Override + protected void initRequest(QueryForm form) throws ServletException + { + QueryDefinition query = form.getQueryDef(); + setContainerFilterForImport(query, getContainer(), getUser()); + List qpe = new ArrayList<>(); + TableInfo t = query.getTable(form.getSchema(), qpe, true); + + if (!qpe.isEmpty()) + throw qpe.getFirst(); + if (!getOptionParamValue(Params.crossTypeImport) && null != t) + { + setTarget(t); + setShowMergeOption(t.supportsInsertOption(QueryUpdateService.InsertOption.MERGE)); + setShowUpdateOption(t.supportsInsertOption(QueryUpdateService.InsertOption.UPDATE)); + } + + _auditBehaviorType = form.getAuditBehavior(); + _auditUserComment = form.getAuditUserComment(); + } + + @Override + protected Map getRenamedColumns() + { + final String renameParamPrefix = "importAlias."; + Map renameColumns = new CaseInsensitiveHashMap<>(); + PropertyValue[] pvs = _form.getInitParameters().getPropertyValues(); + for (PropertyValue pv : pvs) + { + String paramName = pv.getName(); + if (!paramName.startsWith(renameParamPrefix) || pv.getValue() == null) + continue; + + renameColumns.put(paramName.substring(renameParamPrefix.length()), (String) pv.getValue()); + } + return renameColumns; + } + + @Override + protected Set getLineageImportAliases() throws IOException + { + ExpDataClass dataClass = ExperimentServiceImpl.get().getDataClass(getContainer(), _form.getQueryName(), true); + return new CaseInsensitiveHashSet(dataClass.getImportAliases().keySet()); + } + + protected void initContext(DataLoader dl, BatchValidationException errors, @Nullable AuditBehaviorType auditBehaviorType, @Nullable String auditUserComment) + { + _context = createDataIteratorContext(_insertOption, getOptionParamsMap(), getLookupResolutionType(), auditBehaviorType, auditUserComment, errors, null, getContainer()); + } + + @Override + protected int importData(DataLoader dl, FileStream file, String originalName, BatchValidationException errors, @Nullable AuditBehaviorType auditBehaviorType, TransactionAuditProvider.@Nullable TransactionAuditEvent auditEvent, @Nullable String auditUserComment) throws IOException + { + initContext(dl, errors, auditBehaviorType, auditUserComment); + return importData(dl, _target, _updateService, _context, auditEvent, getUser(), getContainer()); + } + + @Override + protected String getQueryImportProviderName() + { + PropertyValue pv = _form.getInitParameters().getPropertyValue(QUERY_IMPORT_PIPELINE_PROVIDER_PARAM); + return pv == null ? null : (String) pv.getValue(); + } + + @Override + protected String getQueryImportDescription() + { + PropertyValue pv = _form.getInitParameters().getPropertyValue(QUERY_IMPORT_PIPELINE_DESCRIPTION_PARAM); + return pv == null ? null : (String) pv.getValue(); + } + + @Override + protected String getQueryImportJobNotificationProviderName() + { + PropertyValue pv = _form.getInitParameters().getPropertyValue(QUERY_IMPORT_NOTIFICATION_PROVIDER_PARAM); + return pv == null ? null : (String) pv.getValue(); + } + + @Override + protected boolean isBackgroundImportSupported(@NotNull String fileName) + { + return true; + } + + @Override + protected boolean allowLineageColumns() + { + return true; + } + + } + + @RequiresPermission(InsertPermission.class) + public static class ImportDataAction extends AbstractExpDataImportAction + { + @Override + public void validateForm(QueryForm queryForm, Errors errors) + { + _form = queryForm; + _form.setSchemaName("exp.data"); + _insertOption = queryForm.getInsertOption(); + super.validateForm(queryForm, errors); + if (queryForm.getQueryName() == null) + errors.reject(ERROR_REQUIRED, "Data class name is required"); + else + { + ExpDataClass dataClass = ExperimentService.get().getDataClass(getContainer(), queryForm.getQueryName(), true); + if (dataClass == null) + { + errors.reject(ERROR_GENERIC, "Data class '" + queryForm.getQueryName() + " not found."); + } + } + } + + @Override + public ModelAndView getView(QueryForm form, BindException errors) throws Exception + { + initRequest(form); + setHelpTopic("dataClass"); // page wide help topic + setImportHelpTopic("dataClass#ui"); // importOptions help topic + setTypeName("data"); + return getDefaultImportView(form, errors); + } + + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Data Classes", ExperimentUrlsImpl.get().getDataClassListURL(getContainer())); + ActionURL url = _form.urlFor(QueryAction.executeQuery); + if (_form.getQueryName() != null && url != null) + root.addChild(_form.getQueryName(), url); + root.addChild("Import Data"); + } + + } + + @RequiresPermission(UpdatePermission.class) + public class ShowUpdateAction extends SimpleViewAction + { + @Override + public ModelAndView getView(ExperimentForm form, BindException errors) + { + form.refreshFromDb(); + Experiment exp = form.getBean(); + if (exp == null) + { + throw new NotFoundException(); + } + ensureCorrectContainer(getContainer(), ExperimentService.get().getExpExperiment(exp.getRowId()), getViewContext()); + + return new ExperimentUpdateView(new DataRegion(), form, errors); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("runGroups"); + addRootNavTrail(root); + root.addChild("Update Run Group"); + } + } + + @RequiresPermission(UpdatePermission.class) + public static class UpdateAction extends FormHandlerAction + { + private Experiment _exp; + + @Override + public void validateCommand(ExperimentForm target, Errors errors) + { + } + + @Override + public boolean handlePost(ExperimentForm form, BindException errors) throws Exception + { + form.doUpdate(); + form.refreshFromDb(); + _exp = form.getBean(); + return true; + } + + @Override + public ActionURL getSuccessURL(ExperimentForm experimentForm) + { + return ExperimentUrlsImpl.get().getExperimentDetailsURL(getContainer(), ExperimentService.get().getExpExperiment(_exp.getRowId())); + } + } + + public static class ExportBean + { + private final LSIDRelativizer _selectedRelativizer; + private final XarExportType _selectedExportType; + private final String _fileName; + private final String _dataRegionSelectionKey; + private final String _error; + private final Long _expRowId; + private final Long _protocolId; + private final ActionURL _postURL; + private final Set _roles; + + public ExportBean(LSIDRelativizer selectedRelativizer, XarExportType selectedExportType, String fileName, ExportOptionsForm form, Set roles, ActionURL postURL) + { + _selectedRelativizer = selectedRelativizer; + _selectedExportType = selectedExportType; + _fileName = fileName; + _dataRegionSelectionKey = form.getDataRegionSelectionKey(); + _error = form.getError(); + _expRowId = form.getExpRowId(); + _postURL = postURL; + _roles = roles; + _protocolId = form.getProtocolId(); + } + + public LSIDRelativizer getSelectedRelativizer() + { + return _selectedRelativizer; + } + + public XarExportType getSelectedExportType() + { + return _selectedExportType; + } + + public String getError() + { + return _error; + } + + public String getFileName() + { + return _fileName; + } + + public Set getRoles() + { + return _roles; + } + + public String getDataRegionSelectionKey() + { + return _dataRegionSelectionKey; + } + + public ActionURL getPostURL() + { + return _postURL; + } + + public Long getProtocolId() + { + return _protocolId; + } + + public Long getExpRowId() + { + return _expRowId; + } + } + + + private String fixupExportName(String runName) + { + runName = runName.replace('/', '-'); + runName = runName.replace('\\', '-'); + return runName; + } + + public static class ExportOptionsForm extends ExperimentRunListForm + { + private String _error; + private XarExportType _exportType; + private LSIDRelativizer _lsidOutputType; + private String _xarFileName; + private String _zipFileName; + private String _fileExportType; + private Long _protocolId; + private Integer _sampleTypeId; + private long[] _dataIds; + private String[] _roles = new String[0]; + + public String getError() + { + return _error; + } + + public void setError(String error) + { + _error = error; + } + + public XarExportType getExportType() + { + return _exportType; + } + + public LSIDRelativizer getLsidOutputType() + { + return _lsidOutputType; + } + + public String getFileExportType() + { + return _fileExportType; + } + + public void setFileExportType(String fileExportType) + { + _fileExportType = fileExportType; + } + + public String getXarFileName() + { + return _xarFileName; + } + + public void setXarFileName(String xarFileName) + { + _xarFileName = xarFileName; + } + + public String getZipFileName() + { + return _zipFileName; + } + + public void setZipFileName(String zipFileName) + { + _zipFileName = zipFileName; + } + + public void setExportType(XarExportType exportType) + { + _exportType = exportType; + } + + public void setLsidOutputType(LSIDRelativizer lsidOutputType) + { + _lsidOutputType = lsidOutputType; + } + + public Long getProtocolId() + { + return _protocolId; + } + + public void setProtocolId(Long protocolId) + { + _protocolId = protocolId; + } + + public String[] getRoles() + { + return _roles; + } + + public void setRoles(String[] roles) + { + _roles = roles; + } + + public Integer getSampleTypeId() + { + return _sampleTypeId; + } + + public void setSampleTypeId(Integer sampleTypeId) + { + _sampleTypeId = sampleTypeId; + } + + public long[] getDataIds() + { + return _dataIds; + } + + public void setDataIds(long[] dataIds) + { + _dataIds = dataIds; + } + + public List lookupProtocols(ViewContext context, boolean clearSelection) + { + List protocols = new ArrayList<>(); + + if (_protocolId != null) + { + ExpProtocol protocol = ExperimentService.get().getExpProtocol(_protocolId.intValue()); + if (protocol == null || !protocol.getContainer().equals(context.getContainer())) + { + throw new NotFoundException(); + } + protocols.add(protocol); + return protocols; + } + + for (Long protocolId : DataRegionSelection.getSelectedIntegers(context, clearSelection)) + { + try + { + ExpProtocol protocol = ExperimentService.get().getExpProtocol(protocolId); + if (protocol == null || !protocol.getContainer().equals(context.getContainer())) + { + throw new NotFoundException(); + } + protocols.add(protocol); + } + catch (NumberFormatException e) + { + throw new NotFoundException("Invalid protocol id: " + protocolId); + } + } + if (protocols.isEmpty()) + { + throw new NotFoundException("No protocols selected"); + } + return protocols; + } + } + + private ActionURL exportXAR(@NotNull XarExportSelection selection, @Nullable String fileName) + throws ExperimentException, IOException, PipelineValidationException + { + return exportXAR(selection, null, null, fileName); + } + + private ActionURL exportXAR(@NotNull XarExportSelection selection, @Nullable LSIDRelativizer lsidRelativizer, @Nullable XarExportType exportType, @Nullable String fileName) + throws ExperimentException, IOException, PipelineValidationException + { + if (lsidRelativizer == null) + lsidRelativizer = LSIDRelativizer.FOLDER_RELATIVE; + + if (exportType == null) + exportType = XarExportType.BROWSER_DOWNLOAD; + + if (fileName == null || fileName.isEmpty()) + fileName = "export.xar"; + + fileName = fixupExportName(fileName); + String xarXmlFileName = null; + if (Strings.CI.endsWith(fileName, ".xar")) + xarXmlFileName = fileName + ".xml"; + + switch (exportType) + { + case BROWSER_DOWNLOAD: + XarExporter exporter = new XarExporter(lsidRelativizer, selection, getUser(), xarXmlFileName, null, getContainer()); + + getViewContext().getResponse().setContentType("application/zip"); + ResponseHelper.setContentDisposition(getViewContext().getResponse(), ResponseHelper.ContentDispositionType.attachment, fileName); + ResponseHelper.setPrivate(getViewContext().getResponse()); + + exporter.writeAsArchive(getViewContext().getResponse().getOutputStream()); + return null; + case PIPELINE_FILE: + if (!PipelineService.get().hasValidPipelineRoot(getContainer())) + { + throw new IllegalStateException("You must set a valid pipeline root before you can export a XAR to it."); + } + PipeRoot pipeRoot = PipelineService.get().findPipelineRoot(getContainer()); + XarExportPipelineJob job = new XarExportPipelineJob(getViewBackgroundInfo(), pipeRoot, fileName, lsidRelativizer, selection, xarXmlFileName); + PipelineService.get().queueJob(job); + PipelineStatusFile status = PipelineService.get().getStatusFile(job.getJobGUID()); + return PageFlowUtil.urlProvider(PipelineUrls.class).statusDetails(getContainer(), status.getRowId()); + default: + throw new IllegalArgumentException("Unknown export type: " + exportType); + } + } + + @RequiresPermission(ReadPermission.class) + public class ExportProtocolsAction extends AbstractExportAction + { + @Override + public boolean handlePost(ExportOptionsForm form, BindException errors) throws Exception + { + List protocols = form.lookupProtocols(getViewContext(), false); + + long[] ids = new long[protocols.size()]; + for (int i = 0; i < ids.length; i++) + { + ids[i] = protocols.get(i).getRowId(); + } + XarExportSelection selection = new XarExportSelection(); + selection.addProtocolIds(ids); + + exportXAR(selection, form.getLsidOutputType(), form.getExportType(), form.getXarFileName()); + + if (form.getDataRegionSelectionKey() != null) + { + // Clear the selection + form.lookupProtocols(getViewContext(), true); + } + return true; + } + } + + public abstract static class AbstractExportAction extends FormViewAction + { + protected ActionURL _resultURL; + + @Override + public void validateCommand(ExportOptionsForm target, Errors errors) + { + } + + @Override + public ActionURL getSuccessURL(ExportOptionsForm exportOptionsForm) + { + return _resultURL; + } + + @Override + public ModelAndView getSuccessView(ExportOptionsForm exportOptionsForm) + { + return null; + } + + @Override + public ModelAndView getView(ExportOptionsForm form, boolean reshow, BindException errors) throws Exception + { + // FormViewAction can reinvoke getView() in response to a POST if we're not redirecting the browser, + // so avoid double-creating the export + if ("get".equalsIgnoreCase(getViewContext().getRequest().getMethod())) + handlePost(form, errors); + return null; + } + + @Override + public void addNavTrail(NavTree root) + { + } + + public List lookupRuns(ExportOptionsForm form) + { + Set runIds; + if (form.getRunIds() != null && form.getRunIds().length > 0) + runIds = new HashSet<>(Arrays.asList(form.getRunIds())); + else + runIds = DataRegionSelection.getSelectedIntegers(getViewContext(), form.getDataRegionSelectionKey(), false); + + if (runIds.isEmpty()) + { + throw new NotFoundException(); + } + List result = new ArrayList<>(); + + for (long id : runIds) + { + ExpRun run = ExperimentService.get().getExpRun(id); + if (run == null || !run.getContainer().hasPermission(getUser(), ReadPermission.class)) + { + throw new NotFoundException("Could not find run " + id); + } + result.add(run); + } + return result; + } + } + + @RequiresPermission(ReadPermission.class) + public class ExportRunsAction extends AbstractExportAction + { + @Override + public boolean handlePost(ExportOptionsForm form, BindException errors) throws Exception + { + XarExportSelection selection = new XarExportSelection(); + if (form.getExpRowId() != null) + { + ExpExperiment experiment = ExperimentService.get().getExpExperiment(form.getExpRowId()); + if (experiment != null && !experiment.getContainer().hasPermission(getUser(), ReadPermission.class)) + { + throw new NotFoundException("Run group " + form.getExpRowId()); + } + selection.addExperimentIds(experiment.getRowId()); + } + selection.addRuns(lookupRuns(form)); + + _resultURL = exportXAR(selection, form.getLsidOutputType(), form.getExportType(), form.getXarFileName()); + if (form.getDataRegionSelectionKey() != null) + DataRegionSelection.clearAll(getViewContext(), form.getDataRegionSelectionKey()); + return true; + } + } + + @RequiresPermission(ReadPermission.class) + public class ExportSampleTypeAction extends AbstractExportAction + { + @Override + public boolean handlePost(ExportOptionsForm form, BindException errors) throws Exception + { + Integer rowId = form.getSampleTypeId(); + if (rowId == null) + { + throw new NotFoundException("No sampleTypeId parameter specified"); + } + ExpSampleType sampleType = SampleTypeService.get().getSampleType(getContainer(), rowId.intValue(), true); + if (sampleType == null) + { + throw new NotFoundException("No such sample type with RowId " + rowId); + } + if (!sampleType.getContainer().hasPermission(getUser(), ReadPermission.class)) + { + throw new UnauthorizedException(); + } + + XarExportSelection selection = new XarExportSelection(); + selection.addSampleType(sampleType); + + _resultURL = exportXAR(selection, form.getLsidOutputType(), form.getExportType(), FileUtil.makeLegalName(sampleType.getName() + ".xar")); + return true; + } + } + + @RequiresPermission(ReadPermission.class) + public class ExportRunFilesAction extends AbstractExportAction + { + @Override + public boolean handlePost(ExportOptionsForm form, BindException errors) throws Exception + { + XarExportSelection selection = new XarExportSelection(); + selection.setIncludeXarXml(false); + if ("role".equalsIgnoreCase(form.getFileExportType())) + { + selection.addRoles(form.getRoles()); + } + selection.addRuns(lookupRuns(form)); + + _resultURL = exportXAR(selection, form.getZipFileName()); + if (form.getDataRegionSelectionKey() != null) + DataRegionSelection.clearAll(getViewContext(), form.getDataRegionSelectionKey()); + return true; + } + } + + @RequiresPermission(ReadPermission.class) + public class ExportFilesAction extends AbstractExportAction + { + @Override + public boolean handlePost(ExportOptionsForm form, BindException errors) throws Exception + { + long[] dataIds = form.getDataIds(); + if (dataIds == null || dataIds.length == 0) + { + throw new NotFoundException(); + } + + try + { + for (long id : dataIds) + { + ExpData data = ExperimentService.get().getExpData(id); + if (data == null || !data.getContainer().hasPermission(getUser(), ReadPermission.class)) + { + throw new NotFoundException("Could not find file " + id); + } + } + + XarExportSelection selection = new XarExportSelection(); + selection.setIncludeXarXml(false); + selection.addDataIds(dataIds); + + _resultURL = exportXAR(selection, form.getZipFileName()); + return true; + } + catch (NumberFormatException e) + { + throw new NotFoundException(Arrays.toString(dataIds)); + } + } + } + + public static class ExperimentRunListForm implements DataRegionSelection.DataSelectionKeyForm + { + private String _dataRegionSelectionKey; + private Long _expRowId; + private Long[] _runIds; + + @Override + public String getDataRegionSelectionKey() + { + return _dataRegionSelectionKey; + } + + @Override + public void setDataRegionSelectionKey(String key) + { + _dataRegionSelectionKey = key; + } + + public Long getExpRowId() + { + return _expRowId; + } + + public void setExpRowId(Long expRowId) + { + _expRowId = expRowId; + } + + public Long[] getRunIds() + { + return _runIds; + } + + public void setRunIds(Long[] runIds) + { + _runIds = runIds; + } + + public ExpExperiment lookupExperiment() + { + return getExpRowId() == null ? null : ExperimentService.get().getExpExperiment(getExpRowId().intValue()); + } + } + + private void addSelectedRunsToExperiment(ExpExperiment exp, String dataRegionSelectionKey) + { + Collection runIds = DataRegionSelection.getSelectedIntegers(getViewContext(), dataRegionSelectionKey, true); + List runs = new ArrayList<>(); + for (long runId : runIds) + { + ExpRun run = ExperimentServiceImpl.get().getExpRun(runId); + if (run != null) + { + runs.add(run); + } + } + exp.addRuns(getUser(), runs.toArray(new ExpRun[0])); + } + + + @RequiresPermission(InsertPermission.class) + public class AddRunsToExperimentAction extends FormHandlerAction + { + @Override + public void validateCommand(ExperimentRunListForm target, Errors errors) + { + } + + @Override + public boolean handlePost(ExperimentRunListForm form, BindException errors) + { + addSelectedRunsToExperiment(form.lookupExperiment(), form.getDataRegionSelectionKey()); + return true; + } + + @Override + public ActionURL getSuccessURL(ExperimentRunListForm form) + { + return ExperimentUrlsImpl.get().getExperimentDetailsURL(getContainer(), form.lookupExperiment()); + } + } + + @RequiresPermission(DeletePermission.class) + public static class RemoveSelectedExpRunsAction extends FormHandlerAction + { + @Override + public void validateCommand(ExperimentRunListForm target, Errors errors) + { + } + + @Override + public boolean handlePost(ExperimentRunListForm form, BindException errors) + { + ExpExperiment exp = form.lookupExperiment(); + if (exp == null || !exp.getContainer().hasPermission(getUser(), DeletePermission.class)) + { + throw new NotFoundException("Could not find run group with RowId " + form.getExpRowId()); + } + + for (long runId : DataRegionSelection.getSelectedIntegers(getViewContext(), form.getDataRegionSelectionKey(), false)) + { + ExpRun run = ExperimentService.get().getExpRun(runId); + if (run == null || !run.getContainer().hasPermission(getUser(), DeletePermission.class)) + { + throw new NotFoundException("Could not find run with RowId " + runId); + } + exp.removeRun(getUser(), run); + } + if (form.getDataRegionSelectionKey() != null) + DataRegionSelection.clearAll(getViewContext(), form.getDataRegionSelectionKey()); + return true; + } + + @Override + public ActionURL getSuccessURL(ExperimentRunListForm form) + { + return ExperimentUrlsImpl.get().getExperimentDetailsURL(getContainer(), form.lookupExperiment()); + } + } + + public static ActionURL getResolveLsidURL(Container c, @NotNull String type, @NotNull String lsid) + { + ActionURL url = new ActionURL(ResolveLSIDAction.class, c); + url.addParameter("type", type); + url.addParameter("lsid", lsid); + + return url; + } + + + @RequiresPermission(ReadPermission.class) + public static class ResolveLSIDAction extends SimpleViewAction + { + @Override + public ModelAndView getView(LsidForm form, BindException errors) + { + String message = ""; + if (!PageFlowUtil.empty(form.getLsid())) + { + try + { + String lsid = Lsid.canonical(form.getLsid().trim()); + ActionURL url = LsidManager.get().getDisplayURL(lsid); + if (url == null && form.getType() != null) + { + url = switch (form.getType().toLowerCase()) + { + case "data" -> LsidType.Data.getDisplayURL(new Lsid(lsid)); + case "material" -> LsidType.Material.getDisplayURL(new Lsid(lsid)); + default -> url; + }; + } + if (null != url) + { + throw new RedirectException(url); + } + message = "Could not map LSID to URL"; + } + catch (IllegalArgumentException e) + { + message = "Invalid LSID"; + } + } + + return new HtmlView("Enter LSID", + DOM.createHtmlFragment( + message, + DOM.FORM(at(action, getViewContext().cloneActionURL().setAction(ResolveLSIDAction.class)), + "LSID: ", + DOM.INPUT(at(type, "text", name, "lsid", size, "80", value, form.getLsid())), + PageFlowUtil.button("Go").submit(true)))); + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Resolve LSID"); + } + } + + public static class LsidForm + { + private String _lsid; + + public String getType() + { + return _type; + } + + public void setType(String type) + { + _type = type; + } + + private String _type; + + public void setLsid(String lsid) + { + _lsid = lsid; + } + + public String getLsid() + { + return _lsid; + } + } + + public static class SetFlagForm extends LsidForm + { + private String _comment; + private boolean _redirect = true; + + public String getComment() + { + return _comment; + } + + public void setComment(String comment) + { + _comment = comment; + } + + public boolean isRedirect() + { + return _redirect; + } + + public void setRedirect(boolean redirect) + { + _redirect = redirect; + } + } + + /** + * Check for update on the object itself + */ + @RequiresNoPermission + public static class SetFlagAction extends FormHandlerAction + { + @Override + public void validateCommand(SetFlagForm target, Errors errors) + { + } + + @Override + public boolean handlePost(SetFlagForm form, BindException errors) throws Exception + { + String lsid = form.getLsid(); + if (lsid == null) + throw new NotFoundException(); + ExpObject obj = ExperimentService.get().findObjectFromLSID(lsid); + if (obj == null) + throw new NotFoundException(); + Container container = obj.getContainer(); + if (!container.hasPermission(getUser(), UpdatePermission.class)) + { + throw new UnauthorizedException(); + } + + obj.setComment(getUser(), form.getComment()); + return true; + } + + @Override + public URLHelper getSuccessURL(SetFlagForm form) + { + return null; + } + } + + @RequiresPermission(InsertPermission.class) + public class DeriveSamplesChooseTargetAction extends SimpleViewAction + { + private List _materials; + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("sampleSets"); + addRootNavTrail(root); + root.addChild("Sample Types", ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); + ExpSampleType sampleType = _materials != null && !_materials.isEmpty() ? _materials.getFirst().getSampleType() : null; + if (sampleType != null) + { + root.addChild(sampleType.getName(), ExperimentUrlsImpl.get().getShowSampleTypeURL(sampleType)); + } + root.addChild("Derive Samples"); + } + + @Override + public void validate(DeriveMaterialForm form, BindException errors) + { + _materials = form.lookupMaterials(); + if (_materials.isEmpty()) + { + throw new NotFoundException("Could not find any matching materials"); + } + } + + @Override + public ModelAndView getView(DeriveMaterialForm form, BindException errors) + { + Container c = getContainer(); + PipeRoot root = PipelineService.get().findPipelineRoot(c); + + if (root == null || !root.isValid()) + { + ActionURL pipelineURL = urlProvider(PipelineUrls.class).urlSetup(c); + return new HtmlView(DIV("You must ", + DOM.A(DOM.at(href, pipelineURL), "configure a valid pipeline root for this folder"), + " before deriving samples.")); + } + else + { + Set materialInputRoles = new TreeSet<>(ExperimentService.get().getMaterialInputRoles(getContainer(), getUser())); + Map materialsWithRoles = new LinkedHashMap<>(); + for (ExpMaterial material : _materials) + { + materialsWithRoles.put(material, null); + } + + List sampleTypes = getUploadableSampleTypes(); + + DeriveSamplesChooseTargetBean bean = new DeriveSamplesChooseTargetBean(form.getDataRegionSelectionKey(), form.getTargetSampleTypeId(), sampleTypes, materialsWithRoles, form.getOutputCount(), materialInputRoles, null); + return new JspView<>("/org/labkey/experiment/deriveSamplesChooseTarget.jsp", bean); + } + } + } + + public static class DeriveSamplesChooseTargetBean implements DataRegionSelection.DataSelectionKeyForm + { + private String _dataRegionSelectionKey; + + private final Integer _targetSampleTypeId; + private final List _sampleTypes; + private final Map _sourceMaterials; + private final int _sampleCount; + private final Collection _inputRoles; + private final DerivedSamplePropertyHelper _propertyHelper; + + public static final String CUSTOM_ROLE = "--CUSTOM--"; + + public DeriveSamplesChooseTargetBean(String dataRegionSelectionKey, Integer targetSampleTypeId, List sampleTypes, Map sourceMaterials, int sampleCount, Collection inputRoles, DerivedSamplePropertyHelper helper) + { + _dataRegionSelectionKey = dataRegionSelectionKey; + _targetSampleTypeId = targetSampleTypeId; + _sampleTypes = sampleTypes; + _sourceMaterials = sourceMaterials; + _sampleCount = sampleCount; + _inputRoles = inputRoles; + _propertyHelper = helper; + } + + public Integer getTargetSampleTypeId() + { + return _targetSampleTypeId; + } + + public DerivedSamplePropertyHelper getPropertyHelper() + { + return _propertyHelper; + } + + public int getSampleCount() + { + return _sampleCount; + } + + public Map getSourceMaterials() + { + return _sourceMaterials; + } + + public List getSampleTypes() + { + return _sampleTypes; + } + + public Collection getInputRoles() + { + return _inputRoles; + } + + @Override + public String getDataRegionSelectionKey() + { + return _dataRegionSelectionKey; + } + + @Override + public void setDataRegionSelectionKey(String key) + { + _dataRegionSelectionKey = key; + } + } + + private List getUploadableSampleTypes() + { + // Make a copy so we can modify it + List sampleTypes = new ArrayList<>(SampleTypeService.get().getSampleTypes(getContainer(), true)); + sampleTypes.removeIf(sampleType -> !sampleType.canImportMoreSamples()); + return sampleTypes; + } + + @RequiresPermission(InsertPermission.class) + public class DeriveSamplesAction extends FormViewAction + { + private List _materials; + private ActionURL _successUrl; + private final Map _inputMaterials = new LinkedHashMap<>(); + + @Override + public ModelAndView getView(DeriveMaterialForm form, boolean reshow, BindException errors) + { + _materials = form.lookupMaterials(); + if (_materials.isEmpty()) + { + throw new NotFoundException("Could not find any matching materials"); + } + + Container c = getContainer(); + + if (form.getOutputCount() <= 0) + { + form.setOutputCount(1); + } + + if (form.getTargetSampleTypeId() == 0) + throw new NotFoundException("Target sample type required for the derived samples"); + + ExpSampleTypeImpl sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), form.getTargetSampleTypeId(), true); + if (sampleType == null) + throw new NotFoundException("Could not find sample type with rowId " + form.getTargetSampleTypeId()); + + InsertView insertView = new InsertView(new DataRegion(), errors); + + DerivedSamplePropertyHelper helper = new DerivedSamplePropertyHelper(sampleType, form.getOutputCount(), c, getUser()); + helper.addSampleColumns(insertView, getUser()); + + int[] rowIds = form.getRowIds(); + for (int i = 0; i < rowIds.length; i++) + { + insertView.getDataRegion().addHiddenFormField("rowIds", Integer.toString(rowIds[i])); + insertView.getDataRegion().addHiddenFormField("inputRole" + i, form.getInputRole(i) == null ? "" : form.getInputRole(i)); + insertView.getDataRegion().addHiddenFormField("customRole" + i, form.getCustomRole(i) == null ? "" : form.getCustomRole(i)); + } + + insertView.getDataRegion().addHiddenFormField("targetSampleTypeId", Integer.toString(form.getTargetSampleTypeId())); + insertView.getDataRegion().addHiddenFormField("outputCount", Integer.toString(form.getOutputCount())); + if (form.getDataRegionSelectionKey() != null) + insertView.getDataRegion().addHiddenFormField(DataRegionSelection.DATA_REGION_SELECTION_KEY, form.getDataRegionSelectionKey()); + insertView.setInitialValues(ViewServlet.adaptParameterMap(getViewContext().getRequest().getParameterMap())); + ButtonBar bar = new ButtonBar(); + bar.setStyle(ButtonBar.Style.separateButtons); + ActionButton submitButton = new ActionButton(DeriveSamplesAction.class, "Submit"); + submitButton.setActionType(ActionButton.Action.POST); + bar.add(submitButton); + insertView.getDataRegion().setButtonBar(bar); + insertView.setTitle("Output Samples"); + + Map materialsWithRoles = new LinkedHashMap<>(); + List materials = form.lookupMaterials(); + for (int i = 0; i < materials.size(); i++) + { + materialsWithRoles.put(materials.get(i), form.determineLabel(i)); + } + + DeriveSamplesChooseTargetBean bean = new DeriveSamplesChooseTargetBean(form.getDataRegionSelectionKey(), form.getTargetSampleTypeId(), getUploadableSampleTypes(), materialsWithRoles, form.getOutputCount(), Collections.emptyList(), helper); + JspView view = new JspView<>("/org/labkey/experiment/summarizeMaterialInputs.jsp", bean); + view.setTitle("Input Samples"); + + return new VBox(view, insertView); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("sampleSets"); + addRootNavTrail(root); + root.addChild("Sample Types", ExperimentUrlsImpl.get().getShowSampleTypeListURL(getContainer())); + ExpSampleType sampleType = _materials != null && !_materials.isEmpty() ? _materials.getFirst().getSampleType() : null; + if (sampleType != null) + { + root.addChild(sampleType.getName(), ExperimentUrlsImpl.get().getShowSampleTypeURL(sampleType)); + } + root.addChild("Derive Samples"); + } + + @Override + public void validateCommand(DeriveMaterialForm form, Errors errors) + { + List materials = form.lookupMaterials(); + + List lockedSamples = new ArrayList<>(); + for (int i = 0; i < materials.size(); i++) + { + ExpMaterial m = materials.get(i); + if (!m.isOperationPermitted(SampleTypeService.SampleOperations.EditLineage)) + { + lockedSamples.add(m); + } + String inputRole = form.determineLabel(i); + if (inputRole == null || inputRole.isEmpty()) + { + ExpSampleType st = m.getSampleType(); + inputRole = st != null ? st.getName() : ExpMaterialRunInput.DEFAULT_ROLE; + } + _inputMaterials.put(materials.get(i), inputRole); + } + + if (!lockedSamples.isEmpty()) + { + errors.reject(ERROR_MSG, SampleTypeService.get().getOperationNotPermittedMessage(lockedSamples, SampleTypeService.SampleOperations.EditLineage)); + } + } + + @Override + public boolean handlePost(DeriveMaterialForm form, BindException errors) + { + ExpSampleTypeImpl sampleType = SampleTypeServiceImpl.get().getSampleType(getContainer(), form.getTargetSampleTypeId(), true); + + DerivedSamplePropertyHelper helper = new DerivedSamplePropertyHelper(sampleType, form.getOutputCount(), getContainer(), getUser()); + + Map, Map> allProperties; + try + { + boolean valid = true; + for (Map.Entry> entry : helper.getPostedPropertyValues(getViewContext().getRequest()).entrySet()) + valid = UploadWizardAction.validatePostedProperties(getViewContext(), entry.getValue(), errors) && valid; + if (!valid) + return false; + + allProperties = helper.getSampleProperties(getViewContext().getRequest(), _inputMaterials.keySet()); + } + catch (DuplicateMaterialException e) + { + errors.addError(new ObjectError(e.getColName(), null, null, e.getMessage())); + return false; + } + catch (ExperimentException e) + { + errors.reject(SpringActionController.ERROR_MSG, e.getMessage()); + return false; + } + + try (DbScope.Transaction tx = ExperimentService.get().ensureTransaction()) + { + Map outputMaterials = new HashMap<>(); + int i = 0; + for (Map.Entry, Map> entry : allProperties.entrySet()) + { + Lsid lsid = entry.getKey().first; + String name = entry.getKey().second; + assert name != null; + + ExpMaterialImpl outputMaterial = ExperimentServiceImpl.get().createExpMaterial(getContainer(), lsid.toString(), name); + if (sampleType != null) + { + outputMaterial.setCpasType(sampleType.getLSID()); + } + outputMaterial.save(getUser()); + + if (sampleType != null) + { + Map pvs = new HashMap<>(); + for (Map.Entry propertyEntry : entry.getValue().entrySet()) + pvs.put(propertyEntry.getKey().getName(), propertyEntry.getValue()); + outputMaterial.setProperties(getUser(), pvs, false); + } + + outputMaterials.put(outputMaterial, helper.getSampleNames().get(i++)); + } + + ExperimentService.get().deriveSamples(_inputMaterials, outputMaterials, getViewBackgroundInfo(), _log); + + tx.commit(); + + // automatically link samples to study, if configured + StudyPublishService.get().autoLinkDerivedSamples(sampleType, outputMaterials.keySet().stream().map(ExpObject::getRowId).collect(toList()), getContainer(), getUser()); + + _successUrl = ExperimentUrlsImpl.get().getShowSampleURL(getContainer(), outputMaterials.keySet().iterator().next()); + + if (form.getDataRegionSelectionKey() != null) + DataRegionSelection.clearAll(getViewContext(), form.getDataRegionSelectionKey()); + } + catch (Exception e) + { + errors.reject(SpringActionController.ERROR_MSG, e.getMessage()); + return false; + } + + return true; + } + + @Override + public URLHelper getSuccessURL(DeriveMaterialForm deriveMaterialForm) + { + return _successUrl; + } + } + + public static class DeriveMaterialForm implements HasViewContext, DataRegionSelection.DataSelectionKeyForm + { + private String _dataRegionSelectionKey; + private int _outputCount = 1; + private int _targetSampleTypeId; + private int[] _rowIds; + private String _name; + + private ViewContext _context; + + @Override + public void setViewContext(ViewContext context) + { + _context = context; + } + + @Override + public ViewContext getViewContext() + { + return _context; + } + + public List lookupMaterials() + { + List result = new ArrayList<>(); + for (int rowId : getRowIds()) + { + ExpMaterial material = ExperimentService.get().getExpMaterial(rowId); + if (material != null) + { + if (material.getContainer().hasPermission(_context.getUser(), ReadPermission.class)) + { + result.add(material); + } + else + { + throw new UnauthorizedException(); + } + } + else + { + throw new NotFoundException("No material with RowId " + rowId); + } + } + result.sort(Comparator.comparing(Identifiable::getName)); + return result; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + @Override + public String getDataRegionSelectionKey() + { + return _dataRegionSelectionKey; + } + + @Override + public void setDataRegionSelectionKey(String dataRegionSelectionKey) + { + _dataRegionSelectionKey = dataRegionSelectionKey; + } + + public int[] getRowIds() + { + if (_rowIds == null) + { + _rowIds = PageFlowUtil.toInts(DataRegionSelection.getSelected(getViewContext(), getDataRegionSelectionKey(), false)); + } + return _rowIds; + } + + public void setRowIds(int[] rowIds) + { + _rowIds = rowIds; + } + + public int getOutputCount() + { + return _outputCount; + } + + public void setOutputCount(int outputCount) + { + _outputCount = outputCount; + } + + public int getTargetSampleTypeId() + { + return _targetSampleTypeId; + } + + public void setTargetSampleTypeId(int targetSampleTypeId) + { + _targetSampleTypeId = targetSampleTypeId; + } + + public String getInputRole(int i) + { + return _context.getRequest().getParameter("inputRole" + i); + } + + public String getCustomRole(int i) + { + return _context.getRequest().getParameter("customRole" + i); + } + + public String determineLabel(int index) + { + String result = getInputRole(index); + if (DeriveSamplesChooseTargetBean.CUSTOM_ROLE.equals(result)) + { + result = getCustomRole(index); + } + if (result != null) + { + result = result.trim(); + } + return result; + } + } + + + public static class ExpInput + { + public String role; + public int rowId; + public Lsid lsid; + } + + public static class DerivationSpec + { + public String role; + public Map values; + } + + public static class DerivationForm + { + public List dataInputs; + public List materialInputs; + + public int dataOutputCount; + public Lsid targetDataClass; + public Map dataDefault; + public List dataOutputs; + + public int materialOutputCount; + public Lsid targetSampleType; + public Map materialDefault; + public List materialOutputs; + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(InsertPermission.class) + public static class DeriveAction extends MutatingApiAction + { + @Override + public void validateForm(DerivationForm form, Errors errors) + { + if (errors.hasErrors()) + return; + + if (form.materialOutputCount > 0 && form.materialOutputs != null && !form.materialOutputs.isEmpty()) + errors.reject(ERROR_MSG, "Either 'materialOutputCount' or 'materialOutputs' property can be specified, but not both."); + + if (form.dataOutputCount > 0 && form.dataOutputs != null && !form.dataOutputs.isEmpty()) + errors.reject(ERROR_MSG, "Either 'dataOutputCount' or 'dataOutputs' property can be specified, but not both."); + + boolean hasMaterialOutputs = form.materialOutputCount > 0 || form.materialOutputs != null && !form.materialOutputs.isEmpty(); + boolean hasDataOutputs = form.dataOutputCount > 0 || form.dataOutputs != null && !form.dataOutputs.isEmpty(); + + if (!hasMaterialOutputs && !hasDataOutputs) + errors.reject(ERROR_MSG, "At least one data output or material output is required"); + + if (hasMaterialOutputs && form.targetSampleType == null) + errors.reject(ERROR_MSG, "targetSampleType lsid required for material outputs"); + + if (hasDataOutputs && form.targetDataClass == null) + errors.reject(ERROR_MSG, "targetDataClass lsid required for data outputs"); + } + + @Override + public Object execute(DerivationForm form, BindException errors) throws Exception + { + // Find material inputs + Map materialInputs = new LinkedHashMap<>(); + if (form.materialInputs != null) + { + for (ExpInput in : form.materialInputs) + { + ExpMaterial m = null; + if (in.lsid != null) + { + m = ExperimentService.get().getExpMaterial(in.lsid.toString()); + if (m == null) + errors.reject(ERROR_MSG, "Can't resolve sample '" + in.lsid + "'"); + } + else if (in.rowId > 0) + { + m = ExperimentService.get().getExpMaterial(in.rowId); + if (m == null) + errors.reject(ERROR_MSG, "Can't resolve sample '" + in.rowId + "'"); + } + + if (m == null) + { + errors.reject(ERROR_MSG, "Material input lsid or rowId required"); + continue; + } + + ExpSampleType st = m.getSampleType(); + if (st == null) + { + errors.reject(ERROR_MSG, "Material input is not a member of a SampleType"); + continue; + } + + String role = in.role; + if (role == null || role.isEmpty()) + { + role = st.getName(); + } + materialInputs.put(m, role); + } + } + + // Find input data + Map dataInputs = new LinkedHashMap<>(); + if (form.dataInputs != null) + { + for (ExpInput in : form.dataInputs) + { + ExpData d = null; + if (in.lsid != null) + { + d = ExperimentService.get().getExpData(in.lsid.toString()); + if (d == null) + errors.reject(ERROR_MSG, "Can't resolve data '" + in.lsid + "'"); + } + else if (in.rowId > 0) + { + d = ExperimentService.get().getExpData(in.rowId); + if (d == null) + errors.reject(ERROR_MSG, "Can't resolve data '" + in.rowId + "'"); + } + + if (d == null) + { + errors.reject(ERROR_MSG, "Data input lsid or rowId required"); + continue; + } + + ExpDataClass dc = d.getDataClass(getUser()); + if (dc == null) + { + errors.reject(ERROR_MSG, "Data input is not a member of a DataClass"); + continue; + } + + String role = in.role; + if (role == null || role.isEmpty()) + { + role = dc.getName(); + } + dataInputs.put(d, role); + } + } + + ExpSampleType outSampleType; + if (form.targetSampleType != null) + { + // TODO: check in scope and has permission + outSampleType = SampleTypeService.get().getSampleType(form.targetSampleType.toString()); + if (outSampleType == null) + errors.reject(ERROR_MSG, "Sample type not found: " + form.targetSampleType.toString()); + } + else + { + outSampleType = null; + } + + ExpDataClass outDataClass; + if (form.targetDataClass != null) + { + // TODO: check in scope and has permission + outDataClass = ExperimentServiceImpl.get().getDataClass(form.targetDataClass.toString()); + if (outDataClass == null) + errors.reject(ERROR_MSG, "DataClass not found: " + form.targetDataClass.toString()); + } + else + { + outDataClass = null; + } + + if (errors.hasErrors()) + return null; + + // TODO: support list of resolved ExpData or ExpMaterial instead of string concatenated names + // Create "MaterialInputs/" columns with a value containing a comma-separated list of Material names + final Map> parentInputNames = new HashMap<>(); + Set inputTypes = new CaseInsensitiveHashSet(); + for (ExpMaterial material : materialInputs.keySet()) + { + ExpSampleType st = material.getSampleType(); + String keyName = ExpMaterial.MATERIAL_INPUT_PARENT + "/" + st.getName(); + inputTypes.add(keyName); + parentInputNames.computeIfAbsent(keyName, (x) -> new LinkedHashSet<>()).add(material.getName()); + } + + // TODO: support list of resolved ExpData or ExpMaterial instead of string concatenated names + // Create "DataInputs/" columns with a value containing a comma-separated list of ExpData names + for (ExpData d : dataInputs.keySet()) + { + ExpDataClass dc = d.getDataClass(getUser()); + String keyName = ExpData.DATA_INPUT_PARENT + "/" + dc.getName(); + inputTypes.add(keyName); + parentInputNames.computeIfAbsent(keyName, (x) -> new LinkedHashSet<>()).add(d.getName()); + } + + + try (DbScope.Transaction tx = ExperimentService.get().ensureTransaction()) + { + Set requiredParentTypes = new CaseInsensitiveHashSet(); + + // output materials + Map outputMaterials = new HashMap<>(); + int materialOutputCount = Math.max(form.materialOutputCount, form.materialOutputs != null ? form.materialOutputs.size() : 0); + if (materialOutputCount > 0 && outSampleType != null) + { + requiredParentTypes.addAll(outSampleType.getRequiredImportAliases().values()); + DerivedOutputs derived = new DerivedOutputs<>(parentInputNames, form.materialDefault, form.materialOutputs, materialOutputCount, ExpMaterial.DEFAULT_CPAS_TYPE) + { + @Override + protected TableInfo createTable() + { + SamplesSchema schema = new SamplesSchema(getUser(), getContainer()); + return schema.getTable(outSampleType.getName()); + } + + @Override + protected List getExpObject(List> insertedRows) + { + List rowIds = insertedRows.stream().map(r -> MapUtils.getLong(r,"rowid")).collect(toList()); + return ExperimentService.get().getExpMaterials(rowIds); + } + }; + + outputMaterials = derived.createOutputs(); + } + + + // create output data + Map outputData = new HashMap<>(); + int dataOutputCount = Math.max(form.dataOutputCount, form.dataOutputs != null ? form.dataOutputs.size() : 0); + if (dataOutputCount > 0 && outDataClass != null) + { + requiredParentTypes.addAll(outDataClass.getRequiredImportAliases().values()); + DerivedOutputs derived = new DerivedOutputs<>(parentInputNames, form.dataDefault, form.dataOutputs, dataOutputCount, ExpData.DEFAULT_CPAS_TYPE) + { + @Override + protected TableInfo createTable() + { + ExpSchema expSchema = new ExpSchema(getUser(), getContainer()); + UserSchema dataSchema = expSchema.getUserSchema(ExpSchema.NestedSchemas.data.name()); + return dataSchema.getTable(outDataClass.getName()); + } + + @Override + protected List getExpObject(List> insertedRows) + { + List lsids = insertedRows.stream().map(r -> (String) r.get("lsid")).collect(toList()); + return ExperimentService.get().getExpDatasByLSID(lsids); + } + }; + + outputData = derived.createOutputs(); + } + + if (outputMaterials.isEmpty() && outputData.isEmpty()) + throw new IllegalStateException("Expected to create " + materialOutputCount + " materials and " + dataOutputCount + " datas"); + + boolean hasMissingRequiredParent = false; + for (String required : requiredParentTypes) + { + if (!inputTypes.contains(required)) + { + hasMissingRequiredParent = true; + break; + } + } + if (hasMissingRequiredParent) + throw new IllegalStateException("Inputs are required: " + String.join(",", requiredParentTypes)); + + // finally, create the derived run if there are any parents + ExpRun run = null; + if (!materialInputs.isEmpty() || !dataInputs.isEmpty()) + run = ExperimentService.get().derive(materialInputs, dataInputs, outputMaterials, outputData, new ViewBackgroundInfo(getContainer(), getUser(), null), _log); + tx.commit(); + + StringBuilder successMessage = new StringBuilder("Created "); + if (!outputMaterials.isEmpty()) + successMessage.append(outputMaterials.size()).append(" materials"); + if (!outputData.isEmpty()) + successMessage.append(outputData.size()).append(" data"); + + JSONObject ret; + if (run != null) + ret = ExperimentJSONConverter.serializeRun(run, null, getUser(), ExperimentJSONConverter.DEFAULT_SETTINGS); + else + ret = ExperimentJSONConverter.serializeRunOutputs(outputData.keySet(), outputMaterials.keySet(), getUser(), ExperimentJSONConverter.DEFAULT_SETTINGS); + + return success(successMessage.toString(), ret); + } + } + + // Helper class that prepares and executes the QueryUpdateService.insertRows() on the data or material table. + private abstract class DerivedOutputs + { + private final @NotNull Map> _parentInputNames; + private final @Nullable Map _defaultValues; + private final @Nullable List _values; + private final int _outputCount; + private final String _rolePrefix; + + + public DerivedOutputs(@NotNull Map> parentInputNames, @Nullable Map defaultValues, @Nullable List values, int outputCount, String rolePrefix) + { + _parentInputNames = parentInputNames; + _defaultValues = defaultValues; + _values = values; + _outputCount = outputCount; + _rolePrefix = rolePrefix; + } + + public Pair>, List> prepareRows() + { + List> rows = new ArrayList<>(); + List roles = new ArrayList<>(); + int unknownOutputDataCount = 0; + + for (int i = 0; i < _outputCount; i++) + { + Map row = new CaseInsensitiveHashMap<>(); + if (_defaultValues != null) + row.putAll(_defaultValues); + DerivationSpec spec = _values != null && i < _values.size() ? _values.get(i) : null; + String role = null; + if (spec != null) + { + row.putAll(spec.values); + role = spec.role; + } + + // NOTE: Input parents are added to each row, but are only used for name generation and not for derivation. + // NOTE: We will derive the inserted samples in a single derivation run after the sample/date have been inserted. + row.putAll(_parentInputNames); + + rows.add(row); + + if (StringUtils.trimToNull(role) == null) + { + role = _rolePrefix + (unknownOutputDataCount == 0 ? "" : Integer.toString(unknownOutputDataCount + 1)); + unknownOutputDataCount++; + } + roles.add(role); + } + return Pair.of(rows, roles); + } + + protected abstract TableInfo createTable(); + + protected abstract List getExpObject(List> insertedRows); + + public Map createOutputs() throws BatchValidationException, DuplicateKeyException, SQLException, QueryUpdateServiceException + { + Pair>, List> pair = prepareRows(); + List> rows = pair.first; + List roles = pair.second; + + TableInfo table = createTable(); + QueryUpdateService qus = table.getUpdateService(); + if (qus == null) + throw new IllegalStateException(); + + Map configParams = new HashMap<>(); + // Skip derivation during insert -- DeriveAction will call ExperimentService.get().derive() after samples are inserted + configParams.put(SampleTypeUpdateServiceDI.Options.SkipDerivation, true); + + BatchValidationException qusErrors = new BatchValidationException(); + List> insertedRows = qus.insertRows(getUser(), getContainer(), rows, qusErrors, configParams, null); + if (qusErrors.hasErrors()) + throw qusErrors; + + if (insertedRows.size() != roles.size()) + throw new IllegalStateException("Expected to create " + roles.size() + " new exp objects for derivation"); + + List outputs = getExpObject(insertedRows); + if (outputs.size() != roles.size()) + throw new IllegalStateException("Expected to create " + roles.size() + " new exp objects for derivation"); + + Map outputMap = new HashMap<>(); + for (int i = 0; i < outputs.size(); i++) + { + String role = roles.get(i); + T data = outputs.get(i); + outputMap.put(data, role); + } + + return outputMap; + } + } + } + + public static class CreateExperimentForm extends ExperimentForm implements DataRegionSelection.DataSelectionKeyForm + { + private boolean _addSelectedRuns; + private String _dataRegionSelectionKey; + + public boolean isAddSelectedRuns() + { + return _addSelectedRuns; + } + + public void setAddSelectedRuns(boolean addSelectedRuns) + { + _addSelectedRuns = addSelectedRuns; + } + + @Override + public String getDataRegionSelectionKey() + { + return _dataRegionSelectionKey; + } + + @Override + public void setDataRegionSelectionKey(String dataRegionSelectionKey) + { + _dataRegionSelectionKey = dataRegionSelectionKey; + } + } + + @RequiresPermission(InsertPermission.class) + @ActionNames("createRunGroup, createExperiment") + public class CreateRunGroupAction extends FormViewAction + { + @Override + public ModelAndView getView(CreateExperimentForm form, boolean reshow, BindException errors) + { + // HACK - convert ExperimentForm to not be a BeanViewForm + form.setAddSelectedRuns("true".equals(getViewContext().getRequest().getParameter("addSelectedRuns"))); + form.setDataRegionSelectionKey(getViewContext().getRequest().getParameter(DataRegionSelection.DATA_REGION_SELECTION_KEY)); + + DataRegion drg = new DataRegion(); + + drg.addHiddenFormField(ActionURL.Param.returnUrl, getViewContext().getRequest().getParameter(ActionURL.Param.returnUrl.name())); + drg.addHiddenFormField("addSelectedRuns", Boolean.toString("true".equals(getViewContext().getRequest().getParameter("addSelectedRuns")))); + form.setDataRegionSelectionKey(getViewContext().getRequest().getParameter(DataRegionSelection.DATA_REGION_SELECTION_KEY)); + // Fix issue 27562 - include session-stored selection + if (form.getDataRegionSelectionKey() != null) + { + for (String rowId : DataRegionSelection.getSelected(getViewContext(), form.getDataRegionSelectionKey(), false)) + { + drg.addHiddenFormField(DataRegion.SELECT_CHECKBOX_NAME, rowId); + } + } + drg.addHiddenFormField(DataRegionSelection.DATA_REGION_SELECTION_KEY, getViewContext().getRequest().getParameter(DataRegionSelection.DATA_REGION_SELECTION_KEY)); + + drg.addColumns(ExperimentServiceImpl.get().getTinfoExperiment(), "RowId,Name,LSID,ContactId,ExperimentDescriptionURL,Hypothesis,Comments,Created"); + + DisplayColumn col = drg.getDisplayColumn("RowId"); + col.setVisible(false); + drg.getDisplayColumn("LSID").setVisible(false); + drg.getDisplayColumn("Created").setVisible(false); + + ButtonBar bb = new ButtonBar(); + bb.setStyle(ButtonBar.Style.separateButtons); + ActionButton insertButton = new ActionButton(new ActionURL(CreateRunGroupAction.class, getContainer()), "Submit", ActionButton.Action.POST); + bb.add(insertButton); + + drg.setButtonBar(bb); + + return new InsertView(drg, errors); + } + + + @Override + public boolean handlePost(CreateExperimentForm form, BindException errors) throws Exception + { + // This is strange... but the "Create new run group..." menu item on the run grid always POSTs, probably to + // allow for long lists of run IDs. This "noPost" parameter on the initial POST is used to inform the action + // that it wants to display the form, not try to save anything yet. + if (!"true".equals(getViewContext().getRequest().getParameter("noPost"))) + { + form.setAddSelectedRuns("true".equals(getViewContext().getRequest().getParameter("addSelectedRuns"))); + form.setDataRegionSelectionKey(getViewContext().getRequest().getParameter(DataRegionSelection.DATA_REGION_SELECTION_KEY)); + + Experiment exp = form.getBean(); + if (exp.getName() == null || exp.getName().trim().isEmpty()) + { + errors.reject(ERROR_MSG, "You must specify a name for the experiment"); + } + else + { + int maxNameLength = ExperimentService.get().getTinfoExperimentRun().getColumn("Name").getScale(); + if (exp.getName().length() > maxNameLength) + { + errors.reject(ERROR_MSG, "Name of the experiment must be " + maxNameLength + " characters or less."); + } + } + + String lsid; + int suffix = 1; + do + { + String template = "urn:lsid:" + XarContext.LSID_AUTHORITY_SUBSTITUTION + ":Experiment.Folder-" + XarContext.CONTAINER_ID_SUBSTITUTION + ":" + exp.getName(); + if (suffix > 1) + { + template = template + suffix; + } + suffix++; + lsid = LsidUtils.resolveLsidFromTemplate(template, new XarContext("Experiment Creation", getContainer(), getUser()), ExpExperiment.DEFAULT_CPAS_TYPE); + } + while (ExperimentService.get().getExpExperiment(lsid) != null); + exp.setLSID(lsid); + exp.setContainer(getContainer()); + + if (errors.getErrorCount() == 0) + { + ExpExperimentImpl wrapper = new ExpExperimentImpl(exp); + wrapper.save(getUser()); + + if (form.isAddSelectedRuns()) + { + addSelectedRunsToExperiment(wrapper, form.getDataRegionSelectionKey()); + } + + if (form.getReturnUrl() != null) + { + throw new RedirectException(form.getReturnActionURL()); + } + throw new RedirectException(ExperimentUrlsImpl.get().getShowExperimentsURL(getContainer())); + } + } + return true; + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("runGroups"); + root.addChild("Create Run Group"); + } + + @Override + public URLHelper getSuccessURL(CreateExperimentForm createExperimentForm) + { + return null; // null is used to show the form in the case where IDs are POSTed from the grid + } + + @Override + public void validateCommand(CreateExperimentForm target, Errors errors) { } + } + + public static class MoveRunsForm implements DataRegionSelection.DataSelectionKeyForm + { + private String _targetContainerId; + private String _dataRegionSelectionKey; + + @Override + public String getDataRegionSelectionKey() + { + return _dataRegionSelectionKey; + } + + @Override + public void setDataRegionSelectionKey(String key) + { + _dataRegionSelectionKey = key; + } + + public String getTargetContainerId() + { + return _targetContainerId; + } + + public void setTargetContainerId(String targetContainerId) + { + _targetContainerId = targetContainerId; + } + } + + @RequiresPermission(DeletePermission.class) + public class MoveRunsLocationAction extends SimpleViewAction + { + @Override + public ModelAndView getView(MoveRunsForm form, BindException errors) + { + ActionURL moveURL = new ActionURL(MoveRunsAction.class, getContainer()); + PipelineRootContainerTree ct = new PipelineRootContainerTree(getUser(), moveURL) + { + private boolean _clickHandlerRegistered = false; + + @Override + protected void renderCellContents(StringBuilder html, Container c, ActionURL url, boolean hasRoot) + { + boolean renderLink = hasRoot && !c.equals(getContainer()); + + if (renderLink) + { + html.append(""); + } + html.append(PageFlowUtil.filter(c.getName())); + if (renderLink) + { + html.append(""); + } + + if (!_clickHandlerRegistered) + { + HttpView.currentPageConfig().addHandlerForQuerySelector("a.move-target-container", "click", "moveTo(this.attributes.getNamedItem('data-objectid').value);" ); + _clickHandlerRegistered = true; + } + } + }; + ct.setInitialLevel(1); + + MoveRunsBean bean = new MoveRunsBean(ct, form.getDataRegionSelectionKey()); + JspView result = new JspView<>("/org/labkey/experiment/moveRunsLocation.jsp", bean); + result.setTitle("Choose Destination Folder"); + result.setFrame(WebPartView.FrameType.PORTAL); + return result; + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Move Runs"); + } + } + + + @RequiresPermission(DeletePermission.class) + public class MoveRunsAction extends FormHandlerAction + { + private Container _targetContainer; + + @Override + public void validateCommand(MoveRunsForm target, Errors errors) + { + } + + @Override + public boolean handlePost(MoveRunsForm form, BindException errors) + { + _targetContainer = ContainerManager.getForId(form.getTargetContainerId()); + if (_targetContainer == null || !_targetContainer.hasPermission(getUser(), InsertPermission.class)) + { + throw new UnauthorizedException(); + } + + Set runIds = DataRegionSelection.getSelectedIntegers(getViewContext(), form.getDataRegionSelectionKey(), false); + List runs = new ArrayList<>(); + for (Long runId : runIds) + { + ExpRun run = ExperimentService.get().getExpRun(runId); + if (run != null) + { + runs.add(run); + } + } + + ViewBackgroundInfo info = getViewBackgroundInfo(); + info.setContainer(_targetContainer); + + try + { + ExperimentService.get().moveRuns(info, getContainer(), runs); + if (form.getDataRegionSelectionKey() != null) + DataRegionSelection.clearAll(getViewContext(), form.getDataRegionSelectionKey()); + } + catch (IOException e) + { + throw new NotFoundException("Failed to initialize move. Check that the pipeline root is configured correctly. " + e); + } + return true; + } + + @Override + public ActionURL getSuccessURL(MoveRunsForm form) + { + return urlProvider(PipelineUrls.class).urlBegin(_targetContainer); + } + } + + public static class ShowExternalDocsForm + { + private String _objectURI; + private String _propertyURI; + + public String getObjectURI() + { + return _objectURI; + } + + public void setObjectURI(String objectURI) + { + _objectURI = objectURI; + } + + public String getPropertyURI() + { + return _propertyURI; + } + + public void setPropertyURI(String propertyURI) + { + _propertyURI = propertyURI; + } + } + + @RequiresPermission(ReadPermission.class) + public static class ShowExternalDocsAction extends SimpleViewAction + { + @Override + public ModelAndView getView(ShowExternalDocsForm form, BindException errors) throws Exception + { + Map props = OntologyManager.getPropertyObjects(getContainer(), form.getObjectURI()); + ObjectProperty prop = props.get(form.getPropertyURI()); + if (prop == null || !getContainer().equals(prop.getContainer())) + { + throw new NotFoundException(); + } + URI uri = new URI(prop.getStringValue()); + File f = new File(uri); + if (!f.exists()) + { + throw new NotFoundException(); + } + + PageFlowUtil.streamFile(getViewContext().getResponse(), f.toPath(), false); + return null; + } + + @Override + public void addNavTrail(NavTree root) + { + } + } + + + // TODO: DotGraph has been adding a "runId" parameter, but ShowGraphMoreListAction + public static ActionURL getShowGraphMoreListURL(Container c, @Nullable Long runId, @NotNull String objtype) + { + ActionURL url = new ActionURL(ShowGraphMoreListAction.class, c); + + if (null != runId) + url.addParameter("runId", runId); + + url.addParameter("objtype", objtype); + + return url; + } + + + @RequiresPermission(ReadPermission.class) + public static class ShowGraphMoreListAction extends SimpleViewAction + { + private ExperimentRunForm _form; + + @Override + public ModelAndView getView(ExperimentRunForm form, BindException errors) + { + _form = form; + return new GraphMoreGrid(getContainer(), errors, getViewContext().getActionURL()); + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild(new NavTree("Experiments", ExperimentUrlsImpl.get().getShowExperimentsURL(getContainer()))); + ExpRun run = ExperimentService.get().getExpRun(_form.getRowId()); + if (run != null) + { + root.addChild(new NavTree("Experiment Run", ExperimentUrlsImpl.get().getRunGraphURL(_form.lookupRun()))); + } + root.addChild(new NavTree("Selected Protocol Applications")); + } + } + + @RequiresPermission(DesignAssayPermission.class) + public class AssayXarFileAction extends MutatingApiAction + { + + @Override + public Object execute(Object o, BindException errors) throws Exception + { + ApiSimpleResponse response = new ApiSimpleResponse(); + + if (!(getViewContext().getRequest() instanceof MultipartHttpServletRequest)) + throw new BadRequestException("Expected MultipartHttpServletRequest when posting files."); + + if (!PipelineService.get().hasValidPipelineRoot(getContainer())) + { + return false; + } + + MultipartFile formFile = getFileMap().get("file"); + if (formFile == null) + { + errors.reject(ERROR_MSG, "No file was posted by the browser."); + return false; + } + + byte[] bytes = formFile.getBytes(); + if (bytes.length == 0) + { + errors.reject(ERROR_MSG, "No file was posted by the browser."); + return false; + } + + PipeRoot pipeRoot = PipelineService.get().findPipelineRoot(getContainer()); + FileLike systemDir = pipeRoot.ensureSystemDirectory(); + FileLike uploadDir = systemDir.resolveChild("UploadedXARs"); + FileUtil.createDirectories(uploadDir); + if (!uploadDir.isDirectory()) + { + errors.reject(ERROR_MSG, "Unable to create a 'system/UploadedXARs' directory under the pipeline root"); + return false; + } + String userDirName = getUser().getEmail(); + if (userDirName == null || userDirName.isEmpty()) + { + userDirName = GUEST_DIRECTORY_NAME; + } + FileLike userDir = uploadDir.resolveChild(userDirName); + FileUtil.createDirectories(userDir); + if (!userDir.isDirectory()) + { + errors.reject(ERROR_MSG, "Unable to create an 'UploadedXARs/" + userDirName + "' directory under the pipeline root"); + return false; + } + + FileLike xarFile = userDir.resolveChild(formFile.getOriginalFilename()); + + // As this is multi-part will need to use finally to close, to prevent a stream closure exception + try (OutputStream out = new BufferedOutputStream(xarFile.openOutputStream())) + { + out.write(bytes); + } + catch (IOException e) + { + errors.reject(ERROR_MSG, "Unable to write uploaded XAR file to " + xarFile); + return false; + } + //noinspection EmptyCatchBlock + + ExperimentPipelineJob job = new ExperimentPipelineJob(getViewBackgroundInfo(), xarFile, + "Uploaded file", true, pipeRoot); + PipelineService.get().queueJob(job); + + response.put("success", true); + return response; + } + } + + @RequiresPermission(InsertPermission.class) + public class ImportXarFileAction extends FormHandlerAction + { + @Override + public void validateCommand(ImportXarForm target, Errors errors) + { + } + + @Override + public boolean handlePost(ImportXarForm form, BindException errors) throws Exception + { + for (FileLike f : form.getValidatedFiles(getContainer())) + { + if (f.isFile()) + { + ExperimentPipelineJob job = new ExperimentPipelineJob(getViewBackgroundInfo(), f, "Experiment Import", false, form.getPipeRoot(getContainer())); + + // TODO: Configure module resources with the appropriate log location per container + if (form.getModule() != null) + { + FileLike logFile = form.getPipeRoot(getContainer()).getLogDirectory(true).resolveChild("module-resource-xar.log"); + job.setLogFile(logFile); + } + + PipelineService.get().queueJob(job); + } + else + { + throw new NotFoundException("Expected a file but found a directory: " + f.getName()); + } + } + + return true; + } + + @Override + public URLHelper getSuccessURL(ImportXarForm importXarForm) + { + return getContainer().getStartURL(getUser()); + } + } + + + @RequiresPermission(InsertPermission.class) + public class ImportXarAction extends MutatingApiAction + { + @Override + public Object execute(ImportXarForm form, BindException errors) throws Exception + { + ApiSimpleResponse response = new ApiSimpleResponse(); + + List> archives = new ArrayList<>(); + for (FileLike f : form.getValidatedFiles(getContainer())) + { + Map archive = new HashMap<>(); + ExperimentPipelineJob job = new ExperimentPipelineJob(getViewBackgroundInfo(), f, "Experiment Import", false, form.getPipeRoot(getContainer())); + + // TODO: Configure module resources with the appropriate log location per container + if (form.getModule() != null) + { + FileLike logFile = form.getPipeRoot(getContainer()).getLogDirectory(true).resolveChild("module-resource-xar.log"); + job.setLogFile(logFile); + } + + PipelineService.get().queueJob(job); + + archive.put("file", f.getName()); + archive.put("job", job.getJobGUID()); + archive.put("path", form.getPath()); // echo back the public path + + archives.add(archive); + } + + response.put("success", true); + response.put("archives", archives); + + return response; + } + } + + + /** + * User: jeckels + * Date: Jan 27, 2008 + */ + public static class ExperimentUrlsImpl implements ExperimentUrls + { + public ActionURL getOverviewURL(Container c) + { + return new ActionURL(BeginAction.class, c); + } + + @Override + public ActionURL getExperimentDetailsURL(Container c, ExpExperiment expExperiment) + { + return new ActionURL(DetailsAction.class, c).addParameter("rowId", expExperiment.getRowId()); + } + + public ActionURL getShowSampleURL(Container c, ExpMaterial material) + { + return getMaterialDetailsBaseURL(c, null).addParameter("rowId", material.getRowId()); + } + + @Override + public ActionURL getExportProtocolURL(Container container, ExpProtocol protocol) + { + return new ActionURL(ExperimentController.ExportProtocolsAction.class, container). + addParameter("protocolId", protocol.getRowId()). + addParameter("xarFileName", protocol.getName() + ".xar"); + } + + @Override + public ActionURL getMoveRunsLocationURL(Container container) + { + return new ActionURL(ExperimentController.MoveRunsLocationAction.class, container); + } + + @Override + public ActionURL getProtocolDetailsURL(ExpProtocol protocol) + { + return new ActionURL(ProtocolDetailsAction.class, protocol.getContainer()).addParameter("rowId", protocol.getRowId()); + } + + @Override + public ActionURL getProtocolApplicationDetailsURL(ExpProtocolApplication app) + { + return getShowApplicationURL(app.getContainer(), app.getRowId()); + } + + public ActionURL getProtocolGridURL(Container c) + { + return new ActionURL(ShowProtocolGridAction.class, c); + } + + public ActionURL getRunGraphDetailURL(ExpRun run) + { + return getShowRunGraphDetailURL(run.getContainer(), run.getRowId()); + } + + @Override + public ActionURL getRunGraphDetailURL(ExpRun run, @Nullable ExpData focus) + { + return getRunGraphDetailURL(run, focus, DotGraph.TYPECODE_DATA); + } + + public ActionURL getRunGraphDetailURL(ExpRun run, @Nullable ExpMaterial focus) + { + return getRunGraphDetailURL(run, focus, DotGraph.TYPECODE_MATERIAL); + } + + public ActionURL getRunGraphDetailURL(ExpRun run, @Nullable ExpProtocolApplication focus) + { + return getRunGraphDetailURL(run, focus, DotGraph.TYPECODE_PROT_APP); + } + + private ActionURL getRunGraphDetailURL(ExpRun run, @Nullable ExpObject focus, String typeCode) + { + ActionURL result = getShowRunGraphDetailURL(run.getContainer(), run.getRowId()); + result.addParameter("detail", "true"); + if (focus != null) + { + result.addParameter("focus", typeCode + focus.getRowId()); + } + return result; + } + + @Override + public ActionURL getRunGraphURL(Container container, long runId) + { + return ExperimentController.getRunGraphURL(container, runId); + } + + @Override + public ActionURL getRunGraphURL(ExpRun run) + { + return getRunGraphURL(run.getContainer(), run.getRowId()); + } + + @Override + public ActionURL getRunTextURL(Container c, long runId) + { + return new ActionURL(ShowRunTextAction.class, c).addParameter("rowId", runId); + } + + @Override + public ActionURL getRunTextURL(ExpRun run) + { + return getRunTextURL(run.getContainer(), run.getRowId()); + } + + @Override + public ActionURL getDeleteExperimentsURL(Container container, URLHelper returnUrl) + { + return new ActionURL(DeleteSelectedExperimentsAction.class, container).addReturnUrl(returnUrl); + } + + @Override + public ActionURL getDeleteProtocolURL(@NotNull ExpProtocol protocol, URLHelper returnUrl) + { + ActionURL result = new ActionURL(DeleteProtocolByRowIdsAction.class, protocol.getContainer()); + result.addParameter("singleObjectRowId", protocol.getRowId()); + if (returnUrl != null) + { + result.addReturnUrl(returnUrl); + } + return result; + } + + @Override + public ActionURL getAddRunsToExperimentURL(Container c, ExpExperiment exp) + { + return new ActionURL(AddRunsToExperimentAction.class, c).addParameter("expRowId", exp.getRowId()); + } + + @Override + public ActionURL getShowRunsURL(Container c, ExperimentRunType type) + { + ActionURL result = new ActionURL(ShowRunsAction.class, c); + result.addParameter("experimentRunFilter", type.getDescription()); + return result; + } + + public ActionURL getShowExperimentsURL(Container c) + { + return new ActionURL(ShowRunGroupsAction.class, c); + } + + @Override + public ActionURL getShowSampleTypeListURL(Container c) + { + return getShowSampleTypeListURL(c, null); + } + + @Override + public ActionURL getShowSampleTypeURL(ExpSampleType sampleType) + { + return getShowSampleTypeURL(sampleType, sampleType.getContainer()); + } + + @Override + public ActionURL getShowSampleTypeURL(ExpSampleType sampleType, Container container) + { + return new ActionURL(ShowSampleTypeAction.class, container).addParameter("rowId", sampleType.getRowId()); + } + + public ActionURL getExperimentListURL(Container container) + { + return new ActionURL(ShowRunGroupsAction.class, container); + } + + public ActionURL getShowSampleTypeListURL(Container c, String errorMessage) + { + ActionURL url = new ActionURL(ListSampleTypesAction.class, c); + if (errorMessage != null) + { + url.addParameter("errorMessage", errorMessage); + } + return url; + } + + @Override + public ActionURL getDataClassListURL(Container c) + { + return getDataClassListURL(c, null); + } + + public ActionURL getDataClassListURL(Container c, String errorMessage) + { + ActionURL url = new ActionURL(ListDataClassAction.class, c); + if (errorMessage != null) + { + url.addParameter("errorMessage", errorMessage); + } + return url; + } + + @Override + public ActionURL getDeleteDatasURL(Container c, URLHelper returnUrl) + { + ActionURL url = new ActionURL(DeleteSelectedDataAction.class, c); + if (returnUrl != null) + url.addReturnUrl(returnUrl); + return url; + } + + public ActionURL getDeleteSelectedExperimentsURL(Container c, URLHelper returnUrl) + { + ActionURL result = new ActionURL(DeleteSelectedExperimentsAction.class, c); + if (returnUrl != null) + result.addReturnUrl(returnUrl); + return result; + } + + @Override + public ActionURL getDeleteSelectedExpRunsURL(Container container, URLHelper returnUrl) + { + return new ActionURL(DeleteSelectedExpRunsAction.class, container).addReturnUrl(returnUrl); + } + + public ActionURL getShowUpdateURL(ExpExperiment experiment) + { + return new ActionURL(ShowUpdateAction.class, experiment.getContainer()).addParameter("rowId", experiment.getRowId()); + } + + @Override + public ActionURL getRemoveSelectedExpRunsURL(Container container, URLHelper returnUrl, ExpExperiment exp) + { + return new ActionURL(RemoveSelectedExpRunsAction.class, container).addReturnUrl(returnUrl).addParameter("expRowId", exp.getRowId()); + } + + @Override + public ActionURL getCreateRunGroupURL(Container container, URLHelper returnUrl, boolean addSelectedRuns) + { + ActionURL result = new ActionURL(CreateRunGroupAction.class, container); + if (returnUrl != null) + { + result.addReturnUrl(returnUrl); + } + if (addSelectedRuns) + { + result.addParameter("addSelectedRuns", "true"); + } + return result; + } + + + public static ExperimentUrlsImpl get() + { + return (ExperimentUrlsImpl) urlProvider(ExperimentUrls.class); + } + + public ActionURL getBeginURL(Container container) + { + return new ActionURL(BeginAction.class, container); + } + + @Override + public ActionURL getDomainEditorURL(Container container, String domainURI, boolean createOrEdit) + { + Domain domain = PropertyService.get().getDomain(container, domainURI); + if (domain != null) + return getDomainEditorURL(container, domain); + + ActionURL url = new ActionURL(PropertyController.EditDomainAction.class, container); + url.addParameter("domainURI", domainURI); + if (createOrEdit) + url.addParameter("createOrEdit", true); + return url; + } + + @Override + public ActionURL getDomainEditorURL(Container container, Domain domain) + { + ActionURL url = new ActionURL(PropertyController.EditDomainAction.class, container); + url.addParameter("domainId", domain.getTypeId()); + return url; + } + + @Override + public ActionURL getCreateDataClassURL(Container container) + { + return new ActionURL(EditDataClassAction.class, container); + } + + @Override + public ActionURL getShowDataClassURL(Container container, long rowId) + { + ActionURL url = new ActionURL(ShowDataClassAction.class, container); + url.addParameter("rowId", rowId); + return url; + } + + @Override + public ActionURL getShowFileURL(ExpData data, boolean inline) + { + ActionURL result = getShowFileURL(data.getContainer()).addParameter("rowId", data.getRowId()); + if (inline) + { + result.addParameter("inline", inline); + } + return result; + } + + @Override + public ActionURL getMaterialDetailsURL(ExpMaterial material) + { + return getMaterialDetailsURL(material.getContainer(), material.getRowId()); + } + + @Override + public ActionURL getMaterialDetailsURL(Container c, long materialRowId) + { + return getMaterialDetailsBaseURL(c, null).addParameter("rowId", materialRowId); + } + + @Override + public ActionURL getMaterialDetailsBaseURL(Container c, @Nullable String materialIdFieldKey) + { + return new ActionURL(ShowMaterialAction.class, c); + } + + @Override + public ActionURL getCreateSampleTypeURL(Container container) + { + return new ActionURL(EditSampleTypeAction.class, container); + } + + @Override + public ActionURL getImportSamplesURL(Container container, String sampleTypeName) + { + ActionURL url = new ActionURL(ImportSamplesAction.class, container); + url.addParameter("query.queryName", sampleTypeName); + url.addParameter("schemaName", "exp.materials"); + return url; + } + + @Override + public ActionURL getImportDataURL(Container container, String dataClassName) + { + ActionURL url = new ActionURL(ImportDataAction.class, container); + url.addParameter("query.queryName", dataClassName); + url.addParameter("schemaName", "exp.data"); + return url; + } + + @Override + public ActionURL getDataDetailsURL(ExpData data) + { + return new ActionURL(ShowDataAction.class, data.getContainer()).addParameter("rowId", data.getRowId()); + } + + @Override + public ActionURL getShowFileURL(Container c) + { + return new ActionURL(ShowFileAction.class, c); + } + + @Override + public ActionURL getSetFlagURL(Container container) + { + return new ActionURL(SetFlagAction.class, container); + } + + @Override + public ActionURL getShowRunGraphURL(ExpRun run) + { + return ExperimentController.getRunGraphURL(run.getContainer(), run.getRowId()); + } + + @Override + public ActionURL getRepairTypeURL(Container container) + { + return new ActionURL(TypesController.RepairAction.class, container); + } + + @Override + public ActionURL getUpdateMaterialQueryRowAction(Container c, TableInfo table) + { + ActionURL url = new ActionURL(UpdateMaterialQueryRowAction.class, c); + url.addParameter("schemaName", "samples"); + url.addParameter(QueryView.DATAREGIONNAME_DEFAULT + "." + QueryParam.queryName, table.getName()); + + return url; + } + + @Override + public ActionURL getInsertMaterialQueryRowAction(Container c, TableInfo table) + { + ActionURL url = new ActionURL(InsertMaterialQueryRowAction.class, c); + url.addParameter(QueryView.DATAREGIONNAME_DEFAULT + "." + QueryParam.queryName, table.getName()); + + return url; + } + + @Override + public ActionURL getDataClassAttachmentDownloadAction(Container c) + { + return new ActionURL(ExperimentController.DataClassAttachmentDownloadAction.class, c); + } + + } + + private static abstract class BaseResolveLsidApiAction extends ReadOnlyApiAction + { + protected Set _seeds; + + @Override + public void validateForm(F form, Errors errors) + { + if (null != form.getLsids()) + { + _seeds = new LinkedHashSet<>(form.getLsids().size()); + for (String lsid : form.getLsids()) + { + Identifiable id = LsidManager.get().getObject(lsid); + if (id == null) + throw new NotFoundException("Unable to resolve object: " + lsid); + + // ensure the user has read permission in the seed container + if (!getContainer().equals(id.getContainer())) + { + if (!id.getContainer().hasPermission(getUser(), ReadPermission.class)) + throw new UnauthorizedException("User does not have permission to read object: " + lsid); + } + + _seeds.add(id); + } + } + else + { + throw new ApiUsageException("Starting lsids required"); + } + } + } + + @RequiresPermission(ReadPermission.class) + public static class ResolveAction extends BaseResolveLsidApiAction + { + @Override + public Object execute(ResolveLsidsForm form, BindException errors) + { + var settings = new ExperimentJSONConverter.Settings(form); + var data = _seeds.stream().map(n -> ExperimentJSONConverter.serialize(n, getUser(), settings)).collect(toList()); + return new ApiSimpleResponse("data", data); + } + } + + @RequiresPermission(ReadPermission.class) + public static class LineageAction extends BaseResolveLsidApiAction + { + @Override + public Object execute(ExpLineageOptions options, BindException errors) throws Exception + { + ExpLineageServiceImpl.get().streamLineage(getContainer(), getUser(), getViewContext().getResponse(), _seeds, options); + return null; + } + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(AdminPermission.class) + public static class RebuildEdgesAction extends MutatingApiAction + { + @Override + public Object execute(ExperimentRunForm form, BindException errors) + { + if (form.getRowId() != 0 || form.getLsid() != null) + { + ExpRun run = form.lookupRun(); + if (!run.getContainer().hasPermission(getUser(), ReadPermission.class)) + throw new UnauthorizedException("Not permitted"); + + ExperimentServiceImpl.get().syncRunEdges(run); + } + else + { + // should this require site admin permissions? + ExperimentServiceImpl.get().rebuildAllRunEdges(); + } + return success(); + } + } + + private static class VerifyEdgesForm extends ExperimentRunForm + { + private Integer _limit; + + public Integer getLimit() + { + return _limit; + } + + public void setLimit(Integer limit) + { + _limit = limit; + } + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(AdminPermission.class) + public static class VerifyEdgesAction extends ReadOnlyApiAction + { + @Override + public Object execute(VerifyEdgesForm form, BindException errors) + { + if (form.getRowId() != 0 || form.getLsid() != null) + { + ExpRun run = form.lookupRun(); + if (!run.getContainer().hasPermission(getUser(), ReadPermission.class)) + throw new UnauthorizedException("Not permitted"); + + ExperimentServiceImpl.get().verifyRunEdges(run); + } + else + { + ExperimentServiceImpl.get().verifyAllEdges(getContainer(), form.getLimit()); + } + return success(); + } + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(AdminPermission.class) + public static class RebuildAncestorsAction extends MutatingApiAction + { + @Override + public Object execute(Object form, BindException errors) + { + ClosureQueryHelper.truncateAndRecreate(); + return success(); + } + } + + + @Marshal(Marshaller.Jackson) + @RequiresPermission(AdminPermission.class) + public static class CheckDataClassesIndexedAction extends ReadOnlyApiAction + { + @Override + public Object execute(Object o, BindException errors) throws Exception + { + List> notInIndex = new ArrayList<>(100); + + List list = ExperimentService.get().getDataClasses(getContainer(), false); + for (ExpDataClass dc : list) + { + for (ExpData d : dc.getDatas()) + { + String docId = d.getDocumentId(); + if (docId != null) + { + SearchService.SearchHit hit = SearchService.get().find(docId); + if (hit == null) + { + JSONObject props = ExperimentJSONConverter.serializeData(d, getUser(), ExperimentJSONConverter.DEFAULT_SETTINGS); + props.put("docid", docId); + notInIndex.add(props.toMap()); + } + } + } + } + + return success(notInIndex); + } + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(AdminPermission.class) + public static class CheckEdgesAction extends ReadOnlyApiAction + { + @Override + public Object execute(Object o, BindException errors) throws Exception + { + List result; + DbSchema schema = ExperimentService.get().getSchema(); + TableInfo edgeTable = schema.getTable("Edge"); + + if (null != edgeTable.getColumn("fromObjectId")) + { + var edges = new SqlSelector(ExperimentService.get().getSchema(), "SELECT fromObjectId, toObjectId FROM exp.Edge") + .resultSetStream() + .map(r -> { try { return new Pair<>(r.getInt(1), r.getInt(2)); } catch (SQLException x) { throw new RuntimeException(x); } }) + .collect(toList()); + var cycles = (new GraphAlgorithms()).detectCycleInDirectedGraph(edges); + result = cycles.stream().map(e -> new Integer[]{e.first, e.second}).collect(toList()); + } + else + { + var edges = new SqlSelector(ExperimentService.get().getSchema(), "SELECT fromLsid, toLsid FROM exp.Edge") + .resultSetStream() + .map(r -> { try { return new Pair<>(r.getString(1), r.getString(2)); } catch (SQLException x) { throw new RuntimeException(x); } }) + .collect(toList()); + var cycles = (new GraphAlgorithms()).detectCycleInDirectedGraph(edges); + result = cycles.stream().map(e -> new String[]{e.first, e.second}).collect(toList()); + } + + JSONObject ret = new JSONObject(); + ret.put("result", result); + ret.put("success", true); + return ret; + } + } + + @RequiresPermission(UpdatePermission.class) + public static class UpdateMaterialQueryRowAction extends UserSchemaAction + { + @Override + protected QueryForm createQueryForm(ViewContext context) + { + QueryForm form = new QueryForm("samples", null); + form.setViewContext(getViewContext()); + form.bindParameters(getViewContext().getBindPropertyValues()); + return form; + } + + @Override + public BindException bindParameters(PropertyValues m) throws Exception + { + BindException bind = super.bindParameters(m); + + QueryUpdateForm tableForm = (QueryUpdateForm)bind.getTarget(); + + long sampleId = getSampleId(tableForm); + + ExpMaterial material = ExperimentService.get().getExpMaterial(sampleId); + if (material == null) + throw new NotFoundException("Invalid material: " + tableForm.getPkVal()); + + return bind; + } + + private static long getSampleId(QueryUpdateForm tableForm) + { + Long sampleId = null; + try + { + sampleId = ConvertHelper.convert(tableForm.getPkVal(), Long.class); + } + catch (ConversionException e) + { + } + if (null == sampleId) + throw new NotFoundException("Invalid RowId: " + tableForm.getPkVal()); + return sampleId; + } + + @Override + public ModelAndView getView(QueryUpdateForm tableForm, boolean reshow, BindException errors) + { + long sampleId = getSampleId(tableForm); + + ExpMaterial material = ExperimentService.get().getExpMaterial(sampleId); + if (material == null) + throw new NotFoundException("Invalid material: " + tableForm.getPkVal()); + + boolean isAliquot = !StringUtils.isEmpty(material.getAliquotedFromLSID()); + + TableInfo tableInfo = tableForm.getTable(); + Map scopedFields = new CaseInsensitiveHashMap<>(); + for (DomainProperty dp : tableInfo.getDomain().getProperties()) + { + if (!ExpSchema.DerivationDataScopeType.All.name().equalsIgnoreCase(dp.getDerivationDataScope())) + scopedFields.put(dp.getName(), ExpSchema.DerivationDataScopeType.ChildOnly.name().equalsIgnoreCase(dp.getDerivationDataScope())); + } + + for (var column : tableInfo.getColumns()) + { + String columnName = column.getName(); + if (scopedFields.containsKey(columnName)) + { + boolean isAliquotField = scopedFields.get(columnName); + boolean show = (isAliquot && isAliquotField) || (!isAliquot && !isAliquotField); + ((BaseColumnInfo)column).setUserEditable(show); + ((BaseColumnInfo)column).setHidden(!show); + } + } + + ButtonBar bb = createSubmitCancelButtonBar(tableForm); + UpdateView view = new UpdateView(tableForm, errors); + view.getDataRegion().setButtonBar(bb); + return view; + } + + @Override + public boolean handlePost(QueryUpdateForm tableForm, BindException errors) + { + doInsertUpdate(tableForm, errors, false); + return 0 == errors.getErrorCount(); + } + + @Override + public void addNavTrail(NavTree root) + { + super.addNavTrail(root); + root.addChild("Edit " + _form.getQueryName()); + } + } + + @RequiresPermission(InsertPermission.class) + public static class InsertMaterialQueryRowAction extends UserSchemaAction + { + @Override + protected QueryForm createQueryForm(ViewContext context) + { + QueryForm form = new QueryForm("samples", null); + form.setViewContext(getViewContext()); + form.bindParameters(getViewContext().getBindPropertyValues()); + + return form; + } + + @Override + public ModelAndView getView(QueryUpdateForm tableForm, boolean reshow, BindException errors) + { + TableInfo tableInfo = tableForm.getTable(); + Map propertyFields = new CaseInsensitiveHashMap<>(); + for (DomainProperty dp : tableInfo.getDomain().getProperties()) + { + propertyFields.put(dp.getName(), ExpSchema.DerivationDataScopeType.ChildOnly.name().equalsIgnoreCase(dp.getDerivationDataScope())); + } + + for (var column : tableInfo.getColumns()) + { + String columnName = column.getName(); + if (propertyFields.containsKey(columnName)) + { + boolean isAliquotField = propertyFields.get(columnName); + ((BaseColumnInfo)column).setUserEditable(!isAliquotField); + ((BaseColumnInfo)column).setHidden(isAliquotField); + } + } + + InsertView view = new InsertView(tableForm, errors); + view.getDataRegion().setButtonBar(createSubmitCancelButtonBar(tableForm)); + return view; + } + + @Override + public boolean handlePost(QueryUpdateForm tableForm, BindException errors) + { + doInsertUpdate(tableForm, errors, true); + return 0 == errors.getErrorCount(); + } + + @Override + public void addNavTrail(NavTree root) + { + super.addNavTrail(root); + root.addChild("Insert " + _form.getQueryName()); + } + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(ReadPermission.class) + public static class SaveFindIdsAction extends ReadOnlyApiAction + { + + public static final String FIND_BY_IDS_SESSION_KEY_PREFIX = "findByIds"; + + @Override + public Object execute(FindByIdsForm form, BindException errors) throws Exception + { + HttpServletRequest request = getViewContext().getRequest(); + String key = form.getSessionKey(); + boolean removePrevious = false; + + if (key == null) + { + removePrevious = true; + key = FIND_BY_IDS_SESSION_KEY_PREFIX + "_" + UniqueID.getServerSessionScopedUID(); + } + + if (request != null) + { + if (removePrevious) + SessionHelper.clearAttributesWithPrefix(request, FIND_BY_IDS_SESSION_KEY_PREFIX); + HttpSession session = request.getSession(false); + if (session != null) + { + @SuppressWarnings("unchecked") + List existingIds = (List) session.getAttribute(key); + + // deduplicate from existing ids + if (existingIds != null && form.getSessionKey() != null) + { + existingIds.addAll(form.getIds().stream().filter(id -> !existingIds.contains(id)).toList()); + session.setAttribute(key, existingIds); + } + else + { + session.setAttribute(key, form.getIds()); + } + return success("Saved ids to session key", key); + } + } + + return new SimpleResponse<>(false, "Unable to save to session. Session or request may be null."); + } + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(ReadPermission.class) + public static class SaveOrderedSamplesQueryAction extends ReadOnlyApiAction + { + private static final String SAMPLE_ID_PREFIX = "s:"; + private static final String UNIQUE_ID_PREFIX = "u:"; + + private List _ids; + private Map> _uniqueIdLsids; + + @Override + public void validateForm(FindByIdsForm form, Errors errors) + { + if (form.getSessionKey() == null) + errors.reject(ERROR_REQUIRED, "sessionKey must be provided"); + else + { + _ids = getFindIdsFromSession(form.getSessionKey()); + if (_ids == null || _ids.isEmpty()) + errors.reject(ERROR_REQUIRED, "No ids found corresponding to session key " + form.getSessionKey()); + } + } + + private void ensureUniqueIdLsids() + { + boolean hasUniqueId = _ids.stream().anyMatch(s -> s.startsWith(UNIQUE_ID_PREFIX)); + if (hasUniqueId && _uniqueIdLsids == null) + { + List uniqueIds = _ids.stream().map(s -> s.substring(UNIQUE_ID_PREFIX.length())).toList(); + _uniqueIdLsids = ExperimentService.get().getUniqueIdLsids(uniqueIds, getUser(), getContainer()); + } + } + + @Override + public Object execute(FindByIdsForm form, BindException errors) throws Exception + { + ensureUniqueIdLsids(); + + SQLFragment select = getOrderedRowsSql(); + // need to set the key field so selections are possible + // need the SampleTypeUnits so we will display using that unit + String metadata = + """ + + + + + true + true + + + true + + + true + + +
+
"""; + QueryDefinition def = QueryService.get().saveSessionQuery(getViewContext(), getContainer(), ExperimentServiceImpl.getExpSchema().getName(), select.getSQL(), metadata); + return success("Session query created", Map.of("queryName", def.getName(), "ids", _ids)); + } + + + private List getFindIdsFromSession(String sessionKey) + { + HttpServletRequest request = getViewContext().getRequest(); + List ids = new ArrayList<>(); + if (request != null) + { + HttpSession session = request.getSession(false); + if (session != null) + { + ids = (List) session.getAttribute(sessionKey); + } + } + return ids; + } + + private SQLFragment getOrderedRowsSql() + { + boolean isFMEnabled = InventoryService.isFreezerManagementEnabled(getContainer()); + String samplesTable = isFMEnabled ? "inventory.SampleItems" : "exp.materials"; + List orderedIdCols = new ArrayList<>(Arrays.asList("Id AS ProvidedID", "RowId", "Ordinal")); + List sampleColumns = new ArrayList<>(); + if (!isFMEnabled) + { + sampleColumns.addAll(Arrays.asList( + "S.Name AS SampleID", + "S.MaterialExpDate AS ExpirationDate", + "S.SampleSet as SampleType", + "S.SampleState", + "S.isAliquot", + "S.Created", + "S.CreatedBy" + )); + } + else + { + sampleColumns.addAll(Arrays.asList( + "S.Name AS SampleID", + "S.MaterialExpDate AS ExpirationDate", + "S.LabelColor", + "S.SampleSet", + "S.SampleState", + "S.StoredAmount", + "S.Units", + "S.SampleTypeUnits", + "S.FreezeThawCount", + "S.StorageStatus", + "S.CheckedOutBy", + "S.StorageLocation", + "S.StorageRow", + "S.StorageCol", + "S.StoragePositionNumber", + "S.IsAliquot", + "S.Created", + "S.CreatedBy" + )); + } + + + String sampleIdComma = ""; + String uniqueIdComma = ""; + int index = 1; + SQLFragment sampleIdValuesSql = new SQLFragment(); + SQLFragment uniqueIdValuesSql = new SQLFragment(); + for (String id : _ids) + { + if (id.startsWith(SAMPLE_ID_PREFIX)) + { + sampleIdValuesSql.append(sampleIdComma).append("\t(").appendValue(index); + sampleIdValuesSql.append(", "); + sampleIdValuesSql.append(LabKeySql.quoteString(id.substring(SAMPLE_ID_PREFIX.length()))); + sampleIdValuesSql.append(", "); + sampleIdValuesSql.append(LabKeySql.quoteString("null")); + sampleIdValuesSql.append(")"); + sampleIdComma = "\n,"; + } + else if (id.startsWith(UNIQUE_ID_PREFIX)) + { + String idClean = id.substring(UNIQUE_ID_PREFIX.length()); + + List lsids = _uniqueIdLsids.get(idClean); + if (lsids != null) + { + for (String lsid : lsids) + { + uniqueIdValuesSql.append(uniqueIdComma).append("\t(").appendValue(index); + uniqueIdValuesSql.append(", "); + uniqueIdValuesSql.append(LabKeySql.quoteString(idClean)); + uniqueIdValuesSql.append(", "); + uniqueIdValuesSql.append(LabKeySql.quoteString(lsid)); + uniqueIdValuesSql.append(")"); + uniqueIdComma = "\n,"; + } + } + } + index++; + } + + boolean haveData = !sampleIdValuesSql.isEmpty() || !_uniqueIdLsids.isEmpty(); + SQLFragment sql = new SQLFragment(); + if (!sampleIdValuesSql.isEmpty()) + { + sql.append("WITH _ordered_ids_ AS (\nSELECT * FROM (VALUES\n"); + sql.append(sampleIdValuesSql); + sql.append("\n) AS _values_ )\n"); // name of the alias here doesn't matter + } + if (!uniqueIdValuesSql.isEmpty()) + { + if (!sampleIdValuesSql.isEmpty()) + sql.append(",\n"); + else + sql.append("WITH "); + + sql.append("_ordered_unique_ids_ AS (\nSELECT * FROM (VALUES\n"); + sql.append(uniqueIdValuesSql); + sql.append("\n) AS _values_ )\n"); // name of the alias here doesn't matter + } + + sql.append("SELECT "); + sql.append("\n\tOID.").append(StringUtils.join(orderedIdCols, ",\n\tOID.")); + sql.append(",\n\t").append(StringUtils.join( sampleColumns, ",\n\t")); + sql.append("\nFROM\n("); + if (!sampleIdValuesSql.isEmpty()) + { + sql.append("SELECT\n\tM.RowId,\n\t_ordered_ids_.column1 as Ordinal,\n\t_ordered_ids_.column2 as Id,\n\t_ordered_ids_.column2 as lsid"); + sql.append("\nFROM _ordered_ids_\n"); + sql.append("INNER JOIN exp.materials M ON _ordered_ids_.column2 = M.Name"); + sql.append("\n"); + } + if (!uniqueIdValuesSql.isEmpty()) + { + if (!sampleIdValuesSql.isEmpty()) + sql.append("\nUNION ALL\n\n"); + + sql.append("SELECT\n\tM.RowId,\n\t_ordered_unique_ids_.column1 as Ordinal,\n\t_ordered_unique_ids_.column2 as Id,\n\t_ordered_unique_ids_.column3 as lsid"); + sql.append("\nFROM _ordered_unique_ids_\n"); + sql.append("INNER JOIN exp.materials M ON _ordered_unique_ids_.column3 = M.lsid"); + sql.append("\n"); + } + if (!haveData) // no data to return but return data in the expected shape. + { + sql = new SQLFragment("SELECT\n"); + sql.append(orderedIdCols.stream() + .map(col -> { + int asIndex = col.indexOf("AS"); + if (asIndex > 0) + return "NULL AS " + col.substring(asIndex+ 3); + else + return "NULL AS " + col; + }) + .collect(Collectors.joining(",\t\n"))); + sql.append(",\t\n").append(StringUtils.join(sampleColumns, ",\t\n")); + sql.append("\nFROM ").append(samplesTable).append(" S WHERE 1 = 2"); + return sql; + } + else + { + sql.append(") OID"); + if (isFMEnabled) + sql.append("\nLEFT JOIN inventory.SampleItems S on S.RowId = OID.RowId"); + else + sql.append("\nINNER JOIN exp.materials S on S.RowId = OID.RowId"); + sql.append("\n\nORDER BY Ordinal"); + return sql; + } + } + } + + public static class FindByIdsForm extends FindSessionKeyForm + { + List _ids; + + public List getIds() + { + return _ids; + } + + public void setIds(List ids) + { + _ids = ids; + } + } + + + public static class FindSessionKeyForm + { + private String _sessionKey; + + public String getSessionKey() + { + return _sessionKey; + } + + public void setSessionKey(String sessionKey) + { + _sessionKey = sessionKey; + } + } + + static void validateEntitySequenceForm(EntitySequenceForm form, Errors errors) + { + String kindName = form.getKindName(); + if (StringUtils.isEmpty(kindName) || form.getSeqType() == null) + { + errors.reject(ERROR_REQUIRED, "KindName and SeqType must be provided"); + return; + } + + if (form.getSeqType() == NameGenerator.EntityCounter.genId) + { + if (form.getRowId() == null) + errors.reject(ERROR_REQUIRED, "Data type RowId must be provided for genId"); + } + else if (!SampleTypeDomainKind.NAME.equalsIgnoreCase(kindName)) + { + errors.reject(ERROR_MSG, form.getSeqType() + " is not supported for " + kindName); + } + + if (!SampleTypeDomainKind.NAME.equalsIgnoreCase(kindName) && !DataClassDomainKind.NAME.equalsIgnoreCase(kindName)) + errors.reject(ERROR_MSG, "Invalid KindName. Should be either " + SampleTypeDomainKind.NAME + " or " + DataClassDomainKind.NAME + "."); + + } + + @RequiresPermission(ReadPermission.class) + public static class GetEntitySequenceAction extends ReadOnlyApiAction + { + @Override + public void validateForm(EntitySequenceForm form, Errors errors) + { + validateEntitySequenceForm(form, errors); + } + + @Override + public Object execute(EntitySequenceForm form, BindException errors) throws Exception + { + long value = -1; + if (SampleTypeDomainKind.NAME.equalsIgnoreCase(form.getKindName())) + { + if (form.getSeqType() == NameGenerator.EntityCounter.genId) + { + ExpSampleType sampleType = SampleTypeService.get().getSampleType(form.getRowId()); + if (sampleType != null) + value = sampleType.getCurrentGenId(); + } + else + { + value = SampleTypeService.get().getCurrentCount(form.getSeqType(), getContainer()); + } + + } + else if (DataClassDomainKind.NAME.equalsIgnoreCase(form.getKindName())) + { + ExpDataClass dataClass = ExperimentService.get().getDataClass(form.getRowId()); + if (dataClass != null) + value = dataClass.getCurrentGenId(); + } + + ApiSimpleResponse resp = new ApiSimpleResponse(); + resp.put("success", value > -1); + resp.put("value", value); + return resp; + } + } + + @RequiresPermission(ReadPermission.class) // actual permission checked later + public static class SetEntitySequenceAction extends MutatingApiAction + { + @Override + public void validateForm(EntitySequenceForm form, Errors errors) + { + validateEntitySequenceForm(form, errors); + + if (form.getNewValue() == null || form.getNewValue() < 0) + errors.reject(ERROR_MSG, "Invalid newValue."); + } + + @Override + public Object execute(EntitySequenceForm form, BindException errors) + { + ApiSimpleResponse resp = new ApiSimpleResponse(); + resp.put("success", true); + + try + { + Domain domain = null; + if (SampleTypeDomainKind.NAME.equalsIgnoreCase(form.getKindName())) + { + if (form.getSeqType() == NameGenerator.EntityCounter.genId) + { + if (!getContainer().hasPermission(getUser(), DesignSampleTypePermission.class)) + throw new UnauthorizedException("Insufficient permissions."); + + ExpSampleType sampleType = SampleTypeService.get().getSampleType(form.getRowId()); + if (sampleType != null) + { + sampleType.ensureMinGenId(form.getNewValue()); + domain = sampleType.getDomain(); + } + else + { + resp.put("success", false); + resp.put("error", "Sample type does not exist."); + } + } + else + { + if (!getContainer().hasPermission(getUser(), AdminPermission.class)) + throw new UnauthorizedException("Insufficient permissions."); + + SampleTypeService.get().ensureMinSampleCount(form.getNewValue(), form.getSeqType(), getContainer()); + } + } + else if (DataClassDomainKind.NAME.equalsIgnoreCase(form.getKindName())) + { + if (!getContainer().hasPermission(getUser(), DesignDataClassPermission.class)) + throw new BadRequestException("Insufficient permissions."); + + ExpDataClass dataClass = ExperimentService.get().getDataClass(form.getRowId()); + if (dataClass != null) + { + dataClass.ensureMinGenId(form.getNewValue(), getContainer()); + domain = dataClass.getDomain(); + } + else + { + resp.put("success", false); + resp.put("error", "DataClass does not exist."); + } + } + + if (domain != null) + { + DomainAuditProvider.DomainAuditEvent event = new DomainAuditProvider.DomainAuditEvent(getContainer(), "The genId for domain " + domain.getName() + " has been updated to " + form.getNewValue() + "."); + event.setDomainUri(domain.getTypeURI()); + event.setDomainName(domain.getName()); + AuditLogService.get().addEvent(getUser(), event); + } + } + catch (ExperimentException e) + { + resp.put("success", false); + resp.put("error", e.getMessage()); + } + + return resp; + } + } + + public static class EntitySequenceForm + { + private String _kindName; + private NameGenerator.EntityCounter _seqType; + private Integer _rowId; + private Long _newValue; + + public Integer getRowId() + { + return _rowId; + } + + public void setRowId(Integer rowId) + { + _rowId = rowId; + } + + public String getKindName() + { + return _kindName; + } + + public void setKindName(String kindName) + { + _kindName = kindName; + } + + public Long getNewValue() + { + return _newValue; + } + + public void setNewValue(Long newValue) + { + this._newValue = newValue; + } + + public NameGenerator.EntityCounter getSeqType() + { + return _seqType; + } + + public void setSeqType(String seqType) + { + _seqType = NameGenerator.EntityCounter.valueOf(seqType); + } + + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(ReadPermission.class) + public static class GetCrossFolderDataSelectionAction extends ReadOnlyApiAction + { + @Override + public void validateForm(CrossFolderSelectionForm form, Errors errors) + { + if (form.getDataRegionSelectionKey() == null && form.getRowIds() == null) + errors.reject(ERROR_REQUIRED, "You must provide either a set of rowIds or a dataRegionSelectionKey."); + if (!"samples".equalsIgnoreCase(form.getDataType()) && !"exp.data".equalsIgnoreCase(form.getDataType())&& !"assay".equalsIgnoreCase(form.getDataType())) + errors.reject(ERROR_REQUIRED, "Data type (sample, data or assayrun) must be specified."); + } + + @Override + public Object execute(CrossFolderSelectionForm form, BindException errors) + { + Pair result = ExperimentServiceImpl.get().getCurrentAndCrossFolderDataCount(form.getIds(false), form.getDataType(), getContainer()); + + ApiSimpleResponse resp = new ApiSimpleResponse(); + resp.put("success", true); + resp.put("currentFolderSelectionCount", result.first); + resp.put("crossFolderSelectionCount", result.second); + + return success(resp); + } + } + + public static class CrossFolderSelectionForm extends DataViewSnapshotSelectionForm + { + private String _dataType; + private String _picklistName; + + public String getDataType() + { + return _dataType; + } + + public void setDataType(String dataType) + { + _dataType = dataType; + } + + public String getPicklistName() + { + return _picklistName; + } + + public void setPicklistName(String picklistName) + { + _picklistName = picklistName; + } + + @Override + public Set getIds(boolean clear) + { + Set selectedIds; + + if (_rowIds != null) + selectedIds = _rowIds; + else if (isUseSnapshotSelection()) + selectedIds = new HashSet<>(DataRegionSelection.getSnapshotSelectedIntegers(getViewContext(), getDataRegionSelectionKey())); + else + selectedIds = DataRegionSelection.getSelectedIntegers(getViewContext(), getDataRegionSelectionKey(), clear); + + if (_picklistName != null) + { + User user = getViewContext().getUser(); + Container container = getViewContext().getContainer(); + UserSchema schema = ListService.get().getUserSchema(user, container); + TableInfo tInfo = schema.getTable(_picklistName); + if (tInfo != null) + { + SimpleFilter filter = SimpleFilter.createContainerFilter(container); + filter.addInClause(FieldKey.fromParts("id"), selectedIds); + TableSelector selector = new TableSelector(tInfo, Collections.singleton("SampleID"), filter, null); + return new HashSet<>(selector.getArrayList(Long.class)); + } + } + return selectedIds; + } + } + + @RequiresPermission(AdminPermission.class) + public static class RecomputeAliquotRollup extends SimpleViewAction + { + @Override + public void addNavTrail(NavTree root) + { + } + + @Override + public ModelAndView getView(Object o, BindException errors) throws SQLException + { + try (var ignore = SpringActionController.ignoreSqlUpdates()) + { + Container container = getContainer(); + + List sampleTypes = SampleTypeService.get() + .getSampleTypes(container, true); + + HtmlStringBuilder builder = HtmlStringBuilder.of(); + builder.unsafeAppend(""); + + SampleTypeService service = SampleTypeService.get(); + for (ExpSampleType sampleType : sampleTypes) + { + int updatedCount; + updatedCount = service.recomputeSampleTypeRollup(sampleType, container); + // we could check "if (0 < updatedCount) refresh(rollup)", but since this is a "manual" usage lets just always refresh + SampleTypeServiceImpl.get().refreshSampleTypeMaterializedView(sampleType, update); + builder.unsafeAppend(""); + } + + builder.unsafeAppend("
Sample Type#Recomputed
") + .append(sampleType.getName()) + .unsafeAppend("") + .append(updatedCount) + .unsafeAppend("
"); + return new HtmlView("Aliquot Rollup Recalculation Result", builder); + } + } + } + + /* Also see API CheckEdgesAction */ + @RequiresPermission(TroubleshooterPermission.class) + public static class CycleCheckAction extends FormViewAction + { + List cycleObjectIds = null; + + @Override + public void validateCommand(Object target, Errors errors) + { + + } + + @Override + public ModelAndView getView(Object o, boolean reshow, BindException errors) + { + if (!reshow) + { + return new HtmlView( + DIV("This operation can use a lot of memory.", + LK.FORM(at(method,"POST"), + PageFlowUtil.button("Continue").submit(true))) + ); + } + + if (null == cycleObjectIds) + return new HtmlView(HtmlString.of("No cycles found")); + + Map map = new LongHashMap<>(); + var cf = new ContainerFilter.AllFolders(getUser()); + var materials = ExperimentServiceImpl.get().getExpMaterialsByObjectId(cf, cycleObjectIds); + materials.forEach( (m) -> map.put(m.getObjectId(), m)); + var datas = ExperimentServiceImpl.get().getExpDatasByObjectId(cf, cycleObjectIds); + datas.forEach( (d) -> map.put(d.getObjectId(), d)); + var runs = ExperimentServiceImpl.get().getRunsByObjectId(cf, cycleObjectIds); + runs.forEach( (r) -> map.put(r.getObjectId(), r)); + + ExperimentUrls urls = ExperimentUrls.get(); + return new HtmlView( + DIV("Cycle found involving these objects.", + UL(cycleObjectIds.stream().map((objectid) -> + { + ExpObject exp = map.get(objectid); + if (exp instanceof ExpMaterial mat) + return LI(A(at(target, "_blank", href, urls.getMaterialDetailsURL(mat)), objectid + " : material - " + mat.getName())); + else if (exp instanceof ExpRun run) + return LI(A(at(target, "_blank", href, urls.getRunTextURL(run)), objectid + " : run - " + run.getName())); + else if (exp instanceof ExpData data) + return LI(A(at(target, "_blank", href, urls.getDataDetailsURL(data)), objectid + " : run - " + data.getName())); + else + return LI(String.valueOf(objectid)); + })) + ) + ); + } + + @Override + public boolean handlePost(Object o, BindException errors) + { + var edges = new SqlSelector(ExperimentService.get().getSchema(), "SELECT fromObjectId, toObjectId FROM exp.Edge") + .resultSetStream() + .map(r -> { try { return new Pair<>(r.getInt(1), r.getInt(2)); } catch (SQLException x) { throw new RuntimeException(x); } }) + .collect(toList()); + var cyclesEdges = (new GraphAlgorithms()).detectCycleInDirectedGraph(edges); + + var set = new LinkedHashSet(); + cyclesEdges.forEach( (edge) -> { + set.add(edge.first); + set.add(edge.second); + }); + cycleObjectIds = set.stream().toList(); + return false; + } + + @Override + public URLHelper getSuccessURL(Object o) + { + return null; + } + + @Override + public void addNavTrail(NavTree root) + { + + } + } + + @RequiresPermission(AdminPermission.class) + public static class MissingFilesCheckAction extends ReadOnlyApiAction + { + @Override + public Object execute(Object o, BindException errors) throws Exception + { + Map> info = ExperimentServiceImpl.get().doMissingFilesCheck(getUser(), getContainer(), true); + JSONObject results = new JSONObject(); + for (String containerId : info.keySet()) + { + JSONObject containerResults = new JSONObject(); + for (String sourceName : info.get(containerId).keySet()) + containerResults.put(sourceName, info.get(containerId).get(sourceName).toJSON()); + results.put(containerId, containerResults); + } + + ApiSimpleResponse response = new ApiSimpleResponse(); + response.put("success", true); + response.put("result", results); + return response; + } + } + +}