diff --git a/api/build.gradle b/api/build.gradle index 28eb3ee8d1e..ec41567f313 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -381,6 +381,32 @@ dependencies { ) ) + BuildUtils.addExternalDependency( + project, + new ExternalDependency( + "org.graphper:graph-support-core:${graphSupportVersion}", + "graph-support-core", + "graph-support", + "https://github.com/jamisonjiang/graph-support", + ExternalDependency.APACHE_2_LICENSE_NAME, + ExternalDependency.APACHE_2_LICENSE_URL, + "Graphviz Java API", + ) + ) + + BuildUtils.addExternalDependency( + project, + new ExternalDependency( + "org.graphper:graph-support-dot:${graphSupportVersion}", + "graph-support-dot", + "graph-support", + "https://github.com/jamisonjiang/graph-support", + ExternalDependency.APACHE_2_LICENSE_NAME, + ExternalDependency.APACHE_2_LICENSE_URL, + "DOT parsing support", + ) + ) + BuildUtils.addExternalDependency( project, new ExternalDependency( diff --git a/api/src/org/labkey/api/ApiModule.java b/api/src/org/labkey/api/ApiModule.java index d613909b2b6..2d85ff99ba8 100644 --- a/api/src/org/labkey/api/ApiModule.java +++ b/api/src/org/labkey/api/ApiModule.java @@ -172,6 +172,7 @@ import org.labkey.api.util.SessionHelper; import org.labkey.api.util.StringExpressionFactory; import org.labkey.api.util.StringUtilsLabKey; +import org.labkey.api.util.SvgUtil; import org.labkey.api.util.SystemMaintenance; import org.labkey.api.util.SystemMaintenanceStartupListener; import org.labkey.api.util.URIUtil; @@ -439,6 +440,7 @@ public void registerServlets(ServletContext servletCtx) StringExpressionFactory.TestCase.class, StringUtilsLabKey.TestCase.class, SubfolderWriter.TestCase.class, + SvgUtil.TestCase.class, SwapQueue.TestCase.class, TSVMapWriter.Tests.class, TSVWriter.TestCase.class, diff --git a/api/src/org/labkey/api/action/SpringActionController.java b/api/src/org/labkey/api/action/SpringActionController.java index 6b178eb8553..0019f9cdcb0 100644 --- a/api/src/org/labkey/api/action/SpringActionController.java +++ b/api/src/org/labkey/api/action/SpringActionController.java @@ -175,6 +175,16 @@ public static Collection getRegisteredActionDescriptors() return new ArrayList<>(_classToDescriptor.values()); } + protected static String h(@Nullable CharSequence s) + { + return PageFlowUtil.filter(s); + } + + protected static String h(@Nullable Object o) + { + return PageFlowUtil.filter(o); + } + // I don't think there is an interface for this public interface ActionResolver { diff --git a/api/src/org/labkey/api/attachments/SvgSource.java b/api/src/org/labkey/api/attachments/SvgSource.java index bf7c068a45d..388c5ae2d3d 100644 --- a/api/src/org/labkey/api/attachments/SvgSource.java +++ b/api/src/org/labkey/api/attachments/SvgSource.java @@ -5,6 +5,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.SvgUtil; +import org.labkey.api.util.SvgUtil.Size; import org.labkey.api.view.NotFoundException; import java.io.BufferedReader; @@ -19,8 +21,7 @@ public class SvgSource { private final String _filteredSvg; - - private Float _height = null; + private final Float _height; public SvgSource(String svg) { @@ -32,16 +33,11 @@ public SvgSource(String svg) if (!svg.contains("xmlns=\"" + SVGDOMImplementation.SVG_NAMESPACE_URI + "\"") && !svg.contains("xmlns='" + SVGDOMImplementation.SVG_NAMESPACE_URI + "'")) svg = svg.replace(" sortModulesByDependencies(List modules) { List>> dependencies = new ArrayList<>(); @@ -101,7 +103,7 @@ public List sortModulesByDependencies(List modules) if (module.getName().equalsIgnoreCase("core")) { result.remove(i); - result.add(0, module); + result.addFirst(module); break; } } @@ -128,41 +130,36 @@ private Module findModuleWithoutDependencies(List>> dep throw new IllegalArgumentException("Module '" + moduleName + "' (" + entry.getKey().getClass().getName() + ") is listed as being dependent on itself."); } - StringBuilder sb = new StringBuilder(); - for (Pair> dependencyInfo : dependencies) - { - if (!sb.isEmpty()) - { - sb.append(", "); - } - sb.append(dependencyInfo.getKey().getName()); - } + String involved = dependencies.stream() + .map(pair -> pair.getKey().getName()) + .collect(Collectors.joining(", ")); // Generate an SVG diagram that shows all remaining dependencies graphModuleDependencies(dependencies, "involved"); - throw new IllegalArgumentException("Unable to resolve module dependencies. The following modules are somehow involved: " + sb); + throw new IllegalArgumentException("Unable to resolve module dependencies. The following modules are somehow involved: " + involved); } private void graphModuleDependencies(List>> dependencies, @SuppressWarnings("SameParameterValue") String adjective) { - Logger log = LogManager.getLogger(ModuleDependencySorter.class); - try { File dir = FileUtil.getTempDirectory(); String dot = buildDigraph(dependencies); + Graphviz graph = DotParser.parse(dot); File svgFile = FileUtil.createTempFile("modules", ".svg", dir); - DotRunner runner = new DotRunner(dir, dot); - runner.addSvgOutput(svgFile); - runner.execute(); - log.info("For a diagram of " + adjective + " module dependencies, see " + svgFile.getAbsolutePath()); + try (GraphResource resource = graph.toSvg()) + { + resource.save(svgFile.getParent(), svgFile.getName()); + } + + LOG.info("For a diagram of {} module dependencies, see {}", adjective, svgFile.getAbsolutePath()); } catch (Exception e) { - log.error("Error running dot", e); + LOG.error("Error running dot", e); } } diff --git a/api/src/org/labkey/api/util/DotRunner.java b/api/src/org/labkey/api/util/DotRunner.java deleted file mode 100644 index b975a34b2b7..00000000000 --- a/api/src/org/labkey/api/util/DotRunner.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2013-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.util; - -import org.apache.commons.lang3.StringUtils; -import org.labkey.api.exp.ExperimentException; -import org.labkey.api.miniprofiler.CustomTiming; -import org.labkey.api.miniprofiler.MiniProfiler; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.util.LinkedList; -import java.util.List; - -/** - * User: adam - * Date: 11/13/11 - * Time: 10:33 AM - */ - -// Handles the basics of testing the dot configuration, creating the dot command line, and invoking dot. This code -// originated in ExperimentRunGraph, but was pulled out to support group diagrams. -public class DotRunner -{ - private static final String dotExePath = "dot"; - - private final String _dotInput; - private final File _directory; - private final List _parameters = new LinkedList<>(); - - public DotRunner(File directory, String dotInput) - { - _directory = directory; - _dotInput = dotInput; - _parameters.add(dotExePath); - } - - public void addPngOutput(File pngFile) - { - addOutputParameter("png", pngFile); - } - - public void addCmapOutput(File cmapFile) - { - addOutputParameter("cmap", cmapFile); - } - - public void addSvgOutput(File svgFile) - { - addOutputParameter("svg", svgFile); - } - - private void addOutputParameter(String typeName, File file) - { - _parameters.add("-T" + typeName); - _parameters.add("-o" + file.getName()); - } - - public void execute() throws IOException, InterruptedException - { - ProcessBuilder pb = new ProcessBuilder(_parameters); - pb.directory(_directory); - ProcessResult result = executeProcess(pb, _dotInput); - - if (result._returnCode != 0) - { - throw new IOException("Graph generation failed with error code " + result._returnCode + ":\n" + result._output); - } - } - - public static void testDotPath(File baseDirectory) throws ExperimentException - { - try - { - File testVersion = FileUtil.appendName(baseDirectory, "dottest.txt"); - if (testVersion.exists()) - return; - - ProcessBuilder pb = new ProcessBuilder(dotExePath, "-V", "-o" + testVersion.getName()); - pb = pb.directory(baseDirectory); - pb.redirectErrorStream(true); - Process p = pb.start(); - int err = p.waitFor(); - - if (err != 0) - { - BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) - { - sb.append(line); - sb.append("\n"); - } - sb.append("(Exit code: "); - sb.append(err); - sb.append(")"); - throw new ExperimentException(getConfigurationError(sb.toString())); - } - } - catch (IOException e) - { - throw new ExperimentException(getConfigurationError(e)); - } - catch (InterruptedException e) - { - throw new ExperimentException("InterruptedException - web server may be shutting down"); - } - } - - public static String getConfigurationError(IOException e) - { - if (e.getMessage() != null) - { - return getConfigurationError(e.getMessage()); - } - else - { - return getConfigurationError(e.toString()); - } - } - - public static String getConfigurationError(String message) - { - StringBuilder sb = new StringBuilder(); - sb.append("Unable to display graph view: cannot run "); - sb.append(dotExePath); - sb.append(" due to an error.\n\n"); - sb.append(message); - sb.append("\n\n"); - sb.append("Regardless of the specific error message, please first verify that the program '" + dotExePath + "' is on your system PATH"); - if (!StringUtils.isEmpty(System.getenv("PATH"))) - { - sb.append(" ("); - sb.append(System.getenv("PATH")); - sb.append(")"); - } - sb.append(".\n\n"); - sb.append("For help on fixing your system configuration, please consult the Graphviz section of the LabKey Server documentation on third party components."); - - return sb.toString(); - } - - private static ProcessResult executeProcess(ProcessBuilder pb, String stdIn) throws IOException, InterruptedException - { - try (CustomTiming t = MiniProfiler.custom("exec", StringUtils.join(pb.command(), " "))) - { - StringBuilder sb = new StringBuilder(); - pb.redirectErrorStream(true); - Process p = pb.start(); - - try (PrintWriter writer = new PrintWriter(p.getOutputStream())) - { - writer.write(stdIn); - } - - try (BufferedReader procReader = new BufferedReader(new InputStreamReader(p.getInputStream()))) - { - String line; - - while ((line = procReader.readLine()) != null) - { - sb.append(line); - sb.append("\n"); - } - } - - int returnCode = p.waitFor(); - return new ProcessResult(returnCode, sb.toString()); - } - } - - private static class ProcessResult - { - private final int _returnCode; - private final String _output; - - public ProcessResult(int returnCode, String output) - { - _returnCode = returnCode; - _output = output; - } - } -} diff --git a/api/src/org/labkey/api/util/SvgUtil.java b/api/src/org/labkey/api/util/SvgUtil.java new file mode 100644 index 00000000000..df148a80d9a --- /dev/null +++ b/api/src/org/labkey/api/util/SvgUtil.java @@ -0,0 +1,214 @@ +package org.labkey.api.util; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; +import org.junit.Assert; +import org.junit.Test; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SvgUtil +{ + public record Size(Float value, @Nullable String units) + { + @Override + public @NonNull String toString() + { + return value + (null != units ? units : ""); + } + } + + public static @Nullable String readAttribute(String svg, String attribute) + { + String ret = null; + + int idx = svg.indexOf(attribute + "="); + if (idx != -1) + { + int start = idx + attribute.length() + 2; + int end = svg.indexOf("\"", start + 1); + ret = svg.substring(start, end); + } + + return ret; + } + + private static final Pattern floatingPointPattern = Pattern.compile("(?(\\d*\\.)?\\d+)(?[^\\d.]*)"); + + public static @Nullable Size readSizeAttribute(String svg, String attribute) + { + Size ret = null; + String value = readAttribute(svg, attribute); + if (value != null && value.length() < 100) // Prevent DOS on regex + { + Matcher fbMatcher = floatingPointPattern.matcher(value); + if (fbMatcher.find()) + { + ret = new Size(Float.parseFloat(fbMatcher.group("value")), StringUtils.trimToNull(fbMatcher.group("units"))); + } + else + { + throw new IllegalStateException("Couldn't match value " + value + " into a valid size pattern"); + } + } + + return ret; + } + + public static @Nullable Size readHeight(String svg) + { + return readSizeAttribute(svg, "height"); + } + + public static String setHeight(String svg, @Nullable Size height) + { + return setSizeAttribute(svg, "height", height); + } + + public static @Nullable Size readWidth(String svg) + { + return readSizeAttribute(svg, "width"); + } + + public static String setWidth(String svg, @Nullable Size width) + { + return setSizeAttribute(svg, "width", width); + } + + public static String scaleSize(String svg, float scale) + { + Size height = SvgUtil.readHeight(svg); + if (height == null) + throw new IllegalStateException("height is null"); + Size newHeight = new Size(height.value() * scale, height.units()); + String updated = setHeight(svg, newHeight); + + Size width = SvgUtil.readWidth(updated); + if (width == null) + throw new IllegalStateException("width is null"); + Size newWidth = new Size(width.value() * scale, width.units()); + return setWidth(updated, newWidth); + } + + // Replaces the existing attribute value with size parameter. If "size" is null, removes the attribute completely. + private static String setSizeAttribute(String svg, String attribute, @Nullable Size size) + { + String ret = svg; + int idx = svg.indexOf(attribute + "="); + + if (idx == -1) + { + if (size != null) + { + int svgElement = svg.indexOf(" + drt/CAexample_mini(DRT2)CAexample_mini.mzXMLBovine_mini1.fastaBovine_mini2.fastaBovine_mini3.fastaTandem SettingsScored SearchResultsProtein Prophetscoresdrt/CAexample_mini + (DRT2)->Scored Search + Resultsdrt/CAexample_mini + (DRT2)->Protein Prophet + scoresCAexample_mini.mzXML->drt/CAexample_mini + (DRT2)mzXMLBovine_mini1.fasta->drt/CAexample_mini + (DRT2)FASTABovine_mini2.fasta->drt/CAexample_mini + (DRT2)FASTABovine_mini3.fasta->drt/CAexample_mini + (DRT2)FASTATandem Settings->drt/CAexample_mini + (DRT2)SearchConfig + """; + + public static class TestCase extends Assert + { + @Test + public void testAttributes() + { + Size height = readHeight(svg); + Assert.assertNotNull(height); + Assert.assertEquals(785.3512f, height.value(), 0.0001f); + Assert.assertEquals("pt", height.units()); + + Size newHeight = new Size(123.4567f, "px"); + Assert.assertEquals(readHeight(setHeight(svg, newHeight)), newHeight); + + Size width = readWidth(svg); + Assert.assertNotNull(width); + Assert.assertEquals(1776.9770f, width.value(), 0.0001f); + Assert.assertEquals("pt", width.units()); + + Size newWidth = new Size(456.7890f, "px"); + Assert.assertEquals(readWidth(setWidth(svg, newWidth)), newWidth); + + // Now clear height and width, then test read and set scenarios when these attributes are missing + + String svgNoSize = setWidth(setHeight(svg, null), null); + Assert.assertNull(readHeight(svgNoSize)); + Assert.assertNull(readWidth(svgNoSize)); + + String changedSvg = setHeight(svgNoSize, new Size(375.1234f, "px")); + height = readHeight(changedSvg); + Assert.assertNotNull(height); + Assert.assertEquals(375.1234f, height.value(), 0.0001f); + Assert.assertEquals("px", height.units()); + + changedSvg = setWidth(svgNoSize, new Size(1023.9876f, "px")); + width = readWidth(changedSvg); + Assert.assertNotNull(width); + Assert.assertEquals(1023.9876f, width.value(), 0.0001f); + Assert.assertEquals("px", width.units()); + } + + @Test + public void testCornerCases() + { + String svg = ""; + assertEquals(new Size(50f, "%"), readWidth(svg)); + assertEquals(new Size(75f, "%"), readHeight(svg)); + assertEquals("", setWidth(svg, new Size(83f, "%"))); + assertEquals("", setHeight(svg, new Size(47f, "%"))); + + svg = ""; + assertNull(readWidth(svg)); + Size size = readHeight(svg); + assertNotNull(size); + assertEquals(new Size(100f, null), size); + assertEquals("", setHeight(svg, new Size(200f, "px"))); + assertEquals("", setHeight(svg, null)); + + svg = ""; + assertNull(readHeight(svg)); + size = readWidth(svg); + assertNotNull(size); + assertEquals(new Size(200f, null), size); + assertEquals("", setWidth(svg, new Size(300f, "px"))); + assertEquals("", setWidth(svg, null)); + + assertEquals("", setWidth("", null)); + assertEquals("", setHeight("", null)); + } + } +} diff --git a/core/src/org/labkey/core/CoreModule.java b/core/src/org/labkey/core/CoreModule.java index 7b89ff030b2..c13e6a705ef 100644 --- a/core/src/org/labkey/core/CoreModule.java +++ b/core/src/org/labkey/core/CoreModule.java @@ -105,7 +105,6 @@ import org.labkey.api.notification.NotificationMenuView; import org.labkey.api.portal.ProjectUrls; import org.labkey.api.premium.AntiVirusProviderRegistry; -import org.labkey.api.products.Product; import org.labkey.api.products.ProductRegistry; import org.labkey.api.qc.DataStateManager; import org.labkey.api.query.DefaultSchema; @@ -267,7 +266,6 @@ import org.labkey.core.portal.CollaborationFolderType; import org.labkey.core.portal.PortalJUnitTest; import org.labkey.core.portal.ProjectController; -import org.labkey.core.portal.UtilController; import org.labkey.core.products.ProductController; import org.labkey.core.project.FolderNavigationForm; import org.labkey.core.qc.CoreQCStateHandler; @@ -443,7 +441,6 @@ protected void init() addController("core", CoreController.class); addController("analytics", AnalyticsController.class); addController("project", ProjectController.class); - addController("util", UtilController.class); addController("logger", LoggerController.class); addController("mini-profiler", MiniProfilerController.class); addController("notification", NotificationController.class); diff --git a/core/src/org/labkey/core/junit/JunitController.java b/core/src/org/labkey/core/junit/JunitController.java index 6d050037ce3..4fec3407617 100644 --- a/core/src/org/labkey/core/junit/JunitController.java +++ b/core/src/org/labkey/core/junit/JunitController.java @@ -757,7 +757,6 @@ public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse re } } - private static List filterStackTrace(Throwable t) { List lines = new ArrayList<>(); @@ -789,9 +788,4 @@ private static void outputStackTrace(Throwable t, PrintWriter out) out.println(PageFlowUtil.filter(line, true)); } } - - private static String h(String s) - { - return PageFlowUtil.filter(s); - } } diff --git a/core/src/org/labkey/core/portal/UtilController.java b/core/src/org/labkey/core/portal/UtilController.java deleted file mode 100644 index a89e401d5bb..00000000000 --- a/core/src/org/labkey/core/portal/UtilController.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2012-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.core.portal; - -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; -import org.labkey.api.action.SimpleViewAction; -import org.labkey.api.action.SpringActionController; -import org.labkey.api.security.RequiresPermission; -import org.labkey.api.security.permissions.ReadPermission; -import org.labkey.api.util.DotRunner; -import org.labkey.api.util.FileUtil; -import org.labkey.api.util.PageFlowUtil; -import org.labkey.api.view.HtmlView; -import org.labkey.api.view.NavTree; -import org.labkey.api.view.UnauthorizedException; -import org.labkey.api.view.WebPartView; -import org.labkey.api.view.template.PageConfig; -import org.springframework.validation.BindException; -import org.springframework.web.servlet.ModelAndView; - -import java.io.File; - -/** - * User: matthewb - * Date: 2011-11-18 - * Time: 10:26 AM - */ -public class UtilController extends SpringActionController -{ - private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(UtilController.class); - private static final Logger _log = LogManager.getLogger(ProjectController.class); - - public UtilController() - { - setActionResolver(_actionResolver); - } - - - public static class DotForm - { - public String getDot() - { - return _text; - } - public void setDot(String text) - { - _text = text; - } - String _text; - } - - - @RequiresPermission(ReadPermission.class) - public static class DotSvgAction extends SimpleViewAction - { - @Override - public ModelAndView getView(DotForm form, BindException errors) throws Exception - { - // Don't allow GET to avoid security holes as this may inject script - if (!isPost()) - throw new UnauthorizedException("use POST"); - - getPageConfig().setTemplate(PageConfig.Template.None); - - String dot = form.getDot(); - File dir = FileUtil.getTempDirectory(); - File svgFile = null; - try - { - svgFile = FileUtil.createTempFile("groups", ".svg", dir); - svgFile.deleteOnExit(); - DotRunner runner = new DotRunner(dir, dot); - runner.addSvgOutput(svgFile); - runner.execute(); - String svg = PageFlowUtil.getFileContentsAsString(svgFile); - String html = svg.substring(svg.indexOf(" + public static class StandardDeleteGroupAction extends FormHandlerAction { @Override public void validateCommand(GroupForm form, Errors errors) {} @@ -800,7 +798,7 @@ public boolean handlePost(UpdateMembersForm form, BindException errors) throws E // Ignore lines of all whitespace, otherwise show an error. String e = trimToNull(rawEmail); if (null != e) - errors.reject(ERROR_MSG, "Could not add user " + filter(e) + ": Invalid email address"); + errors.reject(ERROR_MSG, "Could not add user " + h(e) + ": Invalid email address"); } String[] removeNames = form.getDelete(); @@ -834,7 +832,7 @@ public boolean handlePost(UpdateMembersForm form, BindException errors) throws E // Ignore lines of all whitespace, otherwise show an error. String e = trimToNull(rawEmail); if (null != e) - errors.reject(ERROR_MSG, "Could not remove user " + filter(e) + ": Invalid email address"); + errors.reject(ERROR_MSG, "Could not remove user " + h(e) + ": Invalid email address"); } if (_group != null) @@ -877,7 +875,7 @@ public boolean handlePost(UpdateMembersForm form, BindException errors) throws E if (null != user) { if (!user.isActive()) - errors.reject(ERROR_MSG, "You may not add the user '" + PageFlowUtil.filter(email) + errors.reject(ERROR_MSG, "You may not add the user '" + h(email) + "' to this group because that user account is currently deactivated." + " To reactivate this account, contact your system administrator."); else @@ -1158,7 +1156,7 @@ public void renderGrid(RenderContext ctx, Sheet sheet, List visible } @RequiresPermission(AdminPermission.class) - public class GroupPermissionAction extends SimpleViewAction + public static class GroupPermissionAction extends SimpleViewAction { private Group _requestedGroup; @@ -1492,7 +1490,7 @@ public boolean handlePost(AddUsersForm form, BindException errors) throws Except if (HtmlString.isBlank(result)) { ActionURL url = urlProvider(UserUrls.class).getUserDetailsURL(getContainer(), newUser.getUserId(), returnUrl); - result = HtmlString.unsafe(PageFlowUtil.filter(email) + " was already a registered system user. Click here to see this user's profile and history."); + result = HtmlString.unsafe(h(email) + " was already a registered system user. Click here to see this user's profile and history."); } else if (userToClone != null) { @@ -1511,7 +1509,7 @@ else if (userToClone != null) } } } - form.addMessage(HtmlString.unsafe(String.format("%s", result, newUser.getUserId(), PageFlowUtil.filter(newUser.getEmail())))); + form.addMessage(HtmlString.unsafe(String.format("%s", result, newUser.getUserId(), h(newUser.getEmail())))); } } @@ -1585,7 +1583,7 @@ private void deletePermissions(User user) user.refreshGroups(); // We just deleted them all; refresh so subsequent operations see that // Delete direct role assignments - handleDirectRoleAssignments(user, (policy, roles) -> { + handleDirectRoleAssignments(user, (policy, _) -> { policy.clearAssignedRoles(user); SecurityPolicyManager.savePolicy(policy, getUser()); }); @@ -1807,7 +1805,7 @@ public ModelAndView getView(UserForm form, BindException errors) throws Exceptio if (LoginManager.isVerified(affectedUser)) { - out.write("Can't display " + message.getType().toLowerCase() + "; " + PageFlowUtil.filter(affectedUser.getEmail()) + " has already chosen a password."); + out.write("Can't display " + message.getType().toLowerCase() + "; " + h(affectedUser.getEmail()) + " has already chosen a password."); } else { @@ -1940,9 +1938,9 @@ public ModelAndView getSuccessView(UserForm form) String page = String.format( "

%1$s: Password %2$s.

Email sent. Click here to see the email.

%4$s", - PageFlowUtil.filter(affectedUser.getEmail()), + h(affectedUser.getEmail()), _loginExists ? "reset" : "created", - PageFlowUtil.filter(actionURL.getLocalURIString()), + h(actionURL.getLocalURIString()), PageFlowUtil.button("Done").href(form.getReturnUrlHelper(AppProps.getInstance().getHomePageActionURL())) ); @@ -2088,7 +2086,7 @@ public JspView createView(ViewContext context) public static class GroupDiagramAction extends ReadOnlyApiAction { @Override - public ApiResponse execute(GroupDiagramForm form, BindException errors) throws Exception + public ApiResponse execute(GroupDiagramForm form, BindException errors) { List groups = SecurityManager.getGroups(getContainer().getProject(), false); String html; @@ -2099,42 +2097,15 @@ public ApiResponse execute(GroupDiagramForm form, BindException errors) throws E } else { - String graph = GroupManager.getGroupGraphDot(groups, getUser(), form.getHideUnconnected()); - File dir = FileUtil.getTempDirectory(); - File svgFile = null; - + String dot = GroupManager.getGroupGraphDot(groups, getUser(), form.getHideUnconnected()); + Graphviz graph = DotParser.parse(dot); try { - svgFile = FileUtil.createTempFile("groups", ".svg", dir); - svgFile.deleteOnExit(); - DotRunner runner = new DotRunner(dir, graph); - runner.addSvgOutput(svgFile); - runner.execute(); - String svg = PageFlowUtil.getFileContentsAsString(svgFile); - - int idx = svg.indexOf(" + public static class FolderAccessAction extends SimpleViewAction { @Override public ModelAndView getView(FolderAccessForm form, BindException errors) @@ -2400,16 +2371,16 @@ public void testActionPermissions() // @RequiresPermission(AdminPermission.class) assertForAdminPermission(user, new PermissionsAction(), - controller.new StandardDeleteGroupAction(), + new StandardDeleteGroupAction(), controller.new GroupAction(), new CompleteMemberAction(), new CompleteUserAction(), new GroupExportAction(), - controller.new GroupPermissionAction(), + new GroupPermissionAction(), new UpdatePermissionsAction(), new ShowRegistrationEmailAction(), new GroupDiagramAction(), - controller.new FolderAccessAction() + new FolderAccessAction() ); // @RequiresPermission(UserManagementPermission.class) diff --git a/experiment/src/org/labkey/experiment/DotGraph.java b/experiment/src/org/labkey/experiment/DotGraph.java index f11618bb6bb..5ef8aa766b0 100644 --- a/experiment/src/org/labkey/experiment/DotGraph.java +++ b/experiment/src/org/labkey/experiment/DotGraph.java @@ -35,8 +35,6 @@ /** * Represents the GraphViz format for building an experiment run "flowchart", connecting datas, materials through runs * and protocol applications. - * User: migra - * Date: Jun 13, 2005 */ public class DotGraph { @@ -79,17 +77,13 @@ public DotGraph(PrintWriter out, Container c, boolean bSmallFonts) { _pwOut = out; _c = c; - - if (bSmallFonts) - _pwOut.println("digraph G { node[fontname=\"" + LABEL_FONT + "\" fontsize=" + LABEL_SMALL_FONTSIZE + "]"); - else - _pwOut.println("digraph G { node[fontname=\"" + LABEL_FONT + "\" fontsize=" + LABEL_DEFAULT_FONTSIZE + "]"); + _pwOut.println("digraph G { margin=\"0,0\" node[fontname=\"" + LABEL_FONT + "\" fontsize=" + (bSmallFonts ? LABEL_SMALL_FONTSIZE : LABEL_DEFAULT_FONTSIZE) + "]"); } - public void setFocus(Integer focusid, String objtype) + public void setFocus(Integer focusId, String objType) { - _focusId = focusid; - _objectType = objtype; + _focusId = focusId; + _objectType = objType; } public void dispose() @@ -113,7 +107,7 @@ public Long getDGroupId(long rowIdD) return getGroupId(rowIdD, _pendingDNodes, _writtenDNodes); } - public @Nullable Long getGroupId(Long rowId, Map pendingNodes, Map writtenNodes) + private @Nullable Long getGroupId(Long rowId, Map pendingNodes, Map writtenNodes) { DotNode node = null; if (pendingNodes.containsKey(rowId)) @@ -128,7 +122,7 @@ else if (writtenNodes.containsKey(rowId)) return null; } - public void addStartingMaterial(ExpMaterial m, Long groupId, Integer actionseq, long runId) + public void addStartingMaterial(ExpMaterial m, Long groupId, Integer actionSeq, long runId) { DotNode node = new MNode(m); node.setLinkURL(ExperimentController.getResolveLsidURL(_c, "material", m.getLSID())); @@ -136,13 +130,13 @@ public void addStartingMaterial(ExpMaterial m, Long groupId, Integer actionseq, node.setFocus(true); if (null != groupId) { - node = addNodeToGroup(node, groupId, actionseq, _groupMNodes); + node = addNodeToGroup(node, groupId, actionSeq, _groupMNodes); node.setLinkURL(ExperimentController.getShowGraphMoreListURL(_c, runId, TYPECODE_MATERIAL)); } _pendingMNodes.put(m.getRowId(), node); } - public void addStartingData(ExpData d, Long groupId, Integer actionseq, long runId) + public void addStartingData(ExpData d, Long groupId, Integer actionSeq, long runId) { DotNode node = new DNode(d); node.setLinkURL(ExperimentController.getResolveLsidURL(_c, "data", d.getLSID())); @@ -151,13 +145,13 @@ public void addStartingData(ExpData d, Long groupId, Integer actionseq, long run node.setFocus(true); if (null != groupId) { - node = addNodeToGroup(node, groupId, actionseq, _groupDNodes); + node = addNodeToGroup(node, groupId, actionSeq, _groupDNodes); node.setLinkURL(ExperimentController.getShowGraphMoreListURL(_c, runId, TYPECODE_DATA)); } _pendingDNodes.put(d.getRowId(), node); } - private GroupedNode addNodeToGroup(DotNode node, Long groupId, Integer actionseq, Map groupNodes) + private GroupedNode addNodeToGroup(DotNode node, Long groupId, Integer actionSeq, Map groupNodes) { GroupedNode gnode; if (groupNodes.containsKey(groupId)) @@ -167,14 +161,14 @@ private GroupedNode addNodeToGroup(DotNode node, Long groupId, Integer actionseq } else { - if (null == actionseq) actionseq = 0; - gnode = new GroupedNode(groupId, actionseq, node); + if (null == actionSeq) actionSeq = 0; + gnode = new GroupedNode(groupId, actionSeq, node); groupNodes.put(groupId, gnode); } return gnode; } - public void addMaterial(ExpMaterial m, Long groupId, Integer actionseq, boolean output) + public void addMaterial(ExpMaterial m, Long groupId, Integer actionSeq, boolean output) { if (_writtenMNodes.containsKey(m.getRowId()) || _pendingMNodes.containsKey(m.getRowId())) return; @@ -186,11 +180,11 @@ public void addMaterial(ExpMaterial m, Long groupId, Integer actionseq, boolean if (null != _focusId && TYPECODE_MATERIAL.equalsIgnoreCase(_objectType) && _focusId == m.getRowId()) node.setFocus(true); if (null != groupId) - node = addNodeToGroup(node, groupId, actionseq, _groupMNodes); + node = addNodeToGroup(node, groupId, actionSeq, _groupMNodes); _pendingMNodes.put(m.getRowId(), node); } - public void addData(ExpData d, Long groupId, Integer actionseq, boolean output) + public void addData(ExpData d, Long groupId, Integer actionSeq, boolean output) { if (_writtenDNodes.containsKey(d.getRowId()) || _pendingDNodes.containsKey(d.getRowId())) return; @@ -202,11 +196,11 @@ public void addData(ExpData d, Long groupId, Integer actionseq, boolean output) if (null != _focusId && TYPECODE_DATA.equalsIgnoreCase(_objectType) && _focusId == d.getRowId()) node.setFocus(true); if (null != groupId) - node = addNodeToGroup(node, groupId, actionseq, _groupDNodes); + node = addNodeToGroup(node, groupId, actionSeq, _groupDNodes); _pendingDNodes.put(d.getRowId(), node); } - public void addProtApp(Long groupId, long rowId, String name, Integer actionseq) + public void addProtApp(Long groupId, long rowId, String name, Integer actionSeq) { if (_writtenProcNodes.containsKey(rowId) || _pendingProcNodes.containsKey(rowId)) return; @@ -214,17 +208,17 @@ public void addProtApp(Long groupId, long rowId, String name, Integer actionseq) if (null != _focusId && TYPECODE_PROT_APP.equalsIgnoreCase(_objectType) && _focusId == rowId) node.setFocus(true); if (null != groupId) - node = addNodeToGroup(node, groupId, actionseq, _groupPANodes); + node = addNodeToGroup(node, groupId, actionSeq, _groupPANodes); _pendingProcNodes.put(rowId, node); } - public void addOutputNode(Long groupId, long rowId, String name, Integer actionseq) + public void addOutputNode(Long groupId, long rowId, String name, Integer actionSeq) { if (_writtenProcNodes.containsKey(rowId) || _pendingProcNodes.containsKey(rowId)) return; DotNode node = new OutputNode(rowId, name); if (null != groupId) - node = addNodeToGroup(node, groupId, actionseq, _groupPANodes); + node = addNodeToGroup(node, groupId, actionSeq, _groupPANodes); _pendingProcNodes.put(rowId, node); } @@ -305,22 +299,22 @@ else if ((null != writtenTrgtMap) && writtenTrgtMap.containsKey(trgtRow)) { if (null == trgt._shape && src != null) // it's an output node, drawn just as an arrow to a label { - String outnodekey = src._key + "out"; - connect += outnodekey + " [arrowhead = diamond] "; - connect += "\n" + outnodekey + "[shape=plaintext label=\"Output\"]"; + String outNodeKey = src._key + "out"; + connect += outNodeKey + " [arrowhead = diamond] "; + connect += "\n" + outNodeKey + "[shape=plaintext label=\"Output\"]"; } else connect += trgt._key + "[arrowsize = 2]"; } if (label != null && !(src instanceof GroupedNode) && !(trgt instanceof GroupedNode)) { - connect += " [ style=\"setlinewidth(3)\" label = \"" + escape(label) + "\" fontname=\"" + LABEL_FONT + "\" fontsize=" + LABEL_SMALL_FONTSIZE + " ]"; + connect += " [ penwidth=3 label = \"" + escape(label) + "\" fontname=\"" + LABEL_FONT + "\" fontsize=" + LABEL_SMALL_FONTSIZE + " ]"; } else { - connect += " [ style=\"setlinewidth(3)\" ]"; + connect += " [ penwidth=3 ]"; } - if (!_writtenConnects.contains(connect) && !_pendingConnects.contains(connect)) + if (!_writtenConnects.contains(connect)) _pendingConnects.add(connect); } @@ -348,7 +342,7 @@ public void flushPending() _groupPANodes.clear(); } - public void writePending(Map pendingMap, Map writtenMap) + private void writePending(Map pendingMap, Map writtenMap) { Set nodesToMove = new HashSet<>(); for (Long key : pendingMap.keySet()) @@ -358,11 +352,11 @@ public void writePending(Map pendingMap, Map writt node.save(_pwOut); if (node instanceof GroupedNode) { - for (Long memberkey : ((GroupedNode) node)._gMap.keySet()) + for (Long memberKey : ((GroupedNode) node)._gMap.keySet()) { - assert (pendingMap.containsKey(memberkey)); - writtenMap.put(memberkey, node); - nodesToMove.add(memberkey); + assert (pendingMap.containsKey(memberKey)); + writtenMap.put(memberKey, node); + nodesToMove.add(memberKey); } } else @@ -479,7 +473,7 @@ else if (TYPECODE_MATERIAL.equals(_type)) { out.println(_key + "[" + "label=\"" + escape(_label) + "\", tooltip=\"" + escape(tooltip) + "\" " - + ",style=\"filled" + (_bold ? ", setlinewidth(6)" : ", setlinewidth(2)") + "\" " + + ", style=\"filled\", penwidth=" + (_bold ? "6" : "2") + ", fillcolor=\"" + _color + "\" shape=" + _shape + ((null != _height) ? ", height=\"" + _height + "\"" : "") + ((null != _width) ? ", width=\"" + _width + "\"" : "") @@ -564,9 +558,9 @@ private class GroupedNode extends DotNode private final SortedMap _gMap = new TreeMap<>(); private final String _nodeType; - public GroupedNode(Long groupId, Integer actionseq, DotNode node) + public GroupedNode(Long groupId, Integer actionSeq, DotNode node) { - super(GROUP_ID_PREFIX + actionseq + node._type, node._id, "More... "); + super(GROUP_ID_PREFIX + actionSeq + node._type, node._id, "More... "); _gid = groupId; _gMap.put(node._id, node); //setShape(node.shape, node.color + GROUP_OPACITY); @@ -575,10 +569,10 @@ public GroupedNode(Long groupId, Integer actionseq, DotNode node) _nodeType = node._type; } - public void addNode(DotNode newnode) + public void addNode(DotNode newNode) { - assert (Objects.equals(_gMap.get(_gMap.firstKey())._type, newnode._type)); - _gMap.put(newnode._id, newnode); + assert (Objects.equals(_gMap.get(_gMap.firstKey())._type, newNode._type)); + _gMap.put(newNode._id, newNode); } @Override @@ -596,17 +590,17 @@ public void save(PrintWriter out) url.addParameter("rowId~in", sbIn.toString()); - _label += " (" + _gMap.keySet().size() + " entries)"; + _label += " (" + _gMap.size() + " entries)"; if (null != _shape) { out.println(_key + "[label=\"" + escape(_label) - + "\",style=\"filled\", fillcolor=\"" + _color + "\" shape=" + _shape - + ((null != _height) ? ", height=\"" + _height + "\"" : "") - + ((null != _width) ? ", width=\"" + _width + "\"" : "") - + ((null != _width) || (null != _height) ? ", fixedsize=true" : "") - + (", URL=\"" + escape(url.toString()) + "\"") - + "]"); + + "\",style=\"filled\", fillcolor=\"" + _color + "\" shape=" + _shape + + ((null != _height) ? ", height=\"" + _height + "\"" : "") + + ((null != _width) ? ", width=\"" + _width + "\"" : "") + + ((null != _width) || (null != _height) ? ", fixedsize=true" : "") + + (", URL=\"" + escape(url.toString()) + "\"") + + "]"); } } } diff --git a/experiment/src/org/labkey/experiment/ExperimentRunGraph.java b/experiment/src/org/labkey/experiment/ExperimentRunGraph.java index 4f7b8265315..b9128794c0c 100644 --- a/experiment/src/org/labkey/experiment/ExperimentRunGraph.java +++ b/experiment/src/org/labkey/experiment/ExperimentRunGraph.java @@ -1,22 +1,9 @@ -/* - * Copyright (c) 2008-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package org.labkey.experiment; -import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.graphper.api.Graphviz; +import org.graphper.draw.ExecuteException; +import org.graphper.parser.DotParser; import org.labkey.api.data.Container; import org.labkey.api.exp.ExperimentException; import org.labkey.api.exp.api.ExpData; @@ -25,345 +12,204 @@ import org.labkey.api.exp.api.ExpProtocolApplication; import org.labkey.api.exp.api.ExpRun; import org.labkey.api.exp.api.ExpRunItem; -import org.labkey.api.settings.AppProps; -import org.labkey.api.util.ConfigurationException; -import org.labkey.api.util.DotRunner; -import org.labkey.api.util.FileUtil; -import org.labkey.api.util.ImageUtil; -import org.labkey.api.view.ViewContext; +import org.labkey.api.util.SvgUtil; +import org.labkey.api.util.logging.LogHelper; import org.labkey.experiment.api.ExpDataImpl; import org.labkey.experiment.api.ExpMaterialImpl; import org.labkey.experiment.api.ExpProtocolApplicationImpl; import org.labkey.experiment.api.ExpRunImpl; -import javax.imageio.ImageIO; -import java.lang.ref.Cleaner; -import java.awt.image.BufferedImage; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringWriter; +import java.io.Writer; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.SortedMap; import java.util.TreeMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -/** - * User: migra - * Date: Jun 15, 2005 - * Time: 12:57:02 AM - */ + public class ExperimentRunGraph { - private static File baseDirectory; - private static final Logger _log = LogManager.getLogger(ExperimentRunGraph.class); + private static final Logger LOG = LogHelper.getLogger(ExperimentRunGraph.class, "DotGraph warnings"); + private static final int MAX_WIDTH_SMALL_FONT = 8; private static final int MAX_WIDTH_BIG_FONT = 3; private static final int MAX_SIBLINGS = 5; private static final int MIN_SIBLINGS = 3; - /** - * It's safe for lots of threads to be reading but only one should be creating or deleting at a time. - * We could make separate locks for each folder but leaving it with one global lock for now. - */ - private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock(); + public static void renderSvg(Writer out, Container c, ExpRunImpl run, boolean detail, String focus, String focusType) throws ExecuteException, IOException + { + String dot = getDotGraph(c, run, detail, focus, focusType); + out.write(getSvg(dot)); + } - public synchronized static File getBaseDirectory() throws IOException + private static String getSvg(String dot) throws ExecuteException { - if (baseDirectory == null) - { - File tempDir = FileUtil.appendName(FileUtil.getTempDirectory(), "ExperimentRunGraphs"); - if (tempDir.exists()) - { - FileUtil.deleteDirectoryContents(tempDir); - } - else - { - if (!FileUtil.mkdirs(tempDir)) - { - throw new IOException("Unable to create temporary directory for experiment run graphs: " + tempDir.getPath()); - } - } - baseDirectory = tempDir; - } - return baseDirectory; + Graphviz graph = DotParser.parse(dot); + String svg = graph.toSvgStr(); + + // Scale down to 50% of default size. This is arbitrary but seems reasonable. Diagrams are larger than + // the old image-based ones, but monitors are much higher resolution than when those were scaled. + return SvgUtil.scaleSize(svg, 0.5f); } - private synchronized static File getFolderDirectory(Container container) throws IOException + private static String getDotGraph(Container c, ExpRunImpl run, boolean detail, String focus, String focusType) { - File result = FileUtil.appendName(getBaseDirectory(), "Folder" + container.getRowId()); - FileUtil.mkdirs(result); - for (int i = 0; i < 5; i++) + Integer focusId = null; + String typeCode = focusType; + + if (null != focus && !focus.isEmpty()) { - if (result.isDirectory()) + if (!Character.isDigit(focus.charAt(0))) { - return result; + typeCode = focus.substring(0, 1); + focus = focus.substring(1); } - else + try { - try - { - Thread.sleep(1); - } - catch (InterruptedException e) {} - FileUtil.mkdirs(result); + focusId = Integer.parseInt(focus); + run.trimRunTree(focusId, typeCode); } - } - if (!result.isDirectory()) - { - throw new IOException("Failed to create directory " + result); - } - return result; - } - - /** - * Creates a run graph, given the configuration parameters. Note that this creates a lock on the directory - * that contains the files, which must be cleared by calling release() on the resulting RunGraphFiles object. - */ - public static RunGraphFiles generateRunGraph(ViewContext ctx, ExpRunImpl run, boolean detail, String focus, String focusType) throws ExperimentException, IOException, InterruptedException - { - boolean success = false; - - File imageFile = new File(getBaseFileName(run, detail, focus) + ".png"); - File mapFile = new File(getBaseFileName(run, detail, focus) + ".map"); - - // First acquire a read lock so we know that another thread won't be deleting these files out from under us - Lock readLock = LOCK.readLock(); - readLock.lock(); - - try - { - if (!AppProps.getInstance().isDevMode() && imageFile.exists() && mapFile.exists()) + catch (ExperimentException ee) { - success = true; - return new RunGraphFiles(mapFile, imageFile, readLock); + LOG.warn("Exception while trimming run tree", ee); } - } - finally - { - // If we found useful files, don't release the lock because the caller will want to read them - if (!success) + catch (NumberFormatException nfe) { - readLock.unlock(); + LOG.warn("Exception while parsing focus {}", focus, nfe); } } - // We need to create files to open up a write lock - Lock writeLock = LOCK.writeLock(); - writeLock.lock(); + StringWriter writer = new StringWriter(); - try + try (PrintWriter out = new PrintWriter(writer)) { - testDotPath(); + GraphCtrlProps ctrlProps = analyzeGraph(run); - Integer focusId = null; - String typeCode = focusType; + DotGraph dg = new DotGraph(out, c, ctrlProps.fUseSmallFonts); - if (null != focus && !focus.isEmpty()) - { - if (!Character.isDigit(focus.charAt(0))) - { - typeCode = focus.substring(0, 1); - focus = focus.substring(1); - } - try - { - focusId = Integer.parseInt(focus); - run.trimRunTree(focusId, typeCode); - } - catch (NumberFormatException ignored) {} - } - - StringWriter writer = new StringWriter(); - - try (PrintWriter out = new PrintWriter(writer)) + if (!detail) + generateSummaryGraph(run, dg, ctrlProps); + else { - GraphCtrlProps ctrlProps = analyzeGraph(run); - - DotGraph dg = new DotGraph(out, ctx.getContainer(), ctrlProps.fUseSmallFonts); - - if (!detail) - generateSummaryGraph(run, dg, ctrlProps); - else + if (null != focusId) + dg.setFocus(focusId, typeCode); + + // add starting inputs to graph if they need grouping + Map materialRoles = run.getMaterialInputs(); + List inputMaterials = new ArrayList<>(materialRoles.keySet()); + inputMaterials.sort(new RoleAndNameComparator<>(materialRoles)); + Map dataRoles = run.getDataInputs(); + List inputDatas = new ArrayList<>(dataRoles.keySet()); + inputDatas.sort(new RoleAndNameComparator<>(dataRoles)); + if (!run.getProtocolApplications().isEmpty()) { - if (null != focusId) - dg.setFocus(focusId, typeCode); - - // add starting inputs to graph if they need grouping - Map materialRoles = run.getMaterialInputs(); - List inputMaterials = new ArrayList<>(materialRoles.keySet()); - inputMaterials.sort(new RoleAndNameComparator<>(materialRoles)); - Map dataRoles = run.getDataInputs(); - List inputDatas = new ArrayList<>(dataRoles.keySet()); - inputDatas.sort(new RoleAndNameComparator<>(dataRoles)); - if (!run.getProtocolApplications().isEmpty()) - { - long groupId = run.getProtocolApplications().get(0).getRowId(); - addStartingInputs(inputMaterials, inputDatas, groupId, dg, run.getRowId(), ctrlProps); - generateDetailGraph(run, dg, ctrlProps); - } + long groupId = run.getProtocolApplications().getFirst().getRowId(); + addStartingInputs(inputMaterials, inputDatas, groupId, dg, run.getRowId(), ctrlProps); + generateDetailGraph(run, dg, ctrlProps); } - dg.dispose(); } - - String dotInput = writer.getBuffer().toString(); - DotRunner runner = new DotRunner(getFolderDirectory(run.getContainer()), dotInput); - runner.addCmapOutput(mapFile); - runner.addPngOutput(imageFile); - runner.execute(); - - mapFile.deleteOnExit(); - imageFile.deleteOnExit(); - - BufferedImage originalImage = ImageIO.read(imageFile); - if (originalImage == null) - { - throw new IOException("Unable to read image file " + imageFile.getAbsolutePath() + " of size " + imageFile.length() + " - disk may be full?"); - } - - try (FileOutputStream fOut = new FileOutputStream(imageFile)) - { - // Write it back out to disk - double scale = ImageUtil.resizeImage(originalImage, fOut, .85, 6, BufferedImage.TYPE_INT_RGB); - - // Need to rewrite the image map to change the coordinates according to the scaling factor - resizeImageMap(mapFile, scale); - } - - // Start the procedure of downgrade our lock from write to read so that the caller can use the files - readLock.lock(); - return new RunGraphFiles(mapFile, imageFile, readLock); - } - catch (UnsatisfiedLinkError | NoClassDefFoundError e) - { - throw new ConfigurationException("Unable to resize image, likely a problem with missing Java Runtime libraries not being available", e); - } - finally - { - writeLock.unlock(); + dg.dispose(); } + + return writer.getBuffer().toString(); } - /** - * Shrink all the coordinates in an image map by a fixed ratio - */ - private static void resizeImageMap(File mapFile, double finalScale) throws IOException + private static GraphCtrlProps analyzeGraph(ExpRunImpl exp) { - StringBuilder sb = new StringBuilder(); + int maxSiblingsPerParent = MAX_SIBLINGS; + int maxMD = MIN_SIBLINGS; + int iMaxLevelStart = 0; + int iMaxLevelEnd = 0; + int curMI = 0; + int curDI = 0; + int curMO = 0; + int curDO = 0; + int prevS = 0; + int iLevelStart = 0; + int iLevelEnd = 0; + GraphCtrlProps ctrlProps = new GraphCtrlProps(); - try (FileInputStream fIn = new FileInputStream(mapFile)) + int i = 0; + List steps = exp.getProtocolApplications(); + for (ExpProtocolApplicationImpl step : steps) { - // Read in the original file, line by line - BufferedReader reader = new BufferedReader(new InputStreamReader(fIn)); - String line; - while ((line = reader.readLine()) != null) + int curS = step.getActionSequence(); + + Integer countSeq = ctrlProps.mPANodesPerSequence.get(curS); + if (null == countSeq) + countSeq = Integer.valueOf(1); + else + countSeq++; + ctrlProps.mPANodesPerSequence.put(curS, countSeq); + + if (curS != prevS) { - int coordsIndex = line.indexOf("coords=\""); - if (coordsIndex != -1) + if (curMI + curDI > maxMD) { - int openIndex = coordsIndex + "coords=\"".length(); - int closeIndex = line.indexOf("\"", openIndex); - if (closeIndex != -1) - { - // Parse and scale the coordinates - String coordsOriginal = line.substring(openIndex, closeIndex); - String[] coords = coordsOriginal.split(",|(\\s)"); - StringBuilder newLine = new StringBuilder(); - newLine.append(line.substring(0, openIndex)); - String separator = ""; - for (String coord : coords) - { - newLine.append(separator); - separator = ","; - newLine.append((int) (Integer.parseInt(coord.trim()) * finalScale)); - } - newLine.append(line.substring(closeIndex)); - line = newLine.toString(); - } + maxMD = curMI + curDI; + iMaxLevelStart = iLevelStart; + iMaxLevelEnd = iLevelEnd; + } + if (curMO + curDO > maxMD) + { + maxMD = curMO + curDO; + iMaxLevelStart = iLevelStart; + iMaxLevelEnd = iLevelEnd; } - sb.append(line); - sb.append("\n"); + prevS = curS; + curMI = 0; + curDI = 0; + curMO = 0; + curDO = 0; + iLevelStart = i; } + iLevelEnd = i; + curMI += Math.min(step.getInputMaterials().size(), maxSiblingsPerParent); + curDI += Math.min(step.getInputDatas().size(), maxSiblingsPerParent); + curMO += Math.min(step.getOutputMaterials().size(), maxSiblingsPerParent); + curDO += Math.min(step.getOutputDatas().size(), maxSiblingsPerParent); + i++; } - // Write the file back to the disk - try (FileOutputStream mapOut = new FileOutputStream(mapFile)) - { - OutputStreamWriter mapWriter = new OutputStreamWriter(mapOut); - mapWriter.write(sb.toString()); - mapWriter.flush(); - } - } - - private static class CacheClearer implements Runnable - { - private final Container _container; - - public CacheClearer(Container container) - { - _container = container; - } - - @Override - public boolean equals(Object o) + if (maxMD > MAX_WIDTH_BIG_FONT) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CacheClearer that = (CacheClearer) o; - return Objects.equals(_container, that._container); + ctrlProps.fUseSmallFonts = true; + ctrlProps.maxNodesWidth = MAX_WIDTH_SMALL_FONT; } - - @Override - public int hashCode() + else { - return Objects.hash(_container); + ctrlProps.fUseSmallFonts = false; + ctrlProps.maxNodesWidth = MAX_WIDTH_BIG_FONT; } - @Override - public void run() + // try to adjust the number of siblings to fit the levels within the max width + while ((maxMD > ctrlProps.maxNodesWidth) && (maxSiblingsPerParent > MIN_SIBLINGS)) { - clearCache(_container); + curMI = 0; + curDI = 0; + curMO = 0; + curDO = 0; + maxSiblingsPerParent--; + for (i = iMaxLevelStart; i <= (Math.min(iMaxLevelEnd, iMaxLevelStart + maxSiblingsPerParent - 1)); i++) + { + ExpProtocolApplication step = steps.get(i); + curMI += Math.min(step.getInputMaterials().size(), maxSiblingsPerParent); + curDI += Math.min(step.getInputDatas().size(), maxSiblingsPerParent); + curMO += Math.min(step.getOutputMaterials().size(), maxSiblingsPerParent); + curDO += Math.min(step.getOutputDatas().size(), maxSiblingsPerParent); + } + maxMD = Math.max(curMO + curDO, curMI + curDI); } - } - public static Runnable getCacheClearingCommitTask(Container c) - { - return new CacheClearer(c); - } + ctrlProps.maxSiblingNodes = maxSiblingsPerParent; + if (exp.getMaterialInputs().size() + exp.getDataInputs().size() > ctrlProps.maxNodesWidth) + ctrlProps.fGroupInputs = true; - /** - * Clears out the cache of files for this container. Must be called after any operation that changes the way a graph - * would be generated. Typically, this includes deleting or inserting any run in the container, because that - * can change the connections between the runs, which is reflected in the graphs. - */ - public static void clearCache(Container container) - { - Lock deleteLock = LOCK.writeLock(); - deleteLock.lock(); - try - { - FileUtil.deleteDir(getFolderDirectory(container)); - } - catch (IOException e) - { - // Non-fatal - _log.error("Failed to clear cached experiment run graphs for container " + container, e); - } - finally - { - deleteLock.unlock(); - } + return ctrlProps; } /** @@ -421,7 +267,7 @@ private static void generateDetailGraph(ExpRunImpl expRun, DotGraph dg, GraphCtr { continue; } - + List inputMaterials = protApp.getInputMaterials(); List inputDatas = protApp.getInputDatas(); List outputMaterials = protApp.getOutputMaterials(); @@ -643,198 +489,4 @@ public int getPACountForSequence(int seq) return c.intValue(); } } - - - private static GraphCtrlProps analyzeGraph(ExpRunImpl exp) - { - int maxSiblingsPerParent = MAX_SIBLINGS; - int maxMD = MIN_SIBLINGS; - int iMaxLevelStart = 0; - int iMaxLevelEnd = 0; - int curMI = 0; - int curDI = 0; - int curMO = 0; - int curDO = 0; - int prevS = 0; - int iLevelStart = 0; - int iLevelEnd = 0; - GraphCtrlProps ctrlProps = new GraphCtrlProps(); - - int i = 0; - List steps = exp.getProtocolApplications(); - for (ExpProtocolApplicationImpl step : steps) - { - int curS = step.getActionSequence(); - - Integer countSeq = ctrlProps.mPANodesPerSequence.get(curS); - if (null == countSeq) - countSeq = Integer.valueOf(1); - else - countSeq++; - ctrlProps.mPANodesPerSequence.put(curS, countSeq); - - if (curS != prevS) - { - if (curMI + curDI > maxMD) - { - maxMD = curMI + curDI; - iMaxLevelStart = iLevelStart; - iMaxLevelEnd = iLevelEnd; - } - if (curMO + curDO > maxMD) - { - maxMD = curMO + curDO; - iMaxLevelStart = iLevelStart; - iMaxLevelEnd = iLevelEnd; - } - prevS = curS; - curMI = 0; - curDI = 0; - curMO = 0; - curDO = 0; - iLevelStart = i; - } - iLevelEnd = i; - curMI += Math.min(step.getInputMaterials().size(), maxSiblingsPerParent); - curDI += Math.min(step.getInputDatas().size(), maxSiblingsPerParent); - curMO += Math.min(step.getOutputMaterials().size(), maxSiblingsPerParent); - curDO += Math.min(step.getOutputDatas().size(), maxSiblingsPerParent); - i++; - } - - if (maxMD > MAX_WIDTH_BIG_FONT) - { - ctrlProps.fUseSmallFonts = true; - ctrlProps.maxNodesWidth = MAX_WIDTH_SMALL_FONT; - } - else - { - ctrlProps.fUseSmallFonts = false; - ctrlProps.maxNodesWidth = MAX_WIDTH_BIG_FONT; - } - - // try to adjust the number of siblings to fit the levels within the max width - while ((maxMD > ctrlProps.maxNodesWidth) && (maxSiblingsPerParent > MIN_SIBLINGS)) - { - curMI = 0; - curDI = 0; - curMO = 0; - curDO = 0; - maxSiblingsPerParent--; - for (i = iMaxLevelStart; i <= (Math.min(iMaxLevelEnd, iMaxLevelStart + maxSiblingsPerParent - 1)); i++) - { - ExpProtocolApplication step = steps.get(i); - curMI += Math.min(step.getInputMaterials().size(), maxSiblingsPerParent); - curDI += Math.min(step.getInputDatas().size(), maxSiblingsPerParent); - curMO += Math.min(step.getOutputMaterials().size(), maxSiblingsPerParent); - curDO += Math.min(step.getOutputDatas().size(), maxSiblingsPerParent); - } - maxMD = Math.max(curMO + curDO, curMI + curDI); - } - - ctrlProps.maxSiblingNodes = maxSiblingsPerParent; - if (exp.getMaterialInputs().size() + exp.getDataInputs().size() > ctrlProps.maxNodesWidth) - ctrlProps.fGroupInputs = true; - - return ctrlProps; - } - - private static void testDotPath() throws ExperimentException - { - File dir; - - try - { - dir = getBaseDirectory(); - } - catch (IOException e) - { - throw new ExperimentException(DotRunner.getConfigurationError(e)); - } - - DotRunner.testDotPath(dir); - } - - - private static String getBaseFileName(ExpRun run, boolean detail, String focus) throws IOException - { - String fileName; - if (null != focus) - fileName = getFolderDirectory(run.getContainer()) + File.separator + "run" + run.getRowId() + "Focus" + focus; - else - fileName = getFolderDirectory(run.getContainer()) + File.separator + "run" + run.getRowId() + (detail ? "Detail" : ""); - return fileName; - } - - private static final Cleaner CLEANER = Cleaner.create(); - - private static class FileLockState implements Runnable - { - private final Throwable _allocation; - private Lock _lock; - - private FileLockState(Throwable allocation, Lock lock) - { - _allocation = allocation; - _lock = lock; - } - - @Override - public void run() - { - if (_lock != null) - { - _log.error("Lock was not released. Creation was at:", _allocation); - _lock.unlock(); - _lock = null; - } - } - - private void release() - { - if (_lock != null) - { - _lock.unlock(); - _lock = null; - } - } - } - - /** - * Results for run graph generation. Must be released once the files have been consumed by the caller. - */ - public static class RunGraphFiles - { - private final File _mapFile; - private final File _imageFile; - private final FileLockState _state; - private final Cleaner.Cleanable _cleanable; - - public RunGraphFiles(File mapFile, File imageFile, Lock lock) - { - _mapFile = mapFile; - _imageFile = imageFile; - _state = new FileLockState(new Throwable(), lock); - _cleanable = CLEANER.register(this, _state); - } - - public File getMapFile() - { - return _mapFile; - } - - public File getImageFile() - { - return _imageFile; - } - - /** - * Release the lock on the files. - */ - public void release() - { - _state.release(); - _cleanable.clean(); - } - } } diff --git a/experiment/src/org/labkey/experiment/XarReader.java b/experiment/src/org/labkey/experiment/XarReader.java index fe38014852d..a455b73c05e 100644 --- a/experiment/src/org/labkey/experiment/XarReader.java +++ b/experiment/src/org/labkey/experiment/XarReader.java @@ -26,7 +26,33 @@ import org.apache.xmlbeans.XmlException; import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.XmlOptions; -import org.fhcrc.cpas.exp.xml.*; +import org.fhcrc.cpas.exp.xml.ContactType; +import org.fhcrc.cpas.exp.xml.DataBaseType; +import org.fhcrc.cpas.exp.xml.DataClassType; +import org.fhcrc.cpas.exp.xml.DataProtocolInputType; +import org.fhcrc.cpas.exp.xml.DataType; +import org.fhcrc.cpas.exp.xml.DomainDescriptorType; +import org.fhcrc.cpas.exp.xml.ExperimentArchiveDocument; +import org.fhcrc.cpas.exp.xml.ExperimentArchiveType; +import org.fhcrc.cpas.exp.xml.ExperimentLogEntryType; +import org.fhcrc.cpas.exp.xml.ExperimentRunType; +import org.fhcrc.cpas.exp.xml.ExperimentType; +import org.fhcrc.cpas.exp.xml.ImportAlias; +import org.fhcrc.cpas.exp.xml.InputOutputRefsType; +import org.fhcrc.cpas.exp.xml.MaterialBaseType; +import org.fhcrc.cpas.exp.xml.MaterialProtocolInputType; +import org.fhcrc.cpas.exp.xml.MaterialType; +import org.fhcrc.cpas.exp.xml.PropertyCollectionType; +import org.fhcrc.cpas.exp.xml.PropertyObjectDeclarationType; +import org.fhcrc.cpas.exp.xml.PropertyObjectType; +import org.fhcrc.cpas.exp.xml.ProtocolActionSetType; +import org.fhcrc.cpas.exp.xml.ProtocolActionType; +import org.fhcrc.cpas.exp.xml.ProtocolApplicationBaseType; +import org.fhcrc.cpas.exp.xml.ProtocolBaseType; +import org.fhcrc.cpas.exp.xml.SampleSetType; +import org.fhcrc.cpas.exp.xml.SimpleTypeNames; +import org.fhcrc.cpas.exp.xml.SimpleValueCollectionType; +import org.fhcrc.cpas.exp.xml.SimpleValueType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.admin.FolderImportContext; @@ -92,7 +118,30 @@ import org.labkey.api.util.GUID; import org.labkey.api.util.Pair; import org.labkey.api.util.logging.LogHelper; -import org.labkey.experiment.api.*; +import org.labkey.experiment.api.AliasInsertHelper; +import org.labkey.experiment.api.Data; +import org.labkey.experiment.api.DataClass; +import org.labkey.experiment.api.DataInput; +import org.labkey.experiment.api.ExpDataClassImpl; +import org.labkey.experiment.api.ExpDataImpl; +import org.labkey.experiment.api.ExpMaterialImpl; +import org.labkey.experiment.api.ExpProtocolApplicationImpl; +import org.labkey.experiment.api.ExpProtocolImpl; +import org.labkey.experiment.api.ExpRunImpl; +import org.labkey.experiment.api.ExpSampleTypeImpl; +import org.labkey.experiment.api.Experiment; +import org.labkey.experiment.api.ExperimentRun; +import org.labkey.experiment.api.ExperimentServiceImpl; +import org.labkey.experiment.api.IdentifiableEntity; +import org.labkey.experiment.api.Material; +import org.labkey.experiment.api.MaterialInput; +import org.labkey.experiment.api.Protocol; +import org.labkey.experiment.api.ProtocolAction; +import org.labkey.experiment.api.ProtocolActionPredecessor; +import org.labkey.experiment.api.ProtocolActionStepDetail; +import org.labkey.experiment.api.ProtocolApplication; +import org.labkey.experiment.api.RunItem; +import org.labkey.experiment.api.SampleTypeServiceImpl; import org.labkey.experiment.api.property.DomainImpl; import org.labkey.experiment.pipeline.MoveRunsPipelineJob; import org.labkey.experiment.xar.AbstractXarImporter; @@ -127,11 +176,11 @@ import static org.labkey.api.dataiterator.SimpleTranslator.getContainerFileRootPath; import static org.labkey.api.dataiterator.SimpleTranslator.getFileRootSubstitutedFilePath; +import static org.labkey.api.exp.api.ColumnExporter.FILE_ROOT_SUBSTITUTION; import static org.labkey.api.exp.api.ExperimentService.SAMPLE_ALIQUOT_PROTOCOL_LSID; import static org.labkey.api.exp.api.ExperimentService.SAMPLE_DERIVATION_PROTOCOL_LSID; import static org.labkey.api.study.publish.StudyPublishService.STUDY_PUBLISH_PROTOCOL_LSID; import static org.labkey.experiment.XarExporter.GPAT_ASSAY_PROTOCOL_LSID_SUB; -import static org.labkey.api.exp.api.ColumnExporter.FILE_ROOT_SUBSTITUTION; public class XarReader extends AbstractXarImporter { @@ -449,19 +498,18 @@ private void loadDoc() throws ExperimentException throw new XarFormatException(e); } - ExperimentRunGraph.clearCache(getContainer()); - try { for (DeferredDataLoad deferredDataLoad : _deferredDataLoads) { Path path = deferredDataLoad.getData().getFilePath(); - if (path == null) - continue; - else if (Files.exists(path)) - deferredDataLoad.getData().importDataFile(_job, _xarSource); - else - getLog().warn("Data file " + FileUtil.getFileName(path) + " does not exist and could not be loaded."); + if (path != null) + { + if (Files.exists(path)) + deferredDataLoad.getData().importDataFile(_job, _xarSource); + else + getLog().warn("Data file {} does not exist and could not be loaded.", FileUtil.getFileName(path)); + } } } catch (SQLException e) diff --git a/experiment/src/org/labkey/experiment/api/ExpMaterialImpl.java b/experiment/src/org/labkey/experiment/api/ExpMaterialImpl.java index e091339cb5b..645d0b3731b 100644 --- a/experiment/src/org/labkey/experiment/api/ExpMaterialImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExpMaterialImpl.java @@ -647,7 +647,7 @@ else if (values.containsKey(dp.getPropertyURI())) } for (var entry : values.entrySet()) { - PropertyDescriptor pd = OntologyManager.getPropertyDescriptor(entry.getKey(), st.getContainer()); + PropertyDescriptor pd = OntologyManager.getPropertyDescriptor(entry.getKey(), st == null ? getContainer() : st.getContainer()); if (null != pd) super.setProperty(user, pd, entry.getValue(), insertNullValues); } diff --git a/experiment/src/org/labkey/experiment/api/ExpRunImpl.java b/experiment/src/org/labkey/experiment/api/ExpRunImpl.java index c55c241e91f..4db94a4e583 100644 --- a/experiment/src/org/labkey/experiment/api/ExpRunImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExpRunImpl.java @@ -56,14 +56,12 @@ import org.labkey.api.query.QueryRowReference; import org.labkey.api.security.User; import org.labkey.api.security.permissions.DeletePermission; -import org.labkey.api.security.permissions.SampleWorkflowDeletePermission; import org.labkey.api.security.permissions.UpdatePermission; import org.labkey.api.util.FileUtil; import org.labkey.api.util.NetworkDrive; import org.labkey.api.view.ActionURL; import org.labkey.api.view.UnauthorizedException; import org.labkey.experiment.DotGraph; -import org.labkey.experiment.ExperimentRunGraph; import java.io.File; import java.io.IOException; @@ -82,6 +80,8 @@ public class ExpRunImpl extends ExpIdentifiableEntityImpl implements ExpRun { + private static final Logger LOG = LogManager.getLogger(ExpRunImpl.class); + public static final String NAMESPACE_PREFIX = "Run"; private boolean _populated; @@ -92,8 +92,6 @@ public class ExpRunImpl extends ExpIdentifiableEntityImpl impleme private List _dataOutputs = new ArrayList<>(); private ExpRunImpl _replacedByRun; private Integer _maxOutputActionSequence = null; - private static final Logger LOG = LogManager.getLogger(ExpRunImpl.class); - private ExpProtocolApplication _workflowTask; static public List fromRuns(List runs) { @@ -561,10 +559,6 @@ public void deleteProtocolApplications(List datasToDelete, User use deleteRunProtocolApps(); clearCache(); - - // Clear the cache in a commit task, which allows us to do a single clear (which is semi-expensive) if multiple - // runs are being deleted in the same transaction, like deleting a container - svc.getSchema().getScope().addCommitTask(ExperimentRunGraph.getCacheClearingCommitTask(getContainer()), DbScope.CommitTaskOption.POSTCOMMIT); } @Override diff --git a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java index f2bdbe61ede..ec3248cf7b4 100644 --- a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java +++ b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java @@ -32,7 +32,6 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.jspecify.annotations.NonNull; import org.labkey.api.action.ApiJsonWriter; import org.labkey.api.action.ApiResponse; import org.labkey.api.action.ApiSimpleResponse; @@ -265,7 +264,6 @@ import org.labkey.experiment.DotGraph; import org.labkey.experiment.ExpDataFileListener; import org.labkey.experiment.ExperimentRunDisplayColumn; -import org.labkey.experiment.ExperimentRunGraph; import org.labkey.experiment.LineageGraphDisplayColumn; import org.labkey.experiment.MissingFilesCheckInfo; import org.labkey.experiment.MoveRunsBean; @@ -315,12 +313,10 @@ import org.springframework.web.servlet.ModelAndView; import javax.imageio.ImageIO; -import java.awt.*; import java.awt.image.BufferedImage; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -1848,7 +1844,6 @@ public static ActionURL getRunGraphURL(Container c, long runId) return new ActionURL(ShowRunGraphAction.class, c).addParameter("rowId", runId); } - @RequiresPermission(ReadPermission.class) public class ShowRunGraphAction extends AbstractShowRunAction { @@ -1856,55 +1851,9 @@ public class ShowRunGraphAction extends AbstractShowRunAction protected VBox createLowerView(ExpRunImpl experimentRun, BindException errors) { return new VBox( - createRunViewTabs(experimentRun, false, true, true), - new ExperimentRunGraphView(experimentRun, false)); - } - } - - - @RequiresPermission(ReadPermission.class) - public static class DownloadGraphAction extends SimpleViewAction - { - @Override - public ModelAndView getView(ExperimentRunForm form, BindException errors) throws Exception - { - boolean detail = form.isDetail(); - String focus = form.getFocus(); - String focusType = form.getFocusType(); - - ExpRunImpl experimentRun = (ExpRunImpl) form.lookupRun(); - ensureCorrectContainer(getContainer(), experimentRun, getViewContext()); - - ExperimentRunGraph.RunGraphFiles files; - try - { - files = ExperimentRunGraph.generateRunGraph(getViewContext(), experimentRun, detail, focus, focusType); - } - catch (ExperimentException e) - { - PageFlowUtil.streamTextAsImage(getViewContext().getResponse(), "ERROR: " + e.getMessage(), 600, 150, Color.RED); - return null; - } - - try - { - PageFlowUtil.streamFile(getViewContext().getResponse(), files.getImageFile().toPath(), false); - } - catch (FileNotFoundException e) - { - throw new RedirectException(new URLHelper(getViewContext().getRequest().getContextPath() + "/experiment/ExperimentRunNotFound.gif")); - } - finally - { - files.release(); - } - return null; - } - - @Override - public void addNavTrail(NavTree root) - { - throw new UnsupportedOperationException(); + createRunViewTabs(experimentRun, false, true, true), + new ExperimentRunGraphView(experimentRun, false) + ); } } @@ -7110,21 +7059,6 @@ public static ExperimentUrlsImpl get() return (ExperimentUrlsImpl) urlProvider(ExperimentUrls.class); } - public ActionURL getDownloadGraphURL(ExpRun run, boolean detail, String focus, String focusType) - { - ActionURL result = new ActionURL(DownloadGraphAction.class, run.getContainer()); - result.addParameter("rowId", run.getRowId()).addParameter("detail", detail); - if (focus != null) - { - result.addParameter("focus", focus); - } - if (focusType != null) - { - result.addParameter("focusType", focusType); - } - return result; - } - public ActionURL getBeginURL(Container container) { return new ActionURL(BeginAction.class, container); diff --git a/experiment/src/org/labkey/experiment/controllers/exp/experimentRunGraphView.jsp b/experiment/src/org/labkey/experiment/controllers/exp/experimentRunGraphView.jsp index 6461ae484ca..01caafdfa75 100644 --- a/experiment/src/org/labkey/experiment/controllers/exp/experimentRunGraphView.jsp +++ b/experiment/src/org/labkey/experiment/controllers/exp/experimentRunGraphView.jsp @@ -15,19 +15,12 @@ * limitations under the License. */ %> -<%@ page import="org.apache.commons.io.IOUtils" %> -<%@ page import="org.labkey.api.exp.ExperimentException" %> -<%@ page import="org.labkey.api.reader.Readers" %> +<%@ page import="org.graphper.draw.ExecuteException" %> <%@ page import="org.labkey.api.util.UniqueID" %> -<%@ page import="org.labkey.api.view.ActionURL" %> <%@ page import="org.labkey.api.view.HttpView" %> -<%@ page import="org.labkey.api.view.ViewContext" %> <%@ page import="org.labkey.api.view.template.ClientDependencies" %> <%@ page import="org.labkey.experiment.ExperimentRunGraph" %> -<%@ page import="org.labkey.experiment.controllers.exp.ExperimentController" %> <%@ page import="org.labkey.experiment.controllers.exp.ExperimentRunGraphModel" %> -<%@ page import="java.io.IOException" %> -<%@ page import="java.io.Reader" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <%! @Override @@ -38,7 +31,6 @@ } %> <% - ViewContext context = getViewContext(); ExperimentRunGraphModel model = (ExperimentRunGraphModel)HttpView.currentModel(); boolean isSummaryView = !model.isDetail(); boolean isBetaViewEnabled = getActionURL().getParameter("betaGraph") != null; @@ -49,21 +41,8 @@ String graphTabId = "graph-tab-" + uniqueId; String graphTabBetaId = "graph-tab-beta-" + uniqueId; - try + if (isSummaryView) { - ExperimentRunGraph.RunGraphFiles files = ExperimentRunGraph.generateRunGraph(context, - model.getRun(), - model.isDetail(), - model.getFocus(), - model.getFocusType()); - - ActionURL imgSrc = ExperimentController.ExperimentUrlsImpl.get().getDownloadGraphURL(model.getRun(), - model.isDetail(), - model.getFocus(), - model.getFocusType()); - - if (isSummaryView) - { %> <%=button("Toggle Beta Graph (new!)").id(toggleBtnId).style("display: inline-block; float: right;")%>