Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<query xmlns="http://labkey.org/data/xml/query" hidden="true">
<metadata>
<tables xmlns="http://labkey.org/data/xml">
<table tableName="DocumentsGroupedByParentType" tableDbType="NOT_IN_DB">
<columns>
<column columnName="Count">
<url replaceMissing="blankValue">admin-attachmentsForType.view?core.ParentType~eq=${ParentType}&amp;core.containerFilterName=${containerFilterName}</url>
<formatString>#,##0</formatString>
</column>
</columns>
</table>
</tables>
</metadata>
</query>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Identical to DocumentsGroupedByParentType, but this query's .query.xml provides an admin-console-specific Count URL
SELECT ParentType, COUNT(*) AS "Count"
FROM Documents
GROUP BY ParentType
ORDER BY ParentType
2 changes: 0 additions & 2 deletions api/src/org/labkey/api/attachments/AttachmentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,6 @@ static AttachmentService get()
**/
Collection<AttachmentParentType> getAttachmentParentTypes();

HttpView<?> getAdminView(ActionURL currentUrl);

HttpView<?> getFindAttachmentParentsView();

class DuplicateFilenameException extends IOException implements SkipMothershipLogging
Expand Down
126 changes: 93 additions & 33 deletions core/src/org/labkey/core/admin/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import org.labkey.api.action.Marshaller;
import org.labkey.api.action.MutatingApiAction;
import org.labkey.api.action.QueryViewAction;
import org.labkey.api.action.QueryViewAction.QueryExportForm;
import org.labkey.api.action.ReadOnlyApiAction;
import org.labkey.api.action.ReturnUrlForm;
import org.labkey.api.action.SimpleApiJsonForm;
Expand Down Expand Up @@ -105,6 +106,7 @@
import org.labkey.api.data.ConnectionWrapper;
import org.labkey.api.data.Container;
import org.labkey.api.data.Container.ContainerException;
import org.labkey.api.data.ContainerFilter;
import org.labkey.api.data.ContainerManager;
import org.labkey.api.data.ContainerType;
import org.labkey.api.data.ConvertHelper;
Expand Down Expand Up @@ -171,11 +173,9 @@
import org.labkey.api.products.ProductRegistry;
import org.labkey.api.query.DefaultSchema;
import org.labkey.api.query.FieldKey;
import org.labkey.api.query.QueryParam;
import org.labkey.api.query.QuerySchema;
import org.labkey.api.query.QueryService;
import org.labkey.api.query.QuerySettings;
import org.labkey.api.query.QueryUrls;
import org.labkey.api.query.QueryView;
import org.labkey.api.query.RuntimeValidationException;
import org.labkey.api.query.SchemaKey;
Expand All @@ -189,6 +189,7 @@
import org.labkey.api.security.AdminConsoleAction;
import org.labkey.api.security.CSRF;
import org.labkey.api.security.Directive;
import org.labkey.api.security.ElevatedUser;
import org.labkey.api.security.Group;
import org.labkey.api.security.GroupManager;
import org.labkey.api.security.IgnoresTermsOfUse;
Expand Down Expand Up @@ -477,9 +478,7 @@ public static void registerAdminConsoleLinks()

// Diagnostics
AdminConsole.addLink(Diagnostics, "actions", new ActionURL(ActionsAction.class, root));
AdminConsole.addLink(Diagnostics, "attachments", PageFlowUtil.urlProvider(QueryUrls.class).urlExecuteQuery(root, "core", "DocumentsGroupedByParentType")
.addParameter("query." + QueryParam.containerFilterName, "AllFolders"), ApplicationAdminPermission.class);
AdminConsole.addLink(Diagnostics, "attachments - old", new ActionURL(AttachmentsAction.class, root));
AdminConsole.addLink(Diagnostics, "attachments", new ActionURL(AttachmentsAction.class, root));
AdminConsole.addLink(Diagnostics, "caches", new ActionURL(CachesAction.class, root));
AdminConsole.addLink(Diagnostics, "check database", new ActionURL(DbCheckerAction.class, root), AdminOperationsPermission.class);
AdminConsole.addLink(Diagnostics, "credits", new ActionURL(CreditsAction.class, root));
Expand Down Expand Up @@ -2609,49 +2608,36 @@ public void addNavTrail(NavTree root)
}
}

