From 4dc6ea911fe3ce8a619bdfe00c61c659acfbe133 Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 14 Oct 2025 18:35:11 -0700 Subject: [PATCH 1/7] Issue 54062: Strip folder name from displayed name a file field --- .../api/data/AbstractFileDisplayColumn.java | 651 ++++++------ .../study/assay/FileLinkDisplayColumn.java | 932 +++++++++--------- .../study/StudyDatasetFileFieldTest.java | 7 +- 3 files changed, 800 insertions(+), 790 deletions(-) diff --git a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java index 74bb606c48c..28a0a633c92 100644 --- a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java +++ b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java @@ -1,324 +1,329 @@ -/* - * Copyright (c) 2011-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.data; - -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.attachments.Attachment; -import org.labkey.api.util.DOM; -import org.labkey.api.util.DOM.Renderable; -import org.labkey.api.util.GUID; -import org.labkey.api.util.HtmlString; -import org.labkey.api.util.InputBuilder; -import org.labkey.api.util.LinkBuilder; -import org.labkey.api.util.MimeMap; -import org.labkey.api.util.PageFlowUtil; -import org.labkey.api.util.StringExpression; -import org.labkey.api.view.HttpView; -import org.labkey.api.writer.HtmlWriter; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.InputStream; - -import static org.labkey.api.util.DOM.A; -import static org.labkey.api.util.DOM.Attribute.alt; -import static org.labkey.api.util.DOM.Attribute.href; -import static org.labkey.api.util.DOM.Attribute.src; -import static org.labkey.api.util.DOM.Attribute.style; -import static org.labkey.api.util.DOM.Attribute.target; -import static org.labkey.api.util.DOM.Attribute.title; -import static org.labkey.api.util.DOM.DIV; -import static org.labkey.api.util.DOM.IMG; -import static org.labkey.api.util.DOM.at; -import static org.labkey.api.util.DOM.id; -import static org.labkey.api.util.PageFlowUtil.jsString; - -/** - * Provides a consistent UI for both attachment (BLOB) and file link (file system) files - */ -public abstract class AbstractFileDisplayColumn extends DataColumn -{ - protected String _thumbnailWidth; - protected String _popupWidth; - - public static final String UNAVAILABLE_FILE_SUFFIX = " (unavailable)"; - - public AbstractFileDisplayColumn(ColumnInfo col) - { - super(col); - } - - @Override - public void renderDetailsCellContents(RenderContext ctx, HtmlWriter out) - { - renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); - } - - @Override - public void renderGridCellContents(RenderContext ctx, HtmlWriter out) - { - renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); - } - - /** @return the short name of the file (not including full path) */ - protected abstract String getFileName(RenderContext ctx, Object value); - - protected abstract InputStream getFileContents(RenderContext ctx, Object value) throws FileNotFoundException; - - protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, boolean link, boolean thumbnail) - { - renderIconAndFilename(ctx, out, fileValue, null, null, link, thumbnail); - } - - protected boolean isImage(String filename) - { - return filename.toLowerCase().endsWith(".png") - || filename.toLowerCase().endsWith(".jpeg") - || filename.toLowerCase().endsWith(".jpg") - || filename.toLowerCase().endsWith(".gif"); - } - - protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean link, boolean thumbnail) - { - if (null != fileValue && !StringUtils.isEmpty(fileValue)) - { - // equivalent of DisplayColumn.renderURL. - // Don't want to call renderUrl (DataColumn.renderUrl) to skip unnecessary displayValue check - StringExpression s = compileExpression(ctx.getViewContext()); - String displayName = getFileName(ctx, fileValue); - boolean unavailable = displayName.endsWith(UNAVAILABLE_FILE_SUFFIX); - String url = null == s || unavailable ? null : s.eval(ctx); - boolean isImage = isImage(fileValue); - FileImageRenderHelper renderHelper = createRenderHelper(ctx, url, fileValue, displayName, fileIconUrl, popupIconUrl, thumbnail, isImage); - - - if (link && null != url) - { - A( - at(title, "Download attached file") - .at(getLinkTarget() != null && MimeMap.DEFAULT.canInlineFor(fileValue), target, getLinkTarget()) - .at(href, url), - (Renderable) ret -> { - renderPopup(renderHelper, url, fileIconUrl, displayName, fileValue, thumbnail, isImage, out); - return ret; - } - ).appendTo(out); - } - else - { - renderPopup(renderHelper, url, fileIconUrl, displayName, fileValue, thumbnail, isImage, out); - } - } - else - { - out.write(HtmlString.NBSP); - } - } - - private void renderPopup(FileImageRenderHelper renderHelper, String url, @Nullable String fileIconUrl, String displayName, String filename, boolean thumbnail, boolean isImage, HtmlWriter out) - { - if ((url != null || fileIconUrl != null) && thumbnail && isImage) - { - // controls whether to render a popup image on hover, otherwise just render an image with a click handler - // to navigate to the url - if (renderHelper.renderPopupImage()) - out.write(PageFlowUtil.popupHelp(renderHelper.createPopupImage(), displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); - else - out.write(PageFlowUtil.popupHelp(displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); - } - else - { - if (url != null && thumbnail && MimeMap.DEFAULT.isInlineImageFor(new File(filename)) ) - { - if (renderHelper.renderPopupImage()) - out.write(PageFlowUtil.popupHelp(renderHelper.createPopupImage(), displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); - else - out.write(PageFlowUtil.popupHelp(displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); - } - else - { - renderHelper.createThumbnailImage().appendTo(out); - } - } - } - - protected FileImageRenderHelper createRenderHelper(RenderContext ctx, String url, String filename, String displayName, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean isThumbnail, boolean isImage) - { - return new FileImageRenderHelper(ctx, url, filename, displayName, fileIconUrl, popupIconUrl, isThumbnail, isImage); - } - - /** - * Helper class to generate the HTML for the various portions of a file or image grid cell content - * - * Tests to run if you touch this class: FileAttachmentColumnTest, InlineImagesAssayTest, InlineImagesListTest, SimpleModuleTest - */ - public class FileImageRenderHelper - { - protected RenderContext _ctx; - protected String _displayName; - protected String _url; - protected String _filename; - protected String _fileIconUrl; - protected String _popupIconUrl; - protected boolean _isThumbnail; - protected boolean _isImage; - - public FileImageRenderHelper(RenderContext ctx, String url, String filename, String displayName, String fileIconUrl, String popupIconUrl, boolean isThumbnail, boolean isImage) - { - _ctx = ctx; - _url = url; - _filename = filename; - _displayName = displayName; - _fileIconUrl = fileIconUrl; - _popupIconUrl = popupIconUrl; - _isThumbnail = isThumbnail; - _isImage = isImage; - } - - // render the grid cell content - public Renderable createThumbnailImage() - { - if (_url != null && _isThumbnail && _isImage) - { - return IMG( - at( - style, "display:block; height:auto; vertical-align:middle; " + (_thumbnailWidth != null ? "width:" + _thumbnailWidth : "max-width:32px"), - src, _url, title, _displayName - ) - ); - } - else - { - return DOM.createHtmlFragment( - IMG( - at(src, _ctx.getRequest().getContextPath() + (null != _fileIconUrl ? _fileIconUrl : Attachment.getFileIcon(_filename)), alt, "icon") - ), - HtmlString.NBSP, - _displayName - ); - } - } - - public boolean renderPopupImage() - { - return true; - } - - // render the popup image to display on hover - public Renderable createPopupImage() - { - return _url != null ? IMG( - at( - style, "height:auto; " + (_popupWidth != null ? "width:" + _popupWidth : "max-width:300px"), - src, _url - ) - ) : HtmlString.EMPTY_STRING; - } - - // render the click script when a user clicks on the grid cell - public String createClickScript() - { - if (_url == null) - { - return null; - } - if (getLinkTarget() != null) - { - return "window.open(" + jsString(_url) + "," + jsString(getLinkTarget()) + ", 'noopener,noreferrer')"; - } - return "window.location = " + jsString(_url); - } - } - - protected String ensureAbsoluteUrl(RenderContext ctx, String url) - { - if (!url.startsWith(ctx.getRequest().getContextPath())) - { - String lcUrl = url.toLowerCase(); - if (!lcUrl.startsWith("http:") && !lcUrl.startsWith("https:")) - { - if (url.startsWith("/")) - return ctx.getRequest().getContextPath() + url; - else - return ctx.getRequest().getContextPath() + "/" + url; - } - } - return url; - } - - protected boolean hasFileInputHtml() - { - return true; - } - - @Override - public void renderInputHtml(RenderContext ctx, HtmlWriter out, Object value) - { - if (hasFileInputHtml()) - { - String filename = getFileName(ctx, value); - String formFieldName = ctx.getForm().getFormFieldName(getBoundColumn()); - - InputBuilder input = InputBuilder.file() - .name(formFieldName) - .disabled(isDisabledInput(ctx)) - .needsWrapping(false); - - if (null != filename) - { - // Existing value, so tell the user the file name, allow the file to be removed, and a new file uploaded - renderThumbnailAndRemoveLink(out, ctx, filename, input); - } - else - { - // No existing value, so render just the regular element - input.appendTo(out); - } - } - else - super.renderInputHtml(ctx, out, value); - } - - /** - * Enable subclasses to override the warning text - * @param filename being displayed - */ - protected String getRemovalWarningText(String filename) - { - return "Previous file " + filename + " will be removed."; - } - - private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, String filename, InputBuilder filePicker) - { - String divId = GUID.makeGUID(); - String linkId = "remove" + divId; - - DIV( - id(divId), - (Renderable) ret -> { - renderIconAndFilename(ctx, out, filename, false, false); - out.write(HtmlString.NBSP); - out.write("["); - out.write(LinkBuilder.simpleLink("remove", "#").id(linkId)); - out.write("]"); - return ret; - } - ).appendTo(out); - String innerHtml = filePicker + "" + getRemovalWarningText(filename) + ""; - HttpView.currentPageConfig().addHandler(linkId, "click", "document.getElementById(" + jsString(divId) + ").innerHTML = " + jsString(innerHtml)); - } +/* + * Copyright (c) 2011-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.data; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.attachments.Attachment; +import org.labkey.api.util.DOM; +import org.labkey.api.util.DOM.Renderable; +import org.labkey.api.util.GUID; +import org.labkey.api.util.HtmlString; +import org.labkey.api.util.InputBuilder; +import org.labkey.api.util.LinkBuilder; +import org.labkey.api.util.MimeMap; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.StringExpression; +import org.labkey.api.view.HttpView; +import org.labkey.api.writer.HtmlWriter; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; + +import static org.labkey.api.util.DOM.A; +import static org.labkey.api.util.DOM.Attribute.alt; +import static org.labkey.api.util.DOM.Attribute.href; +import static org.labkey.api.util.DOM.Attribute.src; +import static org.labkey.api.util.DOM.Attribute.style; +import static org.labkey.api.util.DOM.Attribute.target; +import static org.labkey.api.util.DOM.Attribute.title; +import static org.labkey.api.util.DOM.DIV; +import static org.labkey.api.util.DOM.IMG; +import static org.labkey.api.util.DOM.at; +import static org.labkey.api.util.DOM.id; +import static org.labkey.api.util.PageFlowUtil.jsString; + +/** + * Provides a consistent UI for both attachment (BLOB) and file link (file system) files + */ +public abstract class AbstractFileDisplayColumn extends DataColumn +{ + protected String _thumbnailWidth; + protected String _popupWidth; + + public static final String UNAVAILABLE_FILE_SUFFIX = " (unavailable)"; + + public AbstractFileDisplayColumn(ColumnInfo col) + { + super(col); + } + + @Override + public void renderDetailsCellContents(RenderContext ctx, HtmlWriter out) + { + renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); + } + + @Override + public void renderGridCellContents(RenderContext ctx, HtmlWriter out) + { + renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); + } + + /** @return the short name of the file (not including full path) */ + protected abstract String getFileName(RenderContext ctx, Object value); + + protected String getFileName(RenderContext ctx, Object value, boolean isDisplay) + { + return getFileName(ctx, value); + } + + protected abstract InputStream getFileContents(RenderContext ctx, Object value) throws FileNotFoundException; + + protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, boolean link, boolean thumbnail) + { + renderIconAndFilename(ctx, out, fileValue, null, null, link, thumbnail); + } + + protected boolean isImage(String filename) + { + return filename.toLowerCase().endsWith(".png") + || filename.toLowerCase().endsWith(".jpeg") + || filename.toLowerCase().endsWith(".jpg") + || filename.toLowerCase().endsWith(".gif"); + } + + protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean link, boolean thumbnail) + { + if (null != fileValue && !StringUtils.isEmpty(fileValue)) + { + // equivalent of DisplayColumn.renderURL. + // Don't want to call renderUrl (DataColumn.renderUrl) to skip unnecessary displayValue check + StringExpression s = compileExpression(ctx.getViewContext()); + String displayName = getFileName(ctx, fileValue, true); + boolean unavailable = displayName.endsWith(UNAVAILABLE_FILE_SUFFIX); + String url = null == s || unavailable ? null : s.eval(ctx); + boolean isImage = isImage(fileValue); + FileImageRenderHelper renderHelper = createRenderHelper(ctx, url, fileValue, displayName, fileIconUrl, popupIconUrl, thumbnail, isImage); + + + if (link && null != url) + { + A( + at(title, "Download attached file") + .at(getLinkTarget() != null && MimeMap.DEFAULT.canInlineFor(fileValue), target, getLinkTarget()) + .at(href, url), + (Renderable) ret -> { + renderPopup(renderHelper, url, fileIconUrl, displayName, fileValue, thumbnail, isImage, out); + return ret; + } + ).appendTo(out); + } + else + { + renderPopup(renderHelper, url, fileIconUrl, displayName, fileValue, thumbnail, isImage, out); + } + } + else + { + out.write(HtmlString.NBSP); + } + } + + private void renderPopup(FileImageRenderHelper renderHelper, String url, @Nullable String fileIconUrl, String displayName, String filename, boolean thumbnail, boolean isImage, HtmlWriter out) + { + if ((url != null || fileIconUrl != null) && thumbnail && isImage) + { + // controls whether to render a popup image on hover, otherwise just render an image with a click handler + // to navigate to the url + if (renderHelper.renderPopupImage()) + out.write(PageFlowUtil.popupHelp(renderHelper.createPopupImage(), displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); + else + out.write(PageFlowUtil.popupHelp(displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); + } + else + { + if (url != null && thumbnail && MimeMap.DEFAULT.isInlineImageFor(new File(filename)) ) + { + if (renderHelper.renderPopupImage()) + out.write(PageFlowUtil.popupHelp(renderHelper.createPopupImage(), displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); + else + out.write(PageFlowUtil.popupHelp(displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); + } + else + { + renderHelper.createThumbnailImage().appendTo(out); + } + } + } + + protected FileImageRenderHelper createRenderHelper(RenderContext ctx, String url, String filename, String displayName, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean isThumbnail, boolean isImage) + { + return new FileImageRenderHelper(ctx, url, filename, displayName, fileIconUrl, popupIconUrl, isThumbnail, isImage); + } + + /** + * Helper class to generate the HTML for the various portions of a file or image grid cell content + * + * Tests to run if you touch this class: FileAttachmentColumnTest, InlineImagesAssayTest, InlineImagesListTest, SimpleModuleTest + */ + public class FileImageRenderHelper + { + protected RenderContext _ctx; + protected String _displayName; + protected String _url; + protected String _filename; + protected String _fileIconUrl; + protected String _popupIconUrl; + protected boolean _isThumbnail; + protected boolean _isImage; + + public FileImageRenderHelper(RenderContext ctx, String url, String filename, String displayName, String fileIconUrl, String popupIconUrl, boolean isThumbnail, boolean isImage) + { + _ctx = ctx; + _url = url; + _filename = filename; + _displayName = displayName; + _fileIconUrl = fileIconUrl; + _popupIconUrl = popupIconUrl; + _isThumbnail = isThumbnail; + _isImage = isImage; + } + + // render the grid cell content + public Renderable createThumbnailImage() + { + if (_url != null && _isThumbnail && _isImage) + { + return IMG( + at( + style, "display:block; height:auto; vertical-align:middle; " + (_thumbnailWidth != null ? "width:" + _thumbnailWidth : "max-width:32px"), + src, _url, title, _displayName + ) + ); + } + else + { + return DOM.createHtmlFragment( + IMG( + at(src, _ctx.getRequest().getContextPath() + (null != _fileIconUrl ? _fileIconUrl : Attachment.getFileIcon(_filename)), alt, "icon") + ), + HtmlString.NBSP, + _displayName + ); + } + } + + public boolean renderPopupImage() + { + return true; + } + + // render the popup image to display on hover + public Renderable createPopupImage() + { + return _url != null ? IMG( + at( + style, "height:auto; " + (_popupWidth != null ? "width:" + _popupWidth : "max-width:300px"), + src, _url + ) + ) : HtmlString.EMPTY_STRING; + } + + // render the click script when a user clicks on the grid cell + public String createClickScript() + { + if (_url == null) + { + return null; + } + if (getLinkTarget() != null) + { + return "window.open(" + jsString(_url) + "," + jsString(getLinkTarget()) + ", 'noopener,noreferrer')"; + } + return "window.location = " + jsString(_url); + } + } + + protected String ensureAbsoluteUrl(RenderContext ctx, String url) + { + if (!url.startsWith(ctx.getRequest().getContextPath())) + { + String lcUrl = url.toLowerCase(); + if (!lcUrl.startsWith("http:") && !lcUrl.startsWith("https:")) + { + if (url.startsWith("/")) + return ctx.getRequest().getContextPath() + url; + else + return ctx.getRequest().getContextPath() + "/" + url; + } + } + return url; + } + + protected boolean hasFileInputHtml() + { + return true; + } + + @Override + public void renderInputHtml(RenderContext ctx, HtmlWriter out, Object value) + { + if (hasFileInputHtml()) + { + String filename = getFileName(ctx, value); + String formFieldName = ctx.getForm().getFormFieldName(getBoundColumn()); + + InputBuilder input = InputBuilder.file() + .name(formFieldName) + .disabled(isDisabledInput(ctx)) + .needsWrapping(false); + + if (null != filename) + { + // Existing value, so tell the user the file name, allow the file to be removed, and a new file uploaded + renderThumbnailAndRemoveLink(out, ctx, filename, input); + } + else + { + // No existing value, so render just the regular element + input.appendTo(out); + } + } + else + super.renderInputHtml(ctx, out, value); + } + + /** + * Enable subclasses to override the warning text + * @param filename being displayed + */ + protected String getRemovalWarningText(String filename) + { + return "Previous file " + filename + " will be removed."; + } + + private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, String filename, InputBuilder filePicker) + { + String divId = GUID.makeGUID(); + String linkId = "remove" + divId; + + DIV( + id(divId), + (Renderable) ret -> { + renderIconAndFilename(ctx, out, filename, false, false); + out.write(HtmlString.NBSP); + out.write("["); + out.write(LinkBuilder.simpleLink("remove", "#").id(linkId)); + out.write("]"); + return ret; + } + ).appendTo(out); + String innerHtml = filePicker + "" + getRemovalWarningText(filename) + ""; + HttpView.currentPageConfig().addHandler(linkId, "click", "document.getElementById(" + jsString(divId) + ").innerHTML = " + jsString(innerHtml)); + } } \ No newline at end of file diff --git a/api/src/org/labkey/api/study/assay/FileLinkDisplayColumn.java b/api/src/org/labkey/api/study/assay/FileLinkDisplayColumn.java index 7e23d8cb357..bd38585f554 100644 --- a/api/src/org/labkey/api/study/assay/FileLinkDisplayColumn.java +++ b/api/src/org/labkey/api/study/assay/FileLinkDisplayColumn.java @@ -1,461 +1,471 @@ -/* - * 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.study.assay; - -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.admin.CoreUrls; -import org.labkey.api.attachments.Attachment; -import org.labkey.api.data.AbstractFileDisplayColumn; -import org.labkey.api.data.ColumnInfo; -import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerManager; -import org.labkey.api.data.DisplayColumn; -import org.labkey.api.data.RemappingDisplayColumnFactory; -import org.labkey.api.data.RenderContext; -import org.labkey.api.data.TableViewForm; -import org.labkey.api.exp.PropertyDescriptor; -import org.labkey.api.files.FileContentService; -import org.labkey.api.pipeline.PipeRoot; -import org.labkey.api.pipeline.PipelineService; -import org.labkey.api.query.DetailsURL; -import org.labkey.api.query.FieldKey; -import org.labkey.api.query.SchemaKey; -import org.labkey.api.security.User; -import org.labkey.api.security.permissions.ReadPermission; -import org.labkey.api.settings.AppProps; -import org.labkey.api.util.ContainerContext; -import org.labkey.api.util.FileUtil; -import org.labkey.api.util.NetworkDrive; -import org.labkey.api.util.PageFlowUtil; -import org.labkey.api.util.Path; -import org.labkey.api.util.URIUtil; -import org.labkey.api.view.ActionURL; -import org.labkey.api.webdav.WebdavResource; -import org.labkey.api.webdav.WebdavService; -import org.labkey.api.writer.HtmlWriter; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class FileLinkDisplayColumn extends AbstractFileDisplayColumn -{ - // Issue 46282 - let admins choose if files should be rendered inside browser or downloaded as files - public static final String AS_ATTACHMENT_FORMAT = "attachment"; - public static final String AS_INLINE_FORMAT = "inline"; - - public static class Factory implements RemappingDisplayColumnFactory - { - private final Container _container; - - private PropertyDescriptor _pd; - private DetailsURL _detailsUrl; - private SchemaKey _schemaKey; - private String _queryName; - private FieldKey _pkFieldKey; - private FieldKey _objectURIFieldKey; - - public Factory(PropertyDescriptor pd, Container c, @NotNull SchemaKey schemaKey, @NotNull String queryName, @NotNull FieldKey pkFieldKey) - { - _pd = pd; - _container = c; - _schemaKey = schemaKey; - _queryName = queryName; - _pkFieldKey = pkFieldKey; - } - - public Factory(PropertyDescriptor pd, Container c, @NotNull FieldKey lsidColumnFieldKey) - { - _pd = pd; - _container = c; - _objectURIFieldKey = lsidColumnFieldKey; - } - - public Factory(DetailsURL detailsURL, Container c, @NotNull FieldKey pkFieldKey) - { - _detailsUrl = detailsURL; - _container = c; - _pkFieldKey = pkFieldKey; - } - - @Override - public Factory remapFieldKeys(@Nullable FieldKey parent, @Nullable Map remap) - { - Factory remapped = this.clone(); - if (remapped._pkFieldKey != null) - { - remapped._pkFieldKey = FieldKey.remap(_pkFieldKey, parent, remap); - if (null == remapped._pkFieldKey) - remapped._pkFieldKey = _pkFieldKey; - } - if (remapped._objectURIFieldKey != null) - { - remapped._objectURIFieldKey = FieldKey.remap(_objectURIFieldKey, parent, remap); - if (null == remapped._objectURIFieldKey) - remapped._objectURIFieldKey = _objectURIFieldKey; - } - return remapped; - } - - @Override - public DisplayColumn createRenderer(ColumnInfo col) - { - if (_pd == null && _detailsUrl != null) - return new FileLinkDisplayColumn(col, _detailsUrl, _container, _pkFieldKey); - else if (_pkFieldKey != null) - return new FileLinkDisplayColumn(col, _pd, _container, _schemaKey, _queryName, _pkFieldKey); - else if (_container != null) - return new FileLinkDisplayColumn(col, _pd, _container, _objectURIFieldKey); - else - throw new IllegalArgumentException("Cannot create a renderer from the specified configuration properties"); - } - - @Override - public FileLinkDisplayColumn.Factory clone() - { - try - { - return (Factory)super.clone(); - } - catch (CloneNotSupportedException e) - { - throw new RuntimeException(e); - } - } - } - - private final Container _container; - - private FieldKey _pkFieldKey; - private FieldKey _objectURIFieldKey; - - /** Use schemaName/queryName and pk FieldKey value to resolve File in CoreController.DownloadFileLinkAction. */ - public FileLinkDisplayColumn(ColumnInfo col, PropertyDescriptor pd, Container container, @NotNull SchemaKey schemaKey, @NotNull String queryName, @NotNull FieldKey pkFieldKey) - { - super(col); - _container = container; - _pkFieldKey = pkFieldKey; - - if (pd.getURL() == null) - { - // Don't stomp over an explicitly configured URL on this column - StringBuilder sb = new StringBuilder("/core/downloadFileLink.view?propertyId="); - sb.append(pd.getPropertyId()); - sb.append("&schemaName="); - sb.append(PageFlowUtil.encodeURIComponent(schemaKey.toString())); - sb.append("&queryName="); - sb.append(PageFlowUtil.encodeURIComponent(queryName)); - sb.append("&pk=${"); - sb.append(pkFieldKey); - sb.append("}"); - sb.append("&modified=${Modified}"); - if (AS_ATTACHMENT_FORMAT.equalsIgnoreCase(col.getFormat())) - { - sb.append("&inline=false"); - } - ContainerContext context = new ContainerContext.FieldKeyContext(new FieldKey(pkFieldKey.getParent(), "Folder")); - setURLExpression(DetailsURL.fromString(sb.toString(), context)); - } - } - - /** Use LSID FieldKey value as ObjectURI to resolve File in CoreController.DownloadFileLinkAction. */ - public FileLinkDisplayColumn(ColumnInfo col, PropertyDescriptor pd, Container container, @NotNull FieldKey objectURIFieldKey) - { - super(col); - _container = container; - _objectURIFieldKey = objectURIFieldKey; - - if (pd.getURL() == null) - { - // Don't stomp over an explicitly configured URL on this column - - ActionURL baseUrl = PageFlowUtil.urlProvider(CoreUrls.class).getDownloadFileLinkBaseURL(container, pd); - if (AS_ATTACHMENT_FORMAT.equalsIgnoreCase(col.getFormat())) - { - baseUrl.addParameter("inline", "false"); - } - else - { - setLinkTarget("_blank"); - } - DetailsURL url = new DetailsURL(baseUrl, "objectURI", objectURIFieldKey); - setURLExpression(url); - } - } - - public FileLinkDisplayColumn(ColumnInfo col, DetailsURL detailsURL, Container container, @NotNull FieldKey pkFieldKey) - { - super(col); - _container = container; - _pkFieldKey = pkFieldKey; - - setURLExpression(detailsURL); - } - - @Override - protected Object getInputValue(RenderContext ctx) - { - ColumnInfo col = getColumnInfo(); - Object val = null; - TableViewForm viewForm = ctx.getForm(); - - if (col != null) - { - if (null != viewForm && viewForm.contains(this, ctx)) - { - val = viewForm.get(getFormFieldName(ctx)); - } - else if (ctx.getRow() != null) - val = col.getValue(ctx); - } - - return val; - } - - @Override - public void addQueryFieldKeys(Set keys) - { - super.addQueryFieldKeys(keys); - keys.add(FieldKey.fromParts("Modified")); - if (_pkFieldKey != null) - keys.add(_pkFieldKey); - if (_objectURIFieldKey != null) - keys.add(_objectURIFieldKey); - } - - public static boolean filePathExist(String path, Container container, User user) - { - String davPath = path; - if (FileUtil.isUrlEncoded(davPath)) - davPath = FileUtil.decodeURL(davPath); - var resolver = WebdavService.get().getResolver(); - // Resolve path under webdav root - Path parsed = Path.parse(StringUtils.trim(davPath)); - - // Issue 52968: handle context path - Path contextPath = AppProps.getInstance().getParsedContextPath(); - if (parsed.startsWith(contextPath)) - parsed = parsed.subpath(contextPath.size(), parsed.size()); - - WebdavResource resource = resolver.lookup(parsed); - if ((null == resource || !resource.exists()) && !parsed.startsWith(new Path("_webdav"))) - resource = resolver.lookup(new Path("_webdav").append(parsed)); - if (resource != null && resource.isFile() && resource.canRead(user, true)) - { - return true; - } - else - { - // Resolve file under pipeline root - PipeRoot root = PipelineService.get().findPipelineRoot(container); - if (root != null) - { - // Attempt absolute path first, then relative path from pipeline root - File f = new File(path); - if (!root.isUnderRoot(f)) - f = root.resolvePath(path); - - return (NetworkDrive.exists(f) && root.isUnderRoot(f) && root.hasPermission(container, user, ReadPermission.class)); - } - } - - return false; - } - - @Override - protected String getFileName(RenderContext ctx, Object value) - { - String result = value == null ? null : StringUtils.trimToNull(value.toString()); - if (result != null) - { - File f = null; - if (result.startsWith("file:")) - { - try - { - f = new File(new URI(result)); - } - catch (URISyntaxException x) - { - // try to recover - result = result.substring("file:".length()); - } - } - if (null == f) - f = FileUtil.getAbsoluteCaseSensitiveFile(new File(result)); - NetworkDrive.ensureDrive(f.getPath()); - List fileRootTypes = List.of(FileContentService.ContentType.files, FileContentService.ContentType.pipeline, FileContentService.ContentType.assayfiles); - boolean valid = false; - List containers = new ArrayList<>(); - containers.add(_container); - // Not ideal, but needed in case data is queried from cross folder context - if (ctx.get("folder") != null || ctx.get("container") != null) - { - Object folderObj = ctx.get("folder"); - if (folderObj == null) - folderObj = ctx.get("container"); - if (folderObj instanceof String containerId) - { - Container dataContainer = ContainerManager.getForId(containerId); - if (dataContainer != null && !dataContainer.equals(_container)) - containers.add(dataContainer); - } - } - for (Container container : containers) - { - if (valid) - break; - - for (FileContentService.ContentType fileRootType : fileRootTypes) - { - result = relativize(f, FileContentService.get().getFileRoot(container, fileRootType)); - if (result != null) - { - valid = true; - break; - } - } - } - if (result == null) - { - result = f.getName(); - } - - if ((!valid || !f.exists()) && !result.endsWith(UNAVAILABLE_FILE_SUFFIX)) - result += UNAVAILABLE_FILE_SUFFIX; - } - return result; - } - - public static String relativize(File f, File fileRoot) - { - if (fileRoot != null) - { - NetworkDrive.ensureDrive(fileRoot.getPath()); - fileRoot = FileUtil.getAbsoluteCaseSensitiveFile(fileRoot); - if (URIUtil.isDescendant(fileRoot.toURI(), f.toURI())) - { - try - { - return FileUtil.relativize(fileRoot, f, false); - } - catch (IOException ignored) {} - } - } - return null; - } - - @Override - protected InputStream getFileContents(RenderContext ctx, Object ignore) throws FileNotFoundException - { - Object value = getValue(ctx); - String s = value == null ? null : StringUtils.trimToNull(value.toString()); - if (s != null) - { - File f = new File(s); - if (f.isFile()) - return new FileInputStream(f); - } - return null; - } - - @Override - protected void renderIconAndFilename( - RenderContext ctx, - HtmlWriter out, - String fileValue /*Could be raw path value, or processed filename by `getFileName`*/, - boolean link, - boolean thumbnail) - { - Object value = getValue(ctx); - String strValue = value == null ? null : StringUtils.trimToNull(value.toString()); - if (strValue != null && !fileValue.endsWith(UNAVAILABLE_FILE_SUFFIX)) - { - File f; - if (strValue.startsWith("file:")) - f = new File(URI.create(strValue)); - else - f = new File(strValue); - - if (!f.exists()) - { - // try all file root - List fileRootTypes = List.of(FileContentService.ContentType.files, FileContentService.ContentType.pipeline, FileContentService.ContentType.assayfiles); - for (FileContentService.ContentType fileRootType : fileRootTypes) - { - String fullPath = FileContentService.get().getFileRoot(_container, fileRootType).getAbsolutePath()+ File.separator + value; - f = new File(fullPath); - if (f.exists()) - break; - } - } - - // It's probably a file, so check that first - if (f.isFile()) - { - super.renderIconAndFilename(ctx, out, strValue, link, thumbnail); - } - else if (f.isDirectory()) - { - super.renderIconAndFilename(ctx, out, strValue, Attachment.getFileIcon(".folder"), null, link, false); - } - else - { - // It's not on the file system anymore, so don't offer a link and tell the user it's unavailable - super.renderIconAndFilename(ctx, out, strValue, Attachment.getFileIcon(fileValue), null, false, false); - } - } - else - { - super.renderIconAndFilename(ctx, out, fileValue, link, thumbnail); - } - } - - @Override - public Object getDisplayValue(RenderContext ctx) - { - return getFileName(ctx, super.getDisplayValue(ctx)); - } - - @Override - public Object getJsonValue(RenderContext ctx) - { - return getDisplayValue(ctx); - } - - @Override - public boolean isFilterable() - { - return false; - } - @Override - public boolean isSortable() - { - return false; - } - -} +/* + * 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.study.assay; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.admin.CoreUrls; +import org.labkey.api.attachments.Attachment; +import org.labkey.api.data.AbstractFileDisplayColumn; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.DisplayColumn; +import org.labkey.api.data.RemappingDisplayColumnFactory; +import org.labkey.api.data.RenderContext; +import org.labkey.api.data.TableViewForm; +import org.labkey.api.exp.PropertyDescriptor; +import org.labkey.api.files.FileContentService; +import org.labkey.api.pipeline.PipeRoot; +import org.labkey.api.pipeline.PipelineService; +import org.labkey.api.query.DetailsURL; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.SchemaKey; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.settings.AppProps; +import org.labkey.api.util.ContainerContext; +import org.labkey.api.util.FileUtil; +import org.labkey.api.util.NetworkDrive; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.Path; +import org.labkey.api.util.URIUtil; +import org.labkey.api.view.ActionURL; +import org.labkey.api.webdav.WebdavResource; +import org.labkey.api.webdav.WebdavService; +import org.labkey.api.writer.HtmlWriter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class FileLinkDisplayColumn extends AbstractFileDisplayColumn +{ + // Issue 46282 - let admins choose if files should be rendered inside browser or downloaded as files + public static final String AS_ATTACHMENT_FORMAT = "attachment"; + public static final String AS_INLINE_FORMAT = "inline"; + + public static class Factory implements RemappingDisplayColumnFactory + { + private final Container _container; + + private PropertyDescriptor _pd; + private DetailsURL _detailsUrl; + private SchemaKey _schemaKey; + private String _queryName; + private FieldKey _pkFieldKey; + private FieldKey _objectURIFieldKey; + + public Factory(PropertyDescriptor pd, Container c, @NotNull SchemaKey schemaKey, @NotNull String queryName, @NotNull FieldKey pkFieldKey) + { + _pd = pd; + _container = c; + _schemaKey = schemaKey; + _queryName = queryName; + _pkFieldKey = pkFieldKey; + } + + public Factory(PropertyDescriptor pd, Container c, @NotNull FieldKey lsidColumnFieldKey) + { + _pd = pd; + _container = c; + _objectURIFieldKey = lsidColumnFieldKey; + } + + public Factory(DetailsURL detailsURL, Container c, @NotNull FieldKey pkFieldKey) + { + _detailsUrl = detailsURL; + _container = c; + _pkFieldKey = pkFieldKey; + } + + @Override + public Factory remapFieldKeys(@Nullable FieldKey parent, @Nullable Map remap) + { + Factory remapped = this.clone(); + if (remapped._pkFieldKey != null) + { + remapped._pkFieldKey = FieldKey.remap(_pkFieldKey, parent, remap); + if (null == remapped._pkFieldKey) + remapped._pkFieldKey = _pkFieldKey; + } + if (remapped._objectURIFieldKey != null) + { + remapped._objectURIFieldKey = FieldKey.remap(_objectURIFieldKey, parent, remap); + if (null == remapped._objectURIFieldKey) + remapped._objectURIFieldKey = _objectURIFieldKey; + } + return remapped; + } + + @Override + public DisplayColumn createRenderer(ColumnInfo col) + { + if (_pd == null && _detailsUrl != null) + return new FileLinkDisplayColumn(col, _detailsUrl, _container, _pkFieldKey); + else if (_pkFieldKey != null) + return new FileLinkDisplayColumn(col, _pd, _container, _schemaKey, _queryName, _pkFieldKey); + else if (_container != null) + return new FileLinkDisplayColumn(col, _pd, _container, _objectURIFieldKey); + else + throw new IllegalArgumentException("Cannot create a renderer from the specified configuration properties"); + } + + @Override + public FileLinkDisplayColumn.Factory clone() + { + try + { + return (Factory)super.clone(); + } + catch (CloneNotSupportedException e) + { + throw new RuntimeException(e); + } + } + } + + private final Container _container; + + private FieldKey _pkFieldKey; + private FieldKey _objectURIFieldKey; + + /** Use schemaName/queryName and pk FieldKey value to resolve File in CoreController.DownloadFileLinkAction. */ + public FileLinkDisplayColumn(ColumnInfo col, PropertyDescriptor pd, Container container, @NotNull SchemaKey schemaKey, @NotNull String queryName, @NotNull FieldKey pkFieldKey) + { + super(col); + _container = container; + _pkFieldKey = pkFieldKey; + + if (pd.getURL() == null) + { + // Don't stomp over an explicitly configured URL on this column + StringBuilder sb = new StringBuilder("/core/downloadFileLink.view?propertyId="); + sb.append(pd.getPropertyId()); + sb.append("&schemaName="); + sb.append(PageFlowUtil.encodeURIComponent(schemaKey.toString())); + sb.append("&queryName="); + sb.append(PageFlowUtil.encodeURIComponent(queryName)); + sb.append("&pk=${"); + sb.append(pkFieldKey); + sb.append("}"); + sb.append("&modified=${Modified}"); + if (AS_ATTACHMENT_FORMAT.equalsIgnoreCase(col.getFormat())) + { + sb.append("&inline=false"); + } + ContainerContext context = new ContainerContext.FieldKeyContext(new FieldKey(pkFieldKey.getParent(), "Folder")); + setURLExpression(DetailsURL.fromString(sb.toString(), context)); + } + } + + /** Use LSID FieldKey value as ObjectURI to resolve File in CoreController.DownloadFileLinkAction. */ + public FileLinkDisplayColumn(ColumnInfo col, PropertyDescriptor pd, Container container, @NotNull FieldKey objectURIFieldKey) + { + super(col); + _container = container; + _objectURIFieldKey = objectURIFieldKey; + + if (pd.getURL() == null) + { + // Don't stomp over an explicitly configured URL on this column + + ActionURL baseUrl = PageFlowUtil.urlProvider(CoreUrls.class).getDownloadFileLinkBaseURL(container, pd); + if (AS_ATTACHMENT_FORMAT.equalsIgnoreCase(col.getFormat())) + { + baseUrl.addParameter("inline", "false"); + } + else + { + setLinkTarget("_blank"); + } + DetailsURL url = new DetailsURL(baseUrl, "objectURI", objectURIFieldKey); + setURLExpression(url); + } + } + + public FileLinkDisplayColumn(ColumnInfo col, DetailsURL detailsURL, Container container, @NotNull FieldKey pkFieldKey) + { + super(col); + _container = container; + _pkFieldKey = pkFieldKey; + + setURLExpression(detailsURL); + } + + @Override + protected Object getInputValue(RenderContext ctx) + { + ColumnInfo col = getColumnInfo(); + Object val = null; + TableViewForm viewForm = ctx.getForm(); + + if (col != null) + { + if (null != viewForm && viewForm.contains(this, ctx)) + { + val = viewForm.get(getFormFieldName(ctx)); + } + else if (ctx.getRow() != null) + val = col.getValue(ctx); + } + + return val; + } + + @Override + public void addQueryFieldKeys(Set keys) + { + super.addQueryFieldKeys(keys); + keys.add(FieldKey.fromParts("Modified")); + if (_pkFieldKey != null) + keys.add(_pkFieldKey); + if (_objectURIFieldKey != null) + keys.add(_objectURIFieldKey); + } + + public static boolean filePathExist(String path, Container container, User user) + { + String davPath = path; + if (FileUtil.isUrlEncoded(davPath)) + davPath = FileUtil.decodeURL(davPath); + var resolver = WebdavService.get().getResolver(); + // Resolve path under webdav root + Path parsed = Path.parse(StringUtils.trim(davPath)); + + // Issue 52968: handle context path + Path contextPath = AppProps.getInstance().getParsedContextPath(); + if (parsed.startsWith(contextPath)) + parsed = parsed.subpath(contextPath.size(), parsed.size()); + + WebdavResource resource = resolver.lookup(parsed); + if ((null == resource || !resource.exists()) && !parsed.startsWith(new Path("_webdav"))) + resource = resolver.lookup(new Path("_webdav").append(parsed)); + if (resource != null && resource.isFile() && resource.canRead(user, true)) + { + return true; + } + else + { + // Resolve file under pipeline root + PipeRoot root = PipelineService.get().findPipelineRoot(container); + if (root != null) + { + // Attempt absolute path first, then relative path from pipeline root + File f = new File(path); + if (!root.isUnderRoot(f)) + f = root.resolvePath(path); + + return (NetworkDrive.exists(f) && root.isUnderRoot(f) && root.hasPermission(container, user, ReadPermission.class)); + } + } + + return false; + } + + @Override + protected String getFileName(RenderContext ctx, Object value) + { + return getFileName(ctx, value, false); + } + + @Override + protected String getFileName(RenderContext ctx, Object value, boolean isDisplay) + { + String result = value == null ? null : StringUtils.trimToNull(value.toString()); + if (result != null) + { + File f = null; + if (result.startsWith("file:")) + { + try + { + f = new File(new URI(result)); + } + catch (URISyntaxException x) + { + // try to recover + result = result.substring("file:".length()); + } + } + if (null == f) + f = FileUtil.getAbsoluteCaseSensitiveFile(new File(result)); + NetworkDrive.ensureDrive(f.getPath()); + List fileRootTypes = List.of(FileContentService.ContentType.files, FileContentService.ContentType.pipeline, FileContentService.ContentType.assayfiles); + boolean valid = false; + List containers = new ArrayList<>(); + containers.add(_container); + // Not ideal, but needed in case data is queried from cross folder context + if (ctx.get("folder") != null || ctx.get("container") != null) + { + Object folderObj = ctx.get("folder"); + if (folderObj == null) + folderObj = ctx.get("container"); + if (folderObj instanceof String containerId) + { + Container dataContainer = ContainerManager.getForId(containerId); + if (dataContainer != null && !dataContainer.equals(_container)) + containers.add(dataContainer); + } + } + for (Container container : containers) + { + if (valid) + break; + + for (FileContentService.ContentType fileRootType : fileRootTypes) + { + result = relativize(f, FileContentService.get().getFileRoot(container, fileRootType)); + if (result != null) + { + // Issue 54062: Strip folder name from displayed name + if (isDisplay) + result = f.getName(); + + valid = true; + break; + } + } + } + if (result == null) + { + result = f.getName(); + } + + if ((!valid || !f.exists()) && !result.endsWith(UNAVAILABLE_FILE_SUFFIX)) + result += UNAVAILABLE_FILE_SUFFIX; + } + return result; + } + + public static String relativize(File f, File fileRoot) + { + if (fileRoot != null) + { + NetworkDrive.ensureDrive(fileRoot.getPath()); + fileRoot = FileUtil.getAbsoluteCaseSensitiveFile(fileRoot); + if (URIUtil.isDescendant(fileRoot.toURI(), f.toURI())) + { + try + { + return FileUtil.relativize(fileRoot, f, false); + } + catch (IOException ignored) {} + } + } + return null; + } + + @Override + protected InputStream getFileContents(RenderContext ctx, Object ignore) throws FileNotFoundException + { + Object value = getValue(ctx); + String s = value == null ? null : StringUtils.trimToNull(value.toString()); + if (s != null) + { + File f = new File(s); + if (f.isFile()) + return new FileInputStream(f); + } + return null; + } + + @Override + protected void renderIconAndFilename( + RenderContext ctx, + HtmlWriter out, + String fileValue /*Could be raw path value, or processed filename by `getFileName`*/, + boolean link, + boolean thumbnail) + { + Object value = getValue(ctx); + String strValue = value == null ? null : StringUtils.trimToNull(value.toString()); + if (strValue != null && !fileValue.endsWith(UNAVAILABLE_FILE_SUFFIX)) + { + File f; + if (strValue.startsWith("file:")) + f = new File(URI.create(strValue)); + else + f = new File(strValue); + + if (!f.exists()) + { + // try all file root + List fileRootTypes = List.of(FileContentService.ContentType.files, FileContentService.ContentType.pipeline, FileContentService.ContentType.assayfiles); + for (FileContentService.ContentType fileRootType : fileRootTypes) + { + String fullPath = FileContentService.get().getFileRoot(_container, fileRootType).getAbsolutePath()+ File.separator + value; + f = new File(fullPath); + if (f.exists()) + break; + } + } + + // It's probably a file, so check that first + if (f.isFile()) + { + super.renderIconAndFilename(ctx, out, strValue, link, thumbnail); + } + else if (f.isDirectory()) + { + super.renderIconAndFilename(ctx, out, strValue, Attachment.getFileIcon(".folder"), null, link, false); + } + else + { + // It's not on the file system anymore, so don't offer a link and tell the user it's unavailable + super.renderIconAndFilename(ctx, out, strValue, Attachment.getFileIcon(fileValue), null, false, false); + } + } + else + { + super.renderIconAndFilename(ctx, out, fileValue, link, thumbnail); + } + } + + @Override + public Object getDisplayValue(RenderContext ctx) + { + return getFileName(ctx, super.getDisplayValue(ctx), true); + } + + @Override + public Object getJsonValue(RenderContext ctx) + { + return getFileName(ctx, super.getDisplayValue(ctx)); + } + + @Override + public boolean isFilterable() + { + return false; + } + @Override + public boolean isSortable() + { + return false; + } + +} diff --git a/study/test/src/org/labkey/test/tests/study/StudyDatasetFileFieldTest.java b/study/test/src/org/labkey/test/tests/study/StudyDatasetFileFieldTest.java index 962f186a0f0..81b436d8f6b 100644 --- a/study/test/src/org/labkey/test/tests/study/StudyDatasetFileFieldTest.java +++ b/study/test/src/org/labkey/test/tests/study/StudyDatasetFileFieldTest.java @@ -146,12 +146,7 @@ public void testFileField() throws IOException, CommandException .selectDatasetByName(datasetName) .clickViewData(); - String expectedText; - - if (SystemUtils.IS_OS_WINDOWS) - expectedText = "datasetdata\\sample.txt"; - else - expectedText = "datasetdata/sample.txt"; + String expectedText = "sample.txt"; assertElementPresent("Did not find the expected sample.txt from the imported dataset.", Locator.tagContainingText("a", expectedText), 1); downloadedFile = doAndWaitForDownload(() -> waitAndClick(WAIT_FOR_JAVASCRIPT, Locator.tagWithAttribute("a", "title", "Download attached file"), 0)); From cc2f0a6a631f47382cbd92f5cd81d5d69b47716c Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 15 Oct 2025 09:08:36 -0700 Subject: [PATCH 2/7] fix crlf in diff --- .../api/data/AbstractFileDisplayColumn.java | 656 ++++++------ .../study/assay/FileLinkDisplayColumn.java | 942 +++++++++--------- 2 files changed, 799 insertions(+), 799 deletions(-) diff --git a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java index 28a0a633c92..605c08d3799 100644 --- a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java +++ b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java @@ -1,329 +1,329 @@ -/* - * Copyright (c) 2011-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.data; - -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.attachments.Attachment; -import org.labkey.api.util.DOM; -import org.labkey.api.util.DOM.Renderable; -import org.labkey.api.util.GUID; -import org.labkey.api.util.HtmlString; -import org.labkey.api.util.InputBuilder; -import org.labkey.api.util.LinkBuilder; -import org.labkey.api.util.MimeMap; -import org.labkey.api.util.PageFlowUtil; -import org.labkey.api.util.StringExpression; -import org.labkey.api.view.HttpView; -import org.labkey.api.writer.HtmlWriter; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.InputStream; - -import static org.labkey.api.util.DOM.A; -import static org.labkey.api.util.DOM.Attribute.alt; -import static org.labkey.api.util.DOM.Attribute.href; -import static org.labkey.api.util.DOM.Attribute.src; -import static org.labkey.api.util.DOM.Attribute.style; -import static org.labkey.api.util.DOM.Attribute.target; -import static org.labkey.api.util.DOM.Attribute.title; -import static org.labkey.api.util.DOM.DIV; -import static org.labkey.api.util.DOM.IMG; -import static org.labkey.api.util.DOM.at; -import static org.labkey.api.util.DOM.id; -import static org.labkey.api.util.PageFlowUtil.jsString; - -/** - * Provides a consistent UI for both attachment (BLOB) and file link (file system) files - */ -public abstract class AbstractFileDisplayColumn extends DataColumn -{ - protected String _thumbnailWidth; - protected String _popupWidth; - - public static final String UNAVAILABLE_FILE_SUFFIX = " (unavailable)"; - - public AbstractFileDisplayColumn(ColumnInfo col) - { - super(col); - } - - @Override - public void renderDetailsCellContents(RenderContext ctx, HtmlWriter out) - { - renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); - } - - @Override - public void renderGridCellContents(RenderContext ctx, HtmlWriter out) - { - renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); - } - - /** @return the short name of the file (not including full path) */ - protected abstract String getFileName(RenderContext ctx, Object value); - - protected String getFileName(RenderContext ctx, Object value, boolean isDisplay) - { - return getFileName(ctx, value); - } - - protected abstract InputStream getFileContents(RenderContext ctx, Object value) throws FileNotFoundException; - - protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, boolean link, boolean thumbnail) - { - renderIconAndFilename(ctx, out, fileValue, null, null, link, thumbnail); - } - - protected boolean isImage(String filename) - { - return filename.toLowerCase().endsWith(".png") - || filename.toLowerCase().endsWith(".jpeg") - || filename.toLowerCase().endsWith(".jpg") - || filename.toLowerCase().endsWith(".gif"); - } - - protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean link, boolean thumbnail) - { - if (null != fileValue && !StringUtils.isEmpty(fileValue)) - { - // equivalent of DisplayColumn.renderURL. - // Don't want to call renderUrl (DataColumn.renderUrl) to skip unnecessary displayValue check - StringExpression s = compileExpression(ctx.getViewContext()); - String displayName = getFileName(ctx, fileValue, true); - boolean unavailable = displayName.endsWith(UNAVAILABLE_FILE_SUFFIX); - String url = null == s || unavailable ? null : s.eval(ctx); - boolean isImage = isImage(fileValue); - FileImageRenderHelper renderHelper = createRenderHelper(ctx, url, fileValue, displayName, fileIconUrl, popupIconUrl, thumbnail, isImage); - - - if (link && null != url) - { - A( - at(title, "Download attached file") - .at(getLinkTarget() != null && MimeMap.DEFAULT.canInlineFor(fileValue), target, getLinkTarget()) - .at(href, url), - (Renderable) ret -> { - renderPopup(renderHelper, url, fileIconUrl, displayName, fileValue, thumbnail, isImage, out); - return ret; - } - ).appendTo(out); - } - else - { - renderPopup(renderHelper, url, fileIconUrl, displayName, fileValue, thumbnail, isImage, out); - } - } - else - { - out.write(HtmlString.NBSP); - } - } - - private void renderPopup(FileImageRenderHelper renderHelper, String url, @Nullable String fileIconUrl, String displayName, String filename, boolean thumbnail, boolean isImage, HtmlWriter out) - { - if ((url != null || fileIconUrl != null) && thumbnail && isImage) - { - // controls whether to render a popup image on hover, otherwise just render an image with a click handler - // to navigate to the url - if (renderHelper.renderPopupImage()) - out.write(PageFlowUtil.popupHelp(renderHelper.createPopupImage(), displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); - else - out.write(PageFlowUtil.popupHelp(displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); - } - else - { - if (url != null && thumbnail && MimeMap.DEFAULT.isInlineImageFor(new File(filename)) ) - { - if (renderHelper.renderPopupImage()) - out.write(PageFlowUtil.popupHelp(renderHelper.createPopupImage(), displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); - else - out.write(PageFlowUtil.popupHelp(displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); - } - else - { - renderHelper.createThumbnailImage().appendTo(out); - } - } - } - - protected FileImageRenderHelper createRenderHelper(RenderContext ctx, String url, String filename, String displayName, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean isThumbnail, boolean isImage) - { - return new FileImageRenderHelper(ctx, url, filename, displayName, fileIconUrl, popupIconUrl, isThumbnail, isImage); - } - - /** - * Helper class to generate the HTML for the various portions of a file or image grid cell content - * - * Tests to run if you touch this class: FileAttachmentColumnTest, InlineImagesAssayTest, InlineImagesListTest, SimpleModuleTest - */ - public class FileImageRenderHelper - { - protected RenderContext _ctx; - protected String _displayName; - protected String _url; - protected String _filename; - protected String _fileIconUrl; - protected String _popupIconUrl; - protected boolean _isThumbnail; - protected boolean _isImage; - - public FileImageRenderHelper(RenderContext ctx, String url, String filename, String displayName, String fileIconUrl, String popupIconUrl, boolean isThumbnail, boolean isImage) - { - _ctx = ctx; - _url = url; - _filename = filename; - _displayName = displayName; - _fileIconUrl = fileIconUrl; - _popupIconUrl = popupIconUrl; - _isThumbnail = isThumbnail; - _isImage = isImage; - } - - // render the grid cell content - public Renderable createThumbnailImage() - { - if (_url != null && _isThumbnail && _isImage) - { - return IMG( - at( - style, "display:block; height:auto; vertical-align:middle; " + (_thumbnailWidth != null ? "width:" + _thumbnailWidth : "max-width:32px"), - src, _url, title, _displayName - ) - ); - } - else - { - return DOM.createHtmlFragment( - IMG( - at(src, _ctx.getRequest().getContextPath() + (null != _fileIconUrl ? _fileIconUrl : Attachment.getFileIcon(_filename)), alt, "icon") - ), - HtmlString.NBSP, - _displayName - ); - } - } - - public boolean renderPopupImage() - { - return true; - } - - // render the popup image to display on hover - public Renderable createPopupImage() - { - return _url != null ? IMG( - at( - style, "height:auto; " + (_popupWidth != null ? "width:" + _popupWidth : "max-width:300px"), - src, _url - ) - ) : HtmlString.EMPTY_STRING; - } - - // render the click script when a user clicks on the grid cell - public String createClickScript() - { - if (_url == null) - { - return null; - } - if (getLinkTarget() != null) - { - return "window.open(" + jsString(_url) + "," + jsString(getLinkTarget()) + ", 'noopener,noreferrer')"; - } - return "window.location = " + jsString(_url); - } - } - - protected String ensureAbsoluteUrl(RenderContext ctx, String url) - { - if (!url.startsWith(ctx.getRequest().getContextPath())) - { - String lcUrl = url.toLowerCase(); - if (!lcUrl.startsWith("http:") && !lcUrl.startsWith("https:")) - { - if (url.startsWith("/")) - return ctx.getRequest().getContextPath() + url; - else - return ctx.getRequest().getContextPath() + "/" + url; - } - } - return url; - } - - protected boolean hasFileInputHtml() - { - return true; - } - - @Override - public void renderInputHtml(RenderContext ctx, HtmlWriter out, Object value) - { - if (hasFileInputHtml()) - { - String filename = getFileName(ctx, value); - String formFieldName = ctx.getForm().getFormFieldName(getBoundColumn()); - - InputBuilder input = InputBuilder.file() - .name(formFieldName) - .disabled(isDisabledInput(ctx)) - .needsWrapping(false); - - if (null != filename) - { - // Existing value, so tell the user the file name, allow the file to be removed, and a new file uploaded - renderThumbnailAndRemoveLink(out, ctx, filename, input); - } - else - { - // No existing value, so render just the regular element - input.appendTo(out); - } - } - else - super.renderInputHtml(ctx, out, value); - } - - /** - * Enable subclasses to override the warning text - * @param filename being displayed - */ - protected String getRemovalWarningText(String filename) - { - return "Previous file " + filename + " will be removed."; - } - - private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, String filename, InputBuilder filePicker) - { - String divId = GUID.makeGUID(); - String linkId = "remove" + divId; - - DIV( - id(divId), - (Renderable) ret -> { - renderIconAndFilename(ctx, out, filename, false, false); - out.write(HtmlString.NBSP); - out.write("["); - out.write(LinkBuilder.simpleLink("remove", "#").id(linkId)); - out.write("]"); - return ret; - } - ).appendTo(out); - String innerHtml = filePicker + "" + getRemovalWarningText(filename) + ""; - HttpView.currentPageConfig().addHandler(linkId, "click", "document.getElementById(" + jsString(divId) + ").innerHTML = " + jsString(innerHtml)); - } +/* + * Copyright (c) 2011-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.data; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.attachments.Attachment; +import org.labkey.api.util.DOM; +import org.labkey.api.util.DOM.Renderable; +import org.labkey.api.util.GUID; +import org.labkey.api.util.HtmlString; +import org.labkey.api.util.InputBuilder; +import org.labkey.api.util.LinkBuilder; +import org.labkey.api.util.MimeMap; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.StringExpression; +import org.labkey.api.view.HttpView; +import org.labkey.api.writer.HtmlWriter; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; + +import static org.labkey.api.util.DOM.A; +import static org.labkey.api.util.DOM.Attribute.alt; +import static org.labkey.api.util.DOM.Attribute.href; +import static org.labkey.api.util.DOM.Attribute.src; +import static org.labkey.api.util.DOM.Attribute.style; +import static org.labkey.api.util.DOM.Attribute.target; +import static org.labkey.api.util.DOM.Attribute.title; +import static org.labkey.api.util.DOM.DIV; +import static org.labkey.api.util.DOM.IMG; +import static org.labkey.api.util.DOM.at; +import static org.labkey.api.util.DOM.id; +import static org.labkey.api.util.PageFlowUtil.jsString; + +/** + * Provides a consistent UI for both attachment (BLOB) and file link (file system) files + */ +public abstract class AbstractFileDisplayColumn extends DataColumn +{ + protected String _thumbnailWidth; + protected String _popupWidth; + + public static final String UNAVAILABLE_FILE_SUFFIX = " (unavailable)"; + + public AbstractFileDisplayColumn(ColumnInfo col) + { + super(col); + } + + @Override + public void renderDetailsCellContents(RenderContext ctx, HtmlWriter out) + { + renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); + } + + @Override + public void renderGridCellContents(RenderContext ctx, HtmlWriter out) + { + renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); + } + + /** @return the short name of the file (not including full path) */ + protected abstract String getFileName(RenderContext ctx, Object value); + + protected String getFileName(RenderContext ctx, Object value, boolean isDisplay) + { + return getFileName(ctx, value); + } + + protected abstract InputStream getFileContents(RenderContext ctx, Object value) throws FileNotFoundException; + + protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, boolean link, boolean thumbnail) + { + renderIconAndFilename(ctx, out, fileValue, null, null, link, thumbnail); + } + + protected boolean isImage(String filename) + { + return filename.toLowerCase().endsWith(".png") + || filename.toLowerCase().endsWith(".jpeg") + || filename.toLowerCase().endsWith(".jpg") + || filename.toLowerCase().endsWith(".gif"); + } + + protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean link, boolean thumbnail) + { + if (null != fileValue && !StringUtils.isEmpty(fileValue)) + { + // equivalent of DisplayColumn.renderURL. + // Don't want to call renderUrl (DataColumn.renderUrl) to skip unnecessary displayValue check + StringExpression s = compileExpression(ctx.getViewContext()); + String displayName = getFileName(ctx, fileValue, true); + boolean unavailable = displayName.endsWith(UNAVAILABLE_FILE_SUFFIX); + String url = null == s || unavailable ? null : s.eval(ctx); + boolean isImage = isImage(fileValue); + FileImageRenderHelper renderHelper = createRenderHelper(ctx, url, fileValue, displayName, fileIconUrl, popupIconUrl, thumbnail, isImage); + + + if (link && null != url) + { + A( + at(title, "Download attached file") + .at(getLinkTarget() != null && MimeMap.DEFAULT.canInlineFor(fileValue), target, getLinkTarget()) + .at(href, url), + (Renderable) ret -> { + renderPopup(renderHelper, url, fileIconUrl, displayName, fileValue, thumbnail, isImage, out); + return ret; + } + ).appendTo(out); + } + else + { + renderPopup(renderHelper, url, fileIconUrl, displayName, fileValue, thumbnail, isImage, out); + } + } + else + { + out.write(HtmlString.NBSP); + } + } + + private void renderPopup(FileImageRenderHelper renderHelper, String url, @Nullable String fileIconUrl, String displayName, String filename, boolean thumbnail, boolean isImage, HtmlWriter out) + { + if ((url != null || fileIconUrl != null) && thumbnail && isImage) + { + // controls whether to render a popup image on hover, otherwise just render an image with a click handler + // to navigate to the url + if (renderHelper.renderPopupImage()) + out.write(PageFlowUtil.popupHelp(renderHelper.createPopupImage(), displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); + else + out.write(PageFlowUtil.popupHelp(displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); + } + else + { + if (url != null && thumbnail && MimeMap.DEFAULT.isInlineImageFor(new File(filename)) ) + { + if (renderHelper.renderPopupImage()) + out.write(PageFlowUtil.popupHelp(renderHelper.createPopupImage(), displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); + else + out.write(PageFlowUtil.popupHelp(displayName).link(renderHelper.createThumbnailImage()).width(310).script(renderHelper.createClickScript())); + } + else + { + renderHelper.createThumbnailImage().appendTo(out); + } + } + } + + protected FileImageRenderHelper createRenderHelper(RenderContext ctx, String url, String filename, String displayName, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean isThumbnail, boolean isImage) + { + return new FileImageRenderHelper(ctx, url, filename, displayName, fileIconUrl, popupIconUrl, isThumbnail, isImage); + } + + /** + * Helper class to generate the HTML for the various portions of a file or image grid cell content + * + * Tests to run if you touch this class: FileAttachmentColumnTest, InlineImagesAssayTest, InlineImagesListTest, SimpleModuleTest + */ + public class FileImageRenderHelper + { + protected RenderContext _ctx; + protected String _displayName; + protected String _url; + protected String _filename; + protected String _fileIconUrl; + protected String _popupIconUrl; + protected boolean _isThumbnail; + protected boolean _isImage; + + public FileImageRenderHelper(RenderContext ctx, String url, String filename, String displayName, String fileIconUrl, String popupIconUrl, boolean isThumbnail, boolean isImage) + { + _ctx = ctx; + _url = url; + _filename = filename; + _displayName = displayName; + _fileIconUrl = fileIconUrl; + _popupIconUrl = popupIconUrl; + _isThumbnail = isThumbnail; + _isImage = isImage; + } + + // render the grid cell content + public Renderable createThumbnailImage() + { + if (_url != null && _isThumbnail && _isImage) + { + return IMG( + at( + style, "display:block; height:auto; vertical-align:middle; " + (_thumbnailWidth != null ? "width:" + _thumbnailWidth : "max-width:32px"), + src, _url, title, _displayName + ) + ); + } + else + { + return DOM.createHtmlFragment( + IMG( + at(src, _ctx.getRequest().getContextPath() + (null != _fileIconUrl ? _fileIconUrl : Attachment.getFileIcon(_filename)), alt, "icon") + ), + HtmlString.NBSP, + _displayName + ); + } + } + + public boolean renderPopupImage() + { + return true; + } + + // render the popup image to display on hover + public Renderable createPopupImage() + { + return _url != null ? IMG( + at( + style, "height:auto; " + (_popupWidth != null ? "width:" + _popupWidth : "max-width:300px"), + src, _url + ) + ) : HtmlString.EMPTY_STRING; + } + + // render the click script when a user clicks on the grid cell + public String createClickScript() + { + if (_url == null) + { + return null; + } + if (getLinkTarget() != null) + { + return "window.open(" + jsString(_url) + "," + jsString(getLinkTarget()) + ", 'noopener,noreferrer')"; + } + return "window.location = " + jsString(_url); + } + } + + protected String ensureAbsoluteUrl(RenderContext ctx, String url) + { + if (!url.startsWith(ctx.getRequest().getContextPath())) + { + String lcUrl = url.toLowerCase(); + if (!lcUrl.startsWith("http:") && !lcUrl.startsWith("https:")) + { + if (url.startsWith("/")) + return ctx.getRequest().getContextPath() + url; + else + return ctx.getRequest().getContextPath() + "/" + url; + } + } + return url; + } + + protected boolean hasFileInputHtml() + { + return true; + } + + @Override + public void renderInputHtml(RenderContext ctx, HtmlWriter out, Object value) + { + if (hasFileInputHtml()) + { + String filename = getFileName(ctx, value); + String formFieldName = ctx.getForm().getFormFieldName(getBoundColumn()); + + InputBuilder input = InputBuilder.file() + .name(formFieldName) + .disabled(isDisabledInput(ctx)) + .needsWrapping(false); + + if (null != filename) + { + // Existing value, so tell the user the file name, allow the file to be removed, and a new file uploaded + renderThumbnailAndRemoveLink(out, ctx, filename, input); + } + else + { + // No existing value, so render just the regular element + input.appendTo(out); + } + } + else + super.renderInputHtml(ctx, out, value); + } + + /** + * Enable subclasses to override the warning text + * @param filename being displayed + */ + protected String getRemovalWarningText(String filename) + { + return "Previous file " + filename + " will be removed."; + } + + private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, String filename, InputBuilder filePicker) + { + String divId = GUID.makeGUID(); + String linkId = "remove" + divId; + + DIV( + id(divId), + (Renderable) ret -> { + renderIconAndFilename(ctx, out, filename, false, false); + out.write(HtmlString.NBSP); + out.write("["); + out.write(LinkBuilder.simpleLink("remove", "#").id(linkId)); + out.write("]"); + return ret; + } + ).appendTo(out); + String innerHtml = filePicker + "" + getRemovalWarningText(filename) + ""; + HttpView.currentPageConfig().addHandler(linkId, "click", "document.getElementById(" + jsString(divId) + ").innerHTML = " + jsString(innerHtml)); + } } \ No newline at end of file diff --git a/api/src/org/labkey/api/study/assay/FileLinkDisplayColumn.java b/api/src/org/labkey/api/study/assay/FileLinkDisplayColumn.java index bd38585f554..87085810199 100644 --- a/api/src/org/labkey/api/study/assay/FileLinkDisplayColumn.java +++ b/api/src/org/labkey/api/study/assay/FileLinkDisplayColumn.java @@ -1,471 +1,471 @@ -/* - * 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.study.assay; - -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.labkey.api.admin.CoreUrls; -import org.labkey.api.attachments.Attachment; -import org.labkey.api.data.AbstractFileDisplayColumn; -import org.labkey.api.data.ColumnInfo; -import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerManager; -import org.labkey.api.data.DisplayColumn; -import org.labkey.api.data.RemappingDisplayColumnFactory; -import org.labkey.api.data.RenderContext; -import org.labkey.api.data.TableViewForm; -import org.labkey.api.exp.PropertyDescriptor; -import org.labkey.api.files.FileContentService; -import org.labkey.api.pipeline.PipeRoot; -import org.labkey.api.pipeline.PipelineService; -import org.labkey.api.query.DetailsURL; -import org.labkey.api.query.FieldKey; -import org.labkey.api.query.SchemaKey; -import org.labkey.api.security.User; -import org.labkey.api.security.permissions.ReadPermission; -import org.labkey.api.settings.AppProps; -import org.labkey.api.util.ContainerContext; -import org.labkey.api.util.FileUtil; -import org.labkey.api.util.NetworkDrive; -import org.labkey.api.util.PageFlowUtil; -import org.labkey.api.util.Path; -import org.labkey.api.util.URIUtil; -import org.labkey.api.view.ActionURL; -import org.labkey.api.webdav.WebdavResource; -import org.labkey.api.webdav.WebdavService; -import org.labkey.api.writer.HtmlWriter; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class FileLinkDisplayColumn extends AbstractFileDisplayColumn -{ - // Issue 46282 - let admins choose if files should be rendered inside browser or downloaded as files - public static final String AS_ATTACHMENT_FORMAT = "attachment"; - public static final String AS_INLINE_FORMAT = "inline"; - - public static class Factory implements RemappingDisplayColumnFactory - { - private final Container _container; - - private PropertyDescriptor _pd; - private DetailsURL _detailsUrl; - private SchemaKey _schemaKey; - private String _queryName; - private FieldKey _pkFieldKey; - private FieldKey _objectURIFieldKey; - - public Factory(PropertyDescriptor pd, Container c, @NotNull SchemaKey schemaKey, @NotNull String queryName, @NotNull FieldKey pkFieldKey) - { - _pd = pd; - _container = c; - _schemaKey = schemaKey; - _queryName = queryName; - _pkFieldKey = pkFieldKey; - } - - public Factory(PropertyDescriptor pd, Container c, @NotNull FieldKey lsidColumnFieldKey) - { - _pd = pd; - _container = c; - _objectURIFieldKey = lsidColumnFieldKey; - } - - public Factory(DetailsURL detailsURL, Container c, @NotNull FieldKey pkFieldKey) - { - _detailsUrl = detailsURL; - _container = c; - _pkFieldKey = pkFieldKey; - } - - @Override - public Factory remapFieldKeys(@Nullable FieldKey parent, @Nullable Map remap) - { - Factory remapped = this.clone(); - if (remapped._pkFieldKey != null) - { - remapped._pkFieldKey = FieldKey.remap(_pkFieldKey, parent, remap); - if (null == remapped._pkFieldKey) - remapped._pkFieldKey = _pkFieldKey; - } - if (remapped._objectURIFieldKey != null) - { - remapped._objectURIFieldKey = FieldKey.remap(_objectURIFieldKey, parent, remap); - if (null == remapped._objectURIFieldKey) - remapped._objectURIFieldKey = _objectURIFieldKey; - } - return remapped; - } - - @Override - public DisplayColumn createRenderer(ColumnInfo col) - { - if (_pd == null && _detailsUrl != null) - return new FileLinkDisplayColumn(col, _detailsUrl, _container, _pkFieldKey); - else if (_pkFieldKey != null) - return new FileLinkDisplayColumn(col, _pd, _container, _schemaKey, _queryName, _pkFieldKey); - else if (_container != null) - return new FileLinkDisplayColumn(col, _pd, _container, _objectURIFieldKey); - else - throw new IllegalArgumentException("Cannot create a renderer from the specified configuration properties"); - } - - @Override - public FileLinkDisplayColumn.Factory clone() - { - try - { - return (Factory)super.clone(); - } - catch (CloneNotSupportedException e) - { - throw new RuntimeException(e); - } - } - } - - private final Container _container; - - private FieldKey _pkFieldKey; - private FieldKey _objectURIFieldKey; - - /** Use schemaName/queryName and pk FieldKey value to resolve File in CoreController.DownloadFileLinkAction. */ - public FileLinkDisplayColumn(ColumnInfo col, PropertyDescriptor pd, Container container, @NotNull SchemaKey schemaKey, @NotNull String queryName, @NotNull FieldKey pkFieldKey) - { - super(col); - _container = container; - _pkFieldKey = pkFieldKey; - - if (pd.getURL() == null) - { - // Don't stomp over an explicitly configured URL on this column - StringBuilder sb = new StringBuilder("/core/downloadFileLink.view?propertyId="); - sb.append(pd.getPropertyId()); - sb.append("&schemaName="); - sb.append(PageFlowUtil.encodeURIComponent(schemaKey.toString())); - sb.append("&queryName="); - sb.append(PageFlowUtil.encodeURIComponent(queryName)); - sb.append("&pk=${"); - sb.append(pkFieldKey); - sb.append("}"); - sb.append("&modified=${Modified}"); - if (AS_ATTACHMENT_FORMAT.equalsIgnoreCase(col.getFormat())) - { - sb.append("&inline=false"); - } - ContainerContext context = new ContainerContext.FieldKeyContext(new FieldKey(pkFieldKey.getParent(), "Folder")); - setURLExpression(DetailsURL.fromString(sb.toString(), context)); - } - } - - /** Use LSID FieldKey value as ObjectURI to resolve File in CoreController.DownloadFileLinkAction. */ - public FileLinkDisplayColumn(ColumnInfo col, PropertyDescriptor pd, Container container, @NotNull FieldKey objectURIFieldKey) - { - super(col); - _container = container; - _objectURIFieldKey = objectURIFieldKey; - - if (pd.getURL() == null) - { - // Don't stomp over an explicitly configured URL on this column - - ActionURL baseUrl = PageFlowUtil.urlProvider(CoreUrls.class).getDownloadFileLinkBaseURL(container, pd); - if (AS_ATTACHMENT_FORMAT.equalsIgnoreCase(col.getFormat())) - { - baseUrl.addParameter("inline", "false"); - } - else - { - setLinkTarget("_blank"); - } - DetailsURL url = new DetailsURL(baseUrl, "objectURI", objectURIFieldKey); - setURLExpression(url); - } - } - - public FileLinkDisplayColumn(ColumnInfo col, DetailsURL detailsURL, Container container, @NotNull FieldKey pkFieldKey) - { - super(col); - _container = container; - _pkFieldKey = pkFieldKey; - - setURLExpression(detailsURL); - } - - @Override - protected Object getInputValue(RenderContext ctx) - { - ColumnInfo col = getColumnInfo(); - Object val = null; - TableViewForm viewForm = ctx.getForm(); - - if (col != null) - { - if (null != viewForm && viewForm.contains(this, ctx)) - { - val = viewForm.get(getFormFieldName(ctx)); - } - else if (ctx.getRow() != null) - val = col.getValue(ctx); - } - - return val; - } - - @Override - public void addQueryFieldKeys(Set keys) - { - super.addQueryFieldKeys(keys); - keys.add(FieldKey.fromParts("Modified")); - if (_pkFieldKey != null) - keys.add(_pkFieldKey); - if (_objectURIFieldKey != null) - keys.add(_objectURIFieldKey); - } - - public static boolean filePathExist(String path, Container container, User user) - { - String davPath = path; - if (FileUtil.isUrlEncoded(davPath)) - davPath = FileUtil.decodeURL(davPath); - var resolver = WebdavService.get().getResolver(); - // Resolve path under webdav root - Path parsed = Path.parse(StringUtils.trim(davPath)); - - // Issue 52968: handle context path - Path contextPath = AppProps.getInstance().getParsedContextPath(); - if (parsed.startsWith(contextPath)) - parsed = parsed.subpath(contextPath.size(), parsed.size()); - - WebdavResource resource = resolver.lookup(parsed); - if ((null == resource || !resource.exists()) && !parsed.startsWith(new Path("_webdav"))) - resource = resolver.lookup(new Path("_webdav").append(parsed)); - if (resource != null && resource.isFile() && resource.canRead(user, true)) - { - return true; - } - else - { - // Resolve file under pipeline root - PipeRoot root = PipelineService.get().findPipelineRoot(container); - if (root != null) - { - // Attempt absolute path first, then relative path from pipeline root - File f = new File(path); - if (!root.isUnderRoot(f)) - f = root.resolvePath(path); - - return (NetworkDrive.exists(f) && root.isUnderRoot(f) && root.hasPermission(container, user, ReadPermission.class)); - } - } - - return false; - } - - @Override - protected String getFileName(RenderContext ctx, Object value) - { - return getFileName(ctx, value, false); - } - - @Override - protected String getFileName(RenderContext ctx, Object value, boolean isDisplay) - { - String result = value == null ? null : StringUtils.trimToNull(value.toString()); - if (result != null) - { - File f = null; - if (result.startsWith("file:")) - { - try - { - f = new File(new URI(result)); - } - catch (URISyntaxException x) - { - // try to recover - result = result.substring("file:".length()); - } - } - if (null == f) - f = FileUtil.getAbsoluteCaseSensitiveFile(new File(result)); - NetworkDrive.ensureDrive(f.getPath()); - List fileRootTypes = List.of(FileContentService.ContentType.files, FileContentService.ContentType.pipeline, FileContentService.ContentType.assayfiles); - boolean valid = false; - List containers = new ArrayList<>(); - containers.add(_container); - // Not ideal, but needed in case data is queried from cross folder context - if (ctx.get("folder") != null || ctx.get("container") != null) - { - Object folderObj = ctx.get("folder"); - if (folderObj == null) - folderObj = ctx.get("container"); - if (folderObj instanceof String containerId) - { - Container dataContainer = ContainerManager.getForId(containerId); - if (dataContainer != null && !dataContainer.equals(_container)) - containers.add(dataContainer); - } - } - for (Container container : containers) - { - if (valid) - break; - - for (FileContentService.ContentType fileRootType : fileRootTypes) - { - result = relativize(f, FileContentService.get().getFileRoot(container, fileRootType)); - if (result != null) - { - // Issue 54062: Strip folder name from displayed name - if (isDisplay) - result = f.getName(); - - valid = true; - break; - } - } - } - if (result == null) - { - result = f.getName(); - } - - if ((!valid || !f.exists()) && !result.endsWith(UNAVAILABLE_FILE_SUFFIX)) - result += UNAVAILABLE_FILE_SUFFIX; - } - return result; - } - - public static String relativize(File f, File fileRoot) - { - if (fileRoot != null) - { - NetworkDrive.ensureDrive(fileRoot.getPath()); - fileRoot = FileUtil.getAbsoluteCaseSensitiveFile(fileRoot); - if (URIUtil.isDescendant(fileRoot.toURI(), f.toURI())) - { - try - { - return FileUtil.relativize(fileRoot, f, false); - } - catch (IOException ignored) {} - } - } - return null; - } - - @Override - protected InputStream getFileContents(RenderContext ctx, Object ignore) throws FileNotFoundException - { - Object value = getValue(ctx); - String s = value == null ? null : StringUtils.trimToNull(value.toString()); - if (s != null) - { - File f = new File(s); - if (f.isFile()) - return new FileInputStream(f); - } - return null; - } - - @Override - protected void renderIconAndFilename( - RenderContext ctx, - HtmlWriter out, - String fileValue /*Could be raw path value, or processed filename by `getFileName`*/, - boolean link, - boolean thumbnail) - { - Object value = getValue(ctx); - String strValue = value == null ? null : StringUtils.trimToNull(value.toString()); - if (strValue != null && !fileValue.endsWith(UNAVAILABLE_FILE_SUFFIX)) - { - File f; - if (strValue.startsWith("file:")) - f = new File(URI.create(strValue)); - else - f = new File(strValue); - - if (!f.exists()) - { - // try all file root - List fileRootTypes = List.of(FileContentService.ContentType.files, FileContentService.ContentType.pipeline, FileContentService.ContentType.assayfiles); - for (FileContentService.ContentType fileRootType : fileRootTypes) - { - String fullPath = FileContentService.get().getFileRoot(_container, fileRootType).getAbsolutePath()+ File.separator + value; - f = new File(fullPath); - if (f.exists()) - break; - } - } - - // It's probably a file, so check that first - if (f.isFile()) - { - super.renderIconAndFilename(ctx, out, strValue, link, thumbnail); - } - else if (f.isDirectory()) - { - super.renderIconAndFilename(ctx, out, strValue, Attachment.getFileIcon(".folder"), null, link, false); - } - else - { - // It's not on the file system anymore, so don't offer a link and tell the user it's unavailable - super.renderIconAndFilename(ctx, out, strValue, Attachment.getFileIcon(fileValue), null, false, false); - } - } - else - { - super.renderIconAndFilename(ctx, out, fileValue, link, thumbnail); - } - } - - @Override - public Object getDisplayValue(RenderContext ctx) - { - return getFileName(ctx, super.getDisplayValue(ctx), true); - } - - @Override - public Object getJsonValue(RenderContext ctx) - { - return getFileName(ctx, super.getDisplayValue(ctx)); - } - - @Override - public boolean isFilterable() - { - return false; - } - @Override - public boolean isSortable() - { - return false; - } - -} +/* + * 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.study.assay; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.admin.CoreUrls; +import org.labkey.api.attachments.Attachment; +import org.labkey.api.data.AbstractFileDisplayColumn; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.DisplayColumn; +import org.labkey.api.data.RemappingDisplayColumnFactory; +import org.labkey.api.data.RenderContext; +import org.labkey.api.data.TableViewForm; +import org.labkey.api.exp.PropertyDescriptor; +import org.labkey.api.files.FileContentService; +import org.labkey.api.pipeline.PipeRoot; +import org.labkey.api.pipeline.PipelineService; +import org.labkey.api.query.DetailsURL; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.SchemaKey; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.settings.AppProps; +import org.labkey.api.util.ContainerContext; +import org.labkey.api.util.FileUtil; +import org.labkey.api.util.NetworkDrive; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.Path; +import org.labkey.api.util.URIUtil; +import org.labkey.api.view.ActionURL; +import org.labkey.api.webdav.WebdavResource; +import org.labkey.api.webdav.WebdavService; +import org.labkey.api.writer.HtmlWriter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class FileLinkDisplayColumn extends AbstractFileDisplayColumn +{ + // Issue 46282 - let admins choose if files should be rendered inside browser or downloaded as files + public static final String AS_ATTACHMENT_FORMAT = "attachment"; + public static final String AS_INLINE_FORMAT = "inline"; + + public static class Factory implements RemappingDisplayColumnFactory + { + private final Container _container; + + private PropertyDescriptor _pd; + private DetailsURL _detailsUrl; + private SchemaKey _schemaKey; + private String _queryName; + private FieldKey _pkFieldKey; + private FieldKey _objectURIFieldKey; + + public Factory(PropertyDescriptor pd, Container c, @NotNull SchemaKey schemaKey, @NotNull String queryName, @NotNull FieldKey pkFieldKey) + { + _pd = pd; + _container = c; + _schemaKey = schemaKey; + _queryName = queryName; + _pkFieldKey = pkFieldKey; + } + + public Factory(PropertyDescriptor pd, Container c, @NotNull FieldKey lsidColumnFieldKey) + { + _pd = pd; + _container = c; + _objectURIFieldKey = lsidColumnFieldKey; + } + + public Factory(DetailsURL detailsURL, Container c, @NotNull FieldKey pkFieldKey) + { + _detailsUrl = detailsURL; + _container = c; + _pkFieldKey = pkFieldKey; + } + + @Override + public Factory remapFieldKeys(@Nullable FieldKey parent, @Nullable Map remap) + { + Factory remapped = this.clone(); + if (remapped._pkFieldKey != null) + { + remapped._pkFieldKey = FieldKey.remap(_pkFieldKey, parent, remap); + if (null == remapped._pkFieldKey) + remapped._pkFieldKey = _pkFieldKey; + } + if (remapped._objectURIFieldKey != null) + { + remapped._objectURIFieldKey = FieldKey.remap(_objectURIFieldKey, parent, remap); + if (null == remapped._objectURIFieldKey) + remapped._objectURIFieldKey = _objectURIFieldKey; + } + return remapped; + } + + @Override + public DisplayColumn createRenderer(ColumnInfo col) + { + if (_pd == null && _detailsUrl != null) + return new FileLinkDisplayColumn(col, _detailsUrl, _container, _pkFieldKey); + else if (_pkFieldKey != null) + return new FileLinkDisplayColumn(col, _pd, _container, _schemaKey, _queryName, _pkFieldKey); + else if (_container != null) + return new FileLinkDisplayColumn(col, _pd, _container, _objectURIFieldKey); + else + throw new IllegalArgumentException("Cannot create a renderer from the specified configuration properties"); + } + + @Override + public FileLinkDisplayColumn.Factory clone() + { + try + { + return (Factory)super.clone(); + } + catch (CloneNotSupportedException e) + { + throw new RuntimeException(e); + } + } + } + + private final Container _container; + + private FieldKey _pkFieldKey; + private FieldKey _objectURIFieldKey; + + /** Use schemaName/queryName and pk FieldKey value to resolve File in CoreController.DownloadFileLinkAction. */ + public FileLinkDisplayColumn(ColumnInfo col, PropertyDescriptor pd, Container container, @NotNull SchemaKey schemaKey, @NotNull String queryName, @NotNull FieldKey pkFieldKey) + { + super(col); + _container = container; + _pkFieldKey = pkFieldKey; + + if (pd.getURL() == null) + { + // Don't stomp over an explicitly configured URL on this column + StringBuilder sb = new StringBuilder("/core/downloadFileLink.view?propertyId="); + sb.append(pd.getPropertyId()); + sb.append("&schemaName="); + sb.append(PageFlowUtil.encodeURIComponent(schemaKey.toString())); + sb.append("&queryName="); + sb.append(PageFlowUtil.encodeURIComponent(queryName)); + sb.append("&pk=${"); + sb.append(pkFieldKey); + sb.append("}"); + sb.append("&modified=${Modified}"); + if (AS_ATTACHMENT_FORMAT.equalsIgnoreCase(col.getFormat())) + { + sb.append("&inline=false"); + } + ContainerContext context = new ContainerContext.FieldKeyContext(new FieldKey(pkFieldKey.getParent(), "Folder")); + setURLExpression(DetailsURL.fromString(sb.toString(), context)); + } + } + + /** Use LSID FieldKey value as ObjectURI to resolve File in CoreController.DownloadFileLinkAction. */ + public FileLinkDisplayColumn(ColumnInfo col, PropertyDescriptor pd, Container container, @NotNull FieldKey objectURIFieldKey) + { + super(col); + _container = container; + _objectURIFieldKey = objectURIFieldKey; + + if (pd.getURL() == null) + { + // Don't stomp over an explicitly configured URL on this column + + ActionURL baseUrl = PageFlowUtil.urlProvider(CoreUrls.class).getDownloadFileLinkBaseURL(container, pd); + if (AS_ATTACHMENT_FORMAT.equalsIgnoreCase(col.getFormat())) + { + baseUrl.addParameter("inline", "false"); + } + else + { + setLinkTarget("_blank"); + } + DetailsURL url = new DetailsURL(baseUrl, "objectURI", objectURIFieldKey); + setURLExpression(url); + } + } + + public FileLinkDisplayColumn(ColumnInfo col, DetailsURL detailsURL, Container container, @NotNull FieldKey pkFieldKey) + { + super(col); + _container = container; + _pkFieldKey = pkFieldKey; + + setURLExpression(detailsURL); + } + + @Override + protected Object getInputValue(RenderContext ctx) + { + ColumnInfo col = getColumnInfo(); + Object val = null; + TableViewForm viewForm = ctx.getForm(); + + if (col != null) + { + if (null != viewForm && viewForm.contains(this, ctx)) + { + val = viewForm.get(getFormFieldName(ctx)); + } + else if (ctx.getRow() != null) + val = col.getValue(ctx); + } + + return val; + } + + @Override + public void addQueryFieldKeys(Set keys) + { + super.addQueryFieldKeys(keys); + keys.add(FieldKey.fromParts("Modified")); + if (_pkFieldKey != null) + keys.add(_pkFieldKey); + if (_objectURIFieldKey != null) + keys.add(_objectURIFieldKey); + } + + public static boolean filePathExist(String path, Container container, User user) + { + String davPath = path; + if (FileUtil.isUrlEncoded(davPath)) + davPath = FileUtil.decodeURL(davPath); + var resolver = WebdavService.get().getResolver(); + // Resolve path under webdav root + Path parsed = Path.parse(StringUtils.trim(davPath)); + + // Issue 52968: handle context path + Path contextPath = AppProps.getInstance().getParsedContextPath(); + if (parsed.startsWith(contextPath)) + parsed = parsed.subpath(contextPath.size(), parsed.size()); + + WebdavResource resource = resolver.lookup(parsed); + if ((null == resource || !resource.exists()) && !parsed.startsWith(new Path("_webdav"))) + resource = resolver.lookup(new Path("_webdav").append(parsed)); + if (resource != null && resource.isFile() && resource.canRead(user, true)) + { + return true; + } + else + { + // Resolve file under pipeline root + PipeRoot root = PipelineService.get().findPipelineRoot(container); + if (root != null) + { + // Attempt absolute path first, then relative path from pipeline root + File f = new File(path); + if (!root.isUnderRoot(f)) + f = root.resolvePath(path); + + return (NetworkDrive.exists(f) && root.isUnderRoot(f) && root.hasPermission(container, user, ReadPermission.class)); + } + } + + return false; + } + + @Override + protected String getFileName(RenderContext ctx, Object value) + { + return getFileName(ctx, value, false); + } + + @Override + protected String getFileName(RenderContext ctx, Object value, boolean isDisplay) + { + String result = value == null ? null : StringUtils.trimToNull(value.toString()); + if (result != null) + { + File f = null; + if (result.startsWith("file:")) + { + try + { + f = new File(new URI(result)); + } + catch (URISyntaxException x) + { + // try to recover + result = result.substring("file:".length()); + } + } + if (null == f) + f = FileUtil.getAbsoluteCaseSensitiveFile(new File(result)); + NetworkDrive.ensureDrive(f.getPath()); + List fileRootTypes = List.of(FileContentService.ContentType.files, FileContentService.ContentType.pipeline, FileContentService.ContentType.assayfiles); + boolean valid = false; + List containers = new ArrayList<>(); + containers.add(_container); + // Not ideal, but needed in case data is queried from cross folder context + if (ctx.get("folder") != null || ctx.get("container") != null) + { + Object folderObj = ctx.get("folder"); + if (folderObj == null) + folderObj = ctx.get("container"); + if (folderObj instanceof String containerId) + { + Container dataContainer = ContainerManager.getForId(containerId); + if (dataContainer != null && !dataContainer.equals(_container)) + containers.add(dataContainer); + } + } + for (Container container : containers) + { + if (valid) + break; + + for (FileContentService.ContentType fileRootType : fileRootTypes) + { + result = relativize(f, FileContentService.get().getFileRoot(container, fileRootType)); + if (result != null) + { + // Issue 54062: Strip folder name from displayed name + if (isDisplay) + result = f.getName(); + + valid = true; + break; + } + } + } + if (result == null) + { + result = f.getName(); + } + + if ((!valid || !f.exists()) && !result.endsWith(UNAVAILABLE_FILE_SUFFIX)) + result += UNAVAILABLE_FILE_SUFFIX; + } + return result; + } + + public static String relativize(File f, File fileRoot) + { + if (fileRoot != null) + { + NetworkDrive.ensureDrive(fileRoot.getPath()); + fileRoot = FileUtil.getAbsoluteCaseSensitiveFile(fileRoot); + if (URIUtil.isDescendant(fileRoot.toURI(), f.toURI())) + { + try + { + return FileUtil.relativize(fileRoot, f, false); + } + catch (IOException ignored) {} + } + } + return null; + } + + @Override + protected InputStream getFileContents(RenderContext ctx, Object ignore) throws FileNotFoundException + { + Object value = getValue(ctx); + String s = value == null ? null : StringUtils.trimToNull(value.toString()); + if (s != null) + { + File f = new File(s); + if (f.isFile()) + return new FileInputStream(f); + } + return null; + } + + @Override + protected void renderIconAndFilename( + RenderContext ctx, + HtmlWriter out, + String fileValue /*Could be raw path value, or processed filename by `getFileName`*/, + boolean link, + boolean thumbnail) + { + Object value = getValue(ctx); + String strValue = value == null ? null : StringUtils.trimToNull(value.toString()); + if (strValue != null && !fileValue.endsWith(UNAVAILABLE_FILE_SUFFIX)) + { + File f; + if (strValue.startsWith("file:")) + f = new File(URI.create(strValue)); + else + f = new File(strValue); + + if (!f.exists()) + { + // try all file root + List fileRootTypes = List.of(FileContentService.ContentType.files, FileContentService.ContentType.pipeline, FileContentService.ContentType.assayfiles); + for (FileContentService.ContentType fileRootType : fileRootTypes) + { + String fullPath = FileContentService.get().getFileRoot(_container, fileRootType).getAbsolutePath()+ File.separator + value; + f = new File(fullPath); + if (f.exists()) + break; + } + } + + // It's probably a file, so check that first + if (f.isFile()) + { + super.renderIconAndFilename(ctx, out, strValue, link, thumbnail); + } + else if (f.isDirectory()) + { + super.renderIconAndFilename(ctx, out, strValue, Attachment.getFileIcon(".folder"), null, link, false); + } + else + { + // It's not on the file system anymore, so don't offer a link and tell the user it's unavailable + super.renderIconAndFilename(ctx, out, strValue, Attachment.getFileIcon(fileValue), null, false, false); + } + } + else + { + super.renderIconAndFilename(ctx, out, fileValue, link, thumbnail); + } + } + + @Override + public Object getDisplayValue(RenderContext ctx) + { + return getFileName(ctx, super.getDisplayValue(ctx), true); + } + + @Override + public Object getJsonValue(RenderContext ctx) + { + return getFileName(ctx, super.getDisplayValue(ctx)); + } + + @Override + public boolean isFilterable() + { + return false; + } + @Override + public boolean isSortable() + { + return false; + } + +} From 241d268af8d11c6f292e881e2e0abb6d26a83bc1 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 15 Oct 2025 09:27:02 -0700 Subject: [PATCH 3/7] Show folder on edit --- .../api/data/AbstractFileDisplayColumn.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java index 605c08d3799..20d354c163e 100644 --- a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java +++ b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java @@ -65,13 +65,13 @@ public AbstractFileDisplayColumn(ColumnInfo col) @Override public void renderDetailsCellContents(RenderContext ctx, HtmlWriter out) { - renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); + renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true, false); } @Override public void renderGridCellContents(RenderContext ctx, HtmlWriter out) { - renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); + renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true, false); } /** @return the short name of the file (not including full path) */ @@ -84,9 +84,9 @@ protected String getFileName(RenderContext ctx, Object value, boolean isDisplay) protected abstract InputStream getFileContents(RenderContext ctx, Object value) throws FileNotFoundException; - protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, boolean link, boolean thumbnail) + protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, boolean link, boolean thumbnail, boolean input) { - renderIconAndFilename(ctx, out, fileValue, null, null, link, thumbnail); + renderIconAndFilename(ctx, out, fileValue, null, null, link, thumbnail, input); } protected boolean isImage(String filename) @@ -97,14 +97,14 @@ protected boolean isImage(String filename) || filename.toLowerCase().endsWith(".gif"); } - protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean link, boolean thumbnail) + protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean link, boolean thumbnail, boolean input) { if (null != fileValue && !StringUtils.isEmpty(fileValue)) { // equivalent of DisplayColumn.renderURL. // Don't want to call renderUrl (DataColumn.renderUrl) to skip unnecessary displayValue check StringExpression s = compileExpression(ctx.getViewContext()); - String displayName = getFileName(ctx, fileValue, true); + String displayName = getFileName(ctx, fileValue, !input); boolean unavailable = displayName.endsWith(UNAVAILABLE_FILE_SUFFIX); String url = null == s || unavailable ? null : s.eval(ctx); boolean isImage = isImage(fileValue); @@ -286,7 +286,7 @@ public void renderInputHtml(RenderContext ctx, HtmlWriter out, Object value) if (null != filename) { // Existing value, so tell the user the file name, allow the file to be removed, and a new file uploaded - renderThumbnailAndRemoveLink(out, ctx, filename, input); + renderThumbnailAndRemoveLink(out, ctx, filename, input, true); // don't use display } else { @@ -307,7 +307,7 @@ protected String getRemovalWarningText(String filename) return "Previous file " + filename + " will be removed."; } - private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, String filename, InputBuilder filePicker) + private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, String filename, InputBuilder filePicker, boolean input) { String divId = GUID.makeGUID(); String linkId = "remove" + divId; @@ -315,7 +315,7 @@ private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, Str DIV( id(divId), (Renderable) ret -> { - renderIconAndFilename(ctx, out, filename, false, false); + renderIconAndFilename(ctx, out, filename, false, false, input); out.write(HtmlString.NBSP); out.write("["); out.write(LinkBuilder.simpleLink("remove", "#").id(linkId)); From bc26aaef9c9e5b47836b43c09be9314c66d8a124 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 15 Oct 2025 09:30:32 -0700 Subject: [PATCH 4/7] fix build --- .../org/labkey/api/data/AbstractFileDisplayColumn.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java index 20d354c163e..eda32b9e861 100644 --- a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java +++ b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java @@ -65,13 +65,13 @@ public AbstractFileDisplayColumn(ColumnInfo col) @Override public void renderDetailsCellContents(RenderContext ctx, HtmlWriter out) { - renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true, false); + renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); } @Override public void renderGridCellContents(RenderContext ctx, HtmlWriter out) { - renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true, false); + renderIconAndFilename(ctx, out, (String)getValue(ctx), true, true); } /** @return the short name of the file (not including full path) */ @@ -84,9 +84,9 @@ protected String getFileName(RenderContext ctx, Object value, boolean isDisplay) protected abstract InputStream getFileContents(RenderContext ctx, Object value) throws FileNotFoundException; - protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, boolean link, boolean thumbnail, boolean input) + protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, boolean link, boolean thumbnail) { - renderIconAndFilename(ctx, out, fileValue, null, null, link, thumbnail, input); + renderIconAndFilename(ctx, out, fileValue, null, null, link, thumbnail, false); } protected boolean isImage(String filename) @@ -315,7 +315,7 @@ private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, Str DIV( id(divId), (Renderable) ret -> { - renderIconAndFilename(ctx, out, filename, false, false, input); + renderIconAndFilename(ctx, out, filename, null, null, false, false, input); out.write(HtmlString.NBSP); out.write("["); out.write(LinkBuilder.simpleLink("remove", "#").id(linkId)); From 21e482eafa0c8cdc0e79978f182942679551dbce Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 15 Oct 2025 09:32:28 -0700 Subject: [PATCH 5/7] fix build --- api/src/org/labkey/api/data/AbstractFileDisplayColumn.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java index eda32b9e861..58707cc0185 100644 --- a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java +++ b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java @@ -97,6 +97,11 @@ protected boolean isImage(String filename) || filename.toLowerCase().endsWith(".gif"); } + protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean link, boolean thumbnail) + { + renderIconAndFilename(ctx, out, fileValue, fileIconUrl, popupIconUrl, link, thumbnail, false); + } + protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean link, boolean thumbnail, boolean input) { if (null != fileValue && !StringUtils.isEmpty(fileValue)) From fc0426d537f59ec4c71b1a2a5f8f86e616e186a6 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 15 Oct 2025 09:34:25 -0700 Subject: [PATCH 6/7] fix user avatar display --- api/src/org/labkey/api/data/AbstractFileDisplayColumn.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java index 58707cc0185..81eb00b1351 100644 --- a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java +++ b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java @@ -86,7 +86,7 @@ protected String getFileName(RenderContext ctx, Object value, boolean isDisplay) protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, boolean link, boolean thumbnail) { - renderIconAndFilename(ctx, out, fileValue, null, null, link, thumbnail, false); + renderIconAndFilename(ctx, out, fileValue, null, null, link, thumbnail); } protected boolean isImage(String filename) From f5684020d13c6862cad3b2f6521ee97011f3f403 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 15 Oct 2025 09:42:43 -0700 Subject: [PATCH 7/7] revert changes in input --- .../labkey/api/data/AbstractFileDisplayColumn.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java index 81eb00b1351..605c08d3799 100644 --- a/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java +++ b/api/src/org/labkey/api/data/AbstractFileDisplayColumn.java @@ -98,18 +98,13 @@ protected boolean isImage(String filename) } protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean link, boolean thumbnail) - { - renderIconAndFilename(ctx, out, fileValue, fileIconUrl, popupIconUrl, link, thumbnail, false); - } - - protected void renderIconAndFilename(RenderContext ctx, HtmlWriter out, String fileValue, @Nullable String fileIconUrl, @Nullable String popupIconUrl, boolean link, boolean thumbnail, boolean input) { if (null != fileValue && !StringUtils.isEmpty(fileValue)) { // equivalent of DisplayColumn.renderURL. // Don't want to call renderUrl (DataColumn.renderUrl) to skip unnecessary displayValue check StringExpression s = compileExpression(ctx.getViewContext()); - String displayName = getFileName(ctx, fileValue, !input); + String displayName = getFileName(ctx, fileValue, true); boolean unavailable = displayName.endsWith(UNAVAILABLE_FILE_SUFFIX); String url = null == s || unavailable ? null : s.eval(ctx); boolean isImage = isImage(fileValue); @@ -291,7 +286,7 @@ public void renderInputHtml(RenderContext ctx, HtmlWriter out, Object value) if (null != filename) { // Existing value, so tell the user the file name, allow the file to be removed, and a new file uploaded - renderThumbnailAndRemoveLink(out, ctx, filename, input, true); // don't use display + renderThumbnailAndRemoveLink(out, ctx, filename, input); } else { @@ -312,7 +307,7 @@ protected String getRemovalWarningText(String filename) return "Previous file " + filename + " will be removed."; } - private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, String filename, InputBuilder filePicker, boolean input) + private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, String filename, InputBuilder filePicker) { String divId = GUID.makeGUID(); String linkId = "remove" + divId; @@ -320,7 +315,7 @@ private void renderThumbnailAndRemoveLink(HtmlWriter out, RenderContext ctx, Str DIV( id(divId), (Renderable) ret -> { - renderIconAndFilename(ctx, out, filename, null, null, false, false, input); + renderIconAndFilename(ctx, out, filename, false, false); out.write(HtmlString.NBSP); out.write("["); out.write(LinkBuilder.simpleLink("remove", "#").id(linkId));