This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch feat/diagram-chain-redirect-edges in repository https://gitbox.apache.org/repos/asf/struts-intellij-plugin.git
commit c5e25fe466452555231322f80ff5cab6d803eec5 Author: Lukasz Lenart <[email protected]> AuthorDate: Thu Apr 9 07:42:08 2026 +0200 feat(diagram): resolve chain/redirect results as action-to-action edges Extend the Struts config diagram to handle redirectAction, chain, and redirect-action result types. When a result target resolves to an action in the same file, the model emits an action→action edge instead of an unresolvable RESULT node. External/unresolvable targets fall back to a labeled RESULT node with a descriptive arrow label. - Add resolveChainOrRedirectTarget() helper that extracts target from tag body text or <param name="actionName">/<param name="namespace"> - Refactor build() into a two-pass approach: first create nodes and map XmlTag→node for stable identity, then process results - Draw same-column (action→action) edges as dashed curves looping from the source's right edge below both nodes to the target's left edge - Build model asynchronously via ReadAction.nonBlocking to avoid write-intent lock violations on project open - Add test fixtures and tests for body-based and param-based redirect Made-with: Cursor --- .../fileEditor/Struts2DiagramFileEditor.java | 42 +++--- .../diagram/model/StrutsConfigDiagramModel.java | 154 +++++++++++++++++++-- .../diagram/ui/Struts2DiagramComponent.java | 103 +++++++++++--- .../diagram/StrutsConfigDiagramModelTest.java | 75 ++++++++++ src/test/testData/diagram/struts-redirect-body.xml | 45 ++++++ .../testData/diagram/struts-redirect-param.xml | 44 ++++++ 6 files changed, 405 insertions(+), 58 deletions(-) diff --git a/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java b/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java index 6836e58..36449e1 100644 --- a/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java +++ b/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java @@ -17,7 +17,6 @@ package com.intellij.struts2.diagram.fileEditor; import com.intellij.openapi.application.ReadAction; -import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; @@ -34,15 +33,17 @@ import javax.swing.*; /** * Read-only file editor that hosts the lightweight Struts config diagram. * <p> - * Both initial creation and {@link #reset()} go through the same - * {@link #buildModel()} path so the component always reflects the - * current model state — including explicit empty and unavailable - * fallbacks instead of stale or blank content. + * The component is created eagerly (with a {@code null} model) so that + * {@link #getPreferredFocusedComponent()} never triggers PSI/DOM access + * on the UI thread. The model is built via {@link ReadAction#nonBlocking} + * and applied asynchronously; both initial creation and {@link #reset()} + * go through the same path so the component always reflects the current + * model state — including explicit empty and unavailable fallbacks. */ public class Struts2DiagramFileEditor extends PerspectiveFileEditor { private final XmlFile myXmlFile; - private Struts2DiagramComponent myComponent; + private final Struts2DiagramComponent myComponent; public Struts2DiagramFileEditor(final Project project, final VirtualFile file) { super(project, file); @@ -50,6 +51,8 @@ public class Struts2DiagramFileEditor extends PerspectiveFileEditor { final PsiFile psiFile = getPsiFile(); assert psiFile instanceof XmlFile; myXmlFile = (XmlFile) psiFile; + myComponent = new Struts2DiagramComponent(null); + scheduleModelBuild(); } @Override @@ -65,13 +68,13 @@ public class Struts2DiagramFileEditor extends PerspectiveFileEditor { @Override @NotNull protected JComponent createCustomComponent() { - return getDiagramComponent(); + return myComponent; } @Override @Nullable public JComponent getPreferredFocusedComponent() { - return getDiagramComponent(); + return myComponent; } @Override @@ -80,7 +83,7 @@ public class Struts2DiagramFileEditor extends PerspectiveFileEditor { @Override public void reset() { - getDiagramComponent().rebuild(buildModel()); + scheduleModelBuild(); } @Override @@ -89,20 +92,11 @@ public class Struts2DiagramFileEditor extends PerspectiveFileEditor { return "Diagram"; } - private @Nullable StrutsConfigDiagramModel buildModel() { - final StrutsConfigDiagramModel[] model = {null}; - ProgressManager.getInstance().runProcessWithProgressSynchronously( - () -> model[0] = ReadAction.nonBlocking( - () -> StrutsConfigDiagramModel.build(myXmlFile)) - .executeSynchronously(), - "Building Diagram", false, myXmlFile.getProject()); - return model[0]; - } - - private Struts2DiagramComponent getDiagramComponent() { - if (myComponent == null) { - myComponent = new Struts2DiagramComponent(buildModel()); - } - return myComponent; + private void scheduleModelBuild() { + ReadAction.nonBlocking(() -> StrutsConfigDiagramModel.build(myXmlFile)) + .expireWith(this) + .finishOnUiThread(com.intellij.openapi.application.ModalityState.defaultModalityState(), + myComponent::rebuild) + .submit(com.intellij.util.concurrency.AppExecutorUtil.getAppExecutorService()); } } diff --git a/src/main/java/com/intellij/struts2/diagram/model/StrutsConfigDiagramModel.java b/src/main/java/com/intellij/struts2/diagram/model/StrutsConfigDiagramModel.java index 8cca983..081ac4f 100644 --- a/src/main/java/com/intellij/struts2/diagram/model/StrutsConfigDiagramModel.java +++ b/src/main/java/com/intellij/struts2/diagram/model/StrutsConfigDiagramModel.java @@ -24,13 +24,17 @@ import com.intellij.psi.SmartPointerManager; import com.intellij.psi.SmartPsiElementPointer; import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; import com.intellij.struts2.Struts2Icons; import com.intellij.struts2.diagram.presentation.StrutsDiagramPresentation; +import com.intellij.struts2.dom.params.Param; import com.intellij.struts2.dom.struts.StrutsRoot; import com.intellij.struts2.dom.struts.action.Action; import com.intellij.struts2.dom.struts.action.Result; +import com.intellij.struts2.dom.struts.impl.path.ResultTypeResolver; import com.intellij.struts2.dom.struts.model.StrutsManager; import com.intellij.struts2.dom.struts.model.StrutsModel; +import com.intellij.struts2.dom.struts.strutspackage.ResultType; import com.intellij.struts2.dom.struts.strutspackage.StrutsPackage; import com.intellij.util.xml.DomElement; import com.intellij.util.xml.DomFileElement; @@ -49,6 +53,12 @@ import java.util.*; * inherited framework packages (e.g. {@code struts-default}) are not expanded into * full diagram nodes — their names appear only in the package tooltip's "Extends" line. * <p> + * For results whose effective type is {@code chain}, {@code redirectAction}, or + * {@code redirect-action}, the model resolves the target action (from tag body text + * or {@code actionName}/{@code namespace} params). When the target action is declared + * in the same file, an action→action edge is emitted instead of a separate result + * node. External or unresolvable targets fall back to a labeled {@code Kind.RESULT} node. + * <p> * <b>Must be called under a read action.</b> All DOM/PSI access (tooltip computation, * navigation pointer creation) happens here so that Swing event handlers on the EDT * never need to touch PSI directly. @@ -81,9 +91,17 @@ public final class StrutsConfigDiagramModel { List<StrutsPackage> packages = getLocalPackages(xmlFile); if (packages == null) return null; + StrutsModel strutsModel = StrutsManager.getInstance(xmlFile.getProject()).getModelByFile(xmlFile); SmartPointerManager pointerManager = SmartPointerManager.getInstance(xmlFile.getProject()); StrutsConfigDiagramModel model = new StrutsConfigDiagramModel(); + // Pass 1: create package and action nodes; collect XmlTag→node mapping + // Use XmlTag keys rather than Action DOM proxies, because findActionsByName + // may return different proxy instances for the same underlying XML element. + Map<XmlTag, StrutsDiagramNode> actionNodeMap = new IdentityHashMap<>(); + record PendingResult(StrutsDiagramNode actionNode, Result result, String currentNamespace) {} + List<PendingResult> pendingResults = new ArrayList<>(); + for (StrutsPackage strutsPackage : packages) { String pkgName = Objects.toString(strutsPackage.getName().getStringValue(), UNNAMED); StrutsDiagramNode pkgNode = createNode( @@ -91,6 +109,7 @@ public final class StrutsConfigDiagramModel { strutsPackage, pointerManager); model.nodes.add(pkgNode); + String namespace = strutsPackage.searchNamespace(); for (Action action : strutsPackage.getActions()) { String actionName = Objects.toString(action.getName().getStringValue(), UNNAMED); StrutsDiagramNode actionNode = createNode( @@ -98,25 +117,138 @@ public final class StrutsConfigDiagramModel { action, pointerManager); model.nodes.add(actionNode); model.edges.add(new StrutsDiagramEdge(pkgNode, actionNode, "")); + XmlTag actionTag = action.getXmlTag(); + if (actionTag != null) { + actionNodeMap.put(actionTag, actionNode); + } for (Result result : action.getResults()) { - PathReference pathRef = result.getValue(); - String path = pathRef != null ? pathRef.getPath() : UNRESOLVED_RESULT; - Icon resultIcon = resolveResultIcon(result); - StrutsDiagramNode resultNode = createNode( - StrutsDiagramNode.Kind.RESULT, path, resultIcon, - result, pointerManager); - model.nodes.add(resultNode); - - String resultName = result.getName().getStringValue(); - model.edges.add(new StrutsDiagramEdge(actionNode, resultNode, - resultName != null ? resultName : Result.DEFAULT_NAME)); + pendingResults.add(new PendingResult(actionNode, result, namespace)); + } + } + } + + // Pass 2: process results — chain/redirect targets become action→action edges + for (PendingResult pr : pendingResults) { + String resultName = pr.result.getName().getStringValue(); + String edgeLabel = resultName != null ? resultName : Result.DEFAULT_NAME; + + Action targetAction = resolveChainOrRedirectTarget(pr.result, strutsModel, pr.currentNamespace); + if (targetAction != null) { + XmlTag targetTag = targetAction.getXmlTag(); + StrutsDiagramNode targetNode = targetTag != null ? actionNodeMap.get(targetTag) : null; + if (targetNode != null) { + // Target is in the same file — direct action→action edge + model.edges.add(new StrutsDiagramEdge(pr.actionNode, targetNode, edgeLabel)); + continue; } + // Target is in another file — show as labeled result node + String targetLabel = formatExternalActionLabel(targetAction); + StrutsDiagramNode resultNode = createNode( + StrutsDiagramNode.Kind.RESULT, targetLabel, Struts2Icons.Action, + pr.result, pointerManager); + model.nodes.add(resultNode); + model.edges.add(new StrutsDiagramEdge(pr.actionNode, resultNode, edgeLabel)); + continue; } + + // Non-chain/redirect or unresolvable — standard result node + PathReference pathRef = pr.result.getValue(); + String path = pathRef != null ? pathRef.getPath() : UNRESOLVED_RESULT; + Icon resultIcon = resolveResultIcon(pr.result); + StrutsDiagramNode resultNode = createNode( + StrutsDiagramNode.Kind.RESULT, path, resultIcon, + pr.result, pointerManager); + model.nodes.add(resultNode); + model.edges.add(new StrutsDiagramEdge(pr.actionNode, resultNode, edgeLabel)); } return model; } + /** + * Resolves the target {@link Action} for chain/redirect result types. + * Mirrors the resolution logic of + * {@link com.intellij.struts2.dom.struts.impl.path.ActionChainOrRedirectResultContributor}. + * + * @return the uniquely resolved action, or {@code null} if the result is not a + * chain/redirect type or the target cannot be resolved unambiguously. + */ + static @Nullable Action resolveChainOrRedirectTarget(@NotNull Result result, + @Nullable StrutsModel strutsModel, + @NotNull String currentNamespace) { + if (!result.isValid()) return null; + + String typeName = null; + ResultType effectiveType = result.getEffectiveResultType(); + if (effectiveType != null) { + typeName = effectiveType.getName().getStringValue(); + } + if (typeName == null) { + // Fall back to the raw XML attribute when the ResultType DOM can't be resolved + // (e.g. the result-type definition is in struts-default and not in the model) + typeName = result.getType().getStringValue(); + } + if (typeName == null || !ResultTypeResolver.isChainOrRedirectType(typeName)) return null; + + // Determine action path: prefer tag body, fall back to <param name="actionName"> + String actionPath = null; + XmlTag xmlTag = result.getXmlTag(); + if (xmlTag != null) { + String bodyText = xmlTag.getValue().getTrimmedText(); + if (!bodyText.isEmpty()) { + actionPath = bodyText; + } + } + if (actionPath == null) { + actionPath = getParamValue(result, "actionName"); + } + if (actionPath == null || actionPath.isEmpty()) return null; + + // Strip query parameters (e.g. "actionPath2?myParam=myValue") + int queryIdx = actionPath.indexOf('?'); + if (queryIdx != -1) { + actionPath = actionPath.substring(0, queryIdx); + } + + // Determine namespace: from path prefix, explicit param, or current package + String namespace = currentNamespace; + int lastSlash = actionPath.lastIndexOf('/'); + if (lastSlash != -1) { + namespace = actionPath.substring(0, lastSlash); + actionPath = actionPath.substring(lastSlash + 1); + } else { + String nsParam = getParamValue(result, "namespace"); + if (nsParam != null && !nsParam.isEmpty()) { + namespace = nsParam; + } + } + + if (strutsModel == null) return null; + List<Action> actions = strutsModel.findActionsByName(actionPath, namespace); + return actions.size() == 1 ? actions.get(0) : null; + } + + private static @Nullable String getParamValue(@NotNull Result result, @NotNull String paramName) { + for (Param param : result.getParams()) { + XmlTag tag = param.getXmlTag(); + if (tag != null && paramName.equals(tag.getAttributeValue("name"))) { + String value = tag.getValue().getTrimmedText(); + if (!value.isEmpty()) return value; + } + } + return null; + } + + private static @NotNull String formatExternalActionLabel(@NotNull Action action) { + String ns = action.getNamespace(); + String name = action.getName().getStringValue(); + if (name == null) name = UNNAMED; + if (ns != null && !StrutsPackage.DEFAULT_NAMESPACE.equals(ns)) { + return "\u2192 " + ns + "/" + name; + } + return "\u2192 " + name; + } + /** * Resolves the list of packages local to the given file. * Finds the {@link StrutsRoot} for the current file from the model's individual diff --git a/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java b/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java index a0342cb..6650747 100644 --- a/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java +++ b/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java @@ -134,8 +134,14 @@ public final class Struts2DiagramComponent extends JPanel { colX += NODE_WIDTH + H_GAP; maxY = Math.max(maxY, placeColumn(results, colX, PADDING)); + boolean hasSameColumnEdges = edges.stream().anyMatch(e -> { + Rectangle s = nodeBounds.get(e.getSource()); + Rectangle t = nodeBounds.get(e.getTarget()); + return s != null && t != null && s.x == t.x; + }); + int extraHeight = hasSameColumnEdges ? V_GAP + NODE_HEIGHT : 0; int totalWidth = colX + NODE_WIDTH + PADDING; - int totalHeight = maxY + PADDING; + int totalHeight = maxY + extraHeight + PADDING; setPreferredSize(new Dimension(totalWidth, totalHeight)); } @@ -194,32 +200,83 @@ public final class Struts2DiagramComponent extends JPanel { Rectangle tgtRect = nodeBounds.get(edge.getTarget()); if (srcRect == null || tgtRect == null) continue; - int x1 = srcRect.x + srcRect.width; - int y1 = srcRect.y + srcRect.height / 2; - int x2 = tgtRect.x; - int y2 = tgtRect.y + tgtRect.height / 2; - - g2.setColor(JBColor.namedColor("Diagram.edgeColor", JBColor.GRAY)); - int midX = (x1 + x2) / 2; - Path2D path = new Path2D.Float(); - path.moveTo(x1, y1); - path.curveTo(midX, y1, midX, y2, x2, y2); - g2.draw(path); - - drawArrowHead(g2, midX, y2, x2, y2); - - String label = edge.getLabel(); - if (!label.isEmpty()) { - g2.setFont(JBUI.Fonts.smallFont()); - g2.setColor(JBColor.namedColor("Diagram.edgeLabelColor", JBColor.DARK_GRAY)); - FontMetrics fm = g2.getFontMetrics(); - int labelX = midX - fm.stringWidth(label) / 2; - int labelY = (y1 + y2) / 2 - 3; - g2.drawString(label, labelX, labelY); + boolean sameColumn = srcRect.x == tgtRect.x; + if (sameColumn) { + paintSameColumnEdge(g2, edge, srcRect, tgtRect); + } else { + paintCrossColumnEdge(g2, edge, srcRect, tgtRect); } } } + private void paintCrossColumnEdge(Graphics2D g2, StrutsDiagramEdge edge, + Rectangle srcRect, Rectangle tgtRect) { + int x1 = srcRect.x + srcRect.width; + int y1 = srcRect.y + srcRect.height / 2; + int x2 = tgtRect.x; + int y2 = tgtRect.y + tgtRect.height / 2; + + g2.setColor(JBColor.namedColor("Diagram.edgeColor", JBColor.GRAY)); + int midX = (x1 + x2) / 2; + Path2D path = new Path2D.Float(); + path.moveTo(x1, y1); + path.curveTo(midX, y1, midX, y2, x2, y2); + g2.draw(path); + + drawArrowHead(g2, midX, y2, x2, y2); + + String label = edge.getLabel(); + if (!label.isEmpty()) { + g2.setFont(JBUI.Fonts.smallFont()); + g2.setColor(JBColor.namedColor("Diagram.edgeLabelColor", JBColor.DARK_GRAY)); + FontMetrics fm = g2.getFontMetrics(); + int labelX = midX - fm.stringWidth(label) / 2; + int labelY = (y1 + y2) / 2 - 3; + g2.drawString(label, labelX, labelY); + } + } + + /** + * Draws an edge between two nodes in the same column (e.g. action→action for + * chain/redirect results). Departs from the source's <b>right</b> edge, loops + * below both nodes, and arrives at the target's <b>left</b> edge — visually + * distinguishing it from the left-to-right action→result flow. Uses a dashed + * stroke for additional contrast. + */ + private void paintSameColumnEdge(Graphics2D g2, StrutsDiagramEdge edge, + Rectangle srcRect, Rectangle tgtRect) { + // Start at source right edge, end at target left edge + int x1 = srcRect.x + srcRect.width; + int y1 = srcRect.y + srcRect.height / 2; + int x2 = tgtRect.x; + int y2 = tgtRect.y + tgtRect.height / 2; + + // Loop below both nodes + int bottomY = Math.max(srcRect.y + srcRect.height, tgtRect.y + tgtRect.height) + V_GAP + NODE_HEIGHT / 2; + + g2.setColor(JBColor.namedColor("Diagram.edgeColor", JBColor.GRAY)); + g2.setStroke(new BasicStroke(1.2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, + 10.0f, new float[]{6.0f, 4.0f}, 0.0f)); + Path2D path = new Path2D.Float(); + path.moveTo(x1, y1); + path.curveTo(x1 + H_GAP / 3, y1, x1 + H_GAP / 3, bottomY, (x1 + x2) / 2, bottomY); + path.curveTo(x2 - H_GAP / 3, bottomY, x2 - H_GAP / 3, y2, x2, y2); + g2.draw(path); + g2.setStroke(new BasicStroke(1.2f)); + + drawArrowHead(g2, x2 - H_GAP / 3, y2, x2, y2); + + String label = edge.getLabel(); + if (!label.isEmpty()) { + g2.setFont(JBUI.Fonts.smallFont()); + g2.setColor(JBColor.namedColor("Diagram.edgeLabelColor", JBColor.DARK_GRAY)); + FontMetrics fm = g2.getFontMetrics(); + int labelX = (x1 + x2) / 2 - fm.stringWidth(label) / 2; + int labelY = bottomY + fm.getAscent() + 2; + g2.drawString(label, labelX, labelY); + } + } + private static void drawArrowHead(Graphics2D g2, int fromX, int fromY, int toX, int toY) { double angle = Math.atan2(toY - fromY, toX - fromX); int arrowLen = 8; diff --git a/src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java b/src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java index da43a4d..22781d3 100644 --- a/src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java +++ b/src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java @@ -357,6 +357,81 @@ public class StrutsConfigDiagramModelTest extends BasicLightHighlightingTestCase assertTrue("Should have explicit 'input' label, got: " + labels, labels.contains("input")); } + // --- Chain/redirect result type tests --- + + public void testRedirectActionBodyCreatesActionToActionEdge() { + createStrutsFileSet("struts-redirect-body.xml"); + + VirtualFile vf = myFixture.findFileInTempDir("struts-redirect-body.xml"); + assertNotNull(vf); + PsiFile psi = PsiManager.getInstance(getProject()).findFile(vf); + assertInstanceOf(psi, XmlFile.class); + + StrutsConfigDiagramModel model = ReadAction.nonBlocking( + () -> StrutsConfigDiagramModel.build((XmlFile) psi)).executeSynchronously(); + assertNotNull(model); + + // Two packages, two actions, one dispatcher result (dashboard's JSP) + List<StrutsDiagramNode> actions = model.getNodes().stream() + .filter(n -> n.getKind() == StrutsDiagramNode.Kind.ACTION) + .collect(Collectors.toList()); + assertEquals("Should have 2 actions (index, dashboard)", 2, actions.size()); + + // The redirectAction result should NOT produce a RESULT node (resolved same-file) + List<StrutsDiagramNode> results = model.getNodes().stream() + .filter(n -> n.getKind() == StrutsDiagramNode.Kind.RESULT) + .collect(Collectors.toList()); + assertEquals("Only dashboard's JSP result should be a RESULT node", 1, results.size()); + + List<StrutsDiagramEdge> actionToActionEdges = model.getEdges().stream() + .filter(e -> e.getSource().getKind() == StrutsDiagramNode.Kind.ACTION + && e.getTarget().getKind() == StrutsDiagramNode.Kind.ACTION) + .collect(Collectors.toList()); + assertEquals("Should have one action→action edge", 1, actionToActionEdges.size()); + + StrutsDiagramEdge redirectEdge = actionToActionEdges.get(0); + assertEquals("index", redirectEdge.getSource().getName()); + assertEquals("dashboard", redirectEdge.getTarget().getName()); + assertEquals("success", redirectEdge.getLabel()); + } + + public void testRedirectActionParamCreatesActionToActionEdge() { + createStrutsFileSet("struts-redirect-param.xml"); + + VirtualFile vf = myFixture.findFileInTempDir("struts-redirect-param.xml"); + assertNotNull(vf); + PsiFile psi = PsiManager.getInstance(getProject()).findFile(vf); + assertInstanceOf(psi, XmlFile.class); + + StrutsConfigDiagramModel model = ReadAction.nonBlocking( + () -> StrutsConfigDiagramModel.build((XmlFile) psi)).executeSynchronously(); + assertNotNull(model); + + // One package, two actions (index, upload) + List<StrutsDiagramNode> actions = model.getNodes().stream() + .filter(n -> n.getKind() == StrutsDiagramNode.Kind.ACTION) + .collect(Collectors.toList()); + assertEquals("Should have 2 actions (index, upload)", 2, actions.size()); + + // The param-only redirectAction should NOT produce a RESULT node + List<StrutsDiagramNode> results = model.getNodes().stream() + .filter(n -> n.getKind() == StrutsDiagramNode.Kind.RESULT) + .collect(Collectors.toList()); + assertEquals("Only upload's JSP result should be a RESULT node", 1, results.size()); + + // action→action edge from index to upload + List<StrutsDiagramEdge> actionToActionEdges = model.getEdges().stream() + .filter(e -> e.getSource().getKind() == StrutsDiagramNode.Kind.ACTION + && e.getTarget().getKind() == StrutsDiagramNode.Kind.ACTION) + .collect(Collectors.toList()); + assertEquals("Should have one action→action edge", 1, actionToActionEdges.size()); + + StrutsDiagramEdge redirectEdge = actionToActionEdges.get(0); + assertEquals("index", redirectEdge.getSource().getName()); + assertEquals("upload", redirectEdge.getTarget().getName()); + assertEquals("success", redirectEdge.getLabel()); + } + public void testEdgesInDuplicateNameFileAreCorrectlyWired() { createStrutsFileSet("struts-duplicate-names.xml"); diff --git a/src/test/testData/diagram/struts-redirect-body.xml b/src/test/testData/diagram/struts-redirect-body.xml new file mode 100644 index 0000000..54798dd --- /dev/null +++ b/src/test/testData/diagram/struts-redirect-body.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<!-- +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you 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. +--> + +<!DOCTYPE struts PUBLIC + "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" + "http://struts.apache.org/dtds/struts-2.0.dtd"> + +<struts> + + <package name="pkgA" namespace="/a"> + <result-types> + <result-type name="redirectAction" class="org.apache.struts2.result.ServletActionRedirectResult"/> + <result-type name="chain" class="org.apache.struts2.result.ChainResult"/> + </result-types> + + <action name="index"> + <result type="redirectAction">/b/dashboard</result> + </action> + </package> + + <package name="pkgB" namespace="/b"> + <action name="dashboard" class="com.example.DashboardAction"> + <result>/pages/dashboard.jsp</result> + </action> + </package> + +</struts> diff --git a/src/test/testData/diagram/struts-redirect-param.xml b/src/test/testData/diagram/struts-redirect-param.xml new file mode 100644 index 0000000..a4ddf08 --- /dev/null +++ b/src/test/testData/diagram/struts-redirect-param.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<!-- +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you 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. +--> + +<!DOCTYPE struts PUBLIC + "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" + "http://struts.apache.org/dtds/struts-2.0.dtd"> + +<struts> + + <package name="default" namespace="/" extends="struts-default"> + + <default-action-ref name="index"/> + + <action name="index"> + <result type="redirectAction"> + <param name="actionName">upload</param> + </result> + </action> + + <action name="upload" class="org.apache.struts.example.UploadAction"> + <result name="input">/WEB-INF/upload.jsp</result> + </action> + + </package> + +</struts>