private abstract class AbstractPostgresAction extends QueryViewAction<QueryViewAction.QueryExportForm, QueryView>
private abstract class AbstractPostgresAction extends AbstractAdminQueryAction
{
private final String _queryName;

protected AbstractPostgresAction(String queryName)
{
super(QueryExportForm.class);
_queryName = queryName;
super("query", queryName);
}

@Override
protected UserSchema getUserSchema()
{
return new PostgresUserSchema(getUser(), getContainer());
}

@Override
protected QueryView createQueryView(QueryExportForm form, BindException errors, boolean forExport, @Nullable String dataRegion) throws Exception
{
if (!CoreSchema.getInstance().getSqlDialect().isPostgreSQL())
{
throw new NotFoundException("Only available with Postgres as the primary database");
throw new NotFoundException("Available only with Postgres as the primary database");
}

QuerySettings qSettings = new QuerySettings(getViewContext(), "query", _queryName);
QueryView result = new QueryView(new PostgresUserSchema(getUser(), getContainer()), qSettings, errors)
{
@Override
public DataView createDataView()
{
// Troubleshooters don't have normal read access to the root container so grant them special access
// for these queries
DataView view = super.createDataView();
view.getRenderContext().getViewContext().addContextualRole(ReaderRole.class);
return view;
}
};
result.setTitle(_queryName);
result.setFrame(WebPartView.FrameType.PORTAL);
return result;
return super.createQueryView(form, errors, forExport, dataRegion);
}

@Override
public void addNavTrail(NavTree root)
{
setHelpTopic("postgresActivity");
addAdminNavTrail(root, "Postgres " + _queryName, this.getClass());
addAdminNavTrail(root, "Postgres " + getQueryName(), this.getClass());
}

}

@AdminConsoleAction
Expand Down Expand Up @@ -3581,22 +3567,96 @@ public URLHelper getSuccessURL(SystemMaintenanceForm form)
}
}

private abstract static class AbstractAdminQueryAction extends QueryViewAction<QueryExportForm, QueryView>
{
private final String _schemaName;
private final String _queryName;

protected AbstractAdminQueryAction(String schemaName, String queryName)
{
super(QueryExportForm.class);
_schemaName = schemaName;
_queryName = queryName;
}

@Override
public void setViewContext(ViewContext context)
{
// Troubleshooters don't have read permissions but DataRegion requires it. I don't love poking an elevated
// user into the ViewContext, but this is the only way I could get DataRegion to see read permission on
// tables that are wrapped by a query (e.g., core.Documents used by DocumentsGroupedByParentType.sql).
context.setUser(ElevatedUser.getElevatedUser(context.getUser(), ReaderRole.class));
super.setViewContext(context);
}

@Override
protected QueryView createQueryView(QueryExportForm form, BindException errors, boolean forExport, @Nullable String dataRegion) throws Exception
{
QuerySettings qSettings = new QuerySettings(getViewContext(), _schemaName, _queryName);
if (qSettings.getContainerFilterName() == null)
qSettings.setContainerFilterName(ContainerFilter.Type.AllFolders.name());
QueryView result = new QueryView(getUserSchema(), qSettings, errors);
result.setTitle(_queryName);
result.setFrame(WebPartView.FrameType.PORTAL);
return result;
}

protected String getQueryName()
{
return _queryName;
}

abstract protected UserSchema getUserSchema();
}

@AdminConsoleAction
public class AttachmentsAction extends SimpleViewAction<Object>
public class AttachmentsAction extends AbstractAdminQueryAction
{
@SuppressWarnings("unused") // Invoked via reflection
public AttachmentsAction()
{
super("core", "DocumentsGroupedByParentTypeAdmin");
}

@Override
public ModelAndView getView(Object o, BindException errors)
protected UserSchema getUserSchema()
{
return new CoreQuerySchema(getUser(), getContainer(), false);
}

@Override
public void addNavTrail(NavTree root)
{
addAdminNavTrail(root, "Documents Grouped by Parent Type", getClass());
}
}

@SuppressWarnings("unused") // Linked from core.DocumentsGroupedByParentTypeAdmin
@AdminConsoleAction
public class AttachmentsForTypeAction extends AbstractAdminQueryAction
{
@SuppressWarnings("unused") // Invoked via reflection
public AttachmentsForTypeAction()
{
super("core", "Documents");
}

@Override
protected UserSchema getUserSchema()
{
return AttachmentService.get().getAdminView(getViewContext().getActionURL());
return new CoreQuerySchema(getUser(), getContainer(), false);
}

@Override
public void addNavTrail(NavTree root)
{
addAdminNavTrail(root, "Attachments", getClass());
String parentType = getViewContext().getActionURL().getParameter("core.ParentType~eq");
addAdminNavTrail(root, "Documents Belonging to Parent Type" + (parentType != null ? " \"" + parentType + "\"" : ""), getClass());
}
}

// Left behind with no link in the UI; this could be useful to track down orphaned attachments during the migration
// process. Should delete after attachment migration is complete (late 2026?).
@AdminConsoleAction
public class FindAttachmentParentsAction extends SimpleViewAction<Object>
{
Expand Down
129 changes: 2 additions & 127 deletions core/src/org/labkey/core/attachment/AttachmentServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
import org.labkey.api.attachments.AttachmentDirectory;
import org.labkey.api.attachments.AttachmentFile;
import org.labkey.api.attachments.AttachmentParent;
import org.labkey.api.attachments.AttachmentService;
import org.labkey.api.attachments.AttachmentParentType;
import org.labkey.api.attachments.AttachmentService;
import org.labkey.api.attachments.DocumentWriter;
import org.labkey.api.attachments.FileAttachmentFile;
import org.labkey.api.attachments.SpringAttachmentFile;
Expand Down Expand Up @@ -93,18 +93,15 @@
import org.labkey.api.view.ActionURL;
import org.labkey.api.view.HttpView;
import org.labkey.api.view.JspView;
import org.labkey.api.view.NavTree;
import org.labkey.api.view.NotFoundException;
import org.labkey.api.view.UnauthorizedException;
import org.labkey.api.view.VBox;
import org.labkey.api.view.ViewContext;
import org.labkey.api.view.WebPartView;
import org.labkey.api.webdav.AbstractDocumentResource;
import org.labkey.api.webdav.AbstractWebdavResourceCollection;
import org.labkey.api.webdav.DavException;
import org.labkey.api.webdav.WebdavResolver;
import org.labkey.api.webdav.WebdavResource;
import org.labkey.core.admin.AdminController;
import org.labkey.core.query.AttachmentAuditProvider;
import org.springframework.http.ContentDisposition;
import org.springframework.mock.web.MockMultipartFile;
Expand Down Expand Up @@ -762,129 +759,7 @@ public Collection<AttachmentParentType> getAttachmentParentTypes()
}

@Override
public HttpView<?> getAdminView(ActionURL currentUrl)
{
String requestedType = currentUrl.getParameter("type");
AttachmentParentType attachmentParentType = null != requestedType ? ATTACHMENT_TYPE_MAP.get(requestedType) : null;

if (null == attachmentParentType)
{
boolean findAttachmentParents = "1".equals(currentUrl.getParameter("find"));

// The first query lists all the attachment types and the attachment counts for each. A separate select from
// core.Documents for each type is needed to associate the Type values with the associated rows.
List<SQLFragment> selectStatements = new LinkedList<>();

for (AttachmentParentType type : getAttachmentParentTypes())
{
SQLFragment selectStatement = new SQLFragment();

// Adding unique column RowId ensures we get the proper count
selectStatement.append("SELECT RowId, CAST(").appendValue(type.getUniqueName()).append(" AS VARCHAR(500)) AS Type FROM ")
.append(CoreSchema.getInstance().getTableInfoDocuments(), "d")
.append(" WHERE ");
addAndVerifyWhereSql(type, selectStatement);
selectStatement.append("\n");

selectStatements.add(selectStatement);
}

SQLFragment allSql = new SQLFragment("SELECT Type, COUNT(*) AS Count FROM (\n");
allSql.append(SQLFragment.join(selectStatements, "UNION\n"));
allSql.append(") u\nGROUP BY Type\nORDER BY Type");
ActionURL linkUrl = currentUrl.clone().deleteParameters().addParameter("type", null);

// The second query shows all attachments that we can't associate with a type. We just need to assemble a big
// WHERE NOT clause that ORs the conditions from every registered type.
SQLFragment whereSql = new SQLFragment();
String sep = "";

for (AttachmentParentType type : getAttachmentParentTypes())
{
whereSql.append(sep);
sep = " OR";
whereSql.append("\n(");
addAndVerifyWhereSql(type, whereSql);
whereSql.append(")");
}

SQLFragment unknownSql = new SQLFragment("SELECT d.Container, c.Name, d.Parent, d.DocumentName");

if (findAttachmentParents)
unknownSql.append(", e.TableName");

unknownSql.append(" FROM core.Documents d\n");
unknownSql.append("INNER JOIN core.Containers c ON c.EntityId = d.Container\n");

Set<String> schemasToIgnore = Sets.newCaseInsensitiveHashSet(currentUrl.getParameterValues("ignore"));

if (findAttachmentParents)
{
unknownSql.append("LEFT OUTER JOIN (\n");
addSelectAllEntityIdsSql(unknownSql, schemasToIgnore);
unknownSql.append(") e ON e.EntityId = d.Parent\n");
}

unknownSql.append("WHERE NOT (");
unknownSql.append(whereSql);
unknownSql.append(")\n");
unknownSql.append("ORDER BY Container, Parent, DocumentName");

WebPartView<?> unknownView = getResultSetView(unknownSql, "Unknown Attachments", null);
NavTree navMenu = new NavTree();

if (!findAttachmentParents)
{
navMenu.addChild(new NavTree("Search for Attachment Parents (Be Patient)",
new ActionURL(AdminController.AttachmentsAction.class, ContainerManager.getRoot()).addParameter("find", 1).addParameter("ignore", "Audit"))
);
}
else
{
navMenu.addChild(new NavTree("Remove TableName Column",
new ActionURL(AdminController.AttachmentsAction.class, ContainerManager.getRoot()))
);

if (schemasToIgnore.isEmpty())
{
navMenu.addChild(new NavTree("Ignore Audit Schema",
new ActionURL(AdminController.AttachmentsAction.class, ContainerManager.getRoot()).addParameter("find", 1).addParameter("ignore", "Audit"))
);
}
else
{
navMenu.addChild(new NavTree("Include All Schemas",
new ActionURL(AdminController.AttachmentsAction.class, ContainerManager.getRoot()).addParameter("find", 1))
);
}
}
unknownView.setNavMenu(navMenu);

return new VBox(getResultSetView(allSql, "Attachment Types and Counts", linkUrl), unknownView);
}
else
{
// This query lists all the documents associated with a single type.
SQLFragment oneTypeSql = new SQLFragment("SELECT d.Container, c.Name, d.Parent, d.DocumentName FROM core.Documents d\n" +
"INNER JOIN core.Containers c ON c.EntityId = d.Container\n" +
"WHERE ");
addAndVerifyWhereSql(attachmentParentType, oneTypeSql);
oneTypeSql.append("\nORDER BY Container, Parent, DocumentName");

return getResultSetView(oneTypeSql, attachmentParentType.getUniqueName() + " Attachments", null);
}
}

private void addAndVerifyWhereSql(AttachmentParentType attachmentType, SQLFragment sql)
{
int initialLength = sql.length();
attachmentType.addWhereSql(sql, "d.Parent", "d.DocumentName");
if (initialLength == sql.length())
throw new UnsupportedOperationException("AttachmentType: '" + attachmentType.getUniqueName() + "' did not update attachment WHERE clause.");
}

@Override
// Joins each row of core.Documents to the table(s) (if any) that contain an entityid matching the document's parent
// Joins each row of core.Documents to the table(s) (if any) that contain an EntityId matching the document's parent
public WebPartView<?> getFindAttachmentParentsView()
{
SQLFragment sql = new SQLFragment("SELECT RowId, CreatedBy, Created, ModifiedBy, Modified, Container, DocumentName, TableName FROM core.Documents LEFT OUTER JOIN (\n");
Expand Down
Loading