This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts-intellij-plugin.git
The following commit(s) were added to refs/heads/main by this push:
new fc7862c Migrate Diagram tab to IntelliJ Show Diagram API (#117) (#119)
fc7862c is described below
commit fc7862c79816c876cd565cfb6d5dd4bbc25d61b0
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Jul 25 21:34:45 2026 +0200
Migrate Diagram tab to IntelliJ Show Diagram API (#117) (#119)
* docs: add design for Diagrams API migration (#117)
Capture the approved Show Diagram host swap: hard com.intellij.diagram
dependency, keep toolkit-neutral model/presentation, remove the Swing tab.
Co-authored-by: Cursor <[email protected]>
* docs: add implementation plan for Diagrams API migration (#117)
Co-authored-by: Cursor <[email protected]>
* build: hard-depend on IntelliJ Diagrams plugin for #117
Co-authored-by: Cursor <[email protected]>
* feat(diagram): extract DomEvent same-file helpers for Diagrams host
Co-authored-by: Cursor <[email protected]>
* feat(diagram): register Show Diagram provider for Struts configs
Co-authored-by: Cursor <[email protected]>
* feat(diagram): map Struts snapshot into API nodes
Co-authored-by: Cursor <[email protected]>
* test(diagram): assert snapshot-to-API edge triple mapping
Co-authored-by: Cursor <[email protected]>
* feat(diagram): refresh model on same-file DomEvents
Co-authored-by: Cursor <[email protected]>
* fix(diagram): refresh presentation after DomEvents
Build diagram API objects under the non-blocking read action so the EDT
only swaps collections and requests a builder refresh.
Co-authored-by: Cursor <[email protected]>
* refactor(diagram): remove Swing Diagram tab in favor of Show Diagram
Co-authored-by: Cursor <[email protected]>
* docs: changelog and package-info for Diagrams API migration
Co-authored-by: Cursor <[email protected]>
* fix(diagram): preserve node identity across VFS resolution
Resolve snapshot node IDs from diagram FQNs and prevent callers from
mutating the live diagram collections.
Co-authored-by: Cursor <[email protected]>
* feat(diagram): keep Swing Diagram tab alongside Show Diagram
Restore the PerspectiveFileEditor host so both UIs can be compared
while the Diagrams API presentation is polished.
Co-authored-by: Cursor <[email protected]>
* fix(diagram): navigate to XML on Show Diagram double-click
Editor-mode double-click uses EditNodeHandler, not DiagramNode.navigate().
Wire Extras + PSI data context so Jump to Source works as well.
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
---
CHANGELOG.md | 6 +-
build.gradle.kts | 2 +
.../plans/2026-07-25-diagrams-api-migration.md | 1200 ++++++++++++++++++++
.../2026-07-25-diagrams-api-migration-design.md | 182 +++
.../struts2/diagram/model/package-info.java | 22 +-
.../diagram/provider/StrutsDiagramApiEdge.java | 43 +
.../diagram/provider/StrutsDiagramApiNode.java | 68 ++
.../diagram/provider/StrutsDiagramDataModel.java | 173 +++
.../diagram/provider/StrutsDiagramDomRefresh.java | 44 +
.../provider/StrutsDiagramElementManager.java | 100 ++
.../diagram/provider/StrutsDiagramExtras.java | 100 ++
.../diagram/provider/StrutsDiagramItem.java | 88 ++
.../diagram/provider/StrutsDiagramProvider.java | 79 ++
.../diagram/provider/StrutsDiagramVfsResolver.java | 71 ++
src/main/resources/META-INF/plugin.xml | 2 +
.../diagram/StrutsDiagramDataModelMappingTest.java | 210 ++++
.../diagram/StrutsDiagramDomRefreshTest.java | 64 ++
.../struts2/diagram/StrutsDiagramProviderTest.java | 139 +++
18 files changed, 2576 insertions(+), 17 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d80a47e..e88f23d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,10 +13,8 @@
- Dependencies - upgrade `org.jetbrains.kotlin.jvm` to `2.3.0`
- Dependencies - upgrade Qodana linter image to
`jetbrains/qodana-jvm:2026.2-eap`
- Dependencies - declare IntelliJ Platform 2026.2 v2 content modules
`intellij.javascript.parser`, `intellij.javascript.backend`,
`intellij.javaee.jsp.shared`, `intellij.css`, and `intellij.css.backend`
([#103](https://github.com/apache/struts-intellij-plugin/issues/103))
-
-### Fixed
-
-- Fix blank Diagram tab on IntelliJ 2026.2 by invoking the
`PerspectiveFileEditor` select/deselect hooks so the custom component is
installed; placeholder sizing remains as a defensive fallback
+- Add IntelliJ **Show Diagram** support for Struts 2 configs via
`com.intellij.diagram`, alongside the existing Diagram editor tab for
comparison ([#117](https://github.com/apache/struts-intellij-plugin/issues/117))
+- Dependencies - hard-depend on `com.intellij.diagram` (Ultimate Diagrams)
## [261.19027.1] - 2026-07-13
diff --git a/build.gradle.kts b/build.gradle.kts
index a3cc4aa..b0524bf 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -59,6 +59,7 @@ dependencies {
bundledModule("intellij.javaee.jsp.shared")
bundledPlugin("com.intellij.spring")
bundledPlugin("com.intellij.java-i18n")
+ bundledPlugin("com.intellij.diagram")
bundledPlugin("com.intellij.freemarker")
bundledPlugin("com.intellij.velocity")
bundledPlugin("org.intellij.groovy")
@@ -75,6 +76,7 @@ dependencies {
testBundledPlugins(
"com.intellij.java",
+ "com.intellij.diagram",
"intellij.structureView.plugin",
"intellij.todo.plugin",
"intellij.structuralSearch.plugin",
diff --git a/docs/superpowers/plans/2026-07-25-diagrams-api-migration.md
b/docs/superpowers/plans/2026-07-25-diagrams-api-migration.md
new file mode 100644
index 0000000..38ff162
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-25-diagrams-api-migration.md
@@ -0,0 +1,1200 @@
+# Migrate Struts Diagram to Diagrams API Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development (recommended) or
superpowers:executing-plans to implement this plan task-by-task. Steps use
checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the custom Swing Diagram editor tab with an IntelliJ **Show
Diagram** provider that consumes the existing toolkit-neutral Struts config
snapshot.
+
+**Architecture:** Keep `StrutsConfigDiagramModel` +
`StrutsDiagramPresentation`. Add a `diagram.provider` package implementing
`BaseDiagramProvider` / `DiagramDataModel` that maps snapshot nodes/edges into
Diagrams API types, registers `com.intellij.diagram.Provider`, hard-depends on
`com.intellij.diagram`, and removes `diagram.ui` + `diagram.fileEditor`.
+
+**Tech Stack:** IntelliJ IDEA Ultimate 2026.2 (262), `com.intellij.diagram`
(`BaseDiagramProvider`, `DiagramDataModel`, `DiagramNodeBase`,
`DiagramEdgeBase`), DomEventListener, JUnit 4 light tests
(`BasicLightHighlightingTestCase`).
+
+**Spec:** `docs/superpowers/specs/2026-07-25-diagrams-api-migration-design.md`
+
+## Global Constraints
+
+- Target platform remains IntelliJ IDEA **2026.2** / build **262** only
(`pluginSinceBuild=262`, `pluginUntilBuild=262.*`).
+- Hard dependency on plugin id **`com.intellij.diagram`** (bundled
UML/Diagrams).
+- Entry point is **Show Diagram only** — remove the Diagram
`PerspectiveFileEditor` tab.
+- Show Diagram accepts any Struts 2 config
(`StrutsManager.isStruts2ConfigFile`), not only file-set members.
+- Do **not** change `StrutsConfigDiagramModel` build semantics or
`StrutsDiagramPresentation` tooltip/navigation logic.
+- Do **not** implement #96–#100 in this plan.
+- Dom live refresh: while the data model is alive, same-file DomEvents
debounce **300 ms**, then `refreshDataModel()`.
+- Tests gate: `./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"`.
+- Learn API details from local IU SDK jars under Gradle caches
(`plugins/uml/lib/uml-support.jar`) and Spring Integration’s
`SpringIntegrationDiagramProvider` pattern when signatures drift.
+
+---
+
+## File Structure
+
+| File | Action | Responsibility |
+|---|---|---|
+| `build.gradle.kts` | Modify | `bundledPlugin("com.intellij.diagram")` (+
test bundled if needed) |
+| `src/main/resources/META-INF/plugin.xml` | Modify | Hard `<plugin
id="com.intellij.diagram"/>`; register `diagram.Provider`; remove
`fileEditorProvider` |
+| `src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramItem.java`
| Create | Identifying element `T` for Diagrams API (file root + snapshot node)
|
+|
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java`
| Create | Pure DomEvent same-file helpers (migrated from
`Struts2DiagramFileEditor`) |
+|
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramElementManager.java`
| Create | `findInDataContext` / `isAcceptableAsNode` / titles / tooltips |
+|
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramVfsResolver.java`
| Create | Persist/resolve root `XmlFile` by URL |
+|
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiNode.java`
| Create | `DiagramNodeBase` wrapping `StrutsDiagramNode` |
+|
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiEdge.java`
| Create | `DiagramEdgeBase` wrapping snapshot edges |
+|
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java`
| Create | Snapshot → nodes/edges; DomEvent debounce refresh |
+|
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramProvider.java`
| Create | `BaseDiagramProvider` + EP implementation |
+| `src/main/java/com/intellij/struts2/diagram/model/package-info.java` |
Modify | Document Show Diagram host; drop Swing references |
+| `src/main/java/com/intellij/struts2/diagram/ui/*` | Delete | Custom Swing
renderer |
+| `src/main/java/com/intellij/struts2/diagram/fileEditor/*` | Delete |
`PerspectiveFileEditor` host |
+| `src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java`
| Create | Accept / reject / EP presence (replaces editor provider tests) |
+|
`src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java` |
Create | Renamed/moved Dom filter tests |
+|
`src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java`
| Create | Snapshot → API node/edge mapping |
+|
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java`
| Delete | Tab host tests |
+|
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorDomFilterTest.java`
| Delete | Replaced by Dom refresh test |
+|
`src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java`
| Keep | Semantic model tests (remove any Swing-only assertions if present) |
+| `CHANGELOG.md` | Modify | Unreleased Changed/Removed for Show Diagram
migration |
+
+No new public plugin extension points.
+
+---
+
+### Task 1: Hard-depend on `com.intellij.diagram`
+
+**Files:**
+- Modify: `build.gradle.kts`
+- Modify: `src/main/resources/META-INF/plugin.xml`
+- Test: compile/plugin classpath (no unit test yet)
+
+**Interfaces:**
+- Consumes: IntelliJ Platform Gradle `bundledPlugin`
+- Produces: compile-time access to `com.intellij.diagram.*`; runtime hard
dependency on plugin id `com.intellij.diagram`
+
+- [ ] **Step 1: Add Gradle bundled plugin**
+
+In `build.gradle.kts` inside `dependencies { intellijPlatform { ... } }`,
after the existing bundled plugins (near `com.intellij.java-i18n`), add:
+
+```kotlin
+bundledPlugin("com.intellij.diagram")
+```
+
+Also add `"com.intellij.diagram"` to `testBundledPlugins(...)` so light tests
can load the EP.
+
+- [ ] **Step 2: Declare hard plugin dependency in `plugin.xml`**
+
+Inside the existing `<dependencies>` block, add:
+
+```xml
+<plugin id="com.intellij.diagram"/>
+```
+
+Do **not** register `diagram.Provider` yet (next tasks).
+
+- [ ] **Step 3: Verify the dependency resolves**
+
+Run:
+
+```bash
+./gradlew compileJava -q
+```
+
+Expected: BUILD SUCCESSFUL (no missing `com.intellij.diagram` types yet
because no new sources).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add build.gradle.kts src/main/resources/META-INF/plugin.xml
+git commit -m "$(cat <<'EOF'
+build: hard-depend on IntelliJ Diagrams plugin for #117
+
+EOF
+)"
+```
+
+---
+
+### Task 2: Dom refresh helpers (pure, testable)
+
+**Files:**
+- Create:
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java`
+- Create:
`src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java`
+- Delete (later Task 6, not now): old Dom filter test still references file
editor — leave it until migration completes, **or** rewrite now and leave a
temporary compile dependency. Prefer: rewrite test now against the new helper;
keep old file editor until Task 6.
+
+**Interfaces:**
+- Consumes: `DomEvent`, `DomElement`, `DomUtil`, `VirtualFile`
+- Produces:
+ - `static boolean isEventForMyFile(@NotNull DomEvent event, @NotNull
VirtualFile file)`
+ - `static boolean isDomElementInFile(@Nullable DomElement element, @NotNull
VirtualFile file)`
+
+- [ ] **Step 1: Write failing Dom filter tests**
+
+Create `StrutsDiagramDomRefreshTest.java` (copy logic from
`Struts2DiagramFileEditorDomFilterTest`, change call sites to
`StrutsDiagramDomRefresh`):
+
+```java
+package com.intellij.struts2.diagram;
+
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.BasicLightHighlightingTestCase;
+import com.intellij.struts2.diagram.provider.StrutsDiagramDomRefresh;
+import com.intellij.struts2.dom.struts.StrutsRoot;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomFileElement;
+import com.intellij.util.xml.DomManager;
+import org.jetbrains.annotations.NotNull;
+
+public class StrutsDiagramDomRefreshTest extends
BasicLightHighlightingTestCase {
+
+ @Override
+ @NotNull
+ protected String getTestDataLocation() {
+ return "diagram";
+ }
+
+ public void testIsDomElementInFileMatchesSameVirtualFile() {
+ createStrutsFileSet("struts-local-a.xml", "struts-local-b.xml");
+ VirtualFile vfA = myFixture.findFileInTempDir("struts-local-a.xml");
+ VirtualFile vfB = myFixture.findFileInTempDir("struts-local-b.xml");
+ assertNotNull(vfA);
+ assertNotNull(vfB);
+
+ ReadAction.run(() -> {
+ XmlFile fileA = (XmlFile)
PsiManager.getInstance(getProject()).findFile(vfA);
+ assertNotNull(fileA);
+ DomFileElement<StrutsRoot> root =
+
DomManager.getDomManager(getProject()).getFileElement(fileA, StrutsRoot.class);
+ assertNotNull(root);
+ DomElement pkg = root.getRootElement().getPackages().getFirst();
+ assertTrue(StrutsDiagramDomRefresh.isDomElementInFile(pkg, vfA));
+ assertFalse(StrutsDiagramDomRefresh.isDomElementInFile(pkg, vfB));
+ });
+ }
+
+ public void testIsDomElementInFileRejectsNullElement() {
+ createStrutsFileSet("struts-local-a.xml");
+ VirtualFile vfA = myFixture.findFileInTempDir("struts-local-a.xml");
+ assertNotNull(vfA);
+ assertFalse(StrutsDiagramDomRefresh.isDomElementInFile(null, vfA));
+ }
+}
+```
+
+Use the same Apache license header as sibling test files.
+
+- [ ] **Step 2: Run test to verify it fails**
+
+```bash
+./gradlew test -x rat --tests
"com.intellij.struts2.diagram.StrutsDiagramDomRefreshTest"
+```
+
+Expected: FAIL (class `StrutsDiagramDomRefresh` not found).
+
+- [ ] **Step 3: Implement helper**
+
+Create `StrutsDiagramDomRefresh.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomUtil;
+import com.intellij.util.xml.events.DomEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Pure helpers for same-file DomEvent filtering used by {@link
StrutsDiagramDataModel}.
+ */
+public final class StrutsDiagramDomRefresh {
+
+ private StrutsDiagramDomRefresh() {}
+
+ public static boolean isEventForMyFile(@NotNull DomEvent event, @NotNull
VirtualFile file) {
+ return isDomElementInFile(event.getElement(), file);
+ }
+
+ public static boolean isDomElementInFile(@Nullable DomElement element,
@NotNull VirtualFile file) {
+ if (element == null) {
+ return false;
+ }
+ VirtualFile elementFile =
DomUtil.getFile(element).getOriginalFile().getVirtualFile();
+ return file.equals(elementFile);
+ }
+}
+```
+
+- [ ] **Step 4: Run tests to verify they pass**
+
+```bash
+./gradlew test -x rat --tests
"com.intellij.struts2.diagram.StrutsDiagramDomRefreshTest"
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add
src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java
\
+
src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java
+git commit -m "$(cat <<'EOF'
+feat(diagram): extract DomEvent same-file helpers for Diagrams host
+
+EOF
+)"
+```
+
+---
+
+### Task 3: Identifying item + Provider accept path
+
+**Files:**
+- Create:
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramItem.java`
+- Create:
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramElementManager.java`
+- Create:
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramVfsResolver.java`
+- Create:
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramProvider.java`
(skeleton; data model stub OK temporarily)
+- Create:
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java`
(minimal stub compiling)
+- Modify: `src/main/resources/META-INF/plugin.xml` — register provider
+- Create:
`src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java`
+
+**Interfaces:**
+- Consumes: `StrutsManager.isStruts2ConfigFile(XmlFile)`,
`BaseDiagramProvider`, `AbstractDiagramElementManager`
+- Produces:
+ - `StrutsDiagramItem.forFile(XmlFile)` / `forNode(XmlFile,
StrutsDiagramNode)` / `getXmlFile()` / `getSnapshotNode()`
+ - Provider id constant `StrutsDiagramProvider.ID = "ApacheStrutsConfig"`
+ - `StrutsDiagramElementManager.findInDataContext` → root item when PSI file
is Struts config
+ - `canBeBuiltFrom(Object)` / accept semantics: Struts config **without**
requiring file-set membership
+
+- [ ] **Step 1: Write failing accept tests**
+
+Create `StrutsDiagramProviderTest.java`:
+
+```java
+package com.intellij.struts2.diagram;
+
+import com.intellij.diagram.DiagramProvider;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.BasicLightHighlightingTestCase;
+import com.intellij.struts2.diagram.provider.StrutsDiagramItem;
+import com.intellij.struts2.diagram.provider.StrutsDiagramProvider;
+import org.jetbrains.annotations.NotNull;
+
+public class StrutsDiagramProviderTest extends BasicLightHighlightingTestCase {
+
+ @Override
+ @NotNull
+ protected String getTestDataLocation() {
+ return "diagram";
+ }
+
+ public void testProviderExtensionIsRegistered() {
+ DiagramProvider<?> provider =
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+ assertNotNull("Struts DiagramProvider must be registered via
com.intellij.diagram.Provider",
+ provider);
+ assertInstanceOf(provider, StrutsDiagramProvider.class);
+ }
+
+ public void testAcceptsStrutsConfigWithoutFileSet() {
+ VirtualFile file = myFixture.copyFileToProject("struts-diagram.xml");
+ XmlFile xml = (XmlFile)
PsiManager.getInstance(getProject()).findFile(file);
+ assertNotNull(xml);
+ StrutsDiagramProvider provider = (StrutsDiagramProvider)
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+ assertNotNull(provider);
+ DataContext ctx = SimpleDataContext.builder()
+ .add(CommonDataKeys.PSI_FILE, xml)
+ .add(CommonDataKeys.PROJECT, getProject())
+ .build();
+ StrutsDiagramItem item =
provider.getElementManager().findInDataContext(ctx);
+ assertNotNull("Show Diagram must accept Struts config outside a file
set", item);
+ assertEquals(xml, item.getXmlFile());
+ assertNull(item.getSnapshotNode());
+ }
+
+ public void testRejectsPlainXml() {
+ XmlFile xml = (XmlFile) myFixture.configureByText("plain.xml",
"<root/>");
+ StrutsDiagramProvider provider = (StrutsDiagramProvider)
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+ assertNotNull(provider);
+ DataContext ctx = SimpleDataContext.builder()
+ .add(CommonDataKeys.PSI_FILE, xml)
+ .add(CommonDataKeys.PROJECT, getProject())
+ .build();
+ assertNull(provider.getElementManager().findInDataContext(ctx));
+ assertFalse(provider.getElementManager().canBeBuiltFrom(xml));
+ }
+}
+```
+
+If `SimpleDataContext.builder()` API differs on 262, use the project’s
existing DataContext construction pattern or `MapDataContext` — resolve against
the IU SDK; keep the same assertions.
+
+- [ ] **Step 2: Run tests to verify they fail**
+
+```bash
+./gradlew test -x rat --tests
"com.intellij.struts2.diagram.StrutsDiagramProviderTest"
+```
+
+Expected: FAIL (provider not registered / classes missing).
+
+- [ ] **Step 3: Implement item + managers + stub provider/data model**
+
+`StrutsDiagramItem.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.SmartPointerManager;
+import com.intellij.psi.SmartPsiElementPointer;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Identifying element for Struts Diagrams API nodes.
+ * Root items (snapshotNode == null) seed Show Diagram from an XmlFile.
+ */
+public final class StrutsDiagramItem {
+
+ private final @NotNull SmartPsiElementPointer<XmlFile> filePointer;
+ private final @Nullable StrutsDiagramNode snapshotNode;
+
+ private StrutsDiagramItem(@NotNull SmartPsiElementPointer<XmlFile>
filePointer,
+ @Nullable StrutsDiagramNode snapshotNode) {
+ this.filePointer = filePointer;
+ this.snapshotNode = snapshotNode;
+ }
+
+ public static @NotNull StrutsDiagramItem forFile(@NotNull XmlFile file) {
+ return new StrutsDiagramItem(
+
SmartPointerManager.getInstance(file.getProject()).createSmartPsiElementPointer(file),
+ null);
+ }
+
+ public static @NotNull StrutsDiagramItem forNode(@NotNull XmlFile file,
@NotNull StrutsDiagramNode node) {
+ return new StrutsDiagramItem(
+
SmartPointerManager.getInstance(file.getProject()).createSmartPsiElementPointer(file),
+ node);
+ }
+
+ public @Nullable XmlFile getXmlFile() {
+ return filePointer.getElement();
+ }
+
+ public @Nullable StrutsDiagramNode getSnapshotNode() {
+ return snapshotNode;
+ }
+
+ public boolean isRoot() {
+ return snapshotNode == null;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof StrutsDiagramItem that)) return false;
+ if (snapshotNode == null || that.snapshotNode == null) {
+ return snapshotNode == that.snapshotNode
+ && Objects.equals(fileUrl(), that.fileUrl());
+ }
+ return snapshotNode.equals(that.snapshotNode);
+ }
+
+ @Override
+ public int hashCode() {
+ return snapshotNode != null ? snapshotNode.hashCode() :
Objects.hash(fileUrl());
+ }
+
+ private @Nullable String fileUrl() {
+ PsiFile file = filePointer.getElement();
+ return file != null && file.getVirtualFile() != null ?
file.getVirtualFile().getUrl() : null;
+ }
+}
+```
+
+`StrutsDiagramVfsResolver.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.diagram.DiagramVfsResolver;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class StrutsDiagramVfsResolver implements
DiagramVfsResolver<StrutsDiagramItem> {
+
+ @Override
+ public @Nullable String getQualifiedName(StrutsDiagramItem item) {
+ if (item == null) return null;
+ XmlFile file = item.getXmlFile();
+ if (file == null || file.getVirtualFile() == null) return null;
+ if (item.getSnapshotNode() != null) {
+ return file.getVirtualFile().getUrl() + "#" +
item.getSnapshotNode().getId();
+ }
+ return file.getVirtualFile().getUrl();
+ }
+
+ @Override
+ public @Nullable StrutsDiagramItem resolveElementByFQN(@NotNull String
fqn, @NotNull Project project) {
+ String url = fqn.contains("#") ? fqn.substring(0, fqn.indexOf('#')) :
fqn;
+ VirtualFile vf = VirtualFileManager.getInstance().findFileByUrl(url);
+ if (vf == null) return null;
+ PsiFile psi = PsiManager.getInstance(project).findFile(vf);
+ if (!(psi instanceof XmlFile xmlFile)) return null;
+ return StrutsDiagramItem.forFile(xmlFile);
+ }
+}
+```
+
+`StrutsDiagramElementManager.java` (extend `AbstractDiagramElementManager`):
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.AbstractDiagramElementManager;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.dom.struts.model.StrutsManager;
+import com.intellij.ui.SimpleColoredText;
+import com.intellij.ui.SimpleTextAttributes;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public final class StrutsDiagramElementManager extends
AbstractDiagramElementManager<StrutsDiagramItem> {
+
+ @Override
+ public @Nullable StrutsDiagramItem findInDataContext(@NotNull DataContext
context) {
+ PsiFile psiFile = CommonDataKeys.PSI_FILE.getData(context);
+ if (!(psiFile instanceof XmlFile xmlFile)) {
+ return null;
+ }
+ if
(!StrutsManager.getInstance(xmlFile.getProject()).isStruts2ConfigFile(xmlFile))
{
+ return null;
+ }
+ return StrutsDiagramItem.forFile(xmlFile);
+ }
+
+ @Override
+ public boolean canBeBuiltFrom(Object element) {
+ if (element instanceof StrutsDiagramItem item) {
+ XmlFile file = item.getXmlFile();
+ return file != null
+ &&
StrutsManager.getInstance(file.getProject()).isStruts2ConfigFile(file);
+ }
+ if (element instanceof XmlFile xmlFile) {
+ return
StrutsManager.getInstance(xmlFile.getProject()).isStruts2ConfigFile(xmlFile);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isAcceptableAsNode(Object element) {
+ return element instanceof StrutsDiagramItem item && !item.isRoot();
+ }
+
+ @Override
+ public @Nullable @Nls String getElementTitle(StrutsDiagramItem item) {
+ if (item == null) return null;
+ StrutsDiagramNode node = item.getSnapshotNode();
+ if (node != null) return node.getName();
+ XmlFile file = item.getXmlFile();
+ return file != null ? file.getName() : null;
+ }
+
+ @Override
+ public @Nullable String getNodeTooltip(StrutsDiagramItem item) {
+ if (item == null || item.getSnapshotNode() == null) return null;
+ return item.getSnapshotNode().getTooltipHtml();
+ }
+
+ @Override
+ public Object[] getNodeItems(StrutsDiagramItem element) {
+ return EMPTY_ARRAY;
+ }
+
+ @Override
+ public boolean canCollapse(StrutsDiagramItem element) {
+ return false;
+ }
+
+ @Override
+ public boolean isContainerFor(StrutsDiagramItem parent, StrutsDiagramItem
child) {
+ return false;
+ }
+}
+```
+
+Remove unused imports (`SimpleColoredText`, `SimpleTextAttributes`, `Icon`) if
the compiler flags them. After adding the class, compile once and fix any
remaining 262 abstract-method signatures / `@NotNull` annotations; keep the
accept/tooltip/title behavior above.
+
+Minimal compiling `StrutsDiagramDataModel` stub (full implementation in Task
4):
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramDataModel;
+import com.intellij.diagram.DiagramEdge;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.psi.PsiManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Collections;
+
+public final class StrutsDiagramDataModel extends
DiagramDataModel<StrutsDiagramItem> {
+
+ public StrutsDiagramDataModel(@NotNull Project project,
+ @NotNull StrutsDiagramProvider provider,
+ @Nullable StrutsDiagramItem seed) {
+ super(project, provider);
+ if (seed != null) {
+ setOriginalElement(seed);
+ }
+ }
+
+ @Override
+ public @NotNull ModificationTracker getModificationTracker() {
+ return PsiManager.getInstance(getProject()).getModificationTracker();
+ }
+
+ @Override
+ public @NotNull Collection<? extends DiagramNode<StrutsDiagramItem>>
getNodes() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public @NotNull Collection<? extends DiagramEdge<StrutsDiagramItem>>
getEdges() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public @NotNull String getNodeName(@NotNull DiagramNode<StrutsDiagramItem>
node) {
+ StrutsDiagramItem item = node.getIdentifyingElement();
+ String title = getProvider().getElementManager().getElementTitle(item);
+ return title != null ? title : "";
+ }
+
+ @Override
+ public @Nullable DiagramNode<StrutsDiagramItem> addElement(@Nullable
StrutsDiagramItem element) {
+ return null;
+ }
+
+ @Override
+ public void refreshDataModel() {
+ // Task 4
+ }
+}
+```
+
+`StrutsDiagramProvider.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.BaseDiagramProvider;
+import com.intellij.diagram.DiagramDataModel;
+import com.intellij.diagram.DiagramElementManager;
+import com.intellij.diagram.DiagramPresentationModel;
+import com.intellij.diagram.DiagramVfsResolver;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.struts2.Struts2Icons;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public final class StrutsDiagramProvider extends
BaseDiagramProvider<StrutsDiagramItem> {
+
+ public static final String ID = "ApacheStrutsConfig";
+
+ private final DiagramElementManager<StrutsDiagramItem> elementManager =
new StrutsDiagramElementManager();
+ private final DiagramVfsResolver<StrutsDiagramItem> vfsResolver = new
StrutsDiagramVfsResolver();
+
+ @Override
+ public @NotNull String getID() {
+ return ID;
+ }
+
+ @Override
+ public @NotNull @Nls String getPresentableName() {
+ return "Struts Configuration";
+ }
+
+ @Override
+ public @Nullable Icon getActionIcon(boolean isPopup) {
+ return Struts2Icons.Action;
+ }
+
+ @Override
+ public @NotNull DiagramElementManager<StrutsDiagramItem>
getElementManager() {
+ return elementManager;
+ }
+
+ @Override
+ public @NotNull DiagramVfsResolver<StrutsDiagramItem> getVfsResolver() {
+ return vfsResolver;
+ }
+
+ @Override
+ public @NotNull DiagramDataModel<StrutsDiagramItem>
createDataModel(@NotNull Project project,
+
@Nullable StrutsDiagramItem element,
+
@Nullable VirtualFile file,
+
@NotNull DiagramPresentationModel presentationModel) {
+ return new StrutsDiagramDataModel(project, this, element);
+ }
+}
+```
+
+Register in `plugin.xml` under `<extensions
defaultExtensionNs="com.intellij">`:
+
+```xml
+<diagram.Provider
implementation="com.intellij.struts2.diagram.provider.StrutsDiagramProvider"/>
+```
+
+Keep the old `fileEditorProvider` until Task 6 so the IDE still has a diagram
tab during incremental work.
+
+- [ ] **Step 4: Compile and fix API mismatches**
+
+```bash
+./gradlew compileJava compileTestJava
+```
+
+Fix any 262 signature mismatches (imports, `@NotNull`, removed abstract
methods). Re-run until green compile.
+
+- [ ] **Step 5: Run provider tests**
+
+```bash
+./gradlew test -x rat --tests
"com.intellij.struts2.diagram.StrutsDiagramProviderTest"
+```
+
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/main/java/com/intellij/struts2/diagram/provider \
+ src/main/resources/META-INF/plugin.xml \
+
src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java
+git commit -m "$(cat <<'EOF'
+feat(diagram): register Show Diagram provider for Struts configs
+
+EOF
+)"
+```
+
+---
+
+### Task 4: Snapshot → Diagrams nodes/edges mapping
+
+**Files:**
+- Create:
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiNode.java`
+- Create:
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiEdge.java`
+- Modify:
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java`
(full refresh)
+- Create:
`src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java`
+
+**Interfaces:**
+- Consumes: `StrutsConfigDiagramModel.build(XmlFile)`, `StrutsDiagramNode`,
`StrutsDiagramEdge`, `DiagramRelationships.DEPENDENCY`,
`DiagramRelationshipInfoAdapter` for labeled edges
+- Produces: After `refreshDataModel()`, `getNodes()`/`getEdges()` mirror the
snapshot (same counts, ids, edge endpoints/labels)
+
+- [ ] **Step 1: Write failing mapping test**
+
+```java
+package com.intellij.struts2.diagram;
+
+import com.intellij.diagram.DiagramEdge;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.diagram.DiagramProvider;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.BasicLightHighlightingTestCase;
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.provider.StrutsDiagramDataModel;
+import com.intellij.struts2.diagram.provider.StrutsDiagramItem;
+import com.intellij.struts2.diagram.provider.StrutsDiagramProvider;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class StrutsDiagramDataModelMappingTest extends
BasicLightHighlightingTestCase {
+
+ @Override
+ @NotNull
+ protected String getTestDataLocation() {
+ return "diagram";
+ }
+
+ public void testRefreshMapsSnapshotNodesAndEdges() {
+ createStrutsFileSet("struts-diagram.xml");
+ VirtualFile vf = myFixture.findFileInTempDir("struts-diagram.xml");
+ assertNotNull(vf);
+ XmlFile xml = (XmlFile)
PsiManager.getInstance(getProject()).findFile(vf);
+ assertNotNull(xml);
+
+ StrutsConfigDiagramModel snapshot = ReadAction.nonBlocking(
+ () ->
StrutsConfigDiagramModel.build(xml)).executeSynchronously();
+ assertNotNull(snapshot);
+ assertFalse(snapshot.getNodes().isEmpty());
+
+ StrutsDiagramProvider provider =
+ (StrutsDiagramProvider)
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+ assertNotNull(provider);
+ StrutsDiagramDataModel dataModel =
+ new StrutsDiagramDataModel(getProject(), provider,
StrutsDiagramItem.forFile(xml));
+ try {
+ ReadAction.run(dataModel::refreshDataModel);
+
+ assertEquals(snapshot.getNodes().size(),
dataModel.getNodes().size());
+ assertEquals(snapshot.getEdges().size(),
dataModel.getEdges().size());
+
+ Set<String> snapshotIds = snapshot.getNodes().stream()
+ .map(StrutsDiagramNode::getId).collect(Collectors.toSet());
+ Set<String> apiIds = dataModel.getNodes().stream()
+ .map(DiagramNode::getIdentifyingElement)
+ .map(StrutsDiagramItem::getSnapshotNode)
+ .map(StrutsDiagramNode::getId)
+ .collect(Collectors.toSet());
+ assertEquals(snapshotIds, apiIds);
+
+ for (DiagramEdge<StrutsDiagramItem> edge : dataModel.getEdges()) {
+
assertNotNull(edge.getSource().getIdentifyingElement().getSnapshotNode());
+
assertNotNull(edge.getTarget().getIdentifyingElement().getSnapshotNode());
+ }
+ } finally {
+ dataModel.dispose();
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+```bash
+./gradlew test -x rat --tests
"com.intellij.struts2.diagram.StrutsDiagramDataModelMappingTest"
+```
+
+Expected: FAIL (empty nodes from stub refresh).
+
+- [ ] **Step 3: Implement API node/edge + full `refreshDataModel`**
+
+`StrutsDiagramApiNode.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramNodeBase;
+import com.intellij.diagram.DiagramProvider;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.presentation.StrutsDiagramPresentation;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public final class StrutsDiagramApiNode extends
DiagramNodeBase<StrutsDiagramItem> {
+
+ private final @NotNull StrutsDiagramItem item;
+
+ public StrutsDiagramApiNode(@NotNull DiagramProvider<StrutsDiagramItem>
provider,
+ @NotNull StrutsDiagramItem item) {
+ super(provider);
+ this.item = item;
+ }
+
+ @Override
+ public @NotNull StrutsDiagramItem getIdentifyingElement() {
+ return item;
+ }
+
+ @Override
+ public @Nullable String getTooltip() {
+ StrutsDiagramNode node = item.getSnapshotNode();
+ return node != null ? node.getTooltipHtml() : null;
+ }
+
+ @Override
+ public @Nullable Icon getIcon() {
+ StrutsDiagramNode node = item.getSnapshotNode();
+ return node != null ? node.getIcon() : null;
+ }
+
+ @Override
+ public void navigate(boolean requestFocus) {
+ StrutsDiagramNode node = item.getSnapshotNode();
+ if (node != null) {
+ StrutsDiagramPresentation.navigateToElement(node);
+ }
+ }
+
+ @Override
+ public boolean canNavigate() {
+ StrutsDiagramNode node = item.getSnapshotNode();
+ return node != null && node.getNavigationPointer() != null;
+ }
+}
+```
+
+`StrutsDiagramApiEdge.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramEdgeBase;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.diagram.DiagramRelationshipInfo;
+import com.intellij.diagram.DiagramRelationshipInfoAdapter;
+import com.intellij.diagram.DiagramRelationships;
+import com.intellij.diagram.presentation.DiagramLineType;
+import com.intellij.struts2.diagram.model.StrutsDiagramEdge;
+import org.jetbrains.annotations.NotNull;
+
+public final class StrutsDiagramApiEdge extends
DiagramEdgeBase<StrutsDiagramItem> {
+
+ public StrutsDiagramApiEdge(@NotNull DiagramNode<StrutsDiagramItem> source,
+ @NotNull DiagramNode<StrutsDiagramItem> target,
+ @NotNull StrutsDiagramEdge snapshotEdge) {
+ super(source, target, relationshipFor(snapshotEdge));
+ }
+
+ private static @NotNull DiagramRelationshipInfo relationshipFor(@NotNull
StrutsDiagramEdge edge) {
+ String label = edge.getLabel();
+ if (label == null || label.isEmpty()) {
+ return DiagramRelationships.DEPENDENCY;
+ }
+ return new DiagramRelationshipInfoAdapter(label,
DiagramLineType.SOLID, label);
+ }
+}
+```
+
+If `DiagramRelationshipInfoAdapter` / `DiagramLineType` constructors differ on
262, adjust to the closest public constructor that accepts a center/name label;
unlabeled edges must still use `DiagramRelationships.DEPENDENCY`.
+
+Replace `refreshDataModel` / collections in `StrutsDiagramDataModel`:
+
+```java
+private final List<DiagramNode<StrutsDiagramItem>> nodes = new ArrayList<>();
+private final List<DiagramEdge<StrutsDiagramItem>> edges = new ArrayList<>();
+
+@Override
+public void refreshDataModel() {
+ nodes.clear();
+ edges.clear();
+ StrutsDiagramItem seed = getOriginalElement();
+ XmlFile xmlFile = seed != null ? seed.getXmlFile() : null;
+ if (xmlFile == null) {
+ return;
+ }
+ StrutsConfigDiagramModel snapshot =
StrutsConfigDiagramModel.build(xmlFile);
+ if (snapshot == null) {
+ return;
+ }
+ Map<StrutsDiagramNode, DiagramNode<StrutsDiagramItem>> map = new
IdentityHashMap<>();
+ for (StrutsDiagramNode snapshotNode : snapshot.getNodes()) {
+ StrutsDiagramItem item = StrutsDiagramItem.forNode(xmlFile,
snapshotNode);
+ StrutsDiagramApiNode apiNode = new StrutsDiagramApiNode(getProvider(),
item);
+ nodes.add(apiNode);
+ map.put(snapshotNode, apiNode);
+ }
+ for (StrutsDiagramEdge snapshotEdge : snapshot.getEdges()) {
+ DiagramNode<StrutsDiagramItem> source =
map.get(snapshotEdge.getSource());
+ DiagramNode<StrutsDiagramItem> target =
map.get(snapshotEdge.getTarget());
+ if (source != null && target != null) {
+ edges.add(new StrutsDiagramApiEdge(source, target, snapshotEdge));
+ }
+ }
+}
+```
+
+Return `nodes` / `edges` from `getNodes()` / `getEdges()`. Document that
`refreshDataModel()` must run under a read action (callers: initial Show
Diagram path and Dom refresh bridge).
+
+- [ ] **Step 4: Run mapping test**
+
+```bash
+./gradlew test -x rat --tests
"com.intellij.struts2.diagram.StrutsDiagramDataModelMappingTest"
+```
+
+Expected: PASS. Also run model tests:
+
+```bash
+./gradlew test -x rat --tests
"com.intellij.struts2.diagram.StrutsConfigDiagramModelTest"
+```
+
+Expected: PASS (unchanged semantics).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/main/java/com/intellij/struts2/diagram/provider \
+
src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java
+git commit -m "$(cat <<'EOF'
+feat(diagram): map Struts snapshot model into Diagrams API nodes
+
+EOF
+)"
+```
+
+---
+
+### Task 5: DomEvent live refresh while data model is alive
+
+**Files:**
+- Modify:
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java`
+- Test: extend `StrutsDiagramDomRefreshTest` or mapping test with a small unit
assertion that the listener uses `StrutsDiagramDomRefresh` (full DomEvent UI
pump optional)
+
+**Interfaces:**
+- Consumes: `DomManager.addDomEventListener`, `Alarm` 300 ms,
`StrutsDiagramDomRefresh`, `ReadAction.nonBlocking`
+- Produces: While model not disposed, same-file DomEvents debounce then
rebuild snapshot on background read + apply on EDT via Diagrams refresh
(`refreshDataModel` + builder update if required by 262)
+
+- [ ] **Step 1: Implement listener in data model constructor**
+
+Fields:
+
+```java
+private static final int DOM_UPDATE_DELAY_MS = 300;
+private final Alarm updateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD,
this);
+private final VirtualFile virtualFile; // from seed XmlFile; may be null
+```
+
+In constructor, after `setOriginalElement`:
+
+```java
+XmlFile xml = seed != null ? seed.getXmlFile() : null;
+this.virtualFile = xml != null ? xml.getVirtualFile() : null;
+if (virtualFile != null) {
+ DomManager.getDomManager(project).addDomEventListener(event -> {
+ if (!StrutsDiagramDomRefresh.isEventForMyFile(event, virtualFile)) {
+ return;
+ }
+ queueDebouncedRefresh();
+ }, this);
+}
+```
+
+```java
+private void queueDebouncedRefresh() {
+ updateAlarm.cancelAllRequests();
+ updateAlarm.addRequest(this::scheduleRefresh, DOM_UPDATE_DELAY_MS);
+}
+
+private void scheduleRefresh() {
+ ReadAction.nonBlocking(() -> {
+ refreshDataModel();
+ return null;
+ })
+ .expireWith(this)
+ .coalesceBy(this)
+ .finishOnUiThread(ModalityState.defaultModalityState(), ignored ->
{
+ // If 262 requires an explicit diagram builder update after
mutating
+ // getNodes()/getEdges(), call
DiagramDataModel.refreshDataModelInSmartMode(getBuilder())
+ // or getBuilder().queryUpdate()... per SDK — only when
getBuilder() != null.
+ })
+ .submit(AppExecutorUtil.getAppExecutorService());
+}
+```
+
+**Threading note:** Prefer building the snapshot inside
`ReadAction.nonBlocking` and assigning node lists on the EDT. If
`refreshDataModel()` both reads PSI and mutates lists, split into
`buildSnapshot()` (read) + `applySnapshot()` (EDT) to match the old editor’s
pattern and avoid PSI from EDT.
+
+Recommended split:
+
+```java
+private @Nullable StrutsConfigDiagramModel buildSnapshot() { /*
StrutsConfigDiagramModel.build */ }
+private void applySnapshot(@Nullable StrutsConfigDiagramModel snapshot) { /*
clear/fill nodes+edges */ }
+
+public void refreshDataModel() {
+ // Called under read action by platform OR by scheduleRefresh apply path:
+ applySnapshot(buildSnapshot());
+}
+```
+
+Use the split that compiles cleanly and keeps PSI off the EDT for
Dom-triggered updates.
+
+- [ ] **Step 2: Smoke-test Dom helpers still pass; run diagram suite**
+
+```bash
+./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"
+```
+
+Expected: all current diagram tests PASS (old editor tests still present until
Task 6).
+
+- [ ] **Step 3: Manual checkpoint (optional but recommended)**
+
+```bash
+./gradlew runIde
+```
+
+Open a Struts config → Show Diagram → edit XML → diagram updates after ~300
ms. If refresh does not repaint, fix the builder update call from Step 1 before
committing.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add
src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java
+git commit -m "$(cat <<'EOF'
+feat(diagram): refresh Show Diagram model on same-file DomEvents
+
+EOF
+)"
+```
+
+---
+
+### Task 6: Remove Swing Diagram tab host
+
+**Files:**
+- Delete:
`src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java`
+- Delete:
`src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java`
+- Delete:
`src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditorProvider.java`
+- Delete:
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java`
+- Delete:
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorDomFilterTest.java`
+- Modify: `src/main/resources/META-INF/plugin.xml` — remove
`fileEditorProvider` line
+- Modify:
`src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java`
— remove any imports/assertions against `Struts2DiagramComponent` if present
after #118
+
+**Interfaces:**
+- Consumes: provider path from Tasks 3–5
+- Produces: no Diagram editor tab; Show Diagram is the only host
+
+- [ ] **Step 1: Remove `fileEditorProvider` registration**
+
+Delete this line from `plugin.xml`:
+
+```xml
+<fileEditorProvider
implementation="com.intellij.struts2.diagram.fileEditor.Struts2DiagramFileEditorProvider"/>
+```
+
+- [ ] **Step 2: Delete UI/fileEditor sources and obsolete tests**
+
+Delete the five files listed above. Grep for leftover references:
+
+```bash
+rg -n
"Struts2DiagramComponent|Struts2DiagramFileEditor|diagram\\.ui|diagram\\.fileEditor"
src || true
+```
+
+Expected: no matches (except historical docs/specs).
+
+- [ ] **Step 3: Fix `StrutsConfigDiagramModelTest` if it still touches Swing**
+
+Remove placeholder-size / component tests that import
`Struts2DiagramComponent`. Keep pure model assertions.
+
+- [ ] **Step 4: Run full diagram test suite**
+
+```bash
+./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"
+```
+
+Expected: PASS. No references to deleted classes.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A src/main/java/com/intellij/struts2/diagram \
+ src/test/java/com/intellij/struts2/diagram \
+ src/main/resources/META-INF/plugin.xml
+git commit -m "$(cat <<'EOF'
+refactor(diagram): remove Swing Diagram tab in favor of Show Diagram
+
+EOF
+)"
+```
+
+---
+
+### Task 7: Docs, package-info, changelog, final gate
+
+**Files:**
+- Modify: `src/main/java/com/intellij/struts2/diagram/model/package-info.java`
+- Modify: `CHANGELOG.md`
+- Modify (optional clarity):
`docs/superpowers/specs/2026-07-25-diagrams-api-migration-design.md` status
line only if needed
+
+**Interfaces:**
+- Produces: accurate migration boundary docs + user-facing changelog
+
+- [ ] **Step 1: Update `package-info.java`**
+
+Replace the “future migration” wording so it states the Diagrams API host
lives in `com.intellij.struts2.diagram.provider`, model/presentation remain
toolkit-neutral, and the Swing/`PerspectiveFileEditor` host has been removed.
+
+- [ ] **Step 2: Update `CHANGELOG.md` under `[Unreleased]`**
+
+Add under **Changed** (and **Removed** as appropriate):
+
+```markdown
+### Changed
+
+- Migrate struts.xml diagram visualization from the custom Diagram editor tab
to the IntelliJ **Show Diagram** action (`com.intellij.diagram`)
([#117](https://github.com/apache/struts-intellij-plugin/issues/117))
+- Dependencies - hard-depend on `com.intellij.diagram` (Ultimate Diagrams)
+
+### Removed
+
+- Remove custom Swing Diagram editor tab (`Struts2DiagramFileEditor` /
`Struts2DiagramComponent`); use **Show Diagram** on a Struts 2 config file
instead
+```
+
+Adjust section placement to match existing Unreleased structure.
+
+- [ ] **Step 3: Final automated gate**
+
+```bash
+./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"
+```
+
+Expected: BUILD SUCCESSFUL, all diagram tests green.
+
+- [ ] **Step 4: Manual verification checklist** (`./gradlew runIde`)
+
+1. Open a Struts 2 config **not** in a file set → Show Diagram available →
packages/actions/results render
+2. Chain/redirect action→action edges still appear (`struts-redirect-*.xml`
fixtures)
+3. Edit XML while diagram open → debounced refresh
+4. Double-click node → navigates to XML
+5. Hover → tooltip HTML
+6. Confirm **Diagram** editor tab is gone
+7. Zoom/pan via platform chrome works at smoke level
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/main/java/com/intellij/struts2/diagram/model/package-info.java
CHANGELOG.md
+git commit -m "$(cat <<'EOF'
+docs: changelog and package-info for Diagrams API migration
+
+EOF
+)"
+```
+
+---
+
+## Spec coverage (self-review)
+
+| Spec requirement | Task |
+|---|---|
+| Show Diagram entry (no tab) | 3, 6 |
+| Hard `com.intellij.diagram` dependency | 1 |
+| Any Struts config (not only file set) | 3 |
+| Keep model + presentation | 4 (consume only) |
+| Tooltips + navigate | 4 (`StrutsDiagramApiNode`) |
+| Dom debounce refresh while model alive | 5 |
+| Remove Swing / PerspectiveFileEditor | 6 |
+| Changelog | 7 |
+| Automated diagram tests + manual checklist | 3–7 |
+| Out of scope #96–#100 | Not scheduled |
+
+## Placeholder / consistency notes
+
+- Exact 262 method signatures for `AbstractDiagramElementManager` /
relationship adapters may need compile-driven tweaks; behavior in this plan is
normative.
+- Provider id is fixed: `ApacheStrutsConfig`.
+- Dom helpers live only in `StrutsDiagramDomRefresh` after Task 6.
diff --git a/docs/superpowers/specs/2026-07-25-diagrams-api-migration-design.md
b/docs/superpowers/specs/2026-07-25-diagrams-api-migration-design.md
new file mode 100644
index 0000000..e4460ea
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-25-diagrams-api-migration-design.md
@@ -0,0 +1,182 @@
+# Migrate Struts Diagram to IntelliJ Diagrams API
+
+**Date:** 2026-07-25
+**Status:** Approved for implementation planning
+**Related:**
[#117](https://github.com/apache/struts-intellij-plugin/issues/117); prior
blank-tab host fix
[#118](https://github.com/apache/struts-intellij-plugin/pull/118) /
`2026-07-25-diagram-blank-262-design.md`; auto-refresh
`2026-06-25-diagram-auto-refresh-design.md`; model boundary in
`com.intellij.struts2.diagram.model` package-info
+
+## Problem
+
+The struts.xml **Diagram** tab is a custom Swing panel
(`Struts2DiagramComponent`) hosted by `PerspectiveFileEditor`. That host owns
layout, zoom, and lifecycle, which has already caused platform-version
regressions (blank tab on 262). The model layer (`StrutsConfigDiagramModel`,
nodes/edges) and presentation helpers were intentionally kept toolkit-neutral
so rendering can move to the platform Diagrams API.
+
+Related UX follow-ups (#96 multi-file, #98 selection sync, #99 Structure tool
window, #100 zoom/pan) would largely come for free—or be simpler—on the IDE
diagram chrome, but are **not** in scope for this migration.
+
+## Goals
+
+1. Provide a Struts config diagram via the IntelliJ **Show Diagram** entry
(Diagrams API), not a custom editor tab.
+2. Packages, actions, results, and edges match current model semantics
(including chain/redirect action→action edges).
+3. Tooltips and double-click / navigate-to-XML still work via
`StrutsDiagramPresentation`.
+4. Auto-refresh on DOM changes (or equivalent Diagrams refresh) while the
diagram is open/visible.
+5. Plugin declares a **hard** dependency on `com.intellij.diagram`; builds and
tests against IU.
+6. Custom Swing diagram UI and `PerspectiveFileEditor` path are removed.
+7. Changelog documents the migration and UX change (tab removed → Show
Diagram).
+
+## Non-Goals
+
+- Fixing blank Diagram tab lifecycle of the current Swing host (already
tracked/fixed separately).
+- Rewriting model building or Struts DOM converters.
+- Merged multi-file view (#96), selection sync (#98), Structure tool window
(#99), or custom zoom/pan beyond platform chrome (#100).
+- Optional dependency / Community Edition fallback / keeping a Swing tab as
backup.
+- Robot / UI e2e tests or layout pixel assertions.
+- Experimental Diagrams “v2” Builder API.
+
+## Decisions
+
+| Question | Decision |
+|---|---|
+| Entry point | **Show Diagram only** — remove the Diagram editor tab |
+| `com.intellij.diagram` dependency | **Hard** (plugin already targets IU
features such as JavaEE/JSP) |
+| When Show Diagram is available | Any file recognized as a Struts 2 config
(`StrutsManager.isStruts2ConfigFile`), **not** limited to file-set membership |
+| Scope | Pure host swap; #96–#100 remain separate issues |
+| Implementation approach | Classic `DiagramProvider` + snapshot
`DiagramDataModel` consuming `StrutsConfigDiagramModel.build()` |
+
+### Alternatives considered
+
+| Approach | Verdict |
+|---|---|
+| Keep Diagram editor tab backed by Diagrams API | Rejected — retains custom
editor lifecycle |
+| Show Diagram only (this design) | **Chosen** — matches Ultimate diagram UX;
escapes `PerspectiveFileEditor` |
+| Both tab + Show Diagram | Rejected for v1 — two hosts to maintain |
+| Optional `com.intellij.diagram` + Swing fallback | Rejected — unnecessary
given IU-only surface |
+| Experimental Diagrams v2 Builder API | Rejected — stability risk for Apache
plugin on 262 |
+
+## Architecture
+
+```
+struts.xml (Psi/XmlFile)
+ │
+ ▼
+StrutsConfigDiagramModel.build() ← unchanged (toolkit-neutral snapshot)
+ │
+ ▼
+StrutsDiagramDataModel ← NEW: DiagramDataModel adapter
+ maps StrutsDiagramNode/Edge
+ → Diagrams API nodes/edges
+ │
+ ▼
+StrutsDiagramProvider ← NEW: DiagramProvider + EP
+ Show Diagram action / dedicated diagram editor
+ │
+ ├── tooltips / navigate → StrutsDiagramPresentation (unchanged)
+ └── live refresh → DomEvent (while diagram open) → rebuild
snapshot → refreshDataModel
+```
+
+**Kept:** `diagram.model`, `diagram.presentation`
+**Removed:** `diagram.ui`, `diagram.fileEditor`,
`PerspectiveFileEditorProvider` registration
+**Added:** adapter package (e.g. `diagram.provider`) + hard
`com.intellij.diagram` dependency
+**Semantics:** Still file-local packages/actions/results + chain/redirect
action→action edges — no multi-file merge
+
+## Components
+
+| Unit | Role |
+|---|---|
+| `StrutsConfigDiagramModel` / `StrutsDiagramNode` / `StrutsDiagramEdge` |
Unchanged snapshot builder and graph types |
+| `StrutsDiagramPresentation` | Unchanged tooltips + navigate-to-XML |
+| `StrutsDiagramProvider` | `DiagramProvider` for Struts config; accept ≈
`isStruts2ConfigFile`; wires element manager, VFS resolver, data model factory |
+| `StrutsDiagramDataModel` | Holds current snapshot; `refreshDataModel()`
rebuilds via `StrutsConfigDiagramModel.build(xmlFile)` and republishes
nodes/edges |
+| Node/edge adapters | Thin wrappers: id, kind, name, icon, tooltip from
snapshot; double-click → `navigateToElement` |
+| Dom refresh bridge | While the data model for file F is alive, DomEvents for
F debounce (~300 ms) and call `refreshDataModel()` |
+| `plugin.xml` / Gradle | Hard depend on `com.intellij.diagram`; register
`com.intellij.diagram.Provider`; remove `Struts2DiagramFileEditorProvider` |
+| Deleted | `Struts2DiagramComponent`, `Struts2DiagramFileEditor`,
`Struts2DiagramFileEditorProvider` |
+
+No new public plugin extension points. Follow-ups #96–#100 stay out of this
package set.
+
+Exact Diagrams API type names (`DiagramProvider` vs historical `Provider`,
element manager interfaces, etc.) are resolved against the 262 IU SDK during
implementation; the responsibilities above are fixed.
+
+## Data flow
+
+### Open
+
+1. User invokes Show Diagram on a Struts 2 config `XmlFile`.
+2. `StrutsDiagramProvider` accepts the element.
+3. Platform creates `StrutsDiagramDataModel` for that file.
+4. Initial `refreshDataModel()` runs under a read action →
`StrutsConfigDiagramModel.build(xmlFile)` → adapters publish nodes/edges.
+5. Diagrams UI renders with platform layout/chrome (zoom/pan/toolbar).
+
+### Interact
+
+- Hover → precomputed tooltip HTML from the snapshot (via presentation
helpers).
+- Double-click / navigate → `StrutsDiagramPresentation.navigateToElement`.
+
+### Live refresh
+
+Replaces tab `selectNotify` / `deselectNotify` gating from the auto-refresh
design:
+
+- While the Struts diagram data model for file F is alive (diagram
editor/popup still open): DomEvents for F debounce 300 ms → rebuild snapshot →
`refreshDataModel()`.
+- No Diagram tab → no `selectNotify` catch-up; an already-open diagram
refreshes on DomEvents; after dispose, nothing until Show Diagram again.
+- Other files’ DomEvents ignored; multi-file set merge remains out of scope.
+
+### Close / dispose
+
+- Dom listener and alarms dispose with the diagram/data-model lifecycle so
closed diagrams do not refresh.
+
+## Error handling & edge cases
+
+| Scenario | Behavior |
+|---|---|
+| File not a Struts 2 config | Provider rejects; Show Diagram not offered |
+| `build()` returns `null` | Empty diagram + platform empty state (or provider
message); no crash |
+| Model with no nodes | Empty diagram; same intent as today’s empty
placeholder |
+| Unresolved / external chain-redirect targets | Keep current model semantics
(labeled RESULT fallback) |
+| DomEvent after diagram closed | No-op — listener disposed / gated on open
model |
+| Rapid edits while diagram open | 300 ms debounce; coalesce rebuilds |
+| Invalid / deleted file while diagram open | Refresh yields
empty/unavailable; no PSI access on EDT outside read actions |
+| Navigation pointer stale | `navigateToElement` no-ops safely (existing
behavior) |
+| Exceptions during build | Caught/logged by read-action path; diagram stays
on last good snapshot or empty |
+
+## Testing
+
+### Automated
+
+- Keep `StrutsConfigDiagramModelTest` as the semantic source of truth
(nodes/edges, chain/redirect).
+- Replace editor-provider / `PerspectiveFileEditor` lifecycle tests with
provider-focused tests:
+ - Accept: Struts config yes; non-Struts XML no; **file-set membership not
required**
+ - Adapter mapping: snapshot nodes/edges → Diagrams nodes/edges (ids, kinds,
edge labels) without full UI paint
+ - Dom-refresh helper: same-file vs other-file filtering (pure static
helpers, as today)
+
+### Out of scope for v1
+
+- Robot / UI e2e for Show Diagram chrome
+- Layout pixel assertions
+- Tests for #96–#100
+
+### Manual (`./gradlew runIde` on IU)
+
+1. Show Diagram on a Struts config → packages/actions/results + chain/redirect
edges
+2. Edit XML while diagram open → debounced refresh
+3. Double-click node → navigates to XML
+4. Hover → tooltips
+5. Confirm Diagram **tab** is gone
+6. Zoom/pan via platform chrome works at a smoke level
+
+### Gate
+
+`./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"` green;
`CHANGELOG.md` notes migration, hard `com.intellij.diagram` dependency, and
removal of the Diagram tab.
+
+## UX / changelog note
+
+This is a **breaking UX change** for users who relied on the Diagram editor
tab: the tab is removed; diagrams are opened via the IDE **Show Diagram**
action on a Struts 2 config file. Document that clearly in `CHANGELOG.md`.
+
+## Future work (out of scope)
+
+- [#96](https://github.com/apache/struts-intellij-plugin/issues/96) merged
multi-file view across Struts file set
+- [#98](https://github.com/apache/struts-intellij-plugin/issues/98) sync
selection with XML perspective
+- [#99](https://github.com/apache/struts-intellij-plugin/issues/99) Structure
tool window integration
+- [#100](https://github.com/apache/struts-intellij-plugin/issues/100)
zoom/pan/fit beyond what platform chrome already provides
+
+## References
+
+- Issue [#117](https://github.com/apache/struts-intellij-plugin/issues/117)
+- `com.intellij.struts2.diagram.model` package-info — migration boundary
+- `docs/superpowers/specs/2026-07-25-diagram-blank-262-design.md` — Swing host
fix; migration deferred here
+- `docs/superpowers/specs/2026-06-25-diagram-auto-refresh-design.md` —
DomEvent debounce semantics (adapted to open diagram, not tab selection)
+- Platform Explorer / OSS examples of `com.intellij.diagram.Provider` (e.g.
JHipster) — learn-by-examples for undocumented Ultimate API
diff --git a/src/main/java/com/intellij/struts2/diagram/model/package-info.java
b/src/main/java/com/intellij/struts2/diagram/model/package-info.java
index 635f83c..2e32711 100644
--- a/src/main/java/com/intellij/struts2/diagram/model/package-info.java
+++ b/src/main/java/com/intellij/struts2/diagram/model/package-info.java
@@ -18,20 +18,16 @@
/**
* Toolkit-neutral Struts configuration diagram model.
*
- * <h2>Migration boundary</h2>
* <p>The types in this package ({@link
com.intellij.struts2.diagram.model.StrutsConfigDiagramModel},
* {@link com.intellij.struts2.diagram.model.StrutsDiagramNode},
- * {@link com.intellij.struts2.diagram.model.StrutsDiagramEdge}) are
intentionally independent of
- * the {@code com.intellij.diagram.Provider} (Diagrams API) that may be
adopted in the future.</p>
- * <p>This isolation means that the rendering/editor layer (currently a
lightweight Swing panel in
- * {@code com.intellij.struts2.diagram.ui}) can be replaced without touching
the DOM traversal or
- * presentation logic. A future migration to {@code
com.intellij.diagram.Provider} should:</p>
- * <ol>
- * <li>Implement {@code DiagramProvider} / {@code DiagramDataModel}
consuming the snapshot produced
- * by {@link
com.intellij.struts2.diagram.model.StrutsConfigDiagramModel#build}.</li>
- * <li>Reuse {@link
com.intellij.struts2.diagram.presentation.StrutsDiagramPresentation} for
- * tooltips and navigation.</li>
- * <li>Replace only the {@code diagram.ui} and {@code diagram.fileEditor}
packages.</li>
- * </ol>
+ * {@link com.intellij.struts2.diagram.model.StrutsDiagramEdge}) and the
presentation helpers in
+ * {@code com.intellij.struts2.diagram.presentation} are intentionally
independent of any UI toolkit.</p>
+ *
+ * <p>Two hosts currently consume this snapshot for comparison:</p>
+ * <ul>
+ * <li>Custom Swing Diagram editor tab ({@code
com.intellij.struts2.diagram.ui} /
+ * {@code com.intellij.struts2.diagram.fileEditor})</li>
+ * <li>IntelliJ Diagrams API Show Diagram ({@code
com.intellij.struts2.diagram.provider})</li>
+ * </ul>
*/
package com.intellij.struts2.diagram.model;
diff --git
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiEdge.java
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiEdge.java
new file mode 100644
index 0000000..4a905ce
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiEdge.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramEdgeBase;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.diagram.DiagramRelationshipInfo;
+import com.intellij.diagram.DiagramRelationshipInfoAdapter;
+import com.intellij.diagram.DiagramRelationships;
+import com.intellij.diagram.presentation.DiagramLineType;
+import com.intellij.struts2.diagram.model.StrutsDiagramEdge;
+import org.jetbrains.annotations.NotNull;
+
+public final class StrutsDiagramApiEdge extends
DiagramEdgeBase<StrutsDiagramItem> {
+
+ public StrutsDiagramApiEdge(@NotNull DiagramNode<StrutsDiagramItem> source,
+ @NotNull DiagramNode<StrutsDiagramItem> target,
+ @NotNull StrutsDiagramEdge snapshotEdge) {
+ super(source, target, relationshipFor(snapshotEdge));
+ }
+
+ private static @NotNull DiagramRelationshipInfo relationshipFor(@NotNull
StrutsDiagramEdge edge) {
+ String label = edge.getLabel();
+ if (label.isEmpty()) {
+ return DiagramRelationships.DEPENDENCY;
+ }
+ return new DiagramRelationshipInfoAdapter(label,
DiagramLineType.SOLID, label);
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiNode.java
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiNode.java
new file mode 100644
index 0000000..ebdb04f
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiNode.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramNodeBase;
+import com.intellij.diagram.DiagramProvider;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.presentation.StrutsDiagramPresentation;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public final class StrutsDiagramApiNode extends
DiagramNodeBase<StrutsDiagramItem> {
+
+ private final @NotNull StrutsDiagramItem item;
+
+ public StrutsDiagramApiNode(@NotNull DiagramProvider<StrutsDiagramItem>
provider,
+ @NotNull StrutsDiagramItem item) {
+ super(provider);
+ this.item = item;
+ }
+
+ @Override
+ public @NotNull StrutsDiagramItem getIdentifyingElement() {
+ return item;
+ }
+
+ @Override
+ public @Nullable String getTooltip() {
+ StrutsDiagramNode node = item.getSnapshotNode();
+ return node != null ? node.getTooltipHtml() : null;
+ }
+
+ @Override
+ public @Nullable Icon getIcon() {
+ StrutsDiagramNode node = item.getSnapshotNode();
+ return node != null ? node.getIcon() : null;
+ }
+
+ @Override
+ public void navigate(boolean requestFocus) {
+ StrutsDiagramNode node = item.getSnapshotNode();
+ if (node != null) {
+ StrutsDiagramPresentation.navigateToElement(node);
+ }
+ }
+
+ @Override
+ public boolean canNavigate() {
+ StrutsDiagramNode node = item.getSnapshotNode();
+ return node != null && node.getNavigationPointer() != null;
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java
new file mode 100644
index 0000000..ea10c83
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java
@@ -0,0 +1,173 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramBuilder;
+import com.intellij.diagram.DiagramDataKeys;
+import com.intellij.diagram.DiagramDataModel;
+import com.intellij.diagram.DiagramEdge;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.openapi.application.ModalityState;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramEdge;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.util.Alarm;
+import com.intellij.util.concurrency.AppExecutorUtil;
+import com.intellij.util.xml.DomManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+public final class StrutsDiagramDataModel extends
DiagramDataModel<StrutsDiagramItem> {
+
+ private static final int DOM_UPDATE_DELAY_MS = 300;
+
+ private final List<DiagramNode<StrutsDiagramItem>> nodes = new
ArrayList<>();
+ private final List<DiagramEdge<StrutsDiagramItem>> edges = new
ArrayList<>();
+ private final Alarm updateAlarm = new
Alarm(Alarm.ThreadToUse.SWING_THREAD, this);
+ private final @Nullable XmlFile xmlFile;
+ private final @Nullable VirtualFile virtualFile;
+
+ public StrutsDiagramDataModel(@NotNull Project project,
+ @NotNull StrutsDiagramProvider provider,
+ @Nullable StrutsDiagramItem seed) {
+ super(project, provider);
+ xmlFile = seed != null ? seed.getXmlFile() : null;
+ virtualFile = xmlFile != null ? xmlFile.getVirtualFile() : null;
+ if (seed != null) {
+ setOriginalElement(seed);
+ }
+ if (virtualFile != null) {
+ DomManager.getDomManager(project).addDomEventListener(event -> {
+ if (StrutsDiagramDomRefresh.isEventForMyFile(event,
virtualFile)) {
+ queueDebouncedRefresh();
+ }
+ }, this);
+ }
+ }
+
+ @Override
+ public @NotNull ModificationTracker getModificationTracker() {
+ return PsiManager.getInstance(getProject()).getModificationTracker();
+ }
+
+ @Override
+ public @NotNull Collection<? extends DiagramNode<StrutsDiagramItem>>
getNodes() {
+ return Collections.unmodifiableList(nodes);
+ }
+
+ @Override
+ public @NotNull Collection<? extends DiagramEdge<StrutsDiagramItem>>
getEdges() {
+ return Collections.unmodifiableList(edges);
+ }
+
+ @Override
+ public @NotNull String getNodeName(@NotNull DiagramNode<StrutsDiagramItem>
node) {
+ StrutsDiagramItem item = node.getIdentifyingElement();
+ String title = getProvider().getElementManager().getElementTitle(item);
+ return title != null ? title : "";
+ }
+
+ @Override
+ public @Nullable DiagramNode<StrutsDiagramItem> addElement(@Nullable
StrutsDiagramItem element) {
+ return null;
+ }
+
+ /**
+ * Rebuilds the API model from the current Struts configuration snapshot.
+ * Must be invoked under a read action because model construction accesses
PSI/DOM.
+ */
+ @Override
+ public void refreshDataModel() {
+ applyApiModel(buildApiModel());
+ }
+
+ private @NotNull ApiModel buildApiModel() {
+ StrutsConfigDiagramModel snapshot =
+ xmlFile != null ? StrutsConfigDiagramModel.build(xmlFile) :
null;
+ if (xmlFile == null || snapshot == null) {
+ return ApiModel.EMPTY;
+ }
+ List<DiagramNode<StrutsDiagramItem>> newNodes = new ArrayList<>();
+ List<DiagramEdge<StrutsDiagramItem>> newEdges = new ArrayList<>();
+ Map<StrutsDiagramNode, DiagramNode<StrutsDiagramItem>> map = new
IdentityHashMap<>();
+ for (StrutsDiagramNode snapshotNode : snapshot.getNodes()) {
+ StrutsDiagramItem item = StrutsDiagramItem.forNode(xmlFile,
snapshotNode);
+ StrutsDiagramApiNode apiNode = new
StrutsDiagramApiNode(getProvider(), item);
+ newNodes.add(apiNode);
+ map.put(snapshotNode, apiNode);
+ }
+ for (StrutsDiagramEdge snapshotEdge : snapshot.getEdges()) {
+ DiagramNode<StrutsDiagramItem> source =
map.get(snapshotEdge.getSource());
+ DiagramNode<StrutsDiagramItem> target =
map.get(snapshotEdge.getTarget());
+ if (source != null && target != null) {
+ newEdges.add(new StrutsDiagramApiEdge(source, target,
snapshotEdge));
+ }
+ }
+ return new ApiModel(newNodes, newEdges);
+ }
+
+ private void applyApiModel(@NotNull ApiModel model) {
+ nodes.clear();
+ nodes.addAll(model.nodes());
+ edges.clear();
+ edges.addAll(model.edges());
+ }
+
+ private void applyLiveUpdate(@NotNull ApiModel model) {
+ applyApiModel(model);
+ DiagramBuilder builder = getUserData(DiagramDataKeys.GRAPH_BUILDER);
+ if (builder != null) {
+ DiagramDataModel.refreshDataModelInSmartMode(builder);
+ }
+ }
+
+ private void queueDebouncedRefresh() {
+ updateAlarm.cancelAllRequests();
+ updateAlarm.addRequest(this::scheduleRefresh, DOM_UPDATE_DELAY_MS);
+ }
+
+ private void scheduleRefresh() {
+ ReadAction.nonBlocking(this::buildApiModel)
+ .expireWith(this)
+ .coalesceBy(this)
+ .finishOnUiThread(ModalityState.defaultModalityState(),
this::applyLiveUpdate)
+ .submit(AppExecutorUtil.getAppExecutorService());
+ }
+
+ private record ApiModel(@NotNull List<DiagramNode<StrutsDiagramItem>>
nodes,
+ @NotNull List<DiagramEdge<StrutsDiagramItem>>
edges) {
+
+ private static final ApiModel EMPTY = new ApiModel(List.of(),
List.of());
+ }
+
+ @Override
+ public void dispose() {
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java
new file mode 100644
index 0000000..3c7dbd0
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomUtil;
+import com.intellij.util.xml.events.DomEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Pure helpers for same-file DomEvent filtering used by {@link
StrutsDiagramDataModel}.
+ */
+public final class StrutsDiagramDomRefresh {
+
+ private StrutsDiagramDomRefresh() {}
+
+ public static boolean isEventForMyFile(@NotNull DomEvent event, @NotNull
VirtualFile file) {
+ return isDomElementInFile(event.getElement(), file);
+ }
+
+ public static boolean isDomElementInFile(@Nullable DomElement element,
@NotNull VirtualFile file) {
+ if (element == null) {
+ return false;
+ }
+ VirtualFile elementFile =
DomUtil.getFile(element).getOriginalFile().getVirtualFile();
+ return file.equals(elementFile);
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramElementManager.java
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramElementManager.java
new file mode 100644
index 0000000..4763a42
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramElementManager.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.AbstractDiagramElementManager;
+import com.intellij.diagram.DiagramBuilder;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.dom.struts.model.StrutsManager;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class StrutsDiagramElementManager extends
AbstractDiagramElementManager<StrutsDiagramItem> {
+
+ @Override
+ public @Nullable StrutsDiagramItem findInDataContext(@NotNull DataContext
context) {
+ PsiFile psiFile = CommonDataKeys.PSI_FILE.getData(context);
+ if (!(psiFile instanceof XmlFile xmlFile)) {
+ return null;
+ }
+ if
(!StrutsManager.getInstance(xmlFile.getProject()).isStruts2ConfigFile(xmlFile))
{
+ return null;
+ }
+ return StrutsDiagramItem.forFile(xmlFile);
+ }
+
+ @Override
+ public boolean canBeBuiltFrom(Object element) {
+ if (element instanceof StrutsDiagramItem item) {
+ XmlFile file = item.getXmlFile();
+ return file != null
+ &&
StrutsManager.getInstance(file.getProject()).isStruts2ConfigFile(file);
+ }
+ if (element instanceof XmlFile xmlFile) {
+ return
StrutsManager.getInstance(xmlFile.getProject()).isStruts2ConfigFile(xmlFile);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isAcceptableAsNode(Object element) {
+ return element instanceof StrutsDiagramItem item && !item.isRoot();
+ }
+
+ @Override
+ public @Nullable @Nls String getElementTitle(StrutsDiagramItem item) {
+ if (item == null) return null;
+ StrutsDiagramNode node = item.getSnapshotNode();
+ if (node != null) return node.getName();
+ XmlFile file = item.getXmlFile();
+ return file != null ? file.getName() : null;
+ }
+
+ @Override
+ public @Nullable String getNodeTooltip(StrutsDiagramItem item) {
+ if (item == null || item.getSnapshotNode() == null) return null;
+ return item.getSnapshotNode().getTooltipHtml();
+ }
+
+ @Override
+ public @Nullable PsiElement getItemDocOwner(Object element, DiagramBuilder
builder) {
+ return element instanceof StrutsDiagramItem item
+ ? StrutsDiagramExtras.resolvePsiElement(item)
+ : null;
+ }
+
+ @Override
+ public Object[] getNodeItems(StrutsDiagramItem element) {
+ return EMPTY_ARRAY;
+ }
+
+ @Override
+ public boolean canCollapse(StrutsDiagramItem element) {
+ return false;
+ }
+
+ @Override
+ public boolean isContainerFor(StrutsDiagramItem parent, StrutsDiagramItem
child) {
+ return false;
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramExtras.java
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramExtras.java
new file mode 100644
index 0000000..0ee8f34
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramExtras.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramBuilder;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.diagram.DiagramPresentationModel;
+import com.intellij.diagram.extras.DiagramExtras;
+import com.intellij.diagram.extras.EditNodeHandler;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataSink;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.pom.Navigatable;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.SmartPsiElementPointer;
+import com.intellij.psi.xml.XmlElement;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.presentation.StrutsDiagramPresentation;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Diagrams host hooks for Struts config diagrams.
+ * <p>
+ * Double-click in the diagram <em>editor</em> (non-popup) goes through
+ * {@link EditNodeHandler}, not {@link DiagramNode}'s {@link Navigatable} API.
+ * Popup mode and Jump to Source use {@link #uiDataSnapshot}.
+ */
+public final class StrutsDiagramExtras extends
DiagramExtras<StrutsDiagramItem> {
+
+ private final EditNodeHandler<StrutsDiagramItem> editNodeHandler =
this::navigateNode;
+
+ @Override
+ public @NotNull EditNodeHandler<StrutsDiagramItem> getEditNodeHandler() {
+ return editNodeHandler;
+ }
+
+ @Override
+ public void uiDataSnapshot(@NotNull DataSink sink,
+ @NotNull List<DiagramNode<StrutsDiagramItem>>
nodes,
+ @NotNull DiagramBuilder builder) {
+ DiagramNode<StrutsDiagramItem> only = ContainerUtil.getOnlyItem(nodes);
+ if (only == null) {
+ return;
+ }
+ sink.lazy(CommonDataKeys.PSI_ELEMENT, () ->
resolvePsiElement(only.getIdentifyingElement()));
+ sink.lazy(CommonDataKeys.NAVIGATABLE, () -> {
+ if (only instanceof Navigatable navigatable &&
navigatable.canNavigate()) {
+ return navigatable;
+ }
+ PsiElement psi = resolvePsiElement(only.getIdentifyingElement());
+ return psi instanceof Navigatable ? (Navigatable) psi : null;
+ });
+ }
+
+ private void navigateNode(@NotNull DiagramNode<StrutsDiagramItem> node,
+ DiagramPresentationModel presentationModel) {
+ StrutsDiagramItem item = node.getIdentifyingElement();
+ if (item == null) {
+ return;
+ }
+ StrutsDiagramNode snapshotNode = item.getSnapshotNode();
+ if (snapshotNode != null) {
+ StrutsDiagramPresentation.navigateToElement(snapshotNode);
+ }
+ }
+
+ public static @Nullable PsiElement resolvePsiElement(@Nullable
StrutsDiagramItem item) {
+ if (item == null) {
+ return null;
+ }
+ StrutsDiagramNode snapshotNode = item.getSnapshotNode();
+ if (snapshotNode == null) {
+ return item.getXmlFile();
+ }
+ SmartPsiElementPointer<XmlElement> pointer =
snapshotNode.getNavigationPointer();
+ if (pointer == null) {
+ return null;
+ }
+ return ApplicationManager.getApplication().runReadAction(
+ (com.intellij.openapi.util.Computable<PsiElement>)
pointer::getElement);
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramItem.java
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramItem.java
new file mode 100644
index 0000000..0c50767
--- /dev/null
+++ b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramItem.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.SmartPointerManager;
+import com.intellij.psi.SmartPsiElementPointer;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Identifying element for Struts Diagrams API nodes.
+ * Root items ({@code snapshotNode == null}) seed Show Diagram from an XML
file.
+ */
+public final class StrutsDiagramItem {
+
+ private final @NotNull SmartPsiElementPointer<XmlFile> filePointer;
+ private final @Nullable StrutsDiagramNode snapshotNode;
+
+ private StrutsDiagramItem(@NotNull SmartPsiElementPointer<XmlFile>
filePointer,
+ @Nullable StrutsDiagramNode snapshotNode) {
+ this.filePointer = filePointer;
+ this.snapshotNode = snapshotNode;
+ }
+
+ public static @NotNull StrutsDiagramItem forFile(@NotNull XmlFile file) {
+ return new StrutsDiagramItem(
+
SmartPointerManager.getInstance(file.getProject()).createSmartPsiElementPointer(file),
+ null);
+ }
+
+ public static @NotNull StrutsDiagramItem forNode(@NotNull XmlFile file,
@NotNull StrutsDiagramNode node) {
+ return new StrutsDiagramItem(
+
SmartPointerManager.getInstance(file.getProject()).createSmartPsiElementPointer(file),
+ node);
+ }
+
+ public @Nullable XmlFile getXmlFile() {
+ return filePointer.getElement();
+ }
+
+ public @Nullable StrutsDiagramNode getSnapshotNode() {
+ return snapshotNode;
+ }
+
+ public boolean isRoot() {
+ return snapshotNode == null;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof StrutsDiagramItem that)) return false;
+ if (snapshotNode == null || that.snapshotNode == null) {
+ return snapshotNode == that.snapshotNode
+ && Objects.equals(fileUrl(), that.fileUrl());
+ }
+ return snapshotNode.equals(that.snapshotNode);
+ }
+
+ @Override
+ public int hashCode() {
+ return snapshotNode != null ? snapshotNode.hashCode() :
Objects.hash(fileUrl());
+ }
+
+ private @Nullable String fileUrl() {
+ PsiFile file = filePointer.getElement();
+ return file != null && file.getVirtualFile() != null ?
file.getVirtualFile().getUrl() : null;
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramProvider.java
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramProvider.java
new file mode 100644
index 0000000..be9334c
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramProvider.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.BaseDiagramProvider;
+import com.intellij.diagram.DiagramDataModel;
+import com.intellij.diagram.DiagramElementManager;
+import com.intellij.diagram.DiagramPresentationModel;
+import com.intellij.diagram.DiagramVfsResolver;
+import com.intellij.diagram.extras.DiagramExtras;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.struts2.Struts2Icons;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public final class StrutsDiagramProvider extends
BaseDiagramProvider<StrutsDiagramItem> {
+
+ public static final String ID = "ApacheStrutsConfig";
+
+ private final DiagramElementManager<StrutsDiagramItem> elementManager =
new StrutsDiagramElementManager();
+ private final DiagramVfsResolver<StrutsDiagramItem> vfsResolver = new
StrutsDiagramVfsResolver();
+ private final DiagramExtras<StrutsDiagramItem> extras = new
StrutsDiagramExtras();
+
+ @Override
+ public @NotNull String getID() {
+ return ID;
+ }
+
+ @Override
+ public @NotNull @Nls String getPresentableName() {
+ return "Struts Configuration";
+ }
+
+ @Override
+ public @Nullable Icon getActionIcon(boolean isPopup) {
+ return Struts2Icons.Action;
+ }
+
+ @Override
+ public @NotNull DiagramElementManager<StrutsDiagramItem>
getElementManager() {
+ return elementManager;
+ }
+
+ @Override
+ public @NotNull DiagramVfsResolver<StrutsDiagramItem> getVfsResolver() {
+ return vfsResolver;
+ }
+
+ @Override
+ public @NotNull DiagramExtras<StrutsDiagramItem> getExtras() {
+ return extras;
+ }
+
+ @Override
+ public @NotNull DiagramDataModel<StrutsDiagramItem>
createDataModel(@NotNull Project project,
+
@Nullable StrutsDiagramItem element,
+
@Nullable VirtualFile file,
+
@NotNull DiagramPresentationModel presentationModel) {
+ return new StrutsDiagramDataModel(project, this, element);
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramVfsResolver.java
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramVfsResolver.java
new file mode 100644
index 0000000..8001e33
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramVfsResolver.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramVfsResolver;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class StrutsDiagramVfsResolver implements
DiagramVfsResolver<StrutsDiagramItem> {
+
+ @Override
+ public @Nullable String getQualifiedName(StrutsDiagramItem item) {
+ if (item == null) return null;
+ XmlFile file = item.getXmlFile();
+ if (file == null || file.getVirtualFile() == null) return null;
+ if (item.getSnapshotNode() != null) {
+ return file.getVirtualFile().getUrl() + "#" +
item.getSnapshotNode().getId();
+ }
+ return file.getVirtualFile().getUrl();
+ }
+
+ @Override
+ public @Nullable StrutsDiagramItem resolveElementByFQN(@NotNull String
fqn, @NotNull Project project) {
+ int nodeSeparator = fqn.indexOf('#');
+ String url = nodeSeparator >= 0 ? fqn.substring(0, nodeSeparator) :
fqn;
+ VirtualFile vf = VirtualFileManager.getInstance().findFileByUrl(url);
+ if (vf == null) return null;
+ PsiFile psi = PsiManager.getInstance(project).findFile(vf);
+ if (!(psi instanceof XmlFile xmlFile)) return null;
+ if (nodeSeparator >= 0) {
+ String nodeId = fqn.substring(nodeSeparator + 1);
+ StrutsConfigDiagramModel model =
ApplicationManager.getApplication().isReadAccessAllowed()
+ ? StrutsConfigDiagramModel.build(xmlFile)
+ : ReadAction.compute(() ->
StrutsConfigDiagramModel.build(xmlFile));
+ if (model != null) {
+ StrutsDiagramNode node = model.getNodes().stream()
+ .filter(candidate -> candidate.getId().equals(nodeId))
+ .findFirst()
+ .orElse(null);
+ if (node != null) {
+ return StrutsDiagramItem.forNode(xmlFile, node);
+ }
+ }
+ }
+ return StrutsDiagramItem.forFile(xmlFile);
+ }
+}
diff --git a/src/main/resources/META-INF/plugin.xml
b/src/main/resources/META-INF/plugin.xml
index 27afd7d..681e2f5 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -35,6 +35,7 @@
<module name="intellij.css.backend"/>
<plugin id="intellij.structureView.plugin"/>
<plugin id="com.intellij.java-i18n"/>
+ <plugin id="com.intellij.diagram"/>
</dependencies>
<!-- optional dependencies -->
@@ -181,6 +182,7 @@
implementation="com.intellij.struts2.structure.StrutsStructureViewBuilderProvider"/>
<fileEditorProvider
implementation="com.intellij.struts2.diagram.fileEditor.Struts2DiagramFileEditorProvider"/>
+ <diagram.Provider
implementation="com.intellij.struts2.diagram.provider.StrutsDiagramProvider"/>
<struts2.resultContributor
implementation="com.intellij.struts2.dom.struts.impl.path.DispatchPathResultContributor"/>
diff --git
a/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java
b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java
new file mode 100644
index 0000000..3d63f69
--- /dev/null
+++
b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram;
+
+import com.intellij.diagram.DiagramEdge;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.diagram.DiagramProvider;
+import com.intellij.diagram.DiagramRelationshipInfo;
+import com.intellij.diagram.DiagramRelationships;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.BasicLightHighlightingTestCase;
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramEdge;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.provider.StrutsDiagramDataModel;
+import com.intellij.struts2.diagram.provider.StrutsDiagramItem;
+import com.intellij.struts2.diagram.provider.StrutsDiagramProvider;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class StrutsDiagramDataModelMappingTest extends
BasicLightHighlightingTestCase {
+
+ @Override
+ @NotNull
+ protected String getTestDataLocation() {
+ return "diagram";
+ }
+
+ public void testRefreshMapsSnapshotNodesAndEdges() {
+ createStrutsFileSet("struts-diagram.xml");
+ VirtualFile vf = myFixture.findFileInTempDir("struts-diagram.xml");
+ assertNotNull(vf);
+ XmlFile xml = (XmlFile)
PsiManager.getInstance(getProject()).findFile(vf);
+ assertNotNull(xml);
+
+ StrutsConfigDiagramModel snapshot = ReadAction.nonBlocking(
+ () ->
StrutsConfigDiagramModel.build(xml)).executeSynchronously();
+ assertNotNull(snapshot);
+ assertFalse(snapshot.getNodes().isEmpty());
+
+ DiagramProvider<?> diagramProvider =
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+ assertNotNull(diagramProvider);
+ assertInstanceOf(diagramProvider, StrutsDiagramProvider.class);
+ StrutsDiagramProvider provider = (StrutsDiagramProvider)
diagramProvider;
+ StrutsDiagramDataModel dataModel =
+ new StrutsDiagramDataModel(getProject(), provider,
StrutsDiagramItem.forFile(xml));
+ try {
+ ReadAction.run(dataModel::refreshDataModel);
+
+ assertEquals(snapshot.getNodes().size(),
dataModel.getNodes().size());
+ assertEquals(snapshot.getEdges().size(),
dataModel.getEdges().size());
+
+ Set<String> snapshotIds = snapshot.getNodes().stream()
+ .map(StrutsDiagramNode::getId).collect(Collectors.toSet());
+ Set<String> apiIds = dataModel.getNodes().stream()
+ .map(DiagramNode::getIdentifyingElement)
+ .map(StrutsDiagramItem::getSnapshotNode)
+ .map(StrutsDiagramNode::getId)
+ .collect(Collectors.toSet());
+ assertEquals(snapshotIds, apiIds);
+
+ Set<EdgeTriple> snapshotEdgeTriples = snapshot.getEdges().stream()
+ .map(StrutsDiagramDataModelMappingTest::snapshotEdgeTriple)
+ .collect(Collectors.toSet());
+ Set<EdgeTriple> apiEdgeTriples = dataModel.getEdges().stream()
+ .map(StrutsDiagramDataModelMappingTest::apiEdgeTriple)
+ .collect(Collectors.toSet());
+ assertEquals("API edges must mirror snapshot endpoints and
labels", snapshotEdgeTriples, apiEdgeTriples);
+
+ for (DiagramEdge<StrutsDiagramItem> edge : dataModel.getEdges()) {
+
assertNotNull(edge.getSource().getIdentifyingElement().getSnapshotNode());
+
assertNotNull(edge.getTarget().getIdentifyingElement().getSnapshotNode());
+ verifyRelationshipMapping(edge);
+ }
+ } finally {
+ Disposer.dispose(dataModel);
+ }
+ }
+
+ public void testExposedNodeAndEdgeCollectionsAreUnmodifiable() {
+ createStrutsFileSet("struts-diagram.xml");
+ VirtualFile vf = myFixture.findFileInTempDir("struts-diagram.xml");
+ assertNotNull(vf);
+ XmlFile xml = (XmlFile)
PsiManager.getInstance(getProject()).findFile(vf);
+ assertNotNull(xml);
+
+ DiagramProvider<?> diagramProvider =
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+ assertInstanceOf(diagramProvider, StrutsDiagramProvider.class);
+ StrutsDiagramDataModel dataModel = new StrutsDiagramDataModel(
+ getProject(), (StrutsDiagramProvider) diagramProvider,
StrutsDiagramItem.forFile(xml));
+ try {
+ ReadAction.run(dataModel::refreshDataModel);
+
+ assertThrows(UnsupportedOperationException.class, () ->
dataModel.getNodes().clear());
+ assertThrows(UnsupportedOperationException.class, () ->
dataModel.getEdges().clear());
+ } finally {
+ Disposer.dispose(dataModel);
+ }
+ }
+
+ public void testSameFileDomEventRefreshesLiveDataModel() throws
InterruptedException {
+ createStrutsFileSet("struts-diagram.xml");
+ VirtualFile vf = myFixture.findFileInTempDir("struts-diagram.xml");
+ assertNotNull(vf);
+ XmlFile xml = (XmlFile)
PsiManager.getInstance(getProject()).findFile(vf);
+ assertNotNull(xml);
+
+ DiagramProvider<?> diagramProvider =
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+ assertInstanceOf(diagramProvider, StrutsDiagramProvider.class);
+ StrutsDiagramDataModel dataModel = new StrutsDiagramDataModel(
+ getProject(), (StrutsDiagramProvider) diagramProvider,
StrutsDiagramItem.forFile(xml));
+ try {
+ ReadAction.run(dataModel::refreshDataModel);
+ int initialNodeCount = dataModel.getNodes().size();
+
+ Document document =
PsiDocumentManager.getInstance(getProject()).getDocument(xml);
+ assertNotNull(document);
+ WriteCommandAction.runWriteCommandAction(getProject(), () -> {
+ String updated = document.getText().replace(
+ "</package>",
+ "<action name=\"liveRefresh\"
class=\"MyClass\"/>\n</package>");
+ document.setText(updated);
+
PsiDocumentManager.getInstance(getProject()).commitDocument(document);
+ });
+
+ long deadline = System.currentTimeMillis() + 10_000;
+ while (System.currentTimeMillis() < deadline
+ && dataModel.getNodes().size() == initialNodeCount) {
+ UIUtil.dispatchAllInvocationEvents();
+ Thread.sleep(50);
+ }
+
+ assertTrue("Same-file DomEvent must refresh the live diagram data
model",
+ dataModel.getNodes().size() > initialNodeCount);
+ } finally {
+ Disposer.dispose(dataModel);
+ }
+ }
+
+ private record EdgeTriple(@NotNull String sourceId, @NotNull String
targetId, @NotNull String label) {
+ }
+
+ private static @NotNull EdgeTriple snapshotEdgeTriple(@NotNull
StrutsDiagramEdge edge) {
+ return new EdgeTriple(edge.getSource().getId(),
edge.getTarget().getId(), edge.getLabel());
+ }
+
+ private static @NotNull EdgeTriple apiEdgeTriple(@NotNull
DiagramEdge<StrutsDiagramItem> edge) {
+ StrutsDiagramNode source =
edge.getSource().getIdentifyingElement().getSnapshotNode();
+ StrutsDiagramNode target =
edge.getTarget().getIdentifyingElement().getSnapshotNode();
+ assertNotNull(source);
+ assertNotNull(target);
+ return new EdgeTriple(source.getId(), target.getId(),
apiEdgeLabel(edge));
+ }
+
+ private static @NotNull String apiEdgeLabel(@NotNull
DiagramEdge<StrutsDiagramItem> edge) {
+ DiagramRelationshipInfo relationship = edge.getRelationship();
+ if (relationship == DiagramRelationships.DEPENDENCY) {
+ return "";
+ }
+ String label = centerLabelText(relationship.getUpperCenterLabel());
+ if (label.isEmpty()) {
+ label = centerLabelText(relationship.getBottomCenterLabel());
+ }
+ return label;
+ }
+
+ private static @NotNull String centerLabelText(@Nullable
DiagramRelationshipInfo.Label label) {
+ if (label == null) {
+ return "";
+ }
+ String text = label.getText();
+ return text != null ? text : "";
+ }
+
+ private static void verifyRelationshipMapping(@NotNull
DiagramEdge<StrutsDiagramItem> edge) {
+ String label = apiEdgeLabel(edge);
+ if (label.isEmpty()) {
+ assertSame("Unlabeled edges must use DEPENDENCY",
DiagramRelationships.DEPENDENCY, edge.getRelationship());
+ }
+ else {
+ assertNotSame("Labeled edges must not use DEPENDENCY",
DiagramRelationships.DEPENDENCY, edge.getRelationship());
+ assertEquals(label,
centerLabelText(edge.getRelationship().getUpperCenterLabel()));
+ }
+ }
+}
diff --git
a/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java
b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java
new file mode 100644
index 0000000..70241d8
--- /dev/null
+++
b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram;
+
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.BasicLightHighlightingTestCase;
+import com.intellij.struts2.diagram.provider.StrutsDiagramDomRefresh;
+import com.intellij.struts2.dom.struts.StrutsRoot;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomFileElement;
+import com.intellij.util.xml.DomManager;
+import org.jetbrains.annotations.NotNull;
+
+public class StrutsDiagramDomRefreshTest extends
BasicLightHighlightingTestCase {
+
+ @Override
+ @NotNull
+ protected String getTestDataLocation() {
+ return "diagram";
+ }
+
+ public void testIsDomElementInFileMatchesSameVirtualFile() {
+ createStrutsFileSet("struts-local-a.xml", "struts-local-b.xml");
+ VirtualFile vfA = myFixture.findFileInTempDir("struts-local-a.xml");
+ VirtualFile vfB = myFixture.findFileInTempDir("struts-local-b.xml");
+ assertNotNull(vfA);
+ assertNotNull(vfB);
+
+ ReadAction.run(() -> {
+ XmlFile fileA = (XmlFile)
PsiManager.getInstance(getProject()).findFile(vfA);
+ assertNotNull(fileA);
+ DomFileElement<StrutsRoot> root =
+
DomManager.getDomManager(getProject()).getFileElement(fileA, StrutsRoot.class);
+ assertNotNull(root);
+ DomElement pkg = root.getRootElement().getPackages().getFirst();
+ assertTrue(StrutsDiagramDomRefresh.isDomElementInFile(pkg, vfA));
+ assertFalse(StrutsDiagramDomRefresh.isDomElementInFile(pkg, vfB));
+ });
+ }
+
+ public void testIsDomElementInFileRejectsNullElement() {
+ createStrutsFileSet("struts-local-a.xml");
+ VirtualFile vfA = myFixture.findFileInTempDir("struts-local-a.xml");
+ assertNotNull(vfA);
+ assertFalse(StrutsDiagramDomRefresh.isDomElementInFile(null, vfA));
+ }
+}
diff --git
a/src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java
b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java
new file mode 100644
index 0000000..5ca6956
--- /dev/null
+++ b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+package com.intellij.struts2.diagram;
+
+import com.intellij.diagram.DiagramProvider;
+import com.intellij.diagram.DiagramVfsResolver;
+import com.intellij.diagram.extras.EditNodeHandler;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.BasicLightHighlightingTestCase;
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.provider.StrutsDiagramApiNode;
+import com.intellij.struts2.diagram.provider.StrutsDiagramExtras;
+import com.intellij.struts2.diagram.provider.StrutsDiagramItem;
+import com.intellij.struts2.diagram.provider.StrutsDiagramProvider;
+import org.jetbrains.annotations.NotNull;
+
+public class StrutsDiagramProviderTest extends BasicLightHighlightingTestCase {
+
+ @Override
+ @NotNull
+ protected String getTestDataLocation() {
+ return "diagram";
+ }
+
+ public void testProviderExtensionIsRegistered() {
+ DiagramProvider<?> provider =
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+ assertNotNull("Struts DiagramProvider must be registered via
com.intellij.diagram.Provider",
+ provider);
+ assertInstanceOf(provider, StrutsDiagramProvider.class);
+ }
+
+ public void testAcceptsStrutsConfigWithoutFileSet() {
+ VirtualFile file = myFixture.copyFileToProject("struts-diagram.xml");
+ XmlFile xml = (XmlFile)
PsiManager.getInstance(getProject()).findFile(file);
+ assertNotNull(xml);
+ StrutsDiagramProvider provider = getProvider();
+ DataContext ctx = SimpleDataContext.builder()
+ .add(CommonDataKeys.PSI_FILE, xml)
+ .add(CommonDataKeys.PROJECT, getProject())
+ .build();
+ StrutsDiagramItem item =
provider.getElementManager().findInDataContext(ctx);
+ assertNotNull("Show Diagram must accept Struts config outside a file
set", item);
+ assertEquals(xml, item.getXmlFile());
+ assertNull(item.getSnapshotNode());
+ }
+
+ public void testRejectsPlainXml() {
+ XmlFile xml = (XmlFile) myFixture.configureByText("plain.xml",
"<root/>");
+ StrutsDiagramProvider provider = getProvider();
+ DataContext ctx = SimpleDataContext.builder()
+ .add(CommonDataKeys.PSI_FILE, xml)
+ .add(CommonDataKeys.PROJECT, getProject())
+ .build();
+ assertNull(provider.getElementManager().findInDataContext(ctx));
+ assertFalse(provider.getElementManager().canBeBuiltFrom(xml));
+ }
+
+ public void testVfsResolverRoundTripsSnapshotNodeFqn() {
+ createStrutsFileSet("struts-diagram.xml");
+ VirtualFile file = myFixture.findFileInTempDir("struts-diagram.xml");
+ assertNotNull(file);
+ XmlFile xml = (XmlFile)
PsiManager.getInstance(getProject()).findFile(file);
+ assertNotNull(xml);
+
+ StrutsConfigDiagramModel model = ReadAction.compute(() ->
StrutsConfigDiagramModel.build(xml));
+ assertNotNull(model);
+ StrutsDiagramNode node = model.getNodes().get(0);
+ DiagramVfsResolver<StrutsDiagramItem> resolver =
getProvider().getVfsResolver();
+ String fqn = resolver.getQualifiedName(StrutsDiagramItem.forNode(xml,
node));
+ assertNotNull(fqn);
+
+ StrutsDiagramItem resolved = resolver.resolveElementByFQN(fqn,
getProject());
+
+ assertNotNull(resolved);
+ assertNotNull(resolved.getSnapshotNode());
+ assertEquals(node.getId(), resolved.getSnapshotNode().getId());
+ }
+
+ public void testExtrasProvideEditNodeHandlerForEditorDoubleClick() {
+ StrutsDiagramProvider provider = getProvider();
+ EditNodeHandler<StrutsDiagramItem> handler =
provider.getExtras().getEditNodeHandler();
+ assertNotNull(
+ "Editor-mode double-click uses EditNodeHandler, not
DiagramNode.navigate()",
+ handler);
+ }
+
+ public void testResolvePsiElementFromSnapshotNode() {
+ createStrutsFileSet("struts-diagram.xml");
+ VirtualFile file = myFixture.findFileInTempDir("struts-diagram.xml");
+ assertNotNull(file);
+ XmlFile xml = (XmlFile)
PsiManager.getInstance(getProject()).findFile(file);
+ assertNotNull(xml);
+
+ StrutsConfigDiagramModel model = ReadAction.compute(() ->
StrutsConfigDiagramModel.build(xml));
+ assertNotNull(model);
+ StrutsDiagramNode snapshotNode = model.getNodes().stream()
+ .filter(n -> n.getKind() == StrutsDiagramNode.Kind.ACTION)
+ .findFirst()
+ .orElseThrow();
+ StrutsDiagramItem item = StrutsDiagramItem.forNode(xml, snapshotNode);
+
+ PsiElement psi = StrutsDiagramExtras.resolvePsiElement(item);
+ assertNotNull(psi);
+ assertTrue(psi.isValid());
+ assertEquals(xml, psi.getContainingFile());
+
+ StrutsDiagramApiNode apiNode = new StrutsDiagramApiNode(getProvider(),
item);
+ assertTrue(apiNode.canNavigate());
+ }
+
+ private StrutsDiagramProvider getProvider() {
+ DiagramProvider<?> provider =
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+ assertNotNull(provider);
+ assertInstanceOf(provider, StrutsDiagramProvider.class);
+ return (StrutsDiagramProvider) provider;
+ }
+}