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/module/ModuleDependencySorter.java b/api/src/org/labkey/api/module/ModuleDependencySorter.java index aac12caf743..d3396ebd9ec 100644 --- a/api/src/org/labkey/api/module/ModuleDependencySorter.java +++ b/api/src/org/labkey/api/module/ModuleDependencySorter.java @@ -18,14 +18,16 @@ import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; -import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.graphper.api.GraphResource; +import org.graphper.api.Graphviz; +import org.graphper.parser.DotParser; import org.junit.Assert; import org.junit.Test; import org.labkey.api.collections.CaseInsensitiveHashSet; -import org.labkey.api.util.DotRunner; import org.labkey.api.util.FileUtil; import org.labkey.api.util.Pair; +import org.labkey.api.util.logging.LogHelper; import java.io.File; import java.util.ArrayList; @@ -34,12 +36,12 @@ import java.util.stream.Collectors; /** - * Orders modules so that each module will always be after all of the modules it depends on. - * User: jeckels - * Date: Jun 6, 2006 + * Orders modules so that each module will always be after all the modules it depends on. */ public class ModuleDependencySorter { + private static final Logger LOG = LogHelper.getLogger(ModuleDependencySorter.class, "Module dependency information"); + public List 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/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/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 FolderAccessAction extends SimpleViewAction { @Override public ModelAndView getView(FolderAccessForm form, BindException errors) @@ -2409,7 +2373,7 @@ controller.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..61b52548d35 100644 --- a/experiment/src/org/labkey/experiment/DotGraph.java +++ b/experiment/src/org/labkey/experiment/DotGraph.java @@ -113,7 +113,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)) @@ -348,7 +348,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()) diff --git a/experiment/src/org/labkey/experiment/ExperimentRunGraph.java b/experiment/src/org/labkey/experiment/ExperimentRunGraph.java index 4f7b8265315..d85bf75a665 100644 --- a/experiment/src/org/labkey/experiment/ExperimentRunGraph.java +++ b/experiment/src/org/labkey/experiment/ExperimentRunGraph.java @@ -1,22 +1,8 @@ -/* - * 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,27 +11,11 @@ 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.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.util.ArrayList; @@ -53,317 +23,171 @@ 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 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 synchronized static File getBaseDirectory() throws IOException + public 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(); - private synchronized static File getFolderDirectory(Container container) throws IOException - { - File result = FileUtil.appendName(getBaseDirectory(), "Folder" + container.getRowId()); - FileUtil.mkdirs(result); - for (int i = 0; i < 5; i++) - { - if (result.isDirectory()) - { - return result; - } - else - { - try - { - Thread.sleep(1); - } - catch (InterruptedException e) {} - FileUtil.mkdirs(result); - } - } - if (!result.isDirectory()) - { - throw new IOException("Failed to create directory " + result); - } - return result; + return svg; } - /** - * 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 + public static String getDotGraph(Container c, ExpRunImpl run, boolean detail, String focus, String focusType) { - boolean success = false; - - File imageFile = new File(getBaseFileName(run, detail, focus) + ".png"); - File mapFile = new File(getBaseFileName(run, detail, focus) + ".map"); + Integer focusId = null; + String typeCode = focusType; - // 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 (null != focus && !focus.isEmpty()) { - if (!AppProps.getInstance().isDevMode() && imageFile.exists() && mapFile.exists()) + if (!Character.isDigit(focus.charAt(0))) { - success = true; - return new RunGraphFiles(mapFile, imageFile, readLock); + typeCode = focus.substring(0, 1); + focus = focus.substring(1); } - } - finally - { - // If we found useful files, don't release the lock because the caller will want to read them - if (!success) + try { - readLock.unlock(); + focusId = Integer.parseInt(focus); + run.trimRunTree(focusId, typeCode); } + catch (NumberFormatException | ExperimentException ignored) {} } - // 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(); - - Integer focusId = null; - String typeCode = focusType; - - 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) {} - } + GraphCtrlProps ctrlProps = analyzeGraph(run); - StringWriter writer = new StringWriter(); + DotGraph dg = new DotGraph(out, c, ctrlProps.fUseSmallFonts); - 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 +245,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 +467,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/FileBasedExperimentRunGraph.java b/experiment/src/org/labkey/experiment/FileBasedExperimentRunGraph.java new file mode 100644 index 00000000000..793a070f7a5 --- /dev/null +++ b/experiment/src/org/labkey/experiment/FileBasedExperimentRunGraph.java @@ -0,0 +1,392 @@ +/* + * 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.labkey.api.data.Container; +import org.labkey.api.exp.ExperimentException; +import org.labkey.api.exp.api.ExpRun; +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.experiment.api.ExpRunImpl; + +import javax.imageio.ImageIO; +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.lang.ref.Cleaner; +import java.util.Objects; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +// TODO: This entire class can be eliminated once we're happy with the new graph-support Java implementation +public class FileBasedExperimentRunGraph extends ExperimentRunGraph +{ + private static File baseDirectory; + private static final Logger _log = LogManager.getLogger(FileBasedExperimentRunGraph.class); + + /** + * 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 synchronized static File getBaseDirectory() throws IOException + { + 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; + } + + private synchronized static File getFolderDirectory(Container container) throws IOException + { + File result = FileUtil.appendName(getBaseDirectory(), "Folder" + container.getRowId()); + FileUtil.mkdirs(result); + for (int i = 0; i < 5; i++) + { + if (result.isDirectory()) + { + return result; + } + else + { + try + { + Thread.sleep(1); + } + catch (InterruptedException e) {} + FileUtil.mkdirs(result); + } + } + 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()) + { + success = true; + return new RunGraphFiles(mapFile, imageFile, readLock); + } + } + finally + { + // If we found useful files, don't release the lock because the caller will want to read them + if (!success) + { + readLock.unlock(); + } + } + + // We need to create files to open up a write lock + Lock writeLock = LOCK.writeLock(); + writeLock.lock(); + + try + { + testDotPath(); + + String dotInput = getDotGraph(ctx.getContainer(), run, detail, focus, focusType); + 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(); + } + } + + /** + * Shrink all the coordinates in an image map by a fixed ratio + */ + private static void resizeImageMap(File mapFile, double finalScale) throws IOException + { + StringBuilder sb = new StringBuilder(); + + try (FileInputStream fIn = new FileInputStream(mapFile)) + { + // Read in the original file, line by line + BufferedReader reader = new BufferedReader(new InputStreamReader(fIn)); + String line; + while ((line = reader.readLine()) != null) + { + int coordsIndex = line.indexOf("coords=\""); + if (coordsIndex != -1) + { + 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(); + } + } + sb.append(line); + sb.append("\n"); + } + } + + // 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 (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CacheClearer that = (CacheClearer) o; + return Objects.equals(_container, that._container); + } + + @Override + public int hashCode() + { + return Objects.hash(_container); + } + + @Override + public void run() + { + clearCache(_container); + } + } + + public static Runnable getCacheClearingCommitTask(Container c) + { + return new CacheClearer(c); + } + + /** + * 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(); + } + } + + 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..17909154d0f 100644 --- a/experiment/src/org/labkey/experiment/XarReader.java +++ b/experiment/src/org/labkey/experiment/XarReader.java @@ -449,7 +449,7 @@ private void loadDoc() throws ExperimentException throw new XarFormatException(e); } - ExperimentRunGraph.clearCache(getContainer()); + FileBasedExperimentRunGraph.clearCache(getContainer()); try { diff --git a/experiment/src/org/labkey/experiment/api/ExpRunImpl.java b/experiment/src/org/labkey/experiment/api/ExpRunImpl.java index c55c241e91f..6dc83ea79a1 100644 --- a/experiment/src/org/labkey/experiment/api/ExpRunImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExpRunImpl.java @@ -56,14 +56,13 @@ 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 org.labkey.experiment.FileBasedExperimentRunGraph; import java.io.File; import java.io.IOException; @@ -564,7 +563,7 @@ public void deleteProtocolApplications(List datasToDelete, User use // 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); + svc.getSchema().getScope().addCommitTask(FileBasedExperimentRunGraph.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..7474eebc46f 100644 --- a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java +++ b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java @@ -27,12 +27,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.poi.ss.usermodel.Workbook; +import org.graphper.api.GraphResource; +import org.graphper.parser.DotParser; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; 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; @@ -266,6 +267,7 @@ import org.labkey.experiment.ExpDataFileListener; import org.labkey.experiment.ExperimentRunDisplayColumn; import org.labkey.experiment.ExperimentRunGraph; +import org.labkey.experiment.FileBasedExperimentRunGraph; import org.labkey.experiment.LineageGraphDisplayColumn; import org.labkey.experiment.MissingFilesCheckInfo; import org.labkey.experiment.MoveRunsBean; @@ -325,6 +327,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.sql.SQLException; @@ -1875,10 +1878,10 @@ public ModelAndView getView(ExperimentRunForm form, BindException errors) throws ExpRunImpl experimentRun = (ExpRunImpl) form.lookupRun(); ensureCorrectContainer(getContainer(), experimentRun, getViewContext()); - ExperimentRunGraph.RunGraphFiles files; + FileBasedExperimentRunGraph.RunGraphFiles files; try { - files = ExperimentRunGraph.generateRunGraph(getViewContext(), experimentRun, detail, focus, focusType); + files = FileBasedExperimentRunGraph.generateRunGraph(getViewContext(), experimentRun, detail, focus, focusType); } catch (ExperimentException e) { @@ -1898,6 +1901,10 @@ public ModelAndView getView(ExperimentRunForm form, BindException errors) throws { files.release(); } + + String dot = ExperimentRunGraph.getDotGraph(getContainer(), experimentRun, detail, focus, focusType); + getViewContext().getResponse().getOutputStream().write(ExperimentRunGraph.getSvg(dot).getBytes(StandardCharsets.UTF_8)); + return null; } diff --git a/experiment/src/org/labkey/experiment/controllers/exp/experimentRunGraphView.jsp b/experiment/src/org/labkey/experiment/controllers/exp/experimentRunGraphView.jsp index 6461ae484ca..640a2656ef7 100644 --- a/experiment/src/org/labkey/experiment/controllers/exp/experimentRunGraphView.jsp +++ b/experiment/src/org/labkey/experiment/controllers/exp/experimentRunGraphView.jsp @@ -16,6 +16,7 @@ */ %> <%@ page import="org.apache.commons.io.IOUtils" %> +<%@ page import="org.graphper.draw.ExecuteException" %> <%@ page import="org.labkey.api.exp.ExperimentException" %> <%@ page import="org.labkey.api.reader.Readers" %> <%@ page import="org.labkey.api.util.UniqueID" %> @@ -24,6 +25,7 @@ <%@ 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.FileBasedExperimentRunGraph" %> <%@ page import="org.labkey.experiment.controllers.exp.ExperimentController" %> <%@ page import="org.labkey.experiment.controllers.exp.ExperimentRunGraphModel" %> <%@ page import="java.io.IOException" %> @@ -51,7 +53,7 @@ try { - ExperimentRunGraph.RunGraphFiles files = ExperimentRunGraph.generateRunGraph(context, + FileBasedExperimentRunGraph.RunGraphFiles files = FileBasedExperimentRunGraph.generateRunGraph(context, model.getRun(), model.isDetail(), model.getFocus(), @@ -94,9 +96,17 @@ { files.release(); } + + String dot = ExperimentRunGraph.getDotGraph(getContainer(), + model.getRun(), + model.isDetail(), + model.getFocus(), + model.getFocusType() + ); + out.write(ExperimentRunGraph.getSvg(dot)); } } - catch (ExperimentException | InterruptedException e) + catch (ExperimentException | InterruptedException | ExecuteException e) { %>

<%=h(e.getMessage(), true)%>

<% } diff --git a/experiment/src/org/labkey/experiment/pipeline/ExpGeneratorHelper.java b/experiment/src/org/labkey/experiment/pipeline/ExpGeneratorHelper.java index f31733af934..93aeb97c5bd 100644 --- a/experiment/src/org/labkey/experiment/pipeline/ExpGeneratorHelper.java +++ b/experiment/src/org/labkey/experiment/pipeline/ExpGeneratorHelper.java @@ -51,7 +51,7 @@ import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; import org.labkey.api.util.FileUtil; -import org.labkey.experiment.ExperimentRunGraph; +import org.labkey.experiment.FileBasedExperimentRunGraph; import org.labkey.experiment.api.ExpDataImpl; import org.labkey.experiment.api.ExpMaterialImpl; import org.labkey.experiment.api.ExpRunImpl; @@ -274,7 +274,7 @@ static public ExpRunImpl insertRun(Container container, User user, throw new ExperimentException(e); } - ExperimentRunGraph.clearCache(run.getContainer()); + FileBasedExperimentRunGraph.clearCache(run.getContainer()); } catch (XarFormatException | BatchValidationException e) { diff --git a/pipeline/src/org/labkey/pipeline/analysis/AnalysisController.java b/pipeline/src/org/labkey/pipeline/analysis/AnalysisController.java index 0a53f7d1062..5f664260d43 100644 --- a/pipeline/src/org/labkey/pipeline/analysis/AnalysisController.java +++ b/pipeline/src/org/labkey/pipeline/analysis/AnalysisController.java @@ -22,6 +22,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.graphper.api.Graphviz; +import org.graphper.parser.DotParser; import org.jetbrains.annotations.Nullable; import org.json.JSONArray; import org.json.JSONObject; @@ -658,25 +660,45 @@ private DOM.Renderable generateGraph(@Nullable TaskPipeline pipeline) } File svgFile = null; - try - { - File dir = FileUtil.getTempDirectory(); - String dot = buildDigraph(pipeline); - svgFile = FileUtil.createTempFile("pipeline", ".svg", dir); - DotRunner runner = new DotRunner(dir, dot); - runner.addSvgOutput(svgFile); - runner.execute(); - return HtmlString.unsafe(PageFlowUtil.getFileContentsAsString(svgFile)); - } - catch (Exception e) - { - LOG.error("Error running dot", e); - } - finally + File dir = FileUtil.getTempDirectory(); + String dot = buildDigraph(pipeline, true); + + if (null != dot) { - if (svgFile != null) - svgFile.delete(); + String htmlOld = "Oops... error with DotRunner!"; + try + { + svgFile = FileUtil.createTempFile("pipeline", ".svg", dir); + DotRunner runner = new DotRunner(dir, dot); + runner.addSvgOutput(svgFile); + runner.execute(); + htmlOld = PageFlowUtil.getFileContentsAsString(svgFile); + } + catch (Exception e) + { + LOG.error("Error running dot", e); + } + finally + { + if (svgFile != null) + svgFile.delete(); + } + + String html = "Oops... error with DotParser!"; + try + { + dot = buildDigraph(pipeline, false); + Graphviz graph = DotParser.parse(dot); + html = graph.toSvgStr(); + } + catch (Exception e) + { + LOG.error("Error with DotParser!", e); + } + + return HtmlString.unsafe(htmlOld + "
" + html); } + return null; } @@ -692,7 +714,7 @@ private DOM.Renderable generateGraph(@Nullable TaskPipeline pipeline) * +---------+----------+ * */ - private String buildDigraph(TaskPipeline pipeline) + private String buildDigraph(TaskPipeline pipeline, boolean dotRunner) { TaskId[] progression = pipeline.getTaskProgression(); if (progression == null) @@ -701,6 +723,9 @@ private String buildDigraph(TaskPipeline pipeline) StringBuilder sb = new StringBuilder(); sb.append("digraph pipeline {\n"); + if (!dotRunner) + sb.append("style=\"invis\";\nmargin=\"0,0\";\n"); // TODO: graph-support doesn't seem to respect "transparent" + // First, add all the nodes for (TaskId taskId : progression) { @@ -730,7 +755,7 @@ private String buildDigraph(TaskPipeline pipeline) if (factory instanceof CommandTaskImpl.Factory f) { sb.append(StringUtils.join( - Collections2.transform(f.getInputPaths().keySet(), (Function) input -> escapeDotFieldLabel(input) + "\\l"), + Collections2.transform(f.getInputPaths().keySet(), (Function) this::escapeDotFieldLabel), // + "\\l"), TODO: Add this back once graph-support supports it " | ")); } else @@ -747,7 +772,7 @@ private String buildDigraph(TaskPipeline pipeline) { sb.append(StringUtils.join( - Collections2.transform(f.getOutputPaths().keySet(), (Function) input -> escapeDotFieldLabel(input) + "\\r"), + Collections2.transform(f.getOutputPaths().keySet(), (Function) this::escapeDotFieldLabel), // + "\\r"), TODO: Add this back once graph-support supports it " | ")); } else