From 57adfd55ca67ddec37b6c1bbf7a1a4e86ff968bf Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Tue, 20 Jan 2026 14:47:53 +0800 Subject: [PATCH 01/24] Add App Modernization support with Migrate to Azure feature --- .../.idea/gradle.xml | 4 + .../build.gradle.kts | 5 + .../appmod/IMigrateChildNodeProvider.java | 80 ++++ .../intellij/appmod/InstallPluginDialog.java | 82 ++++ .../intellij/appmod/MigrateNodeData.java | 354 ++++++++++++++++++ .../appmod/MigratePluginInstaller.java | 157 ++++++++ .../intellij/appmod/MigrateToAzureAction.java | 273 ++++++++++++++ .../intellij/appmod/MigrateToAzureNode.java | 125 +++++++ .../intellij/appmod/RestartIdeDialog.java | 82 ++++ .../META-INF/azure-intellij-plugin-appmod.xml | 11 + .../src/main/resources/icons/app_mod.svg | 22 ++ .../src/main/resources/META-INF/plugin.xml | 1 + .../build.gradle.kts | 2 + .../intellij/explorer/AzureExplorer.java | 26 ++ .../build.gradle.kts | 2 + .../projectexplorer/AzureFacetRootNode.java | 8 +- .../MigrateToAzureFacetNode.java | 174 +++++++++ .../gradle.properties | 4 +- .../settings.gradle.kts | 1 + .../src/main/resources/META-INF/plugin.xml | 3 + 20 files changed, 1412 insertions(+), 4 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml index 25aace38038..d604fd81745 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml @@ -14,6 +14,7 @@ + + \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts new file mode 100644 index 00000000000..2bcc6e6fb46 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts @@ -0,0 +1,5 @@ +dependencies { + implementation(project(":azure-intellij-plugin-lib")) + // runtimeOnly project(path: ":azure-intellij-plugin-lib", configuration: "instrumentedJar") + implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java new file mode 100644 index 00000000000..88f4a456c27 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.project.Project; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * Extension point interface for providing migration nodes. + * + * Implementations of this interface will be discovered via IntelliJ's extension point mechanism + * and used to dynamically construct the migration options tree in: + * - Service Explorer (MigrateToAzureNode) + * - Context Menu (MigrateToAzureAction) + * - Project Explorer (MigrateToAzureFacetNode) + * + * Example implementation: + *
+ * public class MyMigrationProvider implements IMigrateChildNodeProvider {
+ *     @Override
+ *     public List<MigrateNodeData> createNodeData(@Nonnull Project project) {
+ *         return List.of(
+ *             MigrateNodeData.builder("My Migration Option")
+ *                 .iconPath("/icons/my_icon.svg")
+ *                 .onClick(() -> performMigration(project))
+ *                 .build()
+ *         );
+ *     }
+ * }
+ * 
+ * + * Registration in plugin.xml: + *
+ * <extensions defaultExtensionNs="com.microsoft.tooling.msservices.intellij.azure">
+ *     <migrateChildNodeProvider implementation="your.package.MyMigrationProvider"/>
+ * </extensions>
+ * 
+ */ +public interface IMigrateChildNodeProvider { + + /** + * Creates migration node data for the Migrate to Azure section. + * + * This method is called each time the migration menu/tree is constructed. + * The returned list can contain multiple MigrateNodeData instances, + * each representing a single action or a group of options. + * + * @param project The current IntelliJ project + * @return A list of MigrateNodeData instances representing the migration option(s) + */ + @Nonnull + List createNodeData(@Nonnull Project project); + + /** + * Returns the priority/order of this node provider. + * Nodes will be displayed in ascending order of priority. + * Lower numbers appear first. + * + * @return Priority value (default: 100) + */ + default int getPriority() { + return 100; + } + + /** + * Determines whether this provider should create a node. + * Can be used to conditionally show/hide migration options based on context. + * + * @param project The current IntelliJ project + * @return true if this provider should contribute a node, false otherwise + */ + default boolean isApplicable(@Nonnull Project project) { + return true; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java new file mode 100644 index 00000000000..9eaa3615e2d --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.Objects; + +/** + * Dialog for confirming plugin installation. + * Similar to ConfirmAndRunDialog used in AzdNode. + */ +public class InstallPluginDialog extends DialogWrapper { + + private final Project project; + private String label; + private Runnable onOkAction; + + public InstallPluginDialog(Project project, String title) { + super(project, true); + this.project = project; + setSize(400, 150); + setTitle(Objects.requireNonNull(title, "Title must not be null")); + setOKButtonText("Install"); + } + + public InstallPluginDialog setLabel(String label) { + this.label = Objects.requireNonNull(label, "Label must not be null"); + return this; + } + + public InstallPluginDialog setOnOkAction(Runnable onOkAction) { + this.onOkAction = onOkAction; + return this; + } + + @Override + public void show() { + init(); + super.show(); + } + + @Override + protected @Nullable JComponent createCenterPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Support HTML for multi-line labels + JLabel labelComponent; + if (label != null && label.contains("\n")) { + // Convert newlines to HTML breaks + String htmlLabel = "" + label.replace("\n", "
") + ""; + labelComponent = new JLabel(htmlLabel); + } else { + labelComponent = new JLabel(label); + } + labelComponent.setHorizontalAlignment(SwingConstants.CENTER); + panel.add(labelComponent, BorderLayout.CENTER); + return panel; + } + + @Override + protected Action @NotNull [] createActions() { + return new Action[]{getOKAction(), getCancelAction()}; + } + + @Override + protected void doOKAction() { + super.doOKAction(); + if (onOkAction != null) { + onOkAction.run(); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java new file mode 100644 index 00000000000..a13f18a1df3 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Unified data structure for migration nodes. + * This class is used by MigrateToAzureNode, MigrateToAzureAction, and MigrateToAzureFacetNode. + * + * Features: + * - Basic properties: label, icon, description, tooltip + * - Click handling for both leaf and parent nodes + * - Static children or lazy-loaded children via childrenLoader + * + * Lazy Loading: + * Provider can declare lazy loading intent via childrenLoader. Each consumer handles it based on capability: + * - Service Explorer: Supports lazy loading → uses Node.withChildrenLoadLazily(true) + * - Project Explorer: Native lazy loading → buildChildren() calls loader + * - Action menus: No lazy loading → calls loader.get() immediately + * + * Usage examples: + * + * 1. Simple leaf node with click action: + *
+ * MigrateNodeData.builder("Option A")
+ *     .iconPath("/icons/my_icon.svg")
+ *     .onClick(e -> doSomething())
+ *     .build();
+ * 
+ * + * 2. Parent node with static children: + *
+ * MigrateNodeData.builder("Parent")
+ *     .addChild(childNode1)
+ *     .addChild(childNode2)
+ *     .build();
+ * 
+ * + * 3. Parent node with lazy-loaded children: + *
+ * MigrateNodeData.builder("Lazy Parent")
+ *     .childrenLoader(() -> loadChildrenFromServer())
+ *     .build();
+ * 
+ */ +@Getter +public class MigrateNodeData { + + // ==================== Basic Properties ==================== + + /** + * Display label for the node. + */ + @Nonnull + private final String label; + + /** + * Icon path for the node (e.g., "/icons/app_mod.svg"). If null, a default icon will be used. + */ + @Nullable + private String iconPath; + + /** + * Description text (shown as secondary text in some views). + */ + @Nullable + private String description; + + /** + * Tooltip text (shown on hover). + */ + @Nullable + private String tooltip; + + // ==================== State ==================== + + /** + * Whether the node is enabled (clickable). + */ + @Setter + private boolean enabled = true; + + /** + * Whether the node is visible. + */ + @Setter + private boolean visible = true; + + // ==================== Click Handling ==================== + + /** + * Click handler. Can be set on any node (leaf or parent). + * For parent nodes in menus, this may be triggered by a specific action. + */ + @Nullable + private Consumer clickHandler; + + /** + * Double-click handler. Useful for tree views where single-click selects + * and double-click performs action. + */ + @Nullable + private Consumer doubleClickHandler; + + // ==================== Children ==================== + + /** + * Static list of child nodes. + */ + @Nonnull + private final List children = new ArrayList<>(); + + /** + * Lazy loader for children. If set, consumers that support lazy loading + * will use this to load children on demand. Consumers without lazy loading + * support will call this immediately. + * + * Note: This is just a declaration of intent. MigrateNodeData does NOT + * manage loading state - each consumer handles that according to its capability. + */ + @Nullable + private Supplier> childrenLoader; + + // ==================== Constructor ==================== + + private MigrateNodeData(@Nonnull String label) { + this.label = label; + } + + // ==================== Builder ==================== + + /** + * Creates a new builder for MigrateNodeData. + * + * @param label The display label for the node + * @return A new builder instance + */ + public static Builder builder(@Nonnull String label) { + return new Builder(label); + } + + // ==================== Methods ==================== + + /** + * Checks if this node has children (static or via loader). + */ + public boolean hasChildren() { + return !children.isEmpty() || childrenLoader != null; + } + + /** + * Gets the static children list. + * For lazy-loaded children, use getChildrenLoader() instead. + */ + @Nonnull + public List getChildren() { + return children; + } + + /** + * Gets the children loader for lazy loading. + * Consumers should check this and handle according to their capability. + */ + @Nullable + public Supplier> getChildrenLoader() { + return childrenLoader; + } + + /** + * Checks if this node uses lazy loading for children. + */ + public boolean isLazyLoading() { + return childrenLoader != null; + } + + /** + * Triggers the click handler. + * + * @param event The event object (can be AnActionEvent, MouseEvent, or null) + */ + public void click(@Nullable Object event) { + if (clickHandler != null && enabled) { + clickHandler.accept(event); + } + } + + /** + * Triggers the double-click handler. + * Falls back to click handler if double-click is not set. + * + * @param event The event object + */ + public void doubleClick(@Nullable Object event) { + if (doubleClickHandler != null && enabled) { + doubleClickHandler.accept(event); + } else { + click(event); + } + } + + /** + * Checks if this node has a click handler. + */ + public boolean hasClickHandler() { + return clickHandler != null; + } + + /** + * Adds a child node dynamically. + */ + public void addChild(@Nonnull MigrateNodeData child) { + children.add(child); + } + + /** + * Removes a child node. + */ + public void removeChild(@Nonnull MigrateNodeData child) { + children.remove(child); + } + + // ==================== Builder Class ==================== + + public static class Builder { + private final MigrateNodeData data; + + private Builder(@Nonnull String label) { + this.data = new MigrateNodeData(label); + } + + /** + * Sets the icon path. + */ + public Builder iconPath(@Nullable String iconPath) { + data.iconPath = iconPath; + return this; + } + + /** + * Sets the description. + */ + public Builder description(@Nullable String description) { + data.description = description; + return this; + } + + /** + * Sets the tooltip. + */ + public Builder tooltip(@Nullable String tooltip) { + data.tooltip = tooltip; + return this; + } + + /** + * Sets the enabled state. + */ + public Builder enabled(boolean enabled) { + data.enabled = enabled; + return this; + } + + /** + * Sets the visible state. + */ + public Builder visible(boolean visible) { + data.visible = visible; + return this; + } + + /** + * Sets the click handler. + */ + public Builder onClick(@Nullable Consumer handler) { + data.clickHandler = handler; + return this; + } + + /** + * Sets the click handler (no-arg version). + */ + public Builder onClick(@Nullable Runnable handler) { + if (handler != null) { + data.clickHandler = e -> handler.run(); + } + return this; + } + + /** + * Sets the double-click handler. + */ + public Builder onDoubleClick(@Nullable Consumer handler) { + data.doubleClickHandler = handler; + return this; + } + + /** + * Sets the double-click handler (no-arg version). + */ + public Builder onDoubleClick(@Nullable Runnable handler) { + if (handler != null) { + data.doubleClickHandler = e -> handler.run(); + } + return this; + } + + /** + * Adds a child node. + */ + public Builder addChild(@Nonnull MigrateNodeData child) { + data.children.add(child); + return this; + } + + /** + * Adds multiple child nodes. + */ + public Builder addChildren(@Nonnull List children) { + data.children.addAll(children); + return this; + } + + /** + * Sets the children loader for lazy loading. + * Consumers that support lazy loading will use this to load children on demand. + * Consumers without lazy loading support will call this immediately. + * + * @param loader Supplier that returns the list of child nodes + */ + public Builder childrenLoader(@Nullable Supplier> loader) { + data.childrenLoader = loader; + return this; + } + + /** + * Builds the MigrateNodeData instance. + */ + public MigrateNodeData build() { + return data; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java new file mode 100644 index 00000000000..37be801300a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.ide.plugins.IdeaPluginDescriptor; +import com.intellij.ide.plugins.PluginManagerCore; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.extensions.PluginId; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; +import com.microsoft.azure.toolkit.lib.common.event.AzureEventBus; +import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; + +import javax.annotation.Nonnull; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Utility class for managing App Modernization plugin installation. + * This centralizes all plugin detection and installation logic to avoid code duplication + * between MigrateToAzureNode and MigrateToAzureAction. + */ +public class MigratePluginInstaller { + private static final String PLUGIN_ID = "com.github.copilot.appmod"; + private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; + + private MigratePluginInstaller() { + // Utility class - prevent instantiation + } + + /** + * Checks if the App Modernization plugin is installed and enabled. + */ + public static boolean isAppModPluginInstalled() { + try { + final PluginId pluginId = PluginId.getId(PLUGIN_ID); + final IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(pluginId); + return plugin != null && plugin.isEnabled(); + } catch (Exception e) { + return false; + } + } + + /** + * Checks if GitHub Copilot plugin is installed and enabled. + */ + public static boolean isCopilotInstalled() { + try { + final PluginId pluginId = PluginId.getId(COPILOT_PLUGIN_ID); + final IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(pluginId); + return plugin != null && plugin.isEnabled(); + } catch (Exception e) { + return false; + } + } + + /** + * Detects if running in development mode (runIde task). + * This helps determine whether to show restart options or dev-mode instructions. + */ + public static boolean isRunningInDevMode() { + try { + final PluginId azureToolkitId = PluginId.getId("com.microsoft.tooling.msservices.intellij.azure"); + final IdeaPluginDescriptor descriptor = PluginManagerCore.getPlugin(azureToolkitId); + if (descriptor != null) { + final String path = descriptor.getPluginPath().toString(); + return path.contains("build") || path.contains("sandbox") || path.contains("out"); + } + } catch (Exception ex) { + // If we can't determine, assume production mode (safer) + } + return false; + } + + /** + * Shows a confirmation dialog for plugin installation. + * Uses a modal dialog similar to AzdNode's ConfirmAndRunDialog. + * + * @param project The current project + * @param onConfirm Callback to execute when user confirms installation + */ + public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Runnable onConfirm) { + final boolean copilotInstalled = isCopilotInstalled(); + + final String title = copilotInstalled + ? "Install GitHub Copilot App Modernization" + : "Install GitHub Copilot and GitHub Copilot App Modernization"; + + final String message = copilotInstalled + ? "Do you want to install GitHub Copilot App Modernization plugin?" + : "Do you want to install GitHub Copilot and GitHub Copilot App Modernization plugins?"; + + new InstallPluginDialog(project, title) + .setLabel(message) + .setOnOkAction(onConfirm) + .show(); + } + + /** + * Installs the App Modernization plugin (and GitHub Copilot if needed). + * IntelliJ platform will handle the restart prompt after installation. + * In dev mode, shows instructions to manually restart runIde task instead. + * + * @param project The current project + */ + public static void installPlugin(@Nonnull Project project) { + final boolean copilotInstalled = isCopilotInstalled(); + final boolean appModInstalled = isAppModPluginInstalled(); + final boolean isDevMode = isRunningInDevMode(); + + // Build plugin ID set - only include plugins that are NOT already installed + final Set pluginsToInstall = new LinkedHashSet<>(); + if (!copilotInstalled) { + pluginsToInstall.add(PluginId.getId(COPILOT_PLUGIN_ID)); + } + if (!appModInstalled) { + pluginsToInstall.add(PluginId.getId(PLUGIN_ID)); + } + + // If all plugins are already installed, nothing to do + if (pluginsToInstall.isEmpty()) { + return; + } + + // Use PluginsAdvertiser.installAndEnable - IntelliJ handles the rest + // The platform will show plugin selection dialog, download, install, and prompt for restart + AzureTaskManager.getInstance().runAndWait(() -> { + PluginsAdvertiser.installAndEnable( + project, + pluginsToInstall, + true, // showDialog + true, // selectAllInDialog - pre-select all plugins + null, // modalityState + () -> { + // Called after user confirms installation + if (isDevMode) { + // Dev mode: Show special instructions + ApplicationManager.getApplication().invokeLater(() -> { + final String message = "Plugins are being installed.\n\n" + + "⚠️ DEVELOPMENT MODE:\n" + + "After installation completes, do NOT restart from this IDE window!\n" + + "Instead, stop your current runIde task and relaunch ./gradlew runIde"; + new RestartIdeDialog(project, "Development Mode Notice", message) + .setShowRestartOption(false) + .show(); + }); + } + // Emit event + AzureEventBus.emit("migrate.plugin.installed"); + } + ); + }); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java new file mode 100644 index 00000000000..cee7bd27b8d --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; +import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Action group that dynamically shows migration options as sub-menu. + * Uses the same extension point as MigrateToAzureNode for consistency. + */ +public class MigrateToAzureAction extends ActionGroup { + private static final ExtensionPointName migrationProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + + private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; + + public MigrateToAzureAction() { + // Set popup=true to show this as a root menu item with expandable sub-menu (▶) + // Without this, children would be displayed directly at root level + super("Migrate to Azure", true); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + // Run on background thread to avoid blocking EDT when loading migration options + return ActionUpdateThread.BGT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + final Project project = e.getProject(); + + if (MigratePluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setText("Migrate to Azure"); + // Only show sub-menu if there are migration options available + e.getPresentation().setEnabledAndVisible(project != null && hasMigrationOptions(project)); + } else { + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String text = copilotInstalled + ? "Migrate to Azure (Install App Modernization)" + : "Migrate to Azure (Install Copilot & App Modernization)"; + e.getPresentation().setText(text); + e.getPresentation().setEnabledAndVisible(true); + } + } + + @Override + public AnAction @NotNull [] getChildren(@Nullable AnActionEvent e) { + if (e == null) { + return AnAction.EMPTY_ARRAY; + } + + final Project project = e.getProject(); + if (project == null) { + return AnAction.EMPTY_ARRAY; + } + + // If plugin not installed, show install action + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String actionText = copilotInstalled + ? "Install GitHub Copilot App Modernization" + : "Install GitHub Copilot and GitHub Copilot App Modernization"; + return new AnAction[]{ + new AnAction(actionText) { + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); + } + } + }; + } + + // Load migration options from extension points + final List migrationNodes = loadMigrationNodes(project); + + if (migrationNodes.isEmpty()) { + return AnAction.EMPTY_ARRAY; + } + + // Convert nodes to actions + return convertNodesToActions(migrationNodes); + } + + /** + * Loads migration nodes from extension point providers. + */ + private List loadMigrationNodes(@Nonnull Project project) { + return migrationProviders.getExtensionList().stream() + .filter(provider -> provider.isApplicable(project)) + .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(project).stream()) + .filter(MigrateNodeData::isVisible) + .collect(Collectors.toList()); + } + + /** + * Checks if there are any migration options available. + */ + private boolean hasMigrationOptions(@Nonnull Project project) { + return migrationProviders.getExtensionList().stream() + .anyMatch(provider -> provider.isApplicable(project)); + } + + /** + * Converts node tree to action tree for sub-menu display. + */ + private AnAction[] convertNodesToActions(List nodes) { + final List actions = new ArrayList<>(); + for (MigrateNodeData node : nodes) { + actions.add(convertNodeToAction(node)); + } + return actions.toArray(new AnAction[0]); + } + + /** + * Converts a single node (and its children) to an action. + */ + private AnAction convertNodeToAction(MigrateNodeData nodeData) { + if (nodeData.hasChildren()) { + // For lazy loading nodes, create a LazyActionGroup that loads children on demand + if (nodeData.isLazyLoading()) { + return new LazyActionGroup(nodeData); + } + + // Node with static children -> create sub-menu with children added immediately + final DefaultActionGroup subgroup = new DefaultActionGroup(); + subgroup.getTemplatePresentation().setText(nodeData.getLabel(), false); + subgroup.setPopup(true); + + // Use node's icon if available, otherwise use default app_mod icon + if (nodeData.getIconPath() != null) { + subgroup.getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); + } else { + subgroup.getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + + // Add static children + for (MigrateNodeData child : nodeData.getChildren()) { + if (child.isVisible()) { + subgroup.add(convertNodeToAction(child)); + } + } + + return subgroup; + } else { + // Leaf node -> create clickable action + return new AnAction(nodeData.getLabel()) { + { + // Use node's icon if available, otherwise use default app_mod icon + if (nodeData.getIconPath() != null) { + getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); + } else { + getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + if (nodeData.getDescription() != null) { + getTemplatePresentation().setDescription(nodeData.getDescription()); + } + } + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabled(nodeData.isEnabled()); + } + + @Override + @AzureOperation(name = "user/common.migrate_to_azure.trigger_option") + public void actionPerformed(@NotNull AnActionEvent e) { + nodeData.click(e); + } + }; + } + } + + /** + * ActionGroup that loads children lazily when the submenu is expanded. + * This avoids blocking the UI when the parent menu is shown. + */ + private class LazyActionGroup extends ActionGroup { + private final MigrateNodeData nodeData; + private volatile AnAction[] cachedChildren; + private volatile boolean isLoading = false; + + // Loading placeholder action + private final AnAction loadingAction = new AnAction("Loading...") { + { + getTemplatePresentation().setEnabled(false); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + // No-op, this is just a placeholder + } + }; + + LazyActionGroup(MigrateNodeData nodeData) { + super(nodeData.getLabel(), true); + this.nodeData = nodeData; + + // Set icon + if (nodeData.getIconPath() != null) { + getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); + } else { + getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + // Load children on background thread to avoid blocking EDT + return ActionUpdateThread.BGT; + } + + @Override + public AnAction @NotNull [] getChildren(@Nullable AnActionEvent e) { + if (cachedChildren != null) { + return cachedChildren; + } + + if (!isLoading) { + isLoading = true; + // Load children lazily + final List children = nodeData.getChildrenLoader().get(); + final List actions = new ArrayList<>(); + for (MigrateNodeData child : children) { + if (child.isVisible()) { + actions.add(convertNodeToAction(child)); + } + } + cachedChildren = actions.isEmpty() + ? new AnAction[]{ createNoOptionsAction() } + : actions.toArray(new AnAction[0]); + return cachedChildren; + } + + // Still loading, show placeholder + return new AnAction[]{ loadingAction }; + } + + private AnAction createNoOptionsAction() { + return new AnAction("No migration options available") { + { + getTemplatePresentation().setEnabled(false); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + // No-op + } + }; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java new file mode 100644 index 00000000000..b6395a1ee62 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.ide.common.component.Node; +import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Service Explorer node for "Migrate to Azure" functionality. + * This node extends the azure-toolkit-ide-common-lib Node class to integrate with the Service Explorer tree. + */ +public final class MigrateToAzureNode extends Node { + private static final ExtensionPointName childProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + + private final Project project; + + private static final AzureIcon APP_MOD_ICON = AzureIcon.builder().iconPath("/icons/app_mod.svg").build(); + + public MigrateToAzureNode(Project project) { + super("Migrate to Azure"); + this.project = project; + withIcon(APP_MOD_ICON); + initializeNode(); + } + + public void initializeNode() { + if (MigratePluginInstaller.isAppModPluginInstalled()) { + showMigrationOptions(); + } else { + showNotInstalled(); + } + } + + private void showNotInstalled() { + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + + // Dynamic description based on what needs to be installed + final String description = copilotInstalled + ? "Install GitHub Copilot App Modernization" + : "Install GitHub Copilot and GitHub Copilot App Modernization"; + withDescription(description); + + onClicked(e -> { + MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); + }); + } + + + public void showMigrationOptions() { + clearClickHandlers(); + withDescription(""); + + // Load migration options from extension points and convert to Node + final List nodeDataList = childProviders.getExtensionList().stream() + .filter(provider -> provider.isApplicable(project)) + .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(project).stream()) + .collect(Collectors.toList()); + + // Convert MigrateNodeData to Node and add as children + nodeDataList.stream() + .map(this::convertToNode) + .forEach(this::addChild); + } + + /** + * Converts MigrateNodeData to Node for Service Explorer compatibility. + */ + private Node convertToNode(MigrateNodeData data) { + Node node = new Node<>(data); + + // Set basic properties + node.withLabel(d -> d.getLabel()); + // Use node's icon if available, otherwise use default app_mod icon + node.withIcon(d -> d.getIconPath() != null ? AzureIcon.builder().iconPath(d.getIconPath()).build() : APP_MOD_ICON); + if (data.getDescription() != null) { + node.withDescription(d -> d.getDescription()); + } + if (data.getTooltip() != null) { + node.withTips(d -> d.getTooltip()); + } + + // Set click handler + if (data.hasClickHandler()) { + node.onClicked(d -> data.click(null)); + } + + // Handle children - lazy or static + if (data.isLazyLoading()) { + // Lazy loading: use Node's native lazy loading mechanism + node.withChildrenLoadLazily(true); + node.addChildren( + d -> d.getChildrenLoader().get(), + (childData, parent) -> convertToNode(childData) + ); + } else if (data.hasChildren()) { + // Static children: add directly + for (MigrateNodeData childData : data.getChildren()) { + node.addChild(convertToNode(childData)); + } + } + + return node; + } + + @Override + public synchronized void refreshView() { + super.refreshView(); + refreshChildrenLater(false); + } + + public static boolean isPluginInstalled() { + return MigratePluginInstaller.isAppModPluginInstalled(); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java new file mode 100644 index 00000000000..ab6ef884794 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; + +/** + * Dialog for prompting user after plugin installation. + * Can be configured to show restart options or just an info message. + */ +public class RestartIdeDialog extends DialogWrapper { + + private final String message; + private boolean showRestartOption = true; + + public RestartIdeDialog(Project project, String title, String message) { + super(project, true); + this.message = message; + setSize(450, 150); + setTitle(title); + setOKButtonText("Restart Now"); + setCancelButtonText("Restart Later"); + init(); + } + + /** + * Sets whether to show restart option or just an info dialog. + * @param showRestartOption true for restart dialog, false for info-only dialog + */ + public RestartIdeDialog setShowRestartOption(boolean showRestartOption) { + this.showRestartOption = showRestartOption; + if (!showRestartOption) { + setOKButtonText("Got it"); + } + return this; + } + + @Override + protected @Nullable JComponent createCenterPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Support HTML for multi-line messages + JLabel labelComponent; + if (message != null && message.contains("\n")) { + String htmlMessage = "" + message.replace("\n", "
") + ""; + labelComponent = new JLabel(htmlMessage); + } else { + labelComponent = new JLabel(message); + } + labelComponent.setHorizontalAlignment(SwingConstants.CENTER); + panel.add(labelComponent, BorderLayout.CENTER); + return panel; + } + + @Override + protected Action @NotNull [] createActions() { + if (showRestartOption) { + return new Action[]{getOKAction(), getCancelAction()}; + } else { + return new Action[]{getOKAction()}; + } + } + + @Override + protected void doOKAction() { + super.doOKAction(); + if (showRestartOption) { + ApplicationManager.getApplication().restart(); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml new file mode 100644 index 00000000000..00a9ad12c52 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg new file mode 100644 index 00000000000..cc40914cd6c --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml index 308a49b55f0..5f24c40ff44 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml @@ -30,6 +30,7 @@ org.jetbrains.idea.maven com.intellij.gradle + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/build.gradle.kts index 2bcc6e6fb46..dbed828fbb9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/build.gradle.kts @@ -1,5 +1,7 @@ dependencies { implementation(project(":azure-intellij-plugin-lib")) // runtimeOnly project(path: ":azure-intellij-plugin-lib", configuration: "instrumentedJar") + implementation(project(":azure-intellij-plugin-appmod")) + // runtimeOnly project(path: ":azure-intellij-plugin-appmod", configuration: "instrumentedJar") implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java index 1b450e7ad6a..9de3264688d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java @@ -32,6 +32,7 @@ import com.microsoft.azure.toolkit.intellij.common.component.Tree; import com.microsoft.azure.toolkit.intellij.common.component.TreeUtils; import com.microsoft.azure.toolkit.intellij.explorer.azd.AzdNode; +import com.microsoft.azure.toolkit.intellij.appmod.MigrateToAzureNode; import com.microsoft.azure.toolkit.lib.Azure; import com.microsoft.azure.toolkit.lib.auth.AzureAccount; import com.microsoft.azure.toolkit.lib.auth.IAccountActions; @@ -72,17 +73,20 @@ public class AzureExplorer extends Tree { public static final AzureExplorerNodeProviderManager manager = new AzureExplorerNodeProviderManager(); public static final String AZURE_ICON = AzureIcons.Common.AZURE.getIconPath(); private final AzdNode azdNode; + private final MigrateToAzureNode migrateToAzureNode; private AzureExplorer(Project project) { super(); this.putClientProperty(PLACE, ResourceCommonActionsContributor.AZURE_EXPLORER); this.azdNode = new AzdNode(project); + this.migrateToAzureNode = new MigrateToAzureNode(project); this.root = new Node<>("Azure") .withChildrenLoadLazily(false) .addChild(buildFavoriteRoot()) .addChild(buildAppGroupedResourcesRoot()) .addChild(buildTypeGroupedResourcesRoot()) .addChildren(buildNonAzServiceNodes()) + .addChild(migrateToAzureNode) .addChild(azdNode); this.init(this.root); @@ -130,6 +134,28 @@ private AzureExplorer(Project project) { } } })); + + AzureEventBus.on("migrate.plugin.installed", new AzureEventBus.EventListener(e -> { + final DefaultTreeModel model = (DefaultTreeModel) this.getModel(); + final TreeNode root = (TreeNode) model.getRoot(); + if (root != null && root.children() != null) { + Iterator iterator = root.children().asIterator(); + while (iterator.hasNext()) { + final TreeNode childNode = (TreeNode) iterator.next(); + final Node childInnerNode = childNode.getInner(); + if (childInnerNode instanceof MigrateToAzureNode) { + final MigrateToAzureNode migrateNode = (MigrateToAzureNode) childInnerNode; + childNode.setAllowsChildren(true); + migrateNode.clearClickHandlers(); + migrateNode.withDescription(""); + migrateNode.showMigrationOptions(); + migrateNode.refreshView(); + childNode.updateChildren(true); + break; + } + } + } + })); } @Override diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/build.gradle.kts index 216c059e068..25e794a25ff 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/build.gradle.kts @@ -1,6 +1,8 @@ dependencies { implementation(project(":azure-intellij-plugin-lib")) // runtimeOnly project(path: ":azure-intellij-plugin-lib", configuration: "instrumentedJar") + implementation(project(":azure-intellij-plugin-appmod")) + // runtimeOnly project(path: ":azure-intellij-plugin-appmod", configuration: "instrumentedJar") implementation(project(":azure-intellij-plugin-service-explorer")) // runtimeOnly project(path: ":azure-intellij-plugin-service-explorer", configuration: "instrumentedJar") implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetRootNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetRootNode.java index a526cac550f..e907ba27829 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetRootNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetRootNode.java @@ -94,10 +94,12 @@ public Collection> buildChildren() { if (Objects.isNull(profile)) { nodes.add(new ActionNode<>(this.getProject(), CONNECT_TO_MODULE, module)); nodes.add(new ActionNode<>(this.getProject(), Action.Id.of(ACTIONS_DEPLOY_TO_AZURE), module.getModule())); - return nodes; + } else { + nodes.add(new DeploymentTargetsNode(this.getProject(), profile.getDeploymentTargetManager())); + nodes.add(new ConnectionsNode(this.getProject(), profile.getConnectionManager())); } - nodes.add(new DeploymentTargetsNode(this.getProject(), profile.getDeploymentTargetManager())); - nodes.add(new ConnectionsNode(this.getProject(), profile.getConnectionManager())); + // Always add Migrate to Azure node at the end + nodes.add(new MigrateToAzureFacetNode(this.getProject(), module)); return nodes; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java new file mode 100644 index 00000000000..8036d60313c --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.connector.projectexplorer; + +import com.intellij.ide.projectView.PresentationData; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.intellij.ui.tree.LeafState; +import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; +import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; +import com.microsoft.azure.toolkit.intellij.appmod.IMigrateChildNodeProvider; +import com.microsoft.azure.toolkit.intellij.appmod.MigrateNodeData; +import com.microsoft.azure.toolkit.intellij.appmod.MigratePluginInstaller; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Project Explorer facet node for "Migrate to Azure" functionality. + * Uses the same extension point as MigrateToAzureNode and MigrateToAzureAction for consistency. + */ +public class MigrateToAzureFacetNode extends AbstractAzureFacetNode { + private static final ExtensionPointName migrationProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + + private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; + + public MigrateToAzureFacetNode(Project project, AzureModule module) { + super(project, module); + initializeNode(); + } + + public void initializeNode() { + updateChildren(); + } + + @Override + public Collection> buildChildren() { + final ArrayList> nodes = new ArrayList<>(); + + if (MigratePluginInstaller.isAppModPluginInstalled()) { + // Load migration options from extension points + final List migrationNodes = loadMigrationNodes(); + + // Convert MigrateNodeData to FacetNode + for (MigrateNodeData nodeData : migrationNodes) { + if (nodeData.isVisible()) { + nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); + } + } + } else { + // Not installed - trigger install confirmation when user tries to expand + ApplicationManager.getApplication().invokeLater(() -> { + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + MigratePluginInstaller.showInstallConfirmation(getProject(), + () -> MigratePluginInstaller.installPlugin(getProject())); + } + }); + } + + return nodes; + } + + /** + * Loads migration nodes from extension point providers. + */ + private List loadMigrationNodes() { + return migrationProviders.getExtensionList().stream() + .filter(provider -> provider.isApplicable(getProject())) + .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(getProject()).stream()) + .collect(Collectors.toList()); + } + + @Override + protected void buildView(@Nonnull PresentationData presentation) { + if (MigratePluginInstaller.isAppModPluginInstalled()) { + presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); + presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } else { + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String text = copilotInstalled + ? "Migrate to Azure (Install App Modernization)" + : "Migrate to Azure (Install Copilot & App Modernization)"; + presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); + presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + } + + /** + * Wrapper class that converts MigrateNodeData to AbstractAzureFacetNode for Project Explorer display. + */ + private static class MigrationNodeWrapper extends AbstractAzureFacetNode { + private final MigrateNodeData nodeData; + + protected MigrationNodeWrapper(Project project, MigrateNodeData nodeData) { + super(project, nodeData); + this.nodeData = nodeData; + } + + @Override + public Collection> buildChildren() { + final ArrayList> children = new ArrayList<>(); + + // Get children - lazy or static (Project Explorer's buildChildren is already lazy) + final List childDataList = nodeData.isLazyLoading() + ? nodeData.getChildrenLoader().get() + : nodeData.getChildren(); + + for (MigrateNodeData child : childDataList) { + if (child.isVisible()) { + children.add(new MigrationNodeWrapper(getProject(), child)); + } + } + + return children; + } + + @Override + protected void buildView(@Nonnull PresentationData presentation) { + presentation.addText(nodeData.getLabel(), com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); + + // Set description if available + if (nodeData.getDescription() != null) { + presentation.setLocationString(nodeData.getDescription()); + } + + // Set tooltip if available + if (nodeData.getTooltip() != null) { + presentation.setTooltip(nodeData.getTooltip()); + } + + // Use node's icon if available, otherwise use default app_mod icon + if (nodeData.getIconPath() != null) { + presentation.setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); + } else { + presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + } + + @Override + public void navigate(boolean requestFocus) { + // Trigger click handler + nodeData.doubleClick(null); + } + + @Override + public boolean canNavigate() { + // Enable navigation for leaf nodes OR nodes with click handlers + return !nodeData.hasChildren() || nodeData.hasClickHandler(); + } + + @Override + public boolean canNavigateToSource() { + // Enable source navigation only for leaf nodes + return !nodeData.hasChildren(); + } + + @Override + public @Nonnull LeafState getLeafState() { + // ALWAYS = leaf node (no expand arrow, double-click triggers navigate) + // NEVER = always show expand arrow + return nodeData.hasChildren() ? LeafState.NEVER : LeafState.ALWAYS; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index c608c642f3c..c5cf3cb789d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -32,7 +32,9 @@ org.gradle.parallel=true org.gradle.console=rich org.gradle.configureondemand=true org.gradle.daemon=true -org.gradle.jvmargs='-Duser.language=en' +org.gradle.workers.max=8 +org.gradle.vfs.watch=true +org.gradle.jvmargs=-Xmx4096m -Xms1024m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -Dfile.encoding=UTF-8 -Duser.language=en org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/settings.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/settings.gradle.kts index 8457ab5446d..e8bb0fa2905 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/settings.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/settings.gradle.kts @@ -51,3 +51,4 @@ include("azure-intellij-plugin-integration-services") include("azure-intellij-plugin-java-sdk") include("azure-intellij-plugin-cloud-shell") include("azure-intellij-plugin-azuremcp") +include("azure-intellij-plugin-appmod") diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml index aa845630aad..b221ed6cb3b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml @@ -44,6 +44,7 @@ + @@ -112,6 +113,8 @@ + + From 75cecba6ac85901345009d1e756b16408501bc18 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 21 Jan 2026 11:38:37 +0800 Subject: [PATCH 02/24] use new extension point interface & refactor --- .../docs/architecture.md | 255 ++++++++++++++++++ ...vider.java => IMigrateOptionProvider.java} | 6 +- .../intellij/appmod/MigrateToAzureAction.java | 58 ++-- .../intellij/appmod/MigrateToAzureNode.java | 6 +- .../MigrateToAzureFacetNode.java | 40 ++- 5 files changed, 320 insertions(+), 45 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{IMigrateChildNodeProvider.java => IMigrateOptionProvider.java} (92%) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md new file mode 100644 index 00000000000..9ca4897f4e6 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md @@ -0,0 +1,255 @@ +# App Modernization Module Architecture + +## Overview + +The `azure-intellij-plugin-appmod` module provides the "Migrate to Azure" functionality in Azure Toolkit for IntelliJ. It serves as a bridge to integrate GitHub Copilot App Modernization plugin with Azure Toolkit. + +## Plugin Relationship Diagram + +``` +┌──────────────────────────────────────────────────────────────────────────────────┐ +│ IntelliJ IDEA │ +│ │ +│ ┌────────────────────────────────────────────────────────────────────────────┐ │ +│ │ Azure Toolkit for IntelliJ │ │ +│ │ │ │ +│ │ ┌────────────────────────┐ ┌────────────────────────────────────────┐ │ │ +│ │ │ service-explorer │ │ resource-connector-lib │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ ┌──────────────────┐ │ │ ┌──────────────────────────────────┐ │ │ │ +│ │ │ │MigrateToAzureNode│ │ │ │ MigrateToAzureFacetNode │ │ │ │ +│ │ │ └────────┬─────────┘ │ │ └───────────────┬──────────────────┘ │ │ │ +│ │ └───────────┼────────────┘ └──────────────────┼─────────────────────┘ │ │ +│ │ │ │ │ │ +│ │ └─────────────────┬──────────────────┘ │ │ +│ │ ▼ │ │ +│ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ azure-intellij-plugin-appmod │ │ │ +│ │ │ │ │ │ +│ │ │ • IMigrateOptionProvider (Extension Point Interface) │ │ │ +│ │ │ • MigrateNodeData (Data Model) │ │ │ +│ │ │ • MigratePluginInstaller (Plugin Detection/Installation) │ │ │ +│ │ │ • MigrateToAzureAction (Context Menu) │ │ │ +│ │ │ │ │ │ +│ │ └──────────────────────────────────┬───────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ │ Extension Point │ │ +│ │ ▼ │ │ +│ └────────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ implements │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────────────┐ │ +│ │ GitHub Copilot App Modernization Plugin │ │ +│ │ (appmod-intellij) │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ MyMigrationProvider implements IMigrateOptionProvider │ │ │ +│ │ │ │ │ │ +│ │ │ • createNodeData() → Returns migration options │ │ │ +│ │ │ • isApplicable() → Check project compatibility │ │ │ +│ │ └──────────────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ Depends on: com.github.copilot (GitHub Copilot) │ │ +│ └────────────────────────────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────┘ +``` + +## Data Flow + +``` +User Action (click/expand) + │ + ▼ +┌─────────────────────┐ +│ Entry Point │ (MigrateToAzureNode / MigrateToAzureFacetNode / MigrateToAzureAction) +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ No ┌─────────────────────┐ +│ Plugin Installed? │────────────▶│ Show Install Dialog │ +└─────────┬───────────┘ └─────────────────────┘ + │ Yes + ▼ +┌─────────────────────┐ +│ Load Extension │ +│ Providers │ +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ +│ Filter by │ +│ isApplicable() │ +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ +│ Sort by Priority │ +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ +│ Call createNodeData │ +│ for each provider │ +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ +│ Display Nodes │ +│ in UI │ +└─────────────────────┘ +``` + +## Module Structure + +``` +azure-intellij-plugin-appmod/ +├── build.gradle.kts +├── docs/ +│ └── architecture.md +└── src/main/ + ├── java/com/microsoft/azure/toolkit/intellij/appmod/ + │ ├── IMigrateOptionProvider.java # Extension Point interface + │ ├── MigrateNodeData.java # Node data model + │ ├── MigratePluginInstaller.java # Plugin detection & installation + │ ├── MigrateToAzureNode.java # Service Explorer entry point + │ ├── MigrateToAzureAction.java # Context menu entry point + │ ├── InstallPluginDialog.java # Installation confirmation dialog + │ └── RestartIdeDialog.java # Restart prompt dialog + └── resources/ + ├── META-INF/azure-intellij-plugin-appmod.xml + └── icons/app_mod.svg +``` + +## Entry Points + +The module provides **three entry points** for users to access migration functionality: + +### 1. Service Explorer Node (`MigrateToAzureNode`) +- **Location**: Azure Explorer panel → "Migrate to Azure" node +- **Behavior**: + - If plugins installed → Shows child nodes from extension providers + - If plugins not installed → Double-click triggers installation dialog + +### 2. Project Explorer Node (`MigrateToAzureFacetNode`) +- **Location**: Project Explorer → Azure facet → "Migrate to Azure" node +- **Note**: Located in `azure-intellij-resource-connector-lib` module (due to `AbstractAzureFacetNode` inheritance) +- **Behavior**: Same as Service Explorer Node + +### 3. Context Menu Action (`MigrateToAzureAction`) +- **Location**: Right-click on project/module → "Migrate to Azure" submenu +- **Behavior**: + - If plugins installed → Shows child actions from extension providers + - If plugins not installed → Single "Install Plugins" action + +## Extension Point + +### Definition +```xml + +``` + +Full ID: `com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider` + +### Interface: `IMigrateOptionProvider` +```java +public interface IMigrateOptionProvider { + // Check if this provider applies to the given project + boolean isApplicable(@Nonnull Project project); + + // Create node data for display (can return multiple nodes) + @Nonnull List createNodeData(@Nonnull Project project); + + // Priority for ordering (lower = first) + default int getPriority() { return 100; } +} +``` + +### Data Model: `MigrateNodeData` +```java +MigrateNodeData.builder() + .label("Node Label") // Required: display text + .description("Optional description") // Shown as location string + .tooltip("Hover tooltip") // Tooltip text + .iconPath("/icons/my_icon.svg") // Icon path (falls back to app_mod.svg) + .visible(true) // Visibility control + .onDoubleClick(anActionEvent -> {...}) // Double-click handler + .children(childList) // Static children + .childrenLoader(() -> loadChildren()) // OR lazy-loaded children + .build(); +``` + +## Plugin Detection & Installation + +### `MigratePluginInstaller` +Central utility class for plugin management: + +```java +// Check if plugins are installed +MigratePluginInstaller.isAppModPluginInstalled(); // com.github.copilot.appmod +MigratePluginInstaller.isCopilotInstalled(); // com.github.copilot + +// Show installation confirmation dialog +MigratePluginInstaller.showInstallConfirmation(project, onConfirmCallback); + +// Trigger installation (IntelliJ handles the rest) +MigratePluginInstaller.installPlugin(project); + +// Dev mode detection (runIde task) +MigratePluginInstaller.isRunningInDevMode(); +``` + +### Installation Flow +1. User triggers install (double-click node or context menu) +2. `showInstallConfirmation()` shows confirmation dialog +3. On confirm, `installPlugin()` calls `PluginsAdvertiser.installAndEnable()` +4. IntelliJ platform handles: + - Plugin selection dialog (with all plugins pre-selected) + - Download and installation + - Restart prompt +5. In dev mode: Special message shown (don't click IDE restart, re-run `./gradlew runIde`) + +## Module Dependencies + +``` +azure-intellij-plugin-appmod (base module) + ↑ + ├── azure-intellij-plugin-service-explorer + │ └── Uses: MigrateToAzureNode, Extension Point + │ + └── azure-intellij-resource-connector-lib + └── Contains: MigrateToAzureFacetNode (due to inheritance constraint) +``` + +### Why `MigrateToAzureFacetNode` is in connector-lib? +- Must extend `AbstractAzureFacetNode` from connector-lib +- Moving `AbstractAzureFacetNode` to appmod would require moving many other classes +- Current design minimizes code changes while maintaining clean architecture + +## External Plugin Integration + +The `appmod-intellij` plugin (GitHub Copilot App Modernization) should: + +1. Add dependency on `azure-intellij-plugin-appmod` +2. Implement `IMigrateOptionProvider` extension +3. Register in its `plugin.xml`: +```xml + + + +``` + +## UI Behavior Summary + +| State | Service Explorer | Project Explorer | Context Menu | +|-------|-----------------|------------------|--------------| +| Plugins NOT installed | Node shows "(Install...)" suffix, double-click triggers install | Same as Service Explorer | Shows "Install Plugins" action | +| Plugins installed | Expand to show child nodes from providers | Same as Service Explorer | Shows submenu with actions from providers | + +## Icon + +- **Path**: `/icons/app_mod.svg` +- **Location**: `azure-intellij-plugin-appmod/src/main/resources/icons/` +- **Usage**: Centralized icon for all migrate-related nodes and actions diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java similarity index 92% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java index 88f4a456c27..f71d88b9e09 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java @@ -21,7 +21,7 @@ * * Example implementation: *
- * public class MyMigrationProvider implements IMigrateChildNodeProvider {
+ * public class MyMigrationProvider implements IMigrateOptionProvider {
  *     @Override
  *     public List<MigrateNodeData> createNodeData(@Nonnull Project project) {
  *         return List.of(
@@ -37,11 +37,11 @@
  * Registration in plugin.xml:
  * 
  * <extensions defaultExtensionNs="com.microsoft.tooling.msservices.intellij.azure">
- *     <migrateChildNodeProvider implementation="your.package.MyMigrationProvider"/>
+ *     <migrateOptionProvider implementation="your.package.MyMigrationProvider"/>
  * </extensions>
  * 
*/ -public interface IMigrateChildNodeProvider { +public interface IMigrateOptionProvider { /** * Creates migration node data for the Migrate to Azure section. diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index cee7bd27b8d..886b38bac78 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -24,24 +24,22 @@ import java.util.stream.Collectors; /** - * Action group that dynamically shows migration options as sub-menu. - * Uses the same extension point as MigrateToAzureNode for consistency. + * Single action group for "Migrate to Azure" functionality. + * - When plugins not installed: sub-menu shows "Install Plugin" option + * - When plugins installed: sub-menu shows migration options from extension providers */ public class MigrateToAzureAction extends ActionGroup { - private static final ExtensionPointName migrationProviders = - ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + private static final ExtensionPointName migrationProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; public MigrateToAzureAction() { - // Set popup=true to show this as a root menu item with expandable sub-menu (▶) - // Without this, children would be displayed directly at root level super("Migrate to Azure", true); } @Override public @NotNull ActionUpdateThread getActionUpdateThread() { - // Run on background thread to avoid blocking EDT when loading migration options return ActionUpdateThread.BGT; } @@ -52,14 +50,10 @@ public void update(@NotNull AnActionEvent e) { if (MigratePluginInstaller.isAppModPluginInstalled()) { e.getPresentation().setText("Migrate to Azure"); - // Only show sub-menu if there are migration options available e.getPresentation().setEnabledAndVisible(project != null && hasMigrationOptions(project)); } else { - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); - final String text = copilotInstalled - ? "Migrate to Azure (Install App Modernization)" - : "Migrate to Azure (Install Copilot & App Modernization)"; - e.getPresentation().setText(text); + // Plugin not installed - still show menu with install option + e.getPresentation().setText("Migrate to Azure"); e.getPresentation().setEnabledAndVisible(true); } } @@ -77,18 +71,7 @@ public void update(@NotNull AnActionEvent e) { // If plugin not installed, show install action if (!MigratePluginInstaller.isAppModPluginInstalled()) { - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); - final String actionText = copilotInstalled - ? "Install GitHub Copilot App Modernization" - : "Install GitHub Copilot and GitHub Copilot App Modernization"; - return new AnAction[]{ - new AnAction(actionText) { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); - } - } - }; + return new AnAction[]{ createInstallAction(project) }; } // Load migration options from extension points @@ -101,6 +84,29 @@ public void actionPerformed(@NotNull AnActionEvent event) { // Convert nodes to actions return convertNodesToActions(migrationNodes); } + + /** + * Creates the install plugin action shown when required plugins are not installed. + */ + private AnAction createInstallAction(@Nonnull Project project) { + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String text = copilotInstalled + ? "Install App Modernization Plugin..." + : "Install Copilot & App Modernization Plugins..."; + + return new AnAction(text) { + { + getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + + @Override + @AzureOperation(name = "user/appmod.install_plugin") + public void actionPerformed(@NotNull AnActionEvent e) { + MigratePluginInstaller.showInstallConfirmation(project, + () -> MigratePluginInstaller.installPlugin(project)); + } + }; + } /** * Loads migration nodes from extension point providers. @@ -108,7 +114,7 @@ public void actionPerformed(@NotNull AnActionEvent event) { private List loadMigrationNodes(@Nonnull Project project) { return migrationProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(project)) - .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(project).stream()) .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index b6395a1ee62..a4c0f08c067 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -19,8 +19,8 @@ * This node extends the azure-toolkit-ide-common-lib Node class to integrate with the Service Explorer tree. */ public final class MigrateToAzureNode extends Node { - private static final ExtensionPointName childProviders = - ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + private static final ExtensionPointName childProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); private final Project project; @@ -63,7 +63,7 @@ public void showMigrationOptions() { // Load migration options from extension points and convert to Node final List nodeDataList = childProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(project)) - .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(project).stream()) .collect(Collectors.toList()); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 8036d60313c..2e812c1c0f9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -6,13 +6,12 @@ package com.microsoft.azure.toolkit.intellij.connector.projectexplorer; import com.intellij.ide.projectView.PresentationData; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.ui.tree.LeafState; import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; -import com.microsoft.azure.toolkit.intellij.appmod.IMigrateChildNodeProvider; +import com.microsoft.azure.toolkit.intellij.appmod.IMigrateOptionProvider; import com.microsoft.azure.toolkit.intellij.appmod.MigrateNodeData; import com.microsoft.azure.toolkit.intellij.appmod.MigratePluginInstaller; @@ -28,8 +27,8 @@ * Uses the same extension point as MigrateToAzureNode and MigrateToAzureAction for consistency. */ public class MigrateToAzureFacetNode extends AbstractAzureFacetNode { - private static final ExtensionPointName migrationProviders = - ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + private static final ExtensionPointName migrationProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; @@ -56,15 +55,8 @@ public Collection> buildChildren() { nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); } } - } else { - // Not installed - trigger install confirmation when user tries to expand - ApplicationManager.getApplication().invokeLater(() -> { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { - MigratePluginInstaller.showInstallConfirmation(getProject(), - () -> MigratePluginInstaller.installPlugin(getProject())); - } - }); } + // When plugin not installed, return empty list - user must double-click to trigger install return nodes; } @@ -75,7 +67,7 @@ public Collection> buildChildren() { private List loadMigrationNodes() { return migrationProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(getProject())) - .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(getProject()).stream()) .collect(Collectors.toList()); } @@ -95,6 +87,28 @@ protected void buildView(@Nonnull PresentationData presentation) { } } + @Override + public void navigate(boolean requestFocus) { + // When plugin not installed, trigger install on double-click + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + MigratePluginInstaller.showInstallConfirmation(getProject(), + () -> MigratePluginInstaller.installPlugin(getProject())); + } + } + + @Override + public boolean canNavigate() { + // Enable navigation (double-click) when plugin is not installed + return !MigratePluginInstaller.isAppModPluginInstalled(); + } + + @Override + public @Nonnull LeafState getLeafState() { + // When plugin not installed, show as leaf node (no expand arrow, double-click triggers navigate) + // When installed, show expand arrow to reveal children + return MigratePluginInstaller.isAppModPluginInstalled() ? LeafState.NEVER : LeafState.ALWAYS; + } + /** * Wrapper class that converts MigrateNodeData to AbstractAzureFacetNode for Project Explorer display. */ From ad32f495decc491acf2da5d7b96f2cfb227f4897 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 21 Jan 2026 11:39:45 +0800 Subject: [PATCH 03/24] update xml --- .../META-INF/azure-intellij-plugin-appmod.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index 00a9ad12c52..e51b0888cd2 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -1,11 +1,13 @@ - + - + From d209bf77b9b73dc5d3992dd212cccb6d9b2e60c1 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 22 Jan 2026 11:27:48 +0800 Subject: [PATCH 04/24] refactor --- .../toolkit/intellij/appmod/Constants.java | 7 + .../appmod/IMigrateOptionProvider.java | 1 - .../intellij/appmod/MigrateNodeData.java | 15 -- .../appmod/MigratePluginInstaller.java | 48 ++--- .../intellij/appmod/MigrateToAzureAction.java | 168 +++--------------- .../appmod/MigrateToAzureInstallAction.java | 55 ++++++ .../intellij/appmod/MigrateToAzureNode.java | 10 +- .../intellij/appmod/RestartIdeDialog.java | 82 --------- .../META-INF/azure-intellij-plugin-appmod.xml | 8 +- .../src/main/resources/icons/app_mod.svg | 22 --- .../src/main/resources/icons/appmod.svg | 13 ++ .../src/main/resources/icons/appmod_dark.svg | 13 ++ .../intellij/common/IntelliJAzureIcons.java | 1 + .../MigrateToAzureFacetNode.java | 23 +-- .../gradle.properties | 2 +- .../src/main/resources/META-INF/plugin.xml | 1 + 16 files changed, 152 insertions(+), 317 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod.svg create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod_dark.svg diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java new file mode 100644 index 00000000000..00b7b605325 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java @@ -0,0 +1,7 @@ +package com.microsoft.azure.toolkit.intellij.appmod; + +public class Constants { + + public static final String ICON_APPMOD_PATH = "/icons/appmod.svg"; + +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java index f71d88b9e09..df7befbf960 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java @@ -26,7 +26,6 @@ * public List<MigrateNodeData> createNodeData(@Nonnull Project project) { * return List.of( * MigrateNodeData.builder("My Migration Option") - * .iconPath("/icons/my_icon.svg") * .onClick(() -> performMigration(project)) * .build() * ); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java index a13f18a1df3..108096ca19a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java @@ -35,7 +35,6 @@ * 1. Simple leaf node with click action: *
  * MigrateNodeData.builder("Option A")
- *     .iconPath("/icons/my_icon.svg")
  *     .onClick(e -> doSomething())
  *     .build();
  * 
@@ -66,12 +65,6 @@ public class MigrateNodeData { @Nonnull private final String label; - /** - * Icon path for the node (e.g., "/icons/app_mod.svg"). If null, a default icon will be used. - */ - @Nullable - private String iconPath; - /** * Description text (shown as secondary text in some views). */ @@ -240,14 +233,6 @@ private Builder(@Nonnull String label) { this.data = new MigrateNodeData(label); } - /** - * Sets the icon path. - */ - public Builder iconPath(@Nullable String iconPath) { - data.iconPath = iconPath; - return this; - } - /** * Sets the description. */ diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java index 37be801300a..82c0cbd6432 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java @@ -86,12 +86,12 @@ public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Ru final boolean copilotInstalled = isCopilotInstalled(); final String title = copilotInstalled - ? "Install GitHub Copilot App Modernization" - : "Install GitHub Copilot and GitHub Copilot App Modernization"; + ? "Install App modernization" + : "Install GitHub Copilot and app modernization"; final String message = copilotInstalled - ? "Do you want to install GitHub Copilot App Modernization plugin?" - : "Do you want to install GitHub Copilot and GitHub Copilot App Modernization plugins?"; + ? "To migrate to Azure, you'll need a plugin: App modernization." + : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; new InstallPluginDialog(project, title) .setLabel(message) @@ -100,31 +100,24 @@ public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Ru } /** - * Installs the App Modernization plugin (and GitHub Copilot if needed). - * IntelliJ platform will handle the restart prompt after installation. - * In dev mode, shows instructions to manually restart runIde task instead. + * Installs the App Modernization plugin. + * IntelliJ platform will automatically install Copilot as a dependency if AppMod declares it. * * @param project The current project */ public static void installPlugin(@Nonnull Project project) { - final boolean copilotInstalled = isCopilotInstalled(); final boolean appModInstalled = isAppModPluginInstalled(); - final boolean isDevMode = isRunningInDevMode(); - // Build plugin ID set - only include plugins that are NOT already installed - final Set pluginsToInstall = new LinkedHashSet<>(); - if (!copilotInstalled) { - pluginsToInstall.add(PluginId.getId(COPILOT_PLUGIN_ID)); - } - if (!appModInstalled) { - pluginsToInstall.add(PluginId.getId(PLUGIN_ID)); - } - - // If all plugins are already installed, nothing to do - if (pluginsToInstall.isEmpty()) { + // If already installed, nothing to do + if (appModInstalled) { return; } + // Only pass AppMod ID - IntelliJ will automatically install Copilot as dependency + // (AppMod's plugin.xml should declare com.github.copilot) + final Set pluginsToInstall = new LinkedHashSet<>(); + pluginsToInstall.add(PluginId.getId(PLUGIN_ID)); + // Use PluginsAdvertiser.installAndEnable - IntelliJ handles the rest // The platform will show plugin selection dialog, download, install, and prompt for restart AzureTaskManager.getInstance().runAndWait(() -> { @@ -135,20 +128,7 @@ public static void installPlugin(@Nonnull Project project) { true, // selectAllInDialog - pre-select all plugins null, // modalityState () -> { - // Called after user confirms installation - if (isDevMode) { - // Dev mode: Show special instructions - ApplicationManager.getApplication().invokeLater(() -> { - final String message = "Plugins are being installed.\n\n" + - "⚠️ DEVELOPMENT MODE:\n" + - "After installation completes, do NOT restart from this IDE window!\n" + - "Instead, stop your current runIde task and relaunch ./gradlew runIde"; - new RestartIdeDialog(project, "Development Mode Notice", message) - .setShowRestartOption(false) - .show(); - }); - } - // Emit event + // Emit event after installation AzureEventBus.emit("migrate.plugin.installed"); } ); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index 886b38bac78..d907191fb4a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -5,6 +5,7 @@ package com.microsoft.azure.toolkit.intellij.appmod; +import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.ActionGroup; import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnAction; @@ -24,16 +25,16 @@ import java.util.stream.Collectors; /** - * Single action group for "Migrate to Azure" functionality. - * - When plugins not installed: sub-menu shows "Install Plugin" option - * - When plugins installed: sub-menu shows migration options from extension providers + * ActionGroup for "Migrate to Azure" functionality. + * Only shown when App Modernization plugin IS installed. + * Shows migration options as sub-menu from extension providers. + * + * Mutually exclusive with MigrateToAzureInstallAction (AnAction) which is shown when plugin is NOT installed. */ public class MigrateToAzureAction extends ActionGroup { private static final ExtensionPointName migrationProviders = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); - private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; - public MigrateToAzureAction() { super("Migrate to Azure", true); } @@ -48,14 +49,14 @@ public void update(@NotNull AnActionEvent e) { super.update(e); final Project project = e.getProject(); - if (MigratePluginInstaller.isAppModPluginInstalled()) { - e.getPresentation().setText("Migrate to Azure"); - e.getPresentation().setEnabledAndVisible(project != null && hasMigrationOptions(project)); - } else { - // Plugin not installed - still show menu with install option - e.getPresentation().setText("Migrate to Azure"); - e.getPresentation().setEnabledAndVisible(true); + // Only visible when plugin IS installed (MigrateToAzureInstallAction handles uninstalled case) + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setEnabledAndVisible(false); + return; } + + e.getPresentation().setText("Migrate to Azure"); + e.getPresentation().setEnabledAndVisible(project != null); } @Override @@ -69,16 +70,11 @@ public void update(@NotNull AnActionEvent e) { return AnAction.EMPTY_ARRAY; } - // If plugin not installed, show install action - if (!MigratePluginInstaller.isAppModPluginInstalled()) { - return new AnAction[]{ createInstallAction(project) }; - } - // Load migration options from extension points final List migrationNodes = loadMigrationNodes(project); if (migrationNodes.isEmpty()) { - return AnAction.EMPTY_ARRAY; + return new AnAction[]{ createNoOptionsAction() }; } // Convert nodes to actions @@ -86,24 +82,17 @@ public void update(@NotNull AnActionEvent e) { } /** - * Creates the install plugin action shown when required plugins are not installed. + * Creates a disabled action shown when no migration options are available. */ - private AnAction createInstallAction(@Nonnull Project project) { - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); - final String text = copilotInstalled - ? "Install App Modernization Plugin..." - : "Install Copilot & App Modernization Plugins..."; - - return new AnAction(text) { + private AnAction createNoOptionsAction() { + return new AnAction("No migration options available") { { - getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + getTemplatePresentation().setEnabled(false); } @Override - @AzureOperation(name = "user/appmod.install_plugin") public void actionPerformed(@NotNull AnActionEvent e) { - MigratePluginInstaller.showInstallConfirmation(project, - () -> MigratePluginInstaller.installPlugin(project)); + // No-op } }; } @@ -120,14 +109,6 @@ private List loadMigrationNodes(@Nonnull Project project) { .collect(Collectors.toList()); } - /** - * Checks if there are any migration options available. - */ - private boolean hasMigrationOptions(@Nonnull Project project) { - return migrationProviders.getExtensionList().stream() - .anyMatch(provider -> provider.isApplicable(project)); - } - /** * Converts node tree to action tree for sub-menu display. */ @@ -144,25 +125,18 @@ private AnAction[] convertNodesToActions(List nodes) { */ private AnAction convertNodeToAction(MigrateNodeData nodeData) { if (nodeData.hasChildren()) { - // For lazy loading nodes, create a LazyActionGroup that loads children on demand - if (nodeData.isLazyLoading()) { - return new LazyActionGroup(nodeData); - } - - // Node with static children -> create sub-menu with children added immediately + // Node with children -> create sub-menu final DefaultActionGroup subgroup = new DefaultActionGroup(); subgroup.getTemplatePresentation().setText(nodeData.getLabel(), false); subgroup.setPopup(true); + subgroup.getTemplatePresentation().setIcon(AllIcons.Vcs.Changelist); + + // Handle lazy loading or static children + final List children = nodeData.isLazyLoading() + ? nodeData.getChildrenLoader().get() + : nodeData.getChildren(); - // Use node's icon if available, otherwise use default app_mod icon - if (nodeData.getIconPath() != null) { - subgroup.getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); - } else { - subgroup.getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); - } - - // Add static children - for (MigrateNodeData child : nodeData.getChildren()) { + for (MigrateNodeData child : children) { if (child.isVisible()) { subgroup.add(convertNodeToAction(child)); } @@ -173,12 +147,7 @@ private AnAction convertNodeToAction(MigrateNodeData nodeData) { // Leaf node -> create clickable action return new AnAction(nodeData.getLabel()) { { - // Use node's icon if available, otherwise use default app_mod icon - if (nodeData.getIconPath() != null) { - getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); - } else { - getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); - } + getTemplatePresentation().setIcon(AllIcons.Vcs.Changelist); if (nodeData.getDescription() != null) { getTemplatePresentation().setDescription(nodeData.getDescription()); } @@ -190,90 +159,11 @@ public void update(@NotNull AnActionEvent e) { } @Override - @AzureOperation(name = "user/common.migrate_to_azure.trigger_option") + @AzureOperation(name = "user/appmod.trigger_migrate_option") public void actionPerformed(@NotNull AnActionEvent e) { nodeData.click(e); } }; } } - - /** - * ActionGroup that loads children lazily when the submenu is expanded. - * This avoids blocking the UI when the parent menu is shown. - */ - private class LazyActionGroup extends ActionGroup { - private final MigrateNodeData nodeData; - private volatile AnAction[] cachedChildren; - private volatile boolean isLoading = false; - - // Loading placeholder action - private final AnAction loadingAction = new AnAction("Loading...") { - { - getTemplatePresentation().setEnabled(false); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - // No-op, this is just a placeholder - } - }; - - LazyActionGroup(MigrateNodeData nodeData) { - super(nodeData.getLabel(), true); - this.nodeData = nodeData; - - // Set icon - if (nodeData.getIconPath() != null) { - getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); - } else { - getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); - } - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - // Load children on background thread to avoid blocking EDT - return ActionUpdateThread.BGT; - } - - @Override - public AnAction @NotNull [] getChildren(@Nullable AnActionEvent e) { - if (cachedChildren != null) { - return cachedChildren; - } - - if (!isLoading) { - isLoading = true; - // Load children lazily - final List children = nodeData.getChildrenLoader().get(); - final List actions = new ArrayList<>(); - for (MigrateNodeData child : children) { - if (child.isVisible()) { - actions.add(convertNodeToAction(child)); - } - } - cachedChildren = actions.isEmpty() - ? new AnAction[]{ createNoOptionsAction() } - : actions.toArray(new AnAction[0]); - return cachedChildren; - } - - // Still loading, show placeholder - return new AnAction[]{ loadingAction }; - } - - private AnAction createNoOptionsAction() { - return new AnAction("No migration options available") { - { - getTemplatePresentation().setEnabled(false); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - // No-op - } - }; - } - } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java new file mode 100644 index 00000000000..37b147d669e --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; +import org.jetbrains.annotations.NotNull; + +/** + * Action shown when App Modernization plugin is NOT installed. + * Click directly triggers plugin installation (no sub-menu). + * + * Mutually exclusive with MigrateToAzureAction (ActionGroup) which is shown when plugin IS installed. + */ +public class MigrateToAzureInstallAction extends AnAction { + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + // Only visible when plugin is NOT installed + if (MigratePluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String text = copilotInstalled + ? "Migrate to Azure (Install App Modernization)" + : "Migrate to Azure (Install Copilot and App Modernization)"; + e.getPresentation().setText(text); + e.getPresentation().setEnabledAndVisible(true); + } + + @Override + @AzureOperation(name = "user/appmod.install_plugin") + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getProject(); + if (project == null) { + return; + } + + MigratePluginInstaller.showInstallConfirmation(project, + () -> MigratePluginInstaller.installPlugin(project)); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index a4c0f08c067..857df2a6012 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -24,7 +24,8 @@ public final class MigrateToAzureNode extends Node { private final Project project; - private static final AzureIcon APP_MOD_ICON = AzureIcon.builder().iconPath("/icons/app_mod.svg").build(); + private static final AzureIcon APP_MOD_ICON = AzureIcon.builder().iconPath(Constants.ICON_APPMOD_PATH).build(); + private static final AzureIcon CHANGELIST_ICON = AzureIcon.builder().iconPath("/icons/changelist").build(); public MigrateToAzureNode(Project project) { super("Migrate to Azure"); @@ -81,11 +82,8 @@ private Node convertToNode(MigrateNodeData data) { // Set basic properties node.withLabel(d -> d.getLabel()); - // Use node's icon if available, otherwise use default app_mod icon - node.withIcon(d -> d.getIconPath() != null ? AzureIcon.builder().iconPath(d.getIconPath()).build() : APP_MOD_ICON); - if (data.getDescription() != null) { - node.withDescription(d -> d.getDescription()); - } + // Use Changelist icon for child nodes + node.withIcon(CHANGELIST_ICON); if (data.getTooltip() != null) { node.withTips(d -> d.getTooltip()); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java deleted file mode 100644 index ab6ef884794..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -package com.microsoft.azure.toolkit.intellij.appmod; - -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.DialogWrapper; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.awt.*; - -/** - * Dialog for prompting user after plugin installation. - * Can be configured to show restart options or just an info message. - */ -public class RestartIdeDialog extends DialogWrapper { - - private final String message; - private boolean showRestartOption = true; - - public RestartIdeDialog(Project project, String title, String message) { - super(project, true); - this.message = message; - setSize(450, 150); - setTitle(title); - setOKButtonText("Restart Now"); - setCancelButtonText("Restart Later"); - init(); - } - - /** - * Sets whether to show restart option or just an info dialog. - * @param showRestartOption true for restart dialog, false for info-only dialog - */ - public RestartIdeDialog setShowRestartOption(boolean showRestartOption) { - this.showRestartOption = showRestartOption; - if (!showRestartOption) { - setOKButtonText("Got it"); - } - return this; - } - - @Override - protected @Nullable JComponent createCenterPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - // Support HTML for multi-line messages - JLabel labelComponent; - if (message != null && message.contains("\n")) { - String htmlMessage = "" + message.replace("\n", "
") + ""; - labelComponent = new JLabel(htmlMessage); - } else { - labelComponent = new JLabel(message); - } - labelComponent.setHorizontalAlignment(SwingConstants.CENTER); - panel.add(labelComponent, BorderLayout.CENTER); - return panel; - } - - @Override - protected Action @NotNull [] createActions() { - if (showRestartOption) { - return new Action[]{getOKAction(), getCancelAction()}; - } else { - return new Action[]{getOKAction()}; - } - } - - @Override - protected void doOKAction() { - super.doOKAction(); - if (showRestartOption) { - ApplicationManager.getApplication().restart(); - } - } -} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index e51b0888cd2..97ccb101911 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -5,9 +5,15 @@ + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg deleted file mode 100644 index cc40914cd6c..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod.svg b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod.svg new file mode 100644 index 00000000000..0558ebd7969 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod_dark.svg b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod_dark.svg new file mode 100644 index 00000000000..f44426f34f1 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod_dark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/IntelliJAzureIcons.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/IntelliJAzureIcons.java index b6efe061ec0..4d5a4f93f11 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/IntelliJAzureIcons.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/IntelliJAzureIcons.java @@ -32,6 +32,7 @@ public class IntelliJAzureIcons { put("/icons/spinner", AnimatedIcon.Default.INSTANCE); put("/icons/error", AllIcons.General.Error); put("/icons/unknown", AllIcons.Nodes.Unknown); + put("/icons/changelist", AllIcons.Vcs.Changelist); } }; private static final Map azureIcons = new ConcurrentHashMap<>() { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 2e812c1c0f9..5ef9f2de18d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -5,10 +5,12 @@ package com.microsoft.azure.toolkit.intellij.connector.projectexplorer; +import com.intellij.icons.AllIcons; import com.intellij.ide.projectView.PresentationData; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.ui.tree.LeafState; +import com.microsoft.azure.toolkit.intellij.appmod.Constants; import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; import com.microsoft.azure.toolkit.intellij.appmod.IMigrateOptionProvider; @@ -30,8 +32,6 @@ public class MigrateToAzureFacetNode extends AbstractAzureFacetNode private static final ExtensionPointName migrationProviders = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); - private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; - public MigrateToAzureFacetNode(Project project, AzureModule module) { super(project, module); initializeNode(); @@ -76,14 +76,14 @@ private List loadMigrationNodes() { protected void buildView(@Nonnull PresentationData presentation) { if (MigratePluginInstaller.isAppModPluginInstalled()) { presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); } else { final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); final String text = copilotInstalled - ? "Migrate to Azure (Install App Modernization)" - : "Migrate to Azure (Install Copilot & App Modernization)"; + ? "Migrate to Azure (Install App modernization)" + : "Migrate to Azure (Install GitHub Copilot and app modernization)"; presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); } } @@ -142,22 +142,13 @@ public Collection> buildChildren() { protected void buildView(@Nonnull PresentationData presentation) { presentation.addText(nodeData.getLabel(), com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - // Set description if available - if (nodeData.getDescription() != null) { - presentation.setLocationString(nodeData.getDescription()); - } - // Set tooltip if available if (nodeData.getTooltip() != null) { presentation.setTooltip(nodeData.getTooltip()); } // Use node's icon if available, otherwise use default app_mod icon - if (nodeData.getIconPath() != null) { - presentation.setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); - } else { - presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); - } + presentation.setIcon(AllIcons.Vcs.Changelist); } @Override diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index c5cf3cb789d..f5051491877 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -3,7 +3,7 @@ intellijDisplayVersion=2025.3 intellij_version=253-EAP-SNAPSHOT platformVersion=253-EAP-SNAPSHOT # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html -pluginSinceBuild=253 +pluginSinceBuild=251 pluginUntilBuild=253.* # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP platformPlugins=org.intellij.scala:2025.3.12 diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml index b221ed6cb3b..fb568944674 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml @@ -113,6 +113,7 @@ + From d4e5f7a0251b4ec9b1a13b3b82e46a81e21202fa Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 22 Jan 2026 15:01:40 +0800 Subject: [PATCH 05/24] add jumpping to app mod panel if there is no recommendation data --- .../intellij/appmod/AppModPanelHelper.java | 41 ++++++++++++++++ .../intellij/appmod/MigrateToAzureAction.java | 15 +++--- .../intellij/appmod/MigrateToAzureNode.java | 29 ++++++++--- .../MigrateToAzureFacetNode.java | 49 +++++++++++++++++-- 4 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java new file mode 100644 index 00000000000..76484f51829 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; +import com.microsoft.azure.toolkit.lib.common.messager.AzureMessager; + +import javax.annotation.Nonnull; + +/** + * Helper class for opening the App Modernization Panel. + */ +public final class AppModPanelHelper { + + public static final String TOOL_WINDOW_ID = "GitHub Copilot app modernization"; + + private AppModPanelHelper() { + // Utility class + } + + /** + * Opens the App Modernization Panel tool window. + * + * @param project the current project + */ + public static void openAppModPanel(@Nonnull Project project) { + final ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project); + final ToolWindow toolWindow = toolWindowManager.getToolWindow(TOOL_WINDOW_ID); + + if (toolWindow != null) { + toolWindow.show(); + } else { + AzureMessager.getMessager().warning("App Modernization Panel is not available. Please ensure the GitHub Copilot App Modernization plugin is installed and enabled."); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index d907191fb4a..7d327e4ada8 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -13,6 +13,8 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; import org.jetbrains.annotations.NotNull; @@ -82,17 +84,16 @@ public void update(@NotNull AnActionEvent e) { } /** - * Creates a disabled action shown when no migration options are available. + * Creates an action to open App Modernization Panel when no migration options are available. */ private AnAction createNoOptionsAction() { - return new AnAction("No migration options available") { - { - getTemplatePresentation().setEnabled(false); - } - + return new AnAction("Get Started with App Modernization") { @Override public void actionPerformed(@NotNull AnActionEvent e) { - // No-op + final Project project = e.getProject(); + if (project != null) { + AppModPanelHelper.openAppModPanel(project); + } } }; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index 857df2a6012..6647002ac6e 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -9,6 +9,7 @@ import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.ide.common.component.Node; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; +import com.microsoft.azure.toolkit.lib.common.messager.AzureMessager; import java.util.Comparator; import java.util.List; @@ -26,6 +27,7 @@ public final class MigrateToAzureNode extends Node { private static final AzureIcon APP_MOD_ICON = AzureIcon.builder().iconPath(Constants.ICON_APPMOD_PATH).build(); private static final AzureIcon CHANGELIST_ICON = AzureIcon.builder().iconPath("/icons/changelist").build(); + private static final AzureIcon TOOLWINDOW_ICON = AzureIcon.builder().iconPath("/icons/toolWindowProject").build(); public MigrateToAzureNode(Project project) { super("Migrate to Azure"); @@ -47,8 +49,8 @@ private void showNotInstalled() { // Dynamic description based on what needs to be installed final String description = copilotInstalled - ? "Install GitHub Copilot App Modernization" - : "Install GitHub Copilot and GitHub Copilot App Modernization"; + ? "Install App modernizationn" + : "Install GitHub Copilot and app modernization"; withDescription(description); onClicked(e -> { @@ -68,10 +70,25 @@ public void showMigrationOptions() { .flatMap(provider -> provider.createNodeData(project).stream()) .collect(Collectors.toList()); - // Convert MigrateNodeData to Node and add as children - nodeDataList.stream() - .map(this::convertToNode) - .forEach(this::addChild); + if (nodeDataList.isEmpty()) { + // No migration options - add prompt to open App Modernization Panel + addChild(createOpenPanelNode()); + } else { + // Convert MigrateNodeData to Node and add as children + nodeDataList.stream() + .map(this::convertToNode) + .forEach(this::addChild); + } + } + + /** + * Creates a node that opens the App Modernization Panel. + */ + private Node createOpenPanelNode() { + Node node = new Node<>("Get Started with App Modernization"); + node.withIcon(TOOLWINDOW_ICON); + node.onClicked(data -> AppModPanelHelper.openAppModPanel(project)); + return node; } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 5ef9f2de18d..a4a705cbb3c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -10,6 +10,7 @@ import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.ui.tree.LeafState; +import com.microsoft.azure.toolkit.intellij.appmod.AppModPanelHelper; import com.microsoft.azure.toolkit.intellij.appmod.Constants; import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; @@ -49,10 +50,15 @@ public Collection> buildChildren() { // Load migration options from extension points final List migrationNodes = loadMigrationNodes(); - // Convert MigrateNodeData to FacetNode - for (MigrateNodeData nodeData : migrationNodes) { - if (nodeData.isVisible()) { - nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); + if (migrationNodes.isEmpty()) { + // No migration options - add prompt to open App Modernization Panel + nodes.add(new OpenPanelNode(getProject())); + } else { + // Convert MigrateNodeData to FacetNode + for (MigrateNodeData nodeData : migrationNodes) { + if (nodeData.isVisible()) { + nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); + } } } } @@ -109,6 +115,41 @@ public boolean canNavigate() { return MigratePluginInstaller.isAppModPluginInstalled() ? LeafState.NEVER : LeafState.ALWAYS; } + /** + * Node that opens the App Modernization Panel when no migration options are available. + */ + private static class OpenPanelNode extends AbstractAzureFacetNode { + protected OpenPanelNode(Project project) { + super(project, "Get Started with App Modernization"); + } + + @Override + public Collection> buildChildren() { + return List.of(); + } + + @Override + protected void buildView(@Nonnull PresentationData presentation) { + presentation.addText("Get Started with App Modernization", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); + presentation.setIcon(AllIcons.Toolwindows.ToolWindowProject); + } + + @Override + public void navigate(boolean requestFocus) { + AppModPanelHelper.openAppModPanel(getProject()); + } + + @Override + public boolean canNavigate() { + return true; + } + + @Override + public @Nonnull LeafState getLeafState() { + return LeafState.ALWAYS; + } + } + /** * Wrapper class that converts MigrateNodeData to AbstractAzureFacetNode for Project Explorer display. */ From 6f437ae3f7c7984597af9d58a4c3daeb32fb1785 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 22 Jan 2026 16:22:35 +0800 Subject: [PATCH 06/24] enhance open app mod panel --- .../intellij/appmod/MigrateToAzureAction.java | 39 +++++---- .../appmod/MigrateToAzureInstallAction.java | 81 +++++++++++++++---- .../intellij/appmod/MigrateToAzureNode.java | 18 +---- .../MigrateToAzureFacetNode.java | 79 ++++++------------ 4 files changed, 115 insertions(+), 102 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index 7d327e4ada8..c1c343c828b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -51,14 +51,30 @@ public void update(@NotNull AnActionEvent e) { super.update(e); final Project project = e.getProject(); - // Only visible when plugin IS installed (MigrateToAzureInstallAction handles uninstalled case) + // Only visible when plugin IS installed AND has migration options + // (MigrateToAzureInstallAction handles not-installed and no-options cases) if (!MigratePluginInstaller.isAppModPluginInstalled()) { e.getPresentation().setEnabledAndVisible(false); return; } + if (project == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Check if there are any migration options + final boolean hasOptions = loadMigrationNodes(project).stream() + .anyMatch(MigrateNodeData::isVisible); + + if (!hasOptions) { + // No options - hide, MigrateToAzureInstallAction will handle this + e.getPresentation().setEnabledAndVisible(false); + return; + } + e.getPresentation().setText("Migrate to Azure"); - e.getPresentation().setEnabledAndVisible(project != null); + e.getPresentation().setEnabledAndVisible(true); } @Override @@ -74,29 +90,10 @@ public void update(@NotNull AnActionEvent e) { // Load migration options from extension points final List migrationNodes = loadMigrationNodes(project); - - if (migrationNodes.isEmpty()) { - return new AnAction[]{ createNoOptionsAction() }; - } // Convert nodes to actions return convertNodesToActions(migrationNodes); } - - /** - * Creates an action to open App Modernization Panel when no migration options are available. - */ - private AnAction createNoOptionsAction() { - return new AnAction("Get Started with App Modernization") { - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - final Project project = e.getProject(); - if (project != null) { - AppModPanelHelper.openAppModPanel(project); - } - } - }; - } /** * Loads migration nodes from extension point providers. diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java index 37b147d669e..21c477af4a7 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java @@ -8,17 +8,33 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; import org.jetbrains.annotations.NotNull; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + /** - * Action shown when App Modernization plugin is NOT installed. - * Click directly triggers plugin installation (no sub-menu). + * Action for "Migrate to Azure" when: + * 1. App Modernization plugin is NOT installed - click triggers installation + * 2. Plugin IS installed but no migration options available - click opens App Mod Panel * - * Mutually exclusive with MigrateToAzureAction (ActionGroup) which is shown when plugin IS installed. + * Mutually exclusive with MigrateToAzureAction (ActionGroup) which is shown when plugin IS installed AND has migration options. */ public class MigrateToAzureInstallAction extends AnAction { + private static final ExtensionPointName migrationProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); + + private enum State { + NOT_INSTALLED, + NO_OPTIONS, + HAS_OPTIONS + } + + private State currentState = State.NOT_INSTALLED; @Override public @NotNull ActionUpdateThread getActionUpdateThread() { @@ -27,29 +43,66 @@ public class MigrateToAzureInstallAction extends AnAction { @Override public void update(@NotNull AnActionEvent e) { - // Only visible when plugin is NOT installed - if (MigratePluginInstaller.isAppModPluginInstalled()) { + final Project project = e.getProject(); + if (project == null) { e.getPresentation().setEnabledAndVisible(false); return; } - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); - final String text = copilotInstalled - ? "Migrate to Azure (Install App Modernization)" - : "Migrate to Azure (Install Copilot and App Modernization)"; - e.getPresentation().setText(text); - e.getPresentation().setEnabledAndVisible(true); + currentState = determineState(project); + + switch (currentState) { + case NOT_INSTALLED: + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String text = copilotInstalled + ? "Migrate to Azure (Install App modernizationn)" + : "Migrate to Azure (Install GitHub Copilot and app modernization)"; + e.getPresentation().setText(text); + e.getPresentation().setEnabledAndVisible(true); + break; + case NO_OPTIONS: + e.getPresentation().setText("Migrate to Azure (Open GitHub Copilot app modernization)"); + e.getPresentation().setEnabledAndVisible(true); + break; + case HAS_OPTIONS: + // Hide - MigrateToAzureAction (ActionGroup) will show instead + e.getPresentation().setEnabledAndVisible(false); + break; + } + } + + private State determineState(Project project) { + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + return State.NOT_INSTALLED; + } + + final boolean hasOptions = migrationProviders.getExtensionList().stream() + .filter(provider -> provider.isApplicable(project)) + .flatMap(provider -> provider.createNodeData(project).stream()) + .anyMatch(MigrateNodeData::isVisible); + + return hasOptions ? State.HAS_OPTIONS : State.NO_OPTIONS; } @Override - @AzureOperation(name = "user/appmod.install_plugin") + @AzureOperation(name = "user/appmod.migrate_action") public void actionPerformed(@NotNull AnActionEvent e) { final Project project = e.getProject(); if (project == null) { return; } - MigratePluginInstaller.showInstallConfirmation(project, - () -> MigratePluginInstaller.installPlugin(project)); + switch (currentState) { + case NOT_INSTALLED: + MigratePluginInstaller.showInstallConfirmation(project, + () -> MigratePluginInstaller.installPlugin(project)); + break; + case NO_OPTIONS: + AppModPanelHelper.openAppModPanel(project); + break; + case HAS_OPTIONS: + // Should not happen - action is hidden in this state + break; + } } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index 6647002ac6e..111d8eb2d5a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -27,7 +27,6 @@ public final class MigrateToAzureNode extends Node { private static final AzureIcon APP_MOD_ICON = AzureIcon.builder().iconPath(Constants.ICON_APPMOD_PATH).build(); private static final AzureIcon CHANGELIST_ICON = AzureIcon.builder().iconPath("/icons/changelist").build(); - private static final AzureIcon TOOLWINDOW_ICON = AzureIcon.builder().iconPath("/icons/toolWindowProject").build(); public MigrateToAzureNode(Project project) { super("Migrate to Azure"); @@ -61,7 +60,6 @@ private void showNotInstalled() { public void showMigrationOptions() { clearClickHandlers(); - withDescription(""); // Load migration options from extension points and convert to Node final List nodeDataList = childProviders.getExtensionList().stream() @@ -71,9 +69,11 @@ public void showMigrationOptions() { .collect(Collectors.toList()); if (nodeDataList.isEmpty()) { - // No migration options - add prompt to open App Modernization Panel - addChild(createOpenPanelNode()); + // No migration options - click to open App Modernization Panel + withDescription("Open GitHub Copilot app modernization"); + onClicked(e -> AppModPanelHelper.openAppModPanel(project)); } else { + withDescription(""); // Convert MigrateNodeData to Node and add as children nodeDataList.stream() .map(this::convertToNode) @@ -81,16 +81,6 @@ public void showMigrationOptions() { } } - /** - * Creates a node that opens the App Modernization Panel. - */ - private Node createOpenPanelNode() { - Node node = new Node<>("Get Started with App Modernization"); - node.withIcon(TOOLWINDOW_ICON); - node.onClicked(data -> AppModPanelHelper.openAppModPanel(project)); - return node; - } - /** * Converts MigrateNodeData to Node for Service Explorer compatibility. */ diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index a4a705cbb3c..065c095e206 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -33,6 +33,8 @@ public class MigrateToAzureFacetNode extends AbstractAzureFacetNode private static final ExtensionPointName migrationProviders = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); + private boolean hasMigrationOptions = false; + public MigrateToAzureFacetNode(Project project, AzureModule module) { super(project, module); initializeNode(); @@ -49,18 +51,15 @@ public Collection> buildChildren() { if (MigratePluginInstaller.isAppModPluginInstalled()) { // Load migration options from extension points final List migrationNodes = loadMigrationNodes(); + hasMigrationOptions = !migrationNodes.isEmpty(); - if (migrationNodes.isEmpty()) { - // No migration options - add prompt to open App Modernization Panel - nodes.add(new OpenPanelNode(getProject())); - } else { - // Convert MigrateNodeData to FacetNode - for (MigrateNodeData nodeData : migrationNodes) { - if (nodeData.isVisible()) { - nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); - } + // Convert MigrateNodeData to FacetNode + for (MigrateNodeData nodeData : migrationNodes) { + if (nodeData.isVisible()) { + nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); } } + // When no migration options, return empty - double-click opens App Mod Panel } // When plugin not installed, return empty list - user must double-click to trigger install @@ -80,74 +79,48 @@ private List loadMigrationNodes() { @Override protected void buildView(@Nonnull PresentationData presentation) { - if (MigratePluginInstaller.isAppModPluginInstalled()) { - presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); - } else { + presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); + + if (!MigratePluginInstaller.isAppModPluginInstalled()) { final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); final String text = copilotInstalled ? "Migrate to Azure (Install App modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"; presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); + } else if (!hasMigrationOptions) { + presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); + presentation.setLocationString("Open GitHub Copilot app modernization"); + } else { + presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); } } @Override public void navigate(boolean requestFocus) { - // When plugin not installed, trigger install on double-click if (!MigratePluginInstaller.isAppModPluginInstalled()) { + // Plugin not installed - trigger install on double-click MigratePluginInstaller.showInstallConfirmation(getProject(), () -> MigratePluginInstaller.installPlugin(getProject())); + } else if (!hasMigrationOptions) { + // No migration options - open App Modernization Panel + AppModPanelHelper.openAppModPanel(getProject()); } } @Override public boolean canNavigate() { - // Enable navigation (double-click) when plugin is not installed - return !MigratePluginInstaller.isAppModPluginInstalled(); + // Enable navigation when plugin is not installed OR when no migration options + return !MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions; } @Override public @Nonnull LeafState getLeafState() { - // When plugin not installed, show as leaf node (no expand arrow, double-click triggers navigate) - // When installed, show expand arrow to reveal children - return MigratePluginInstaller.isAppModPluginInstalled() ? LeafState.NEVER : LeafState.ALWAYS; - } - - /** - * Node that opens the App Modernization Panel when no migration options are available. - */ - private static class OpenPanelNode extends AbstractAzureFacetNode { - protected OpenPanelNode(Project project) { - super(project, "Get Started with App Modernization"); - } - - @Override - public Collection> buildChildren() { - return List.of(); - } - - @Override - protected void buildView(@Nonnull PresentationData presentation) { - presentation.addText("Get Started with App Modernization", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - presentation.setIcon(AllIcons.Toolwindows.ToolWindowProject); - } - - @Override - public void navigate(boolean requestFocus) { - AppModPanelHelper.openAppModPanel(getProject()); - } - - @Override - public boolean canNavigate() { - return true; - } - - @Override - public @Nonnull LeafState getLeafState() { + // Show as leaf node (no expand arrow) when plugin not installed or no migration options + // Show expand arrow when there are migration options to reveal + if (!MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions) { return LeafState.ALWAYS; } + return LeafState.NEVER; } /** From 2c70d74a267805876943c4222d5b0b02f27eb1c6 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 23 Jan 2026 01:18:15 +0800 Subject: [PATCH 07/24] 1. use one MigrateToAzureAction instead MigrateToAzureAction and MigrateToAzureInstallAction 2. Update the extension point invocation timing 3. add refresh button to manual refresh recommendation data --- .../appmod/MigratePluginInstaller.java | 2 - .../intellij/appmod/MigrateToAzureAction.java | 189 +++++++++++------- .../appmod/MigrateToAzureInstallAction.java | 108 ---------- .../intellij/appmod/MigrateToAzureNode.java | 81 ++++++-- .../META-INF/azure-intellij-plugin-appmod.xml | 10 +- .../intellij/explorer/AzureExplorer.java | 22 -- .../MigrateToAzureFacetNode.java | 107 ++++++---- .../src/main/resources/META-INF/plugin.xml | 1 - 8 files changed, 250 insertions(+), 270 deletions(-) delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java index 82c0cbd6432..054d09791b4 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java @@ -128,8 +128,6 @@ public static void installPlugin(@Nonnull Project project) { true, // selectAllInDialog - pre-select all plugins null, // modalityState () -> { - // Emit event after installation - AzureEventBus.emit("migrate.plugin.installed"); } ); }); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index c1c343c828b..5d5c6e6f4b8 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -11,70 +11,142 @@ import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; -import com.intellij.openapi.wm.ToolWindow; -import com.intellij.openapi.wm.ToolWindowManager; -import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; +import com.intellij.openapi.util.Key; import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import javax.annotation.Nonnull; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; /** - * ActionGroup for "Migrate to Azure" functionality. - * Only shown when App Modernization plugin IS installed. - * Shows migration options as sub-menu from extension providers. + * Unified ActionGroup for "Migrate to Azure" functionality. + * Handles all three states: + * 1. Plugin NOT installed - direct click triggers installation + * 2. Plugin installed but no migration options - shows "Open App Mod Panel" action + * 3. Plugin installed with migration options - sub-menu shows migration options * - * Mutually exclusive with MigrateToAzureInstallAction (AnAction) which is shown when plugin is NOT installed. + * Data is loaded once on first access and cached in Project.getUserData. */ public class MigrateToAzureAction extends ActionGroup { - private static final ExtensionPointName migrationProviders = + private static final ExtensionPointName MIGRATION_PROVIDERS = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); + private static final Key STATE_KEY = Key.create("azure.migrate.action.state"); - public MigrateToAzureAction() { - super("Migrate to Azure", true); + private enum State { NOT_INSTALLED, NO_OPTIONS, HAS_OPTIONS } + + private static class MigrationState { + final State state; + final List nodes; + + MigrationState(State state, List nodes) { + this.state = state; + this.nodes = nodes; + } } @Override public @NotNull ActionUpdateThread getActionUpdateThread() { return ActionUpdateThread.BGT; } + + /** + * Gets or computes migration state for the project. + * State is cached in Project.getUserData and loaded once on first access. + */ + private MigrationState getOrComputeState(Project project) { + MigrationState state = project.getUserData(STATE_KEY); + if (state == null) { + state = computeState(project); + project.putUserData(STATE_KEY, state); + } + return state; + } + + /** + * Computes migration state by calling providers. + */ + private MigrationState computeState(Project project) { + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + return new MigrationState(State.NOT_INSTALLED, List.of()); + } + + final List nodes = MIGRATION_PROVIDERS.getExtensionList().stream() + .filter(provider -> provider.isApplicable(project)) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(project).stream()) + .filter(MigrateNodeData::isVisible) + .collect(Collectors.toList()); + + return new MigrationState( + nodes.isEmpty() ? State.NO_OPTIONS : State.HAS_OPTIONS, + nodes + ); + } @Override public void update(@NotNull AnActionEvent e) { - super.update(e); final Project project = e.getProject(); - - // Only visible when plugin IS installed AND has migration options - // (MigrateToAzureInstallAction handles not-installed and no-options cases) - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (project == null) { e.getPresentation().setEnabledAndVisible(false); return; } + final MigrationState migrationState = getOrComputeState(project); + + // Common settings for all states + e.getPresentation().setPopupGroup(true); + e.getPresentation().putClientProperty(ActionUtil.ALWAYS_VISIBLE_GROUP, true); + + switch (migrationState.state) { + case NOT_INSTALLED: + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + e.getPresentation().setText(copilotInstalled + ? "Migrate to Azure (Install App modernization)" + : "Migrate to Azure (Install GitHub Copilot and app modernization)"); + e.getPresentation().setPerformGroup(true); + e.getPresentation().putClientProperty(ActionUtil.SUPPRESS_SUBMENU, true); + break; + case NO_OPTIONS: + e.getPresentation().setText("Migrate to Azure (Open GitHub Copilot app modernization)"); + e.getPresentation().setPerformGroup(true); + e.getPresentation().putClientProperty(ActionUtil.SUPPRESS_SUBMENU, true); + break; + case HAS_OPTIONS: + e.getPresentation().setText("Migrate to Azure"); + e.getPresentation().setPerformGroup(false); + e.getPresentation().putClientProperty(ActionUtil.SUPPRESS_SUBMENU, false); + break; + } + e.getPresentation().setEnabledAndVisible(true); + } + + @Override + @AzureOperation(name = "user/appmod.migrate_action") + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getProject(); if (project == null) { - e.getPresentation().setEnabledAndVisible(false); return; } - // Check if there are any migration options - final boolean hasOptions = loadMigrationNodes(project).stream() - .anyMatch(MigrateNodeData::isVisible); + final MigrationState migrationState = getOrComputeState(project); - if (!hasOptions) { - // No options - hide, MigrateToAzureInstallAction will handle this - e.getPresentation().setEnabledAndVisible(false); - return; + switch (migrationState.state) { + case NOT_INSTALLED: + MigratePluginInstaller.showInstallConfirmation(project, + () -> MigratePluginInstaller.installPlugin(project)); + break; + case NO_OPTIONS: + AppModPanelHelper.openAppModPanel(project); + break; + case HAS_OPTIONS: + // Handled by popup menu + break; } - - e.getPresentation().setText("Migrate to Azure"); - e.getPresentation().setEnabledAndVisible(true); } @Override @@ -82,54 +154,28 @@ public void update(@NotNull AnActionEvent e) { if (e == null) { return AnAction.EMPTY_ARRAY; } - + final Project project = e.getProject(); if (project == null) { return AnAction.EMPTY_ARRAY; } - - // Load migration options from extension points - final List migrationNodes = loadMigrationNodes(project); - - // Convert nodes to actions - return convertNodesToActions(migrationNodes); - } - - /** - * Loads migration nodes from extension point providers. - */ - private List loadMigrationNodes(@Nonnull Project project) { - return migrationProviders.getExtensionList().stream() - .filter(provider -> provider.isApplicable(project)) - .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) - .flatMap(provider -> provider.createNodeData(project).stream()) - .filter(MigrateNodeData::isVisible) - .collect(Collectors.toList()); - } - - /** - * Converts node tree to action tree for sub-menu display. - */ - private AnAction[] convertNodesToActions(List nodes) { - final List actions = new ArrayList<>(); - for (MigrateNodeData node : nodes) { - actions.add(convertNodeToAction(node)); + + final MigrationState migrationState = getOrComputeState(project); + + if (migrationState.state == State.HAS_OPTIONS) { + return migrationState.nodes.stream() + .map(this::convertNodeToAction) + .toArray(AnAction[]::new); } - return actions.toArray(new AnAction[0]); + + return AnAction.EMPTY_ARRAY; } - - /** - * Converts a single node (and its children) to an action. - */ + private AnAction convertNodeToAction(MigrateNodeData nodeData) { if (nodeData.hasChildren()) { - // Node with children -> create sub-menu - final DefaultActionGroup subgroup = new DefaultActionGroup(); - subgroup.getTemplatePresentation().setText(nodeData.getLabel(), false); - subgroup.setPopup(true); + final DefaultActionGroup subgroup = new DefaultActionGroup(nodeData.getLabel(), true); subgroup.getTemplatePresentation().setIcon(AllIcons.Vcs.Changelist); - // Handle lazy loading or static children final List children = nodeData.isLazyLoading() ? nodeData.getChildrenLoader().get() : nodeData.getChildren(); @@ -139,18 +185,9 @@ private AnAction convertNodeToAction(MigrateNodeData nodeData) { subgroup.add(convertNodeToAction(child)); } } - return subgroup; } else { - // Leaf node -> create clickable action - return new AnAction(nodeData.getLabel()) { - { - getTemplatePresentation().setIcon(AllIcons.Vcs.Changelist); - if (nodeData.getDescription() != null) { - getTemplatePresentation().setDescription(nodeData.getDescription()); - } - } - + return new AnAction(nodeData.getLabel(), nodeData.getDescription(), AllIcons.Vcs.Changelist) { @Override public void update(@NotNull AnActionEvent e) { e.getPresentation().setEnabled(nodeData.isEnabled()); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java deleted file mode 100644 index 21c477af4a7..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -package com.microsoft.azure.toolkit.intellij.appmod; - -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.extensions.ExtensionPointName; -import com.intellij.openapi.project.Project; -import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; -import org.jetbrains.annotations.NotNull; - -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Action for "Migrate to Azure" when: - * 1. App Modernization plugin is NOT installed - click triggers installation - * 2. Plugin IS installed but no migration options available - click opens App Mod Panel - * - * Mutually exclusive with MigrateToAzureAction (ActionGroup) which is shown when plugin IS installed AND has migration options. - */ -public class MigrateToAzureInstallAction extends AnAction { - private static final ExtensionPointName migrationProviders = - ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); - - private enum State { - NOT_INSTALLED, - NO_OPTIONS, - HAS_OPTIONS - } - - private State currentState = State.NOT_INSTALLED; - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } - - @Override - public void update(@NotNull AnActionEvent e) { - final Project project = e.getProject(); - if (project == null) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - - currentState = determineState(project); - - switch (currentState) { - case NOT_INSTALLED: - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); - final String text = copilotInstalled - ? "Migrate to Azure (Install App modernizationn)" - : "Migrate to Azure (Install GitHub Copilot and app modernization)"; - e.getPresentation().setText(text); - e.getPresentation().setEnabledAndVisible(true); - break; - case NO_OPTIONS: - e.getPresentation().setText("Migrate to Azure (Open GitHub Copilot app modernization)"); - e.getPresentation().setEnabledAndVisible(true); - break; - case HAS_OPTIONS: - // Hide - MigrateToAzureAction (ActionGroup) will show instead - e.getPresentation().setEnabledAndVisible(false); - break; - } - } - - private State determineState(Project project) { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { - return State.NOT_INSTALLED; - } - - final boolean hasOptions = migrationProviders.getExtensionList().stream() - .filter(provider -> provider.isApplicable(project)) - .flatMap(provider -> provider.createNodeData(project).stream()) - .anyMatch(MigrateNodeData::isVisible); - - return hasOptions ? State.HAS_OPTIONS : State.NO_OPTIONS; - } - - @Override - @AzureOperation(name = "user/appmod.migrate_action") - public void actionPerformed(@NotNull AnActionEvent e) { - final Project project = e.getProject(); - if (project == null) { - return; - } - - switch (currentState) { - case NOT_INSTALLED: - MigratePluginInstaller.showInstallConfirmation(project, - () -> MigratePluginInstaller.installPlugin(project)); - break; - case NO_OPTIONS: - AppModPanelHelper.openAppModPanel(project); - break; - case HAS_OPTIONS: - // Should not happen - action is hidden in this state - break; - } - } -} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index 111d8eb2d5a..bb53f8462b0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -9,8 +9,11 @@ import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.ide.common.component.Node; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; -import com.microsoft.azure.toolkit.lib.common.messager.AzureMessager; +import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; +import com.microsoft.azure.toolkit.lib.common.action.Action; +import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -18,6 +21,8 @@ /** * Service Explorer node for "Migrate to Azure" functionality. * This node extends the azure-toolkit-ide-common-lib Node class to integrate with the Service Explorer tree. + * + * State is computed on initialization and can be refreshed via refresh() method. */ public final class MigrateToAzureNode extends Node { private static final ExtensionPointName childProviders = @@ -32,15 +37,44 @@ public MigrateToAzureNode(Project project) { super("Migrate to Azure"); this.project = project; withIcon(APP_MOD_ICON); + + // Add refresh action to context menu (only once in constructor) + withActions(new ActionGroup( + new Action<>(Action.Id.of("user/appmod.refresh_migrate_node")) + .withLabel("Refresh") + .withIcon(AzureIcons.Action.REFRESH.getIconPath()) + .withHandler((v, e) -> this.refresh()) + .withAuthRequired(false) + )); + + // Use addChildren with a function so it rebuilds on refresh + addChildren(data -> buildChildNodes()); + initializeNode(); } public void initializeNode() { - if (MigratePluginInstaller.isAppModPluginInstalled()) { - showMigrationOptions(); - } else { + // Clear previous state + clearClickHandlers(); + withDescription(""); + + if (!MigratePluginInstaller.isAppModPluginInstalled()) { showNotInstalled(); } + // Don't call showMigrationOptions() here - let buildChildNodes() handle it + // This avoids double loading of extension point data + } + + /** + * Refreshes the node by re-computing migration options. + * Called by RefreshMigrateToAzureAction from context menu. + */ + public void refresh() { + refreshChildren(); // This rebuilds children from addChildren function + } + + public Project getProject() { + return project; } private void showNotInstalled() { @@ -48,7 +82,7 @@ private void showNotInstalled() { // Dynamic description based on what needs to be installed final String description = copilotInstalled - ? "Install App modernizationn" + ? "Install App modernization" : "Install GitHub Copilot and app modernization"; withDescription(description); @@ -56,29 +90,42 @@ private void showNotInstalled() { MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); }); } - - - public void showMigrationOptions() { - clearClickHandlers(); - - // Load migration options from extension points and convert to Node - final List nodeDataList = childProviders.getExtensionList().stream() + + /** + * Load migration options from extension points. + */ + private List loadMigrationNodeData() { + return childProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(project)) .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(project).stream()) + .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); + } + + /** + * Build child nodes - called by Node framework on refresh. + * Also updates description and click handler based on data. + */ + private List> buildChildNodes() { + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + return List.of(); + } + + final List nodeDataList = loadMigrationNodeData(); + // Update description and click handler based on data + clearClickHandlers(); if (nodeDataList.isEmpty()) { - // No migration options - click to open App Modernization Panel withDescription("Open GitHub Copilot app modernization"); onClicked(e -> AppModPanelHelper.openAppModPanel(project)); } else { withDescription(""); - // Convert MigrateNodeData to Node and add as children - nodeDataList.stream() - .map(this::convertToNode) - .forEach(this::addChild); } + + return nodeDataList.stream() + .map(this::convertToNode) + .collect(Collectors.toList()); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index 97ccb101911..e8647c39271 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -5,15 +5,9 @@ - - - - + + text="Migrate to Azure" description="Migrate application to Azure" icon="/icons/appmod.svg"/> diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java index 9de3264688d..6c4f4cf35db 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java @@ -134,28 +134,6 @@ private AzureExplorer(Project project) { } } })); - - AzureEventBus.on("migrate.plugin.installed", new AzureEventBus.EventListener(e -> { - final DefaultTreeModel model = (DefaultTreeModel) this.getModel(); - final TreeNode root = (TreeNode) model.getRoot(); - if (root != null && root.children() != null) { - Iterator iterator = root.children().asIterator(); - while (iterator.hasNext()) { - final TreeNode childNode = (TreeNode) iterator.next(); - final Node childInnerNode = childNode.getInner(); - if (childInnerNode instanceof MigrateToAzureNode) { - final MigrateToAzureNode migrateNode = (MigrateToAzureNode) childInnerNode; - childNode.setAllowsChildren(true); - migrateNode.clearClickHandlers(); - migrateNode.withDescription(""); - migrateNode.showMigrationOptions(); - migrateNode.refreshView(); - childNode.updateChildren(true); - break; - } - } - } - })); } @Override diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 065c095e206..b0646b2783f 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -17,8 +17,13 @@ import com.microsoft.azure.toolkit.intellij.appmod.IMigrateOptionProvider; import com.microsoft.azure.toolkit.intellij.appmod.MigrateNodeData; import com.microsoft.azure.toolkit.intellij.appmod.MigratePluginInstaller; +import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; +import com.microsoft.azure.toolkit.lib.common.action.Action; +import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; +import com.microsoft.azure.toolkit.lib.common.action.IActionGroup; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; @@ -28,54 +33,83 @@ /** * Project Explorer facet node for "Migrate to Azure" functionality. * Uses the same extension point as MigrateToAzureNode and MigrateToAzureAction for consistency. + * + * State is computed on initialization and refreshed when buildChildren() is called (via tree refresh). */ public class MigrateToAzureFacetNode extends AbstractAzureFacetNode { private static final ExtensionPointName migrationProviders = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); - private boolean hasMigrationOptions = false; + // Lazy-loaded state - computed on first access + private List migrationNodes = null; public MigrateToAzureFacetNode(Project project, AzureModule module) { super(project, module); - initializeNode(); + // Don't compute in constructor - use lazy loading } - - public void initializeNode() { - updateChildren(); - } - - @Override - public Collection> buildChildren() { - final ArrayList> nodes = new ArrayList<>(); - - if (MigratePluginInstaller.isAppModPluginInstalled()) { - // Load migration options from extension points - final List migrationNodes = loadMigrationNodes(); - hasMigrationOptions = !migrationNodes.isEmpty(); - - // Convert MigrateNodeData to FacetNode - for (MigrateNodeData nodeData : migrationNodes) { - if (nodeData.isVisible()) { - nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); - } - } - // When no migration options, return empty - double-click opens App Mod Panel + + /** + * Gets migration nodes, computing them lazily on first access. + */ + private List getMigrationNodes() { + if (migrationNodes == null) { + migrationNodes = computeMigrationNodes(); } - // When plugin not installed, return empty list - user must double-click to trigger install - - return nodes; + return migrationNodes; } - + /** - * Loads migration nodes from extension point providers. + * Computes migration nodes from extension point providers. */ - private List loadMigrationNodes() { + private List computeMigrationNodes() { + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + return List.of(); + } return migrationProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(getProject())) .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(getProject()).stream()) + .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); } + + /** + * Checks if there are any visible migration options available. + */ + private boolean hasMigrationOptions() { + return !getMigrationNodes().isEmpty(); + } + + /** + * Refreshes migration nodes and updates the tree view. + */ + public void refresh() { + migrationNodes = null; // Clear cached data to force recompute + updateChildren(); // This also refreshes the view + } + + @Nullable + @Override + public IActionGroup getActionGroup() { + final Action refreshAction = new Action<>(Action.Id.of("user/appmod.refresh_migrate_node")) + .withLabel("Refresh") + .withIcon(AzureIcons.Action.REFRESH.getIconPath()) + .withHandler((v, e) -> refresh()) + .withAuthRequired(false); + return new ActionGroup(refreshAction); + } + + @Override + public Collection> buildChildren() { + final ArrayList> nodes = new ArrayList<>(); + + // Convert MigrateNodeData to FacetNode + for (MigrateNodeData nodeData : getMigrationNodes()) { + nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); + } + + return nodes; + } @Override protected void buildView(@Nonnull PresentationData presentation) { @@ -87,7 +121,7 @@ protected void buildView(@Nonnull PresentationData presentation) { ? "Migrate to Azure (Install App modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"; presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - } else if (!hasMigrationOptions) { + } else if (!hasMigrationOptions()) { presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); presentation.setLocationString("Open GitHub Copilot app modernization"); } else { @@ -101,7 +135,7 @@ public void navigate(boolean requestFocus) { // Plugin not installed - trigger install on double-click MigratePluginInstaller.showInstallConfirmation(getProject(), () -> MigratePluginInstaller.installPlugin(getProject())); - } else if (!hasMigrationOptions) { + } else if (!hasMigrationOptions()) { // No migration options - open App Modernization Panel AppModPanelHelper.openAppModPanel(getProject()); } @@ -110,17 +144,18 @@ public void navigate(boolean requestFocus) { @Override public boolean canNavigate() { // Enable navigation when plugin is not installed OR when no migration options - return !MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions; + return !MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions(); } @Override public @Nonnull LeafState getLeafState() { - // Show as leaf node (no expand arrow) when plugin not installed or no migration options - // Show expand arrow when there are migration options to reveal - if (!MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions) { + // Use ASYNC to avoid triggering extension point loading synchronously + // The actual leaf state will be determined when buildChildren() is called + if (!MigratePluginInstaller.isAppModPluginInstalled()) { return LeafState.ALWAYS; } - return LeafState.NEVER; + // ASYNC means IntelliJ will call buildChildren() to determine if there are children + return LeafState.ASYNC; } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml index fb568944674..b221ed6cb3b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml @@ -113,7 +113,6 @@ - From 07e235b988415b251b9debbe657020f764c35c70 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 23 Jan 2026 09:27:05 +0800 Subject: [PATCH 08/24] remove unuseful properties --- .../intellij/appmod/MigrateNodeData.java | 22 +++++-------------- .../intellij/appmod/MigrateToAzureNode.java | 4 ++-- .../MigrateToAzureFacetNode.java | 4 ++-- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java index 108096ca19a..49541eb262a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java @@ -20,7 +20,7 @@ * This class is used by MigrateToAzureNode, MigrateToAzureAction, and MigrateToAzureFacetNode. * * Features: - * - Basic properties: label, icon, description, tooltip + * - Basic properties: label, description (used as menu description and tooltip) * - Click handling for both leaf and parent nodes * - Static children or lazy-loaded children via childrenLoader * @@ -66,17 +66,13 @@ public class MigrateNodeData { private final String label; /** - * Description text (shown as secondary text in some views). + * Description text. Used as: + * - Menu item description in MigrateToAzureAction + * - Tooltip in MigrateToAzureNode and MigrateToAzureFacetNode */ @Nullable private String description; - /** - * Tooltip text (shown on hover). - */ - @Nullable - private String tooltip; - // ==================== State ==================== /** @@ -234,21 +230,13 @@ private Builder(@Nonnull String label) { } /** - * Sets the description. + * Sets the description (used as menu description and tooltip). */ public Builder description(@Nullable String description) { data.description = description; return this; } - /** - * Sets the tooltip. - */ - public Builder tooltip(@Nullable String tooltip) { - data.tooltip = tooltip; - return this; - } - /** * Sets the enabled state. */ diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index bb53f8462b0..43864b1eba3 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -138,8 +138,8 @@ private Node convertToNode(MigrateNodeData data) { node.withLabel(d -> d.getLabel()); // Use Changelist icon for child nodes node.withIcon(CHANGELIST_ICON); - if (data.getTooltip() != null) { - node.withTips(d -> d.getTooltip()); + if (data.getDescription() != null) { + node.withTips(d -> d.getDescription()); } // Set click handler diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index b0646b2783f..0e1037b1545 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -192,8 +192,8 @@ protected void buildView(@Nonnull PresentationData presentation) { presentation.addText(nodeData.getLabel(), com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); // Set tooltip if available - if (nodeData.getTooltip() != null) { - presentation.setTooltip(nodeData.getTooltip()); + if (nodeData.getDescription() != null) { + presentation.setTooltip(nodeData.getDescription()); } // Use node's icon if available, otherwise use default app_mod icon From fc74e630b9971b0bea97f7cb3d5a6bd6e8245d6b Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 23 Jan 2026 11:30:21 +0800 Subject: [PATCH 09/24] update text for github copilot already installed scenario --- .../azure/toolkit/intellij/appmod/MigratePluginInstaller.java | 4 ++-- .../azure/toolkit/intellij/appmod/MigrateToAzureAction.java | 2 +- .../azure/toolkit/intellij/appmod/MigrateToAzureNode.java | 2 +- .../connector/projectexplorer/MigrateToAzureFacetNode.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java index 054d09791b4..a73769caa4a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java @@ -86,11 +86,11 @@ public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Ru final boolean copilotInstalled = isCopilotInstalled(); final String title = copilotInstalled - ? "Install App modernization" + ? "Install Github Copilot app modernization" : "Install GitHub Copilot and app modernization"; final String message = copilotInstalled - ? "To migrate to Azure, you'll need a plugin: App modernization." + ? "Install this plugin to automate migrating your apps to Azure with Copilot." : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; new InstallPluginDialog(project, title) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index 5d5c6e6f4b8..eecb8eda63c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -106,7 +106,7 @@ public void update(@NotNull AnActionEvent e) { case NOT_INSTALLED: final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); e.getPresentation().setText(copilotInstalled - ? "Migrate to Azure (Install App modernization)" + ? "Migrate to Azure (Install Github Copilot app modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"); e.getPresentation().setPerformGroup(true); e.getPresentation().putClientProperty(ActionUtil.SUPPRESS_SUBMENU, true); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index 43864b1eba3..ae66f0e9341 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -82,7 +82,7 @@ private void showNotInstalled() { // Dynamic description based on what needs to be installed final String description = copilotInstalled - ? "Install App modernization" + ? "Install Github Copilot app modernization" : "Install GitHub Copilot and app modernization"; withDescription(description); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 0e1037b1545..b4fab3d0e19 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -118,7 +118,7 @@ protected void buildView(@Nonnull PresentationData presentation) { if (!MigratePluginInstaller.isAppModPluginInstalled()) { final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); final String text = copilotInstalled - ? "Migrate to Azure (Install App modernization)" + ? "Migrate to Azure (Install Github Copilot app modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"; presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); } else if (!hasMigrationOptions()) { From ff9d4846645e53548536a3f69296bd20cbe0dc23 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 23 Jan 2026 15:16:06 +0800 Subject: [PATCH 10/24] add telemetry support --- .../intellij/appmod/AppModPanelHelper.java | 5 +- .../toolkit/intellij/appmod/AppModUtils.java | 71 +++++++++++++++++++ .../appmod/MigratePluginInstaller.java | 3 + .../intellij/appmod/MigrateToAzureAction.java | 11 ++- .../intellij/appmod/MigrateToAzureNode.java | 19 +++-- .../MigrateToAzureFacetNode.java | 16 ++++- 6 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java index 76484f51829..5cd329c3192 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java @@ -27,14 +27,17 @@ private AppModPanelHelper() { * Opens the App Modernization Panel tool window. * * @param project the current project + * @param source the source of the open request (e.g., "node", "action", "facet") */ - public static void openAppModPanel(@Nonnull Project project) { + public static void openAppModPanel(@Nonnull Project project, @Nonnull String source) { final ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project); final ToolWindow toolWindow = toolWindowManager.getToolWindow(TOOL_WINDOW_ID); if (toolWindow != null) { + AppModUtils.logTelemetryEvent(source + ".open-panel"); toolWindow.show(); } else { + AppModUtils.logTelemetryEvent(source + ".open-panel-failed"); AzureMessager.getMessager().warning("App Modernization Panel is not available. Please ensure the GitHub Copilot App Modernization plugin is installed and enabled."); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java new file mode 100644 index 00000000000..9f265fd94b1 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.diagnostic.Logger; +import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; +import com.microsoft.azure.toolkit.lib.common.telemetry.AzureTelemeter; +import com.microsoft.azure.toolkit.lib.common.telemetry.AzureTelemetry; + +import javax.annotation.Nonnull; +import java.util.Map; + +/** + * Utility class for App Modernization (Migrate to Azure) telemetry and common operations. + */ +public final class AppModUtils { + + private static final Logger LOG = Logger.getInstance(AppModUtils.class); + private static final String SERVICE_NAME = "appmod"; + // Set to true to enable telemetry debug logging (for development only) + private static final boolean DEBUG_TELEMETRY = true; + + private AppModUtils() { + // Utility class, no instantiation allowed + } + + /** + * Logs a telemetry event for App Modernization features. + * + * @param eventName The name of the event to log (e.g., "show-node", "click-option", "refresh") + */ + public static void logTelemetryEvent(@Nonnull String eventName) { + if (DEBUG_TELEMETRY) { + LOG.info("[AppMod Telemetry] " + eventName); + } + final Map properties = Map.of( + AzureTelemeter.OP_NAME, eventName, + AzureTelemeter.OP_PARENT_ID, SERVICE_NAME, + AzureTelemeter.OPERATION_NAME, eventName, + AzureTelemeter.SERVICE_NAME, SERVICE_NAME + ); + AzureTaskManager.getInstance().runLater(() -> { + AzureTelemeter.log(AzureTelemetry.Type.INFO, properties); + }); + } + + /** + * Logs a telemetry event with additional properties. + * + * @param eventName The name of the event to log + * @param additionalProperties Additional properties to include in the telemetry + */ + public static void logTelemetryEvent(@Nonnull String eventName, @Nonnull Map additionalProperties) { + if (DEBUG_TELEMETRY) { + LOG.info("[AppMod Telemetry] " + eventName + " " + additionalProperties); + } + final Map properties = new java.util.HashMap<>(Map.of( + AzureTelemeter.OP_NAME, eventName, + AzureTelemeter.OP_PARENT_ID, SERVICE_NAME, + AzureTelemeter.OPERATION_NAME, eventName, + AzureTelemeter.SERVICE_NAME, SERVICE_NAME + )); + properties.putAll(additionalProperties); + AzureTaskManager.getInstance().runLater(() -> { + AzureTelemeter.log(AzureTelemetry.Type.INFO, properties); + }); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java index a73769caa4a..6290727cf83 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java @@ -16,6 +16,7 @@ import javax.annotation.Nonnull; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; /** @@ -110,6 +111,7 @@ public static void installPlugin(@Nonnull Project project) { // If already installed, nothing to do if (appModInstalled) { + AppModUtils.logTelemetryEvent("plugin.install-skipped", Map.of("reason", "already-installed")); return; } @@ -128,6 +130,7 @@ public static void installPlugin(@Nonnull Project project) { true, // selectAllInDialog - pre-select all plugins null, // modalityState () -> { + AppModUtils.logTelemetryEvent("plugin.install-complete"); } ); }); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index eecb8eda63c..401791d0952 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -21,6 +21,7 @@ import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -82,6 +83,10 @@ private MigrationState computeState(Project project) { .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); + if (nodes.isEmpty()) { + AppModUtils.logTelemetryEvent("action.no-options"); + } + return new MigrationState( nodes.isEmpty() ? State.NO_OPTIONS : State.HAS_OPTIONS, nodes @@ -126,7 +131,6 @@ public void update(@NotNull AnActionEvent e) { } @Override - @AzureOperation(name = "user/appmod.migrate_action") public void actionPerformed(@NotNull AnActionEvent e) { final Project project = e.getProject(); if (project == null) { @@ -137,11 +141,12 @@ public void actionPerformed(@NotNull AnActionEvent e) { switch (migrationState.state) { case NOT_INSTALLED: + AppModUtils.logTelemetryEvent("action.click-install"); MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); break; case NO_OPTIONS: - AppModPanelHelper.openAppModPanel(project); + AppModPanelHelper.openAppModPanel(project, "action"); break; case HAS_OPTIONS: // Handled by popup menu @@ -194,8 +199,8 @@ public void update(@NotNull AnActionEvent e) { } @Override - @AzureOperation(name = "user/appmod.trigger_migrate_option") public void actionPerformed(@NotNull AnActionEvent e) { + AppModUtils.logTelemetryEvent("action.click-option", Map.of("label", nodeData.getLabel())); nodeData.click(e); } }; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index ae66f0e9341..7f1193791e3 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -13,9 +13,9 @@ import com.microsoft.azure.toolkit.lib.common.action.Action; import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -70,6 +70,7 @@ public void initializeNode() { * Called by RefreshMigrateToAzureAction from context menu. */ public void refresh() { + AppModUtils.logTelemetryEvent("node.refresh"); refreshChildren(); // This rebuilds children from addChildren function } @@ -87,6 +88,7 @@ private void showNotInstalled() { withDescription(description); onClicked(e -> { + AppModUtils.logTelemetryEvent("node.click-install"); MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); }); } @@ -95,12 +97,16 @@ private void showNotInstalled() { * Load migration options from extension points. */ private List loadMigrationNodeData() { - return childProviders.getExtensionList().stream() + final List nodes = childProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(project)) .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(project).stream()) .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); + if (nodes.isEmpty()) { + AppModUtils.logTelemetryEvent("node.no-options"); + } + return nodes; } /** @@ -118,7 +124,9 @@ private List> buildChildNodes() { clearClickHandlers(); if (nodeDataList.isEmpty()) { withDescription("Open GitHub Copilot app modernization"); - onClicked(e -> AppModPanelHelper.openAppModPanel(project)); + onClicked(e -> { + AppModPanelHelper.openAppModPanel(project, "node"); + }); } else { withDescription(""); } @@ -144,7 +152,10 @@ private Node convertToNode(MigrateNodeData data) { // Set click handler if (data.hasClickHandler()) { - node.onClicked(d -> data.click(null)); + node.onClicked(d -> { + AppModUtils.logTelemetryEvent("node.click-option", Map.of("label", data.getLabel())); + data.click(null); + }); } // Handle children - lazy or static diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index b4fab3d0e19..f28053e83d6 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -11,6 +11,7 @@ import com.intellij.openapi.project.Project; import com.intellij.ui.tree.LeafState; import com.microsoft.azure.toolkit.intellij.appmod.AppModPanelHelper; +import com.microsoft.azure.toolkit.intellij.appmod.AppModUtils; import com.microsoft.azure.toolkit.intellij.appmod.Constants; import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; @@ -65,12 +66,16 @@ private List computeMigrationNodes() { if (!MigratePluginInstaller.isAppModPluginInstalled()) { return List.of(); } - return migrationProviders.getExtensionList().stream() + final List nodes = migrationProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(getProject())) .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(getProject()).stream()) .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); + if (nodes.isEmpty()) { + AppModUtils.logTelemetryEvent("facet.no-options"); + } + return nodes; } /** @@ -94,7 +99,10 @@ public IActionGroup getActionGroup() { final Action refreshAction = new Action<>(Action.Id.of("user/appmod.refresh_migrate_node")) .withLabel("Refresh") .withIcon(AzureIcons.Action.REFRESH.getIconPath()) - .withHandler((v, e) -> refresh()) + .withHandler((v, e) -> { + AppModUtils.logTelemetryEvent("facet.refresh"); + refresh(); + }) .withAuthRequired(false); return new ActionGroup(refreshAction); } @@ -133,11 +141,12 @@ protected void buildView(@Nonnull PresentationData presentation) { public void navigate(boolean requestFocus) { if (!MigratePluginInstaller.isAppModPluginInstalled()) { // Plugin not installed - trigger install on double-click + AppModUtils.logTelemetryEvent("facet.click-install"); MigratePluginInstaller.showInstallConfirmation(getProject(), () -> MigratePluginInstaller.installPlugin(getProject())); } else if (!hasMigrationOptions()) { // No migration options - open App Modernization Panel - AppModPanelHelper.openAppModPanel(getProject()); + AppModPanelHelper.openAppModPanel(getProject(), "facet"); } } @@ -203,6 +212,7 @@ protected void buildView(@Nonnull PresentationData presentation) { @Override public void navigate(boolean requestFocus) { // Trigger click handler + AppModUtils.logTelemetryEvent("facet.click-option", java.util.Map.of("label", nodeData.getLabel())); nodeData.doubleClick(null); } From f1b37914a7321654643fb626648a3f50bdd284fd Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 23 Jan 2026 15:17:02 +0800 Subject: [PATCH 11/24] set DEBUG_TELEMETRY to false --- .../microsoft/azure/toolkit/intellij/appmod/AppModUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java index 9f265fd94b1..49ace1fa9bd 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java @@ -21,7 +21,7 @@ public final class AppModUtils { private static final Logger LOG = Logger.getInstance(AppModUtils.class); private static final String SERVICE_NAME = "appmod"; // Set to true to enable telemetry debug logging (for development only) - private static final boolean DEBUG_TELEMETRY = true; + private static final boolean DEBUG_TELEMETRY = false; private AppModUtils() { // Utility class, no instantiation allowed From 0b6f8613010dc2bd94e8dde73d688e8e3fe039db Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Mon, 26 Jan 2026 15:03:02 +0800 Subject: [PATCH 12/24] Add java-upgrade promotion --- .../build.gradle.kts | 10 + .../intellij/appmod/MigrateToAzureAction.java | 10 +- .../intellij/appmod/MigrateToAzureNode.java | 11 +- .../AppModPluginInstaller.java} | 9 +- .../{ => common}/InstallPluginDialog.java | 4 +- .../JavaUpgradeCheckStartupActivity.java | 99 +++ .../action/UpgradeActionRegistrar.java | 118 +++ .../action/UpgradeInProblemsViewAction.java | 602 ++++++++++++++++ .../UpgradeInQuickFixIntentionAction.java | 222 ++++++ .../action/UpgradeProjectAction.java | 110 +++ .../javaupgrade/dao/JavaUpgradeIssue.java | 136 ++++ .../JavaUpgradeIssuesInspection.java | 277 +++++++ .../javaupgrade/service/CVECheckService.java | 564 +++++++++++++++ .../service/JavaUpgradeIssuesCache.java | 122 ++++ .../JavaUpgradeIssuesDetectionService.java | 675 ++++++++++++++++++ .../JavaVersionNotificationService.java | 555 ++++++++++++++ .../settings/JavaUpgradeConfigurable.java | 155 ++++ .../META-INF/azure-intellij-plugin-appmod.xml | 40 +- .../MigrateToAzureFacetNode.java | 18 +- 19 files changed, 3710 insertions(+), 27 deletions(-) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{MigratePluginInstaller.java => common/AppModPluginInstaller.java} (95%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => common}/InstallPluginDialog.java (94%) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts index 2bcc6e6fb46..e1a7df7b7b9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts @@ -1,5 +1,15 @@ dependencies { implementation(project(":azure-intellij-plugin-lib")) + implementation(project(":azure-intellij-plugin-lib-java")) // runtimeOnly project(path: ":azure-intellij-plugin-lib", configuration: "instrumentedJar") implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") + intellijPlatform { + // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. + bundledPlugin("com.intellij.java") + bundledPlugin("org.jetbrains.idea.maven") + // bundledPlugin("org.jetbrains.idea.maven.model") + bundledPlugin("com.intellij.gradle") + // Copilot plugin for Java upgrade integration + plugin("com.github.copilot:1.5.59-243") + } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index 401791d0952..f2c5945e469 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -15,7 +15,7 @@ import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; -import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -72,7 +72,7 @@ private MigrationState getOrComputeState(Project project) { * Computes migration state by calling providers. */ private MigrationState computeState(Project project) { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { return new MigrationState(State.NOT_INSTALLED, List.of()); } @@ -109,7 +109,7 @@ public void update(@NotNull AnActionEvent e) { switch (migrationState.state) { case NOT_INSTALLED: - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final boolean copilotInstalled = AppModPluginInstaller.isCopilotInstalled(); e.getPresentation().setText(copilotInstalled ? "Migrate to Azure (Install Github Copilot app modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"); @@ -142,8 +142,8 @@ public void actionPerformed(@NotNull AnActionEvent e) { switch (migrationState.state) { case NOT_INSTALLED: AppModUtils.logTelemetryEvent("action.click-install"); - MigratePluginInstaller.showInstallConfirmation(project, - () -> MigratePluginInstaller.installPlugin(project)); + AppModPluginInstaller.showInstallConfirmation(project, + () -> AppModPluginInstaller.installPlugin(project)); break; case NO_OPTIONS: AppModPanelHelper.openAppModPanel(project, "action"); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index 7f1193791e3..a10e4598ff5 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -10,6 +10,7 @@ import com.microsoft.azure.toolkit.ide.common.component.Node; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.lib.common.action.Action; import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; @@ -58,7 +59,7 @@ public void initializeNode() { clearClickHandlers(); withDescription(""); - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { showNotInstalled(); } // Don't call showMigrationOptions() here - let buildChildNodes() handle it @@ -79,7 +80,7 @@ public Project getProject() { } private void showNotInstalled() { - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final boolean copilotInstalled = AppModPluginInstaller.isCopilotInstalled(); // Dynamic description based on what needs to be installed final String description = copilotInstalled @@ -89,7 +90,7 @@ private void showNotInstalled() { onClicked(e -> { AppModUtils.logTelemetryEvent("node.click-install"); - MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); + AppModPluginInstaller.showInstallConfirmation(project, () -> AppModPluginInstaller.installPlugin(project)); }); } @@ -114,7 +115,7 @@ private List loadMigrationNodeData() { * Also updates description and click handler based on data. */ private List> buildChildNodes() { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { return List.of(); } @@ -183,6 +184,6 @@ public synchronized void refreshView() { } public static boolean isPluginInstalled() { - return MigratePluginInstaller.isAppModPluginInstalled(); + return AppModPluginInstaller.isAppModPluginInstalled(); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java similarity index 95% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 6290727cf83..51c0c60a008 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.common; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.ide.plugins.PluginManagerCore; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; -import com.microsoft.azure.toolkit.lib.common.event.AzureEventBus; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; +import com.microsoft.azure.toolkit.intellij.appmod.AppModUtils; import javax.annotation.Nonnull; import java.util.LinkedHashSet; @@ -24,11 +23,11 @@ * This centralizes all plugin detection and installation logic to avoid code duplication * between MigrateToAzureNode and MigrateToAzureAction. */ -public class MigratePluginInstaller { +public class AppModPluginInstaller { private static final String PLUGIN_ID = "com.github.copilot.appmod"; private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; - private MigratePluginInstaller() { + private AppModPluginInstaller() { // Utility class - prevent instantiation } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java similarity index 94% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java index 9eaa3615e2d..0989aa160fa 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.common; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; @@ -62,7 +62,7 @@ public void show() { } else { labelComponent = new JLabel(label); } - labelComponent.setHorizontalAlignment(SwingConstants.CENTER); + labelComponent.setHorizontalAlignment(SwingConstants.LEFT); panel.add(labelComponent, BorderLayout.CENTER); return panel; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java new file mode 100644 index 00000000000..b0cc134fb81 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade; + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.startup.ProjectActivity; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; +import kotlin.Unit; +import kotlin.coroutines.Continuation; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; +import java.time.Duration; +import java.util.List; + +/** + * Startup activity that detects outdated JDK and framework versions when a project is opened. + * This runs after the project is fully loaded and shows notifications for any detected issues. + */ +public class JavaUpgradeCheckStartupActivity implements ProjectActivity, DumbAware { + + // Additional delay after smart mode to ensure Maven/Gradle sync is complete + private static final long POST_INDEXING_DELAY_SECONDS = 3; + + @Override + public Object execute(@Nonnull Project project, @Nonnull Continuation continuation) { + // Wait for indexing to complete before running the check + DumbService.getInstance(project).runWhenSmart(() -> { + // Add a small delay after smart mode to ensure Maven/Gradle sync is done + Mono.delay(Duration.ofSeconds(POST_INDEXING_DELAY_SECONDS)) + .subscribe( + next -> { + if (project.isDisposed()) { + return; + } + performJavaUpgradeCheck(project); + }, + error -> { /* Error during Java upgrade check startup */ } + ); + }); + + return null; + } + + /** + * Performs the jdk version, framework version and CVE issue check and shows notifications for any issues found. + */ + private void performJavaUpgradeCheck(@Nonnull Project project) { + try { + // Run the analysis in a background thread + AzureTaskManager.getInstance().runInBackground("Checking Java upgrade issues", () -> { + if (project.isDisposed()) { + return; + } + + // Refresh the cache (this populates JDK and dependency issues for use by inspections) + final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); + cache.refresh(); + + // Get all issues including CVEs + final JavaUpgradeIssuesDetectionService detectionService = JavaUpgradeIssuesDetectionService.getInstance(); + final List allIssues = new java.util.ArrayList<>(); + allIssues.addAll(cache.getJdkIssues()); + allIssues.addAll(cache.getDependencyIssues()); + allIssues.addAll(detectionService.getCVEIssues(project)); + + // Update UI on the main thread + AzureTaskManager.getInstance().runLater(() -> { + if (project.isDisposed()) { + return; + } + + // Restart code analysis to refresh inspections in open editors + // This ensures wavy underlines appear for JDK/framework issues + DaemonCodeAnalyzer.getInstance(project).restart(); + + // Show notifications if there are issues + if (!allIssues.isEmpty()) { + final JavaVersionNotificationService notificationService = JavaVersionNotificationService.getInstance(); + notificationService.showNotifications(project, allIssues); + } + }); + }); + + } catch (Exception e) { + // Error performing Java version check + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java new file mode 100644 index 00000000000..fbb892636d8 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.actionSystem.Separator; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.startup.ProjectActivity; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; + +import kotlin.Unit; +import kotlin.coroutines.Continuation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Registers the Upgrade action into the GitHub Copilot context menu at runtime. + * This is needed because the Copilot plugin creates its context menu groups dynamically. + */ +public class UpgradeActionRegistrar implements ProjectActivity { + + private static final Logger LOG = Logger.getInstance(UpgradeActionRegistrar.class); + private static final String UPGRADE_ACTION_ID = "AzureToolkit.UpgradeProject"; + private static final String PROJECT_VIEW_POPUP_MENU = "ProjectViewPopupMenu"; + + @Nullable + @Override + public Object execute(@NotNull Project project, @NotNull Continuation continuation) { + discoverAndRegisterAction(); + return Unit.INSTANCE; + } + + private void discoverAndRegisterAction() { + // Only proceed if Copilot plugin is installed + if (!AppModPluginInstaller.isCopilotInstalled()) { + return; + } + + ActionManager actionManager = ActionManager.getInstance(); + + LOG.info("=== Searching for GitHub Copilot submenu in ProjectViewPopupMenu ==="); + + // Get the ProjectViewPopupMenu group + AnAction projectViewPopup = actionManager.getAction(PROJECT_VIEW_POPUP_MENU); + if (projectViewPopup instanceof DefaultActionGroup) { + DefaultActionGroup popupGroup = (DefaultActionGroup) projectViewPopup; + + // Search for the GitHub Copilot submenu within ProjectViewPopupMenu + DefaultActionGroup copilotGroup = findCopilotSubmenu(popupGroup, actionManager); + + if (copilotGroup != null) { + tryAddToGroup(actionManager, copilotGroup, "GitHub Copilot submenu"); + } else { + LOG.info("GitHub Copilot submenu not found in ProjectViewPopupMenu"); + } + } else { + LOG.warn("ProjectViewPopupMenu not found or not a DefaultActionGroup"); + } + + LOG.info("=== End Copilot Action Discovery ==="); + } + + /** + * Search for the GitHub Copilot submenu within a parent group. + * The Copilot plugin creates this dynamically, so we search by exact presentation text. + */ + private DefaultActionGroup findCopilotSubmenu(DefaultActionGroup parentGroup, ActionManager actionManager) { + for (AnAction child : parentGroup.getChildActionsOrStubs()) { + if (child instanceof DefaultActionGroup) { + DefaultActionGroup childGroup = (DefaultActionGroup) child; + Presentation presentation = childGroup.getTemplatePresentation(); + String text = presentation.getText(); + String actionId = actionManager.getId(child); + + // Match exactly "GitHub Copilot" to avoid false positives + if ("GitHub Copilot".equals(text)) { + LOG.info("Found Copilot submenu by exact text match: " + text + ", id=" + actionId); + return childGroup; + } + } + } + return null; + } + + private void tryAddToGroup(ActionManager actionManager, DefaultActionGroup group, String groupId) { + AnAction upgradeAction = actionManager.getAction(UPGRADE_ACTION_ID); + if (upgradeAction == null) { + LOG.warn("Upgrade action not found: " + UPGRADE_ACTION_ID); + return; + } + + // Check if action is not already added + if (!containsAction(group, UPGRADE_ACTION_ID, actionManager)) { + // Add a separator before the upgrade action to visually group it + group.add(Separator.create()); + group.add(upgradeAction); + LOG.info("Successfully added upgrade action to group: " + groupId); + } else { + LOG.info("Upgrade action already exists in group: " + groupId); + } + } + + private boolean containsAction(DefaultActionGroup group, String actionId, ActionManager actionManager) { + for (AnAction action : group.getChildActionsOrStubs()) { + if (action != null && actionId.equals(actionManager.getId(action))) { + return true; + } + } + return false; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java new file mode 100644 index 00000000000..6746e560406 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java @@ -0,0 +1,602 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.analysis.problemsView.Problem; +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + + +/** + * Action to fix vulnerable dependencies by opening GitHub Copilot chat with an upgrade prompt. + * This action appears in the Problems View context menu for vulnerable dependency issues. + */ +public class UpgradeInProblemsViewAction extends AnAction implements DumbAware { + + private static final String CVE_MARKER = "CVE-"; + + // Data key for problems in the Problems View + private static final String PROBLEMS_VIEW_PROBLEM_KEY = "Problem"; + + public UpgradeInProblemsViewAction() { + super(); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + return; + } + + // Try to get issue from cache using PsiElement context + final JavaUpgradeIssue cachedIssue = findIssueFromContext(e, project); + if (cachedIssue != null) { + final String prompt = buildPromptFromIssue(cachedIssue); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + return; + } + + // Fallback: Get the problem description from the context + final String problemDescription = extractProblemDescription(e); + + if (problemDescription != null && !problemDescription.isEmpty()) { + // Extract dependency info and CVE from the problem description + final String prompt = buildUpgradePrompt(problemDescription); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } else { + // Fallback: generic CVE fix prompt + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java" + ); + } + } + + /** + * Tries to find the JavaUpgradeIssue from the context by examining the PsiElement. + */ + @Nullable + private JavaUpgradeIssue findIssueFromContext(@NotNull AnActionEvent e, @NotNull Project project) { + final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); + if (!cache.isInitialized()) { + return null; + } + + // Try to get PsiElement directly + PsiElement element = e.getData(CommonDataKeys.PSI_ELEMENT); + + // If no direct element, try to get from file + offset + if (element == null) { + element = findElementFromProblem(e, project); + } + + if (element == null) { + return null; + } + + // Navigate to find dependency/parent context + return findIssueFromElement(element, cache); + } + + /** + * Finds the PsiElement from a Problem in the Problems View. + * Uses reflection for cross-version compatibility as Problem API varies. + */ + @Nullable + private PsiElement findElementFromProblem(@NotNull AnActionEvent e, @NotNull Project project) { + try { + final Object problemData = e.getDataContext().getData(PROBLEMS_VIEW_PROBLEM_KEY); + if (problemData instanceof Problem) { + final Problem problem = (Problem) problemData; + + // Use reflection to get file - API varies across IntelliJ versions + VirtualFile file = null; + try { + java.lang.reflect.Method getFileMethod = problem.getClass().getMethod("getFile"); + Object fileObj = getFileMethod.invoke(problem); + if (fileObj instanceof VirtualFile) { + file = (VirtualFile) fileObj; + } + } catch (Exception ignored) { + // getFile method might not exist in this version + System.out.println("error" + ignored.getMessage()); + } + + if (file != null && file.getName().equals("pom.xml")) { + final PsiFile psiFile = PsiManager.getInstance(project).findFile(file); + if (psiFile instanceof XmlFile) { + // Try to get offset from problem and find element + try { + java.lang.reflect.Method getOffsetMethod = problem.getClass().getMethod("getOffset"); + Object offsetObj = getOffsetMethod.invoke(problem); + if (offsetObj instanceof Integer) { + int offset = (Integer) offsetObj; + if (offset >= 0) { + return psiFile.findElementAt(offset); + } + } + } catch (Exception ignored) { + // getOffset method might not exist + System.out.println("error" + ignored.getMessage()); + } + } + } + } + } catch (Exception ignored) { + System.out.println("error" + ignored.getMessage()); + + } + return null; + } + + /** + * Finds the JavaUpgradeIssue based on the XML element context. + */ + @Nullable + private JavaUpgradeIssue findIssueFromElement(@NotNull PsiElement element, @NotNull JavaUpgradeIssuesCache cache) { + // Find the containing XmlTag + XmlTag tag = findParentTag(element); + if (tag == null) { + return null; + } + + // Check if this is a Java version property + if (isJavaVersionContext(tag)) { + return cache.getJdkIssue(); + } + + // Check if this is a dependency or parent version + final String groupId = extractGroupId(tag); + if (groupId != null) { + if (groupId.equals(GROUP_ID_SPRING_BOOT)) { + return cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); + } else if (groupId.equals(GROUP_ID_SPRING_SECURITY)) { + return cache.findDependencyIssue(GROUP_ID_SPRING_SECURITY); + } else if (groupId.equals(GROUP_ID_SPRING_FRAMEWORK)) { + return cache.findDependencyIssue(GROUP_ID_SPRING_FRAMEWORK + ":"); + } + } + + return null; + } + + /** + * Finds the parent XmlTag of an element. + */ + @Nullable + private XmlTag findParentTag(@NotNull PsiElement element) { + PsiElement current = element; + while (current != null) { + if (current instanceof XmlTag) { + return (XmlTag) current; + } + current = current.getParent(); + } + return null; + } + + /** + * Checks if the tag is in a Java version context. + */ + private boolean isJavaVersionContext(@NotNull XmlTag tag) { + String tagName = tag.getName(); + + // Check for properties like java.version, maven.compiler.source, etc. + if ("java.version".equals(tagName) || + "maven.compiler.source".equals(tagName) || + "maven.compiler.target".equals(tagName) || + "maven.compiler.release".equals(tagName)) { + XmlTag parent = tag.getParentTag(); + return parent != null && "properties".equals(parent.getName()); + } + + // Check for maven-compiler-plugin source/target/release + if ("source".equals(tagName) || "target".equals(tagName) || "release".equals(tagName)) { + XmlTag current = tag.getParentTag(); + while (current != null) { + if ("plugin".equals(current.getName())) { + XmlTag artifactIdTag = current.findFirstSubTag("artifactId"); + if (artifactIdTag != null && ARTIFACT_ID_MAVEN_COMPILER_PLUGIN.equals(artifactIdTag.getValue().getText())) { + return true; + } + } + current = current.getParentTag(); + } + } + + return false; + } + + /** + * Extracts the groupId from a dependency or parent tag context. + */ + @Nullable + private String extractGroupId(@NotNull XmlTag tag) { + // If we're on a version tag, look at parent (dependency or parent) + XmlTag container = tag; + if ("version".equals(tag.getName())) { + container = tag.getParentTag(); + } + + if (container == null) { + return null; + } + + // Check if it's a dependency or parent tag + String containerName = container.getName(); + if ("dependency".equals(containerName) || "parent".equals(containerName)) { + XmlTag groupIdTag = container.findFirstSubTag("groupId"); + if (groupIdTag != null) { + return groupIdTag.getValue().getText(); + } + } + + return null; + } + + /** + * Builds a prompt from a cached JavaUpgradeIssue. + */ + @NotNull + private String buildPromptFromIssue(@NotNull JavaUpgradeIssue issue) { + String packageId = issue.getPackageId(); + + // JDK upgrade + if (PACKAGE_ID_JDK.equals(packageId)) { + return String.format( + "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", + issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } + + // Framework upgrade (Spring Boot, Spring Framework, etc.) + return String.format( + "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", + issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } + + @Override + public void update(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Check if we're in the Problems View context with a vulnerability + final String description = extractProblemDescription(e); + if (description == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Also check if the file is pom.xml or build.gradle (common for dependency issues) + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + final boolean isBuildFile = file != null && + (file.getName().equals("pom.xml") || file.getName().endsWith(".gradle") || file.getName().endsWith(".gradle.kts")); + + final boolean isVulnerability = isVulnerabilityDescription(description); + + if (!isBuildFile && !isVulnerability) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Set dynamic text based on issue type detected from description + String actionText = getDynamicActionText(description, project); + e.getPresentation().setText(actionText); + e.getPresentation().setEnabledAndVisible(true); + } + + /** + * Gets dynamic action text based on the problem description. + */ + private String getDynamicActionText(@NotNull String description, @NotNull Project project) { + // Try to detect JDK issue + if (description.toLowerCase().contains("jdk") || + description.toLowerCase().contains("java runtime") || + description.toLowerCase().contains("java version")) { + return "Upgrade JDK with Copilot"; + } + + // Try to detect Spring Boot + if (description.toLowerCase().contains("spring boot")) { + return "Upgrade Spring Boot with Copilot"; + } + + // Try to detect Spring Framework + if (description.toLowerCase().contains("spring framework")) { + return "Upgrade Spring Framework with Copilot"; + } + + // Try to detect Spring Security + if (description.toLowerCase().contains("spring security")) { + return "Upgrade Spring Security with Copilot"; + } + + // Try to get from cache if available + final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); + if (cache.isInitialized()) { + // Check for JDK issue + if (cache.getJdkIssue() != null && description.contains(cache.getJdkIssue().getMessage())) { + return "Upgrade JDK with Copilot"; + } + // Check for Spring Boot + JavaUpgradeIssue springBootIssue = cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); + if (springBootIssue != null && description.contains(springBootIssue.getMessage())) { + return "Upgrade Spring Boot with Copilot"; + } + } + + // Default text + return "Scan and Resolve CVEs with Copilot"; + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + /** + * Checks if the description indicates a vulnerability. + */ + private boolean isVulnerabilityDescription(@NotNull String description) { + final String lowerDescription = description.toLowerCase(); + return lowerDescription.contains("vulnerable") || + lowerDescription.contains("cve-") || + lowerDescription.contains("security") || + lowerDescription.contains("vulnerability"); + } + + /** + * Extracts the problem description from the action event context. + */ + @Nullable + private String extractProblemDescription(@NotNull AnActionEvent e) { + // Try multiple approaches to get the problem description + + // Approach 1: Try to get Problem object directly from Problems View + try { + final Object problemData = e.getDataContext().getData(PROBLEMS_VIEW_PROBLEM_KEY); + if (problemData instanceof Problem) { + final Problem problem = (Problem) problemData; + final String text = problem.getText(); + if (text != null && !text.isEmpty()) { + return text; + } + } + } catch (Exception ignored) { + // Problem class might not be available + } + + // Approach 2: Try "problem.description" data key + try { + @SuppressWarnings("deprecation") + final Object data = e.getDataContext().getData("problem.description"); + if (data instanceof String && !((String) data).isEmpty()) { + return (String) data; + } + } catch (Exception ignored) { + } + + // Approach 3: Try to get selected items from the tree + try { + final Object[] selectedItems = e.getData(PlatformDataKeys.SELECTED_ITEMS); + if (selectedItems != null && selectedItems.length > 0) { + // Concatenate all selected items' string representations + final StringBuilder sb = new StringBuilder(); + for (Object item : selectedItems) { + if (item != null) { + sb.append(item.toString()).append(" "); + } + } + final String result = sb.toString().trim(); + if (!result.isEmpty()) { + return result; + } + } + } catch (Exception ignored) { + } + + // Approach 4: Try SELECTED_ITEM + try { + final Object selectedItem = e.getData(PlatformDataKeys.SELECTED_ITEM); + if (selectedItem != null) { + final String text = selectedItem.toString(); + if (!text.isEmpty()) { + return text; + } + } + } catch (Exception ignored) { + } + + // Approach 5: Try getting from context component + try { + final java.awt.Component component = e.getData(PlatformDataKeys.CONTEXT_COMPONENT); + if (component instanceof javax.swing.JTree) { + final javax.swing.JTree tree = (javax.swing.JTree) component; + final javax.swing.tree.TreePath[] paths = tree.getSelectionPaths(); + if (paths != null && paths.length > 0) { + final StringBuilder sb = new StringBuilder(); + for (javax.swing.tree.TreePath path : paths) { + final Object lastComponent = path.getLastPathComponent(); + if (lastComponent != null) { + sb.append(lastComponent.toString()).append(" "); + } + } + final String result = sb.toString().trim(); + if (!result.isEmpty()) { + return result; + } + } + } + } catch (Exception ignored) { + } + + // Approach 6: Try getting selected text from editor + try { + final com.intellij.openapi.editor.Editor editor = e.getData(CommonDataKeys.EDITOR); + if (editor != null && editor.getSelectionModel().hasSelection()) { + final String selectedText = editor.getSelectionModel().getSelectedText(); + if (selectedText != null && !selectedText.isEmpty()) { + return selectedText; + } + } + } catch (Exception ignored) { + } + + return null; + } + + /** + * Builds an upgrade prompt based on the problem description. + */ + private String buildUpgradePrompt(@NotNull String problemDescription) { + + // Extract dependency coordinates if present + final String dependency = extractDependencyCoordinates(problemDescription); + + if (problemDescription != null) { + // Try to detect JDK issue + if (problemDescription.toLowerCase().contains("jdk") || + problemDescription.toLowerCase().contains("java runtime") || + problemDescription.toLowerCase().contains("java version")) { + return "upgrade java runtime to Java " + MATURE_JAVA_LTS_VERSION + " (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; + } + + // Try to detect Spring Boot + if (problemDescription.toLowerCase().contains("spring boot") || problemDescription.toLowerCase().contains("spring framework") || problemDescription.toLowerCase().contains("spring security")) { + return "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + } + + } + // Default text + return "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; + } + + /** + * Extracts CVE ID from the problem description. + */ + private String extractCVEId(@NotNull String description) { + // Pattern: CVE-YYYY-NNNNN + final int cveIndex = description.toUpperCase().indexOf(CVE_MARKER); + if (cveIndex >= 0) { + final int endIndex = findCVEEndIndex(description, cveIndex); + if (endIndex > cveIndex) { + return description.substring(cveIndex, endIndex); + } + } + return null; + } + + /** + * Finds the end index of a CVE ID in the description. + */ + private int findCVEEndIndex(@NotNull String description, int startIndex) { + int index = startIndex + CVE_MARKER.length(); + // Skip year (4 digits) + while (index < description.length() && Character.isDigit(description.charAt(index))) { + index++; + } + // Skip separator + if (index < description.length() && description.charAt(index) == '-') { + index++; + } + // Skip ID number + while (index < description.length() && Character.isDigit(description.charAt(index))) { + index++; + } + return index; + } + + /** + * Extracts Maven dependency coordinates from the problem description. + * Looks for patterns like groupId:artifactId:version + */ + private String extractDependencyCoordinates(@NotNull String description) { + // Look for Maven coordinate pattern: groupId:artifactId:version + // Common patterns in vulnerability reports + final String[] patterns = { + "maven:", // maven:groupId:artifactId:version + "dependency " // dependency groupId:artifactId + }; + + for (String pattern : patterns) { + final int index = description.toLowerCase().indexOf(pattern); + if (index >= 0) { + return extractCoordinatesAfterPattern(description, index + pattern.length()); + } + } + + // Try to find standalone coordinate pattern (e.g., org.example:artifact:1.0.0) + return findStandaloneCoordinates(description); + } + + /** + * Extracts coordinates after a known pattern. + */ + private String extractCoordinatesAfterPattern(@NotNull String description, int startIndex) { + final StringBuilder coords = new StringBuilder(); + int colonCount = 0; + + for (int i = startIndex; i < description.length(); i++) { + final char c = description.charAt(i); + if (Character.isLetterOrDigit(c) || c == '.' || c == '-' || c == '_') { + coords.append(c); + } else if (c == ':' && colonCount < 2) { + coords.append(c); + colonCount++; + } else if (!coords.isEmpty()) { + break; + } + } + + final String result = coords.toString(); + return result.contains(":") ? result : null; + } + + /** + * Finds standalone Maven coordinates in the description. + */ + private String findStandaloneCoordinates(@NotNull String description) { + // Simple heuristic: look for pattern like "org.xxx:xxx" or "com.xxx:xxx" + final String[] prefixes = {"org.", "com.", "io.", "net."}; + + for (String prefix : prefixes) { + int index = description.indexOf(prefix); + while (index >= 0) { + final String coords = extractCoordinatesAfterPattern(description, index); + if (coords != null && coords.contains(":")) { + return coords; + } + index = description.indexOf(prefix, index + 1); + } + } + + return null; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java new file mode 100644 index 00000000000..ff3db6287ef --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInsight.intention.PriorityAction; +import com.intellij.codeInspection.util.IntentionFamilyName; +import com.intellij.codeInspection.util.IntentionName; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiFile; +import com.intellij.util.IncorrectOperationException; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import org.jetbrains.annotations.NotNull; + +/** + * Intention action to upgrade vulnerable dependencies using GitHub Copilot. + * This action appears in the editor's quick-fix popup (More actions...) for pom.xml files + * when a vulnerable dependency is detected. + */ +public class UpgradeInQuickFixIntentionAction implements IntentionAction, PriorityAction { + + private static final String DEFAULT_TEXT = "Scan and Resolve CVEs by Copilot"; + + // Cached dependency info from isAvailable() for use in getText() + private String cachedGroupId; + private String cachedArtifactId; + + @Override + public @IntentionName @NotNull String getText() { + // Return dynamic text based on cached dependency info + if (cachedGroupId != null) { + // Use a friendly display name for known dependencies + String displayName = getDisplayName(cachedGroupId, cachedArtifactId); + return displayName; + } + return DEFAULT_TEXT; + } + + /** + * Gets a friendly display name for a dependency. + */ + private String getDisplayName(String groupId, String artifactId) { + return "Scan and Resolve CVEs by Copilot"; +// if (groupId == null) { +// return "Dependency"; +// } +// +// // Map known groupIds to friendly names +// if (groupId.equals("org.springframework.boot")) { +// return "Upgrade Spring Boot with Copilot"; +// } else if (groupId.equals("org.springframework.security")) { +// return "Upgrade Spring Security with Copilot"; +// } else if (groupId.equals("org.springframework")) { +// return "Spring Framework"; +// } else if (groupId.startsWith("org.springframework")) { +// return "Spring " + (artifactId != null ? artifactId : "dependency"); +// } +// +// // For other dependencies, use groupId:artifactId or just groupId +// if (artifactId != null) { +// return groupId + ":" + artifactId; +// } +// return groupId; + } + + @Override + public @IntentionFamilyName @NotNull String getFamilyName() { + return "Azure Toolkit"; + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + // Reset cached values + cachedGroupId = null; + cachedArtifactId = null; + + if (file == null || editor == null) { + return false; + } + + // Only available for pom.xml files + final String fileName = file.getName(); + if (!fileName.equals("pom.xml")) { + return false; + } + + try { + final int offset = editor.getCaretModel().getOffset(); + final String documentText = editor.getDocument().getText(); + + // Try to extract dependency info - only show if cursor is within a block + final int dependencyStart = findDependencyStart(documentText, offset); + final int dependencyEnd = findDependencyEnd(documentText, offset); + + if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { + final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); + cachedGroupId = extractXmlValue(dependencyBlock, "groupId"); + cachedArtifactId = extractXmlValue(dependencyBlock, "artifactId"); + + // Only show if we have valid dependency info (not for parent/plugin sections) + return cachedGroupId != null && cachedArtifactId != null; + } + } catch (Exception e) { + // Ignore and return false + } + + return false; + } + + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + if (file == null || editor == null) { + return; + } + + // Try to extract dependency information from the current context + final String prompt = buildPromptFromContext(editor, file); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } + + /** + * Builds a prompt based on the current editor context. + */ + private String buildPromptFromContext(@NotNull Editor editor, @NotNull PsiFile file) { +// try { +// final int offset = editor.getCaretModel().getOffset(); +// final String documentText = editor.getDocument().getText(); +// +// // Find the dependency block around the cursor +// final int dependencyStart = findDependencyStart(documentText, offset); +// final int dependencyEnd = findDependencyEnd(documentText, offset); +// +// if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { +// final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); +// +// // Extract groupId and artifactId +// final String groupId = extractXmlValue(dependencyBlock, "groupId"); +// final String artifactId = extractXmlValue(dependencyBlock, "artifactId"); +// final String version = extractXmlValue(dependencyBlock, "version"); +// +// if (groupId != null && artifactId != null) { +// final StringBuilder prompt = new StringBuilder(); +// prompt.append("Fix security vulnerabilities in "); +// prompt.append(groupId).append(":").append(artifactId); +// if (version != null) { +// prompt.append(":").append(version); +// } +// prompt.append(" by using #validate_cves_for_java"); +// return prompt.toString(); +// } +// } +// } catch (Exception e) { +// // Fall back to generic prompt +// } + + return "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; + } + + /** + * Finds the start of the dependency block containing the given offset. + */ + private int findDependencyStart(@NotNull String text, int offset) { + // Look for tag before the offset + int searchStart = Math.max(0, offset - 500); + String searchArea = text.substring(searchStart, offset); + int lastDependency = searchArea.lastIndexOf(""); + if (lastDependency >= 0) { + return searchStart + lastDependency; + } + return -1; + } + + /** + * Finds the end of the dependency block containing the given offset. + */ + private int findDependencyEnd(@NotNull String text, int offset) { + // Look for tag after the offset + int searchEnd = Math.min(text.length(), offset + 500); + String searchArea = text.substring(offset, searchEnd); + int endDependency = searchArea.indexOf(""); + if (endDependency >= 0) { + return offset + endDependency + "".length(); + } + return -1; + } + + /** + * Extracts a value from an XML tag. + */ + private String extractXmlValue(@NotNull String xml, @NotNull String tagName) { + final String startTag = "<" + tagName + ">"; + final String endTag = ""; + + int start = xml.indexOf(startTag); + if (start < 0) { + return null; + } + start += startTag.length(); + + int end = xml.indexOf(endTag, start); + if (end < 0) { + return null; + } + + return xml.substring(start, end).trim(); + } + + @Override + public boolean startInWriteAction() { + return false; + } + + @Override + public @NotNull Priority getPriority() { + return Priority.NORMAL; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java new file mode 100644 index 00000000000..890848e1d94 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import org.jetbrains.annotations.NotNull; + +/** + * Action to upgrade a Java project using GitHub Copilot. + * This action appears in the GitHub Copilot submenu when right-clicking on: + * - Project root folder + * - pom.xml (Maven projects) + * - build.gradle or build.gradle.kts (Gradle projects) + */ +public class UpgradeProjectAction extends AnAction { + + private static final String UPGRADE_JAVA_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + + // text, description, and icon are defined in azure-intellij-plugin-appmod.xml + public UpgradeProjectAction() { + super(); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + final Project project = e.getProject(); + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + + boolean visible = false; + + if (project != null && file != null) { + // Check if it's a project root, pom.xml, or build.gradle file + visible = isProjectRoot(project, file) || + isMavenBuildFile(file) || + isGradleBuildFile(file); + } + + e.getPresentation().setEnabledAndVisible(visible); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getProject(); + if (project == null) { + return; + } + + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + String prompt = buildUpgradePrompt(project, file); + + // Open Copilot chat with the upgrade prompt + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } + + /** + * Builds the upgrade prompt based on the selected file. + */ + private String buildUpgradePrompt(Project project, VirtualFile file) { + return UPGRADE_JAVA_PROMPT; + } + + /** + * Checks if the file is the project root directory. + */ + private boolean isProjectRoot(Project project, VirtualFile file) { + if (!file.isDirectory()) { + return false; + } + + final VirtualFile projectBaseDir = project.getBaseDir(); + if (projectBaseDir == null) { + return false; + } + + return file.equals(projectBaseDir); + } + + /** + * Checks if the file is a Maven build file (pom.xml). + */ + private boolean isMavenBuildFile(VirtualFile file) { + return file != null && !file.isDirectory() && "pom.xml".equals(file.getName()); + } + + /** + * Checks if the file is a Gradle build file (build.gradle or build.gradle.kts). + */ + private boolean isGradleBuildFile(VirtualFile file) { + if (file == null || file.isDirectory()) { + return false; + } + final String name = file.getName(); + return "build.gradle".equals(name) || "build.gradle.kts".equals(name); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java new file mode 100644 index 00000000000..8224dc19887 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao; + +import lombok.Builder; +import lombok.Data; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Represents a detected upgrade issue in a Java project. + * This class is aligned with the TypeScript implementation in vscode-java-dependency. + * + * @see type.ts + */ +@Data +@Builder +public class JavaUpgradeIssue { + + /** + * The reason why an upgrade is recommended. + * Aligned with UpgradeReason enum from vscode-java-dependency. + */ + public enum UpgradeReason { + /** + * The version has reached end of life and is no longer maintained. + */ + END_OF_LIFE, + /** + * The version is deprecated and should be upgraded. + */ + DEPRECATED, + /** + * The version has known security vulnerabilities (CVEs). + */ + CVE, + /** + * The JRE/JDK version is too old compared to the mature LTS version. + */ + JRE_TOO_OLD + } + + /** + * Severity level of the issue. + */ + public enum Severity { + /** + * The version is very old and may have security vulnerabilities or be unsupported. + */ + CRITICAL, + /** + * The version is outdated and should be upgraded. + */ + WARNING, + /** + * The version is slightly behind the latest, informational only. + */ + INFO + } + + /** + * The package identifier (groupId:artifactId or "jdk" for JDK issues). + */ + @Nonnull + private String packageId; + + /** + * The display name of the package (e.g., "Spring Boot", "JDK"). + */ + @Nonnull + private String packageDisplayName; + + /** + * The reason why upgrade is recommended. + */ + @Nonnull + private UpgradeReason upgradeReason; + + /** + * Severity level of this issue. + */ + @Nonnull + private Severity severity; + + /** + * The current version detected in the project. + */ + @Nullable + private String currentVersion; + + /** + * The supported version range (e.g., "2.7.x || >=3.2"). + */ + @Nullable + private String supportedVersion; + + /** + * The suggested version to upgrade to. + */ + @Nullable + private String suggestedVersion; + + /** + * Detailed message describing the issue. + */ + @Nonnull + private String message; + + /** + * URL for more information about the issue. + */ + @Nullable + private String learnMoreUrl; + + /** + * CVE identifier if this is a CVE issue. + */ + @Nullable + private String cveId; + + /** + * Gets a formatted title for the notification. + */ + public String getTitle() { + return switch (upgradeReason) { + case JRE_TOO_OLD -> "Outdated JDK Version Detected"; + case END_OF_LIFE -> packageDisplayName + " End of Life"; + case DEPRECATED -> packageDisplayName + " Deprecated"; + case CVE -> "Security Vulnerability in " + packageDisplayName; + }; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java new file mode 100644 index 00000000000..41222922e10 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.inspection; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.psi.XmlElementVisitor; +import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; + +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +/** + * Inspection that displays Java upgrade issues detected by JavaUpgradeDetectionService. + * Shows JDK version and framework version issues in pom.xml files with wavy underlines. + * + * Note: Issues are cached at project startup via JavaUpgradeIssueCache to avoid + * repeated expensive scans during inspection runs. + */ +public class JavaUpgradeIssuesInspection extends LocalInspectionTool { + + private static final Logger LOG = Logger.getInstance(JavaUpgradeIssuesInspection.class); + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + final PsiFile file = holder.getFile(); + + // Only process pom.xml files + if (!(file instanceof XmlFile) || !file.getName().equals("pom.xml")) { + return PsiElementVisitor.EMPTY_VISITOR; + } + + final Project project = holder.getProject(); + final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); + + // Skip if cache is not yet initialized (will show issues after startup completes) + if (!cache.isInitialized()) { + return PsiElementVisitor.EMPTY_VISITOR; + } + + // Get cached issues (computed once at project startup) + final JavaUpgradeIssue jdkIssue = cache.getJdkIssue(); + final JavaUpgradeIssue springBootIssue = cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); + final JavaUpgradeIssue springFrameworkIssue = cache.findDependencyIssue(GROUP_ID_SPRING_FRAMEWORK + ":"); + final JavaUpgradeIssue springSecurityIssue = cache.findDependencyIssue(GROUP_ID_SPRING_SECURITY); + + // Debug logging + LOG.info("JavaUpgradeIssuesInspection: Cache initialized=" + cache.isInitialized() + + ", jdkIssue=" + (jdkIssue != null) + + ", springBootIssue=" + (springBootIssue != null ? springBootIssue.getCurrentVersion() : "null") + + ", dependencyIssues=" + cache.getDependencyIssues().size()); + + return new XmlElementVisitor() { + @Override + public void visitXmlTag(@NotNull XmlTag tag) { + super.visitXmlTag(tag); + + // Check for JDK version tags + if (jdkIssue != null) { + if (isJavaVersionProperty(tag) || isCompilerPluginVersionTag(tag)) { + registerProblem(holder, tag, jdkIssue); + } + } + + // Check for Spring Boot parent version + if (springBootIssue != null && isSpringBootParentVersion(tag)) { + registerProblem(holder, tag, springBootIssue); + } + + // Check for Spring Framework dependency version + if (springFrameworkIssue != null && isSpringFrameworkDependencyVersion(tag)) { + registerProblem(holder, tag, springFrameworkIssue); + } + + // Check for Spring Boot dependency version (when not using parent) + if (springBootIssue != null && isSpringBootDependencyVersion(tag)) { + registerProblem(holder, tag, springBootIssue); + } + + // Check for Spring Security dependency version + if (springSecurityIssue != null && isSpringSecurityDependencyVersion(tag)) { + registerProblem(holder, tag, springSecurityIssue); + } + } + }; + } + + private void registerProblem(@NotNull ProblemsHolder holder, @NotNull XmlTag tag, @NotNull JavaUpgradeIssue issue) { + // ProblemHighlightType highlightType = issue.getSeverity() == JavaUpgradeIssue.Severity.CRITICAL + // ? ProblemHighlightType.ERROR + // : ProblemHighlightType.WARNING; + + holder.registerProblem( + tag, + issue.getMessage(), + ProblemHighlightType.WARNING, + new UpgradeWithCopilotQuickFix(issue) + ); + } + + /** + * Checks if the tag is a Java version property (java.version, maven.compiler.source, maven.compiler.target). + */ + private boolean isJavaVersionProperty(@NotNull XmlTag tag) { + String tagName = tag.getName(); + XmlTag parent = tag.getParentTag(); + + if (parent == null || !"properties".equals(parent.getName())) { + return false; + } + + return "java.version".equals(tagName) || + "maven.compiler.source".equals(tagName) || + "maven.compiler.target".equals(tagName) || + "maven.compiler.release".equals(tagName); + } + + /** + * Checks if the tag is a version tag inside maven-compiler-plugin configuration. + */ + private boolean isCompilerPluginVersionTag(@NotNull XmlTag tag) { + String tagName = tag.getName(); + if (!"source".equals(tagName) && !"target".equals(tagName) && !"release".equals(tagName)) { + return false; + } + + // Check if we're inside maven-compiler-plugin + XmlTag current = tag.getParentTag(); + while (current != null) { + if ("plugin".equals(current.getName())) { + XmlTag artifactIdTag = current.findFirstSubTag("artifactId"); + if (artifactIdTag != null && ARTIFACT_ID_MAVEN_COMPILER_PLUGIN.equals(artifactIdTag.getValue().getText())) { + return true; + } + } + current = current.getParentTag(); + } + return false; + } + + /** + * Checks if the tag is the version tag inside a Spring Boot parent declaration. + */ + private boolean isSpringBootParentVersion(@NotNull XmlTag tag) { + if (!"version".equals(tag.getName())) { + return false; + } + + XmlTag parent = tag.getParentTag(); + if (parent == null || !"parent".equals(parent.getName())) { + return false; + } + + XmlTag groupIdTag = parent.findFirstSubTag("groupId"); + XmlTag artifactIdTag = parent.findFirstSubTag("artifactId"); + + return groupIdTag != null && GROUP_ID_SPRING_BOOT.equals(groupIdTag.getValue().getText()) && + artifactIdTag != null && ARTIFACT_ID_SPRING_BOOT_STARTER_PARENT.equals(artifactIdTag.getValue().getText()); + } + + /** + * Checks if the tag is a version tag inside a Spring Boot dependency. + */ + private boolean isSpringBootDependencyVersion(@NotNull XmlTag tag) { + if (!"version".equals(tag.getName())) { + return false; + } + + XmlTag dependency = tag.getParentTag(); + if (dependency == null || !"dependency".equals(dependency.getName())) { + return false; + } + + XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); + return groupIdTag != null && GROUP_ID_SPRING_BOOT.equals(groupIdTag.getValue().getText()); + } + + /** + * Checks if the tag is a version tag inside a Spring Framework dependency. + */ + private boolean isSpringFrameworkDependencyVersion(@NotNull XmlTag tag) { + if (!"version".equals(tag.getName())) { + return false; + } + + XmlTag dependency = tag.getParentTag(); + if (dependency == null || !"dependency".equals(dependency.getName())) { + return false; + } + + XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); + return groupIdTag != null && GROUP_ID_SPRING_FRAMEWORK.equals(groupIdTag.getValue().getText()); + } + + /** + * Checks if the tag is a version tag inside a Spring Security dependency. + */ + private boolean isSpringSecurityDependencyVersion(@NotNull XmlTag tag) { + if (!"version".equals(tag.getName())) { + return false; + } + + XmlTag dependency = tag.getParentTag(); + if (dependency == null || !"dependency".equals(dependency.getName())) { + return false; + } + + XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); + return groupIdTag != null && GROUP_ID_SPRING_SECURITY.equals(groupIdTag.getValue().getText()); + } + + /** + * Quick fix to upgrade using Copilot based on the issue type. + */ + private static class UpgradeWithCopilotQuickFix implements LocalQuickFix { + private final JavaUpgradeIssue issue; + + public UpgradeWithCopilotQuickFix(@NotNull JavaUpgradeIssue issue) { + this.issue = issue; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getFamilyName() { + return "Azure Toolkit"; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getName() { + return "Upgrade " + issue.getPackageDisplayName() + " with Copilot"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + String prompt = buildPromptForIssue(issue); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } + + private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { + String packageId = issue.getPackageId(); + + // JDK upgrade + if (PACKAGE_ID_JDK.equals(packageId)) { + return String.format( + "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", + issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } + + // Framework upgrade (Spring Boot, Spring Framework, etc.) + return String.format( + "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", + issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java new file mode 100644 index 00000000000..d6380a38f6c --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java @@ -0,0 +1,564 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; + +import com.intellij.util.net.ssl.CertificateManager; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.Builder; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.apache.maven.artifact.versioning.ComparableVersion; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Service to check for CVE (Common Vulnerabilities and Exposures) issues in Maven dependencies. + * Uses GitHub's Security Advisory API to fetch vulnerability information. + * + * This implementation is aligned with the TypeScript version in vscode-java-dependency. + * @see cve.ts + */ +public class CVECheckService { + + private static final String GITHUB_API_BASE = "https://api.github.com/advisories"; + private static final int BATCH_SIZE = 30; + private static final int REQUEST_TIMEOUT_SECONDS = 30; + + /** + * Severity levels ordered by criticality (higher number = more critical). + * Aligned with GitHub Security Advisory severity levels. + */ + public enum Severity { + UNKNOWN(0), + LOW(1), + MEDIUM(2), + HIGH(3), + CRITICAL(4); + + private final int level; + + Severity(int level) { + this.level = level; + } + + public int getLevel() { + return level; + } + + public static Severity fromString(String severity) { + if (severity == null) return UNKNOWN; + return switch (severity.toLowerCase()) { + case "critical" -> CRITICAL; + case "high" -> HIGH; + case "medium" -> MEDIUM; + case "low" -> LOW; + default -> UNKNOWN; + }; + } + } + + /** + * Represents a CVE (Common Vulnerabilities and Exposures) entry. + */ + @Data + @Builder + public static class CVE { + private String id; + private String ghsaId; + private Severity severity; + private String summary; + private String description; + private String htmlUrl; + private List affectedDeps; + } + + /** + * Represents a dependency affected by a CVE. + */ + @Data + @Builder + public static class AffectedDependency { + private String name; + private String vulnerableVersionRange; + private String patchedVersion; + } + + /** + * Represents a dependency coordinate (groupId:artifactId:version). + */ + @Data + @Builder + public static class DependencyCoordinate { + private String groupId; + private String artifactId; + private String version; + + public String getName() { + return groupId + ":" + artifactId; + } + + public String getCoordinate() { + return groupId + ":" + artifactId + ":" + version; + } + } + + private static CVECheckService instance; + private final HttpClient httpClient; + private final Gson gson; + + private CVECheckService() { + final HttpClient.Builder builder = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS)); + try { + builder.sslContext(CertificateManager.getInstance().getSslContext()); + } catch (Throwable e) { + // Failed to get IntelliJ SSL context, using default + } + this.httpClient = builder.build(); + this.gson = new Gson(); + } + + public static synchronized CVECheckService getInstance() { + if (instance == null) { + instance = new CVECheckService(); + } + return instance; + } + + /** + * Batch check CVE issues for a list of Maven coordinates. + * Aligned with batchGetCVEIssues() from cve.ts. + * + * @param coordinates List of Maven coordinates in format "groupId:artifactId:version" + * @return List of CVE-related upgrade issues + */ + @Nonnull + public List batchGetCVEIssues(@Nonnull List coordinates) { + final List allIssues = new ArrayList<>(); + + // Process dependencies in batches to avoid URL length limits + for (int i = 0; i < coordinates.size(); i += BATCH_SIZE) { + final List batch = coordinates.subList(i, Math.min(i + BATCH_SIZE, coordinates.size())); + try { + final List batchIssues = getCveUpgradeIssues(batch); + allIssues.addAll(batchIssues); + } catch (Exception e) { + // Error checking CVEs for batch + } + } + + return allIssues; + } + + /** + * Get CVE upgrade issues for a batch of coordinates. + */ + @Nonnull + private List getCveUpgradeIssues(@Nonnull List coordinates) { + if (coordinates.isEmpty()) { + return Collections.emptyList(); + } + + final List deps = coordinates.stream() + .map(this::parseCoordinate) + .filter(Objects::nonNull) + .filter(d -> StringUtils.isNotBlank(d.getVersion())) + .collect(Collectors.toList()); + + if (deps.isEmpty()) { + return Collections.emptyList(); + } + + final List depsCves = fetchCves(deps); + return mapCvesToUpgradeIssues(depsCves); + } + + /** + * Parse a Maven coordinate string into a DependencyCoordinate object. + */ + @Nullable + private DependencyCoordinate parseCoordinate(@Nonnull String coordinate) { + final String[] parts = coordinate.split(":", 3); + if (parts.length < 3) { + return null; + } + return DependencyCoordinate.builder() + .groupId(parts[0]) + .artifactId(parts[1]) + .version(parts[2]) + .build(); + } + + /** + * Represents a dependency with its associated CVEs. + */ + @Data + @Builder + private static class DepsCves { + private String dep; + private String version; + private List cves; + } + + /** + * Fetch CVEs from GitHub Security Advisory API. + */ + @Nonnull + private List fetchCves(@Nonnull List deps) { + if (deps.isEmpty()) { + return Collections.emptyList(); + } + + try { + final List allCves = retrieveVulnerabilityData(deps); + + if (allCves.isEmpty()) { + return Collections.emptyList(); + } + + // Group the CVEs by coordinate + final List depsCves = new ArrayList<>(); + + for (DependencyCoordinate dep : deps) { + final List depCves = allCves.stream() + .filter(cve -> isCveAffectingDep(cve, dep.getName(), dep.getVersion())) + .collect(Collectors.toList()); + + if (!depCves.isEmpty()) { + depsCves.add(DepsCves.builder() + .dep(dep.getName()) + .version(dep.getVersion()) + .cves(depCves) + .build()); + } + } + + return depsCves; + } catch (Exception e) { + // Error fetching CVEs + return Collections.emptyList(); + } + } + + + + /** + * Retrieve vulnerability data from GitHub Security Advisory API. + * Only fetches critical and high severity CVEs for Maven ecosystem. + */ + @Nonnull + private List retrieveVulnerabilityData(@Nonnull List deps) { + if (deps.isEmpty()) { + return Collections.emptyList(); + } + + try { + // Build the affects parameter: package@version format + // Based on TS: deps.map((p) => `${p.name}@${p.version}`) passed as array to octokit. + // Octokit usually serializes array as repeated params affects=a&affects=b OR comma-sep. + // GitHub API docs say "iterable". Standard approach for "iterable" in query string is repeated params. + // But previous Java code used comma. I will stick to comma as it likely works, or change if needed. + // TS Octokit behavior for "affects" param -> comma separated string is often accepted. + final String affects = deps.stream() + .map(d -> URLEncoder.encode(d.getName() + "@" + d.getVersion(), StandardCharsets.UTF_8)) + .collect(Collectors.joining(",")); + + final List allCves = new ArrayList<>(); + int page = 1; + + while (true) { + final String url = String.format( + "%s?ecosystem=maven&affects=%s&per_page=100&sort=published&direction=asc&page=%d", + GITHUB_API_BASE, affects, page + ); + + final HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/vnd.github+json") + .header("X-GitHub-Api-Version", "2022-11-28") + .timeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS)) + .GET() + .build(); + + final HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + // GitHub API returned non-200 status + break; + } + + final JsonArray advisories = gson.fromJson(response.body(), JsonArray.class); + if (advisories.isEmpty()) { + break; + } + + // Parse and add to list + allCves.addAll(parseAdvisories(advisories)); + + if (advisories.size() < 100) { + break; + } + + page++; + } + + return allCves; + + } catch (Exception e) { + // Error retrieving vulnerability data from GitHub + return Collections.emptyList(); + } + } + + /** + * Parse GitHub Security Advisory API response into CVE objects. + */ + @Nonnull + private List parseAdvisories(@Nonnull JsonArray advisories) { + final List cves = new ArrayList<>(); + + try { + for (JsonElement element : advisories) { + final JsonObject advisory = element.getAsJsonObject(); + + // Skip withdrawn advisories + if (advisory.has("withdrawn_at") && !advisory.get("withdrawn_at").isJsonNull()) { + continue; + } + + final String severity = getStringOrNull(advisory, "severity"); + final Severity severityEnum = Severity.fromString(severity); + + // Only consider critical and high severity CVEs + if (severityEnum != Severity.CRITICAL && severityEnum != Severity.HIGH) { + continue; + } + + final String cveId = getStringOrNull(advisory, "cve_id"); + final String ghsaId = getStringOrNull(advisory, "ghsa_id"); + final String id = StringUtils.isNotBlank(cveId) ? cveId : ghsaId; + + final List affectedDeps = new ArrayList<>(); + if (advisory.has("vulnerabilities") && !advisory.get("vulnerabilities").isJsonNull()) { + final JsonArray vulnerabilities = advisory.getAsJsonArray("vulnerabilities"); + for (JsonElement vulnElement : vulnerabilities) { + final JsonObject vuln = vulnElement.getAsJsonObject(); + String packageName = null; + if (vuln.has("package") && !vuln.get("package").isJsonNull()) { + packageName = getStringOrNull(vuln.getAsJsonObject("package"), "name"); + } + + affectedDeps.add(AffectedDependency.builder() + .name(packageName) + .vulnerableVersionRange(getStringOrNull(vuln, "vulnerable_version_range")) + .patchedVersion(getStringOrNull(vuln, "first_patched_version")) + .build()); + } + } + + cves.add(CVE.builder() + .id(id) + .ghsaId(ghsaId) + .severity(severityEnum) + .summary(getStringOrNull(advisory, "summary")) + .description(getStringOrNull(advisory, "description")) + .htmlUrl(getStringOrNull(advisory, "html_url")) + .affectedDeps(affectedDeps) + .build()); + } + + } catch (Exception e) { + // Error parsing advisory JSON + } + + return cves; + } + + /** + * Parse GitHub Security Advisory API response into CVE objects. + * Legacy method for backward compatibility if needed, or helper + */ + @Nonnull + private List parseAdvisories(@Nonnull String jsonResponse) { + try { + return parseAdvisories(gson.fromJson(jsonResponse, JsonArray.class)); + } catch (Exception e) { + // Error parsing advisory JSON + return Collections.emptyList(); + } + } + + @Nullable + private String getStringOrNull(@Nonnull JsonObject obj, @Nonnull String key) { + if (obj.has(key) && !obj.get(key).isJsonNull()) { + return obj.get(key).getAsString(); + } + return null; + } + + /** + * Map CVEs to upgrade issues. + */ + @Nonnull + private List mapCvesToUpgradeIssues(@Nonnull List depsCves) { + if (depsCves.isEmpty()) { + return Collections.emptyList(); + } + + return depsCves.stream() + .map(this::mapDepCvesToUpgradeIssue) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Map a single dependency's CVEs to an upgrade issue. + * Uses the most critical CVE for the issue details. + */ + @Nullable + private JavaUpgradeIssue mapDepCvesToUpgradeIssue(@Nonnull DepsCves depCve) { + if (depCve.getCves() == null || depCve.getCves().isEmpty()) { + return null; + } + + // Find the most critical CVE + final CVE mostCriticalCve = depCve.getCves().stream() + .max(Comparator.comparingInt(cve -> cve.getSeverity().getLevel())) + .orElse(depCve.getCves().get(0)); + + // Build the message + final String message = String.format( + "Security vulnerability %s detected in %s %s. %s", + mostCriticalCve.getId() != null ? mostCriticalCve.getId() : "CVE", + depCve.getDep(), + depCve.getVersion(), + StringUtils.isNotBlank(mostCriticalCve.getSummary()) ? + mostCriticalCve.getSummary() : + "Please upgrade to a patched version." + ); + + // Determine suggested version from patched versions + String suggestedVersion = null; + if (mostCriticalCve.getAffectedDeps() != null) { + suggestedVersion = mostCriticalCve.getAffectedDeps().stream() + .filter(ad -> depCve.getDep().equals(ad.getName())) + .map(AffectedDependency::getPatchedVersion) + .filter(StringUtils::isNotBlank) + .findFirst() + .orElse(null); + } + + return JavaUpgradeIssue.builder() + .packageId(depCve.getDep()) + .packageDisplayName(depCve.getDep()) + .upgradeReason(JavaUpgradeIssue.UpgradeReason.CVE) + .severity(mapCveSeverityToIssueSeverity(mostCriticalCve.getSeverity())) + .currentVersion(depCve.getVersion()) + .suggestedVersion(suggestedVersion) + .message(message) + .learnMoreUrl(mostCriticalCve.getHtmlUrl()) + .cveId(mostCriticalCve.getId()) + .build(); + } + + /** + * Map CVE severity to issue severity. + */ + @Nonnull + private JavaUpgradeIssue.Severity mapCveSeverityToIssueSeverity(@Nonnull Severity cveSeverity) { + return switch (cveSeverity) { + case CRITICAL -> JavaUpgradeIssue.Severity.CRITICAL; + case HIGH -> JavaUpgradeIssue.Severity.WARNING; + default -> JavaUpgradeIssue.Severity.INFO; + }; + } + + /** + * Check if a CVE affects a specific dependency at a specific version. + * Aligned with isCveAffectingDep() from cve.ts. + */ + private boolean isCveAffectingDep(@Nonnull CVE cve, @Nonnull String depName, @Nonnull String depVersion) { + if (cve.getAffectedDeps() == null || cve.getAffectedDeps().isEmpty()) { + return false; + } + + return cve.getAffectedDeps().stream() + .anyMatch(ad -> depName.equals(ad.getName()) && + isVersionInRange(depVersion, ad.getVulnerableVersionRange())); + } + + /** + * Check if a version satisfies a vulnerability version range. + * Handles common range formats like ">= 1.0, < 2.0", "< 3.0", etc. + */ + private boolean isVersionInRange(@Nonnull String version, @Nullable String range) { + if (StringUtils.isBlank(range)) { + return false; + } + + try { + final ComparableVersion currentVersion = new ComparableVersion(version); + + // Split by comma for compound ranges (e.g., ">= 1.0, < 2.0") + final String[] conditions = range.split(","); + + for (String condition : conditions) { + condition = condition.trim(); + + if (!satisfiesCondition(currentVersion, condition)) { + return false; + } + } + + return true; + } catch (Exception e) { + // Error checking version range + return false; + } + } + + /** + * Check if a version satisfies a single condition. + */ + private boolean satisfiesCondition(@Nonnull ComparableVersion version, @Nonnull String condition) { + condition = condition.trim(); + + if (condition.startsWith(">=")) { + final ComparableVersion min = new ComparableVersion(condition.substring(2).trim()); + return version.compareTo(min) >= 0; + } else if (condition.startsWith(">")) { + final ComparableVersion min = new ComparableVersion(condition.substring(1).trim()); + return version.compareTo(min) > 0; + } else if (condition.startsWith("<=")) { + final ComparableVersion max = new ComparableVersion(condition.substring(2).trim()); + return version.compareTo(max) <= 0; + } else if (condition.startsWith("<")) { + final ComparableVersion max = new ComparableVersion(condition.substring(1).trim()); + return version.compareTo(max) < 0; + } else if (condition.startsWith("=")) { + final ComparableVersion exact = new ComparableVersion(condition.substring(1).trim()); + return version.compareTo(exact) == 0; + } else { + // Exact version match + final ComparableVersion exact = new ComparableVersion(condition); + return version.compareTo(exact) == 0; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java new file mode 100644 index 00000000000..71fabe6fea0 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Project-level cache for Java upgrade issues. + * Issues are computed once at startup and cached to avoid repeated expensive scans + * during inspection runs. The cache can be invalidated when project model changes. + */ +@Service(Service.Level.PROJECT) +public final class JavaUpgradeIssuesCache implements Disposable { + + private final Project project; + private final AtomicReference> jdkIssuesCache = new AtomicReference<>(); + private final AtomicReference> dependencyIssuesCache = new AtomicReference<>(); + private final AtomicBoolean initialized = new AtomicBoolean(false); + + public JavaUpgradeIssuesCache(@NotNull Project project) { + this.project = project; + } + + public static JavaUpgradeIssuesCache getInstance(@NotNull Project project) { + return project.getService(JavaUpgradeIssuesCache.class); + } + + /** + * Gets cached JDK issues. Returns empty list if not yet initialized. + */ + @Nonnull + public List getJdkIssues() { + List cached = jdkIssuesCache.get(); + return cached != null ? cached : Collections.emptyList(); + } + + /** + * Gets cached dependency issues. Returns empty list if not yet initialized. + */ + @Nonnull + public List getDependencyIssues() { + List cached = dependencyIssuesCache.get(); + return cached != null ? cached : Collections.emptyList(); + } + + /** + * Finds a specific issue by package ID prefix. + */ + @Nullable + public JavaUpgradeIssue findDependencyIssue(@Nonnull String packageIdPrefix) { + return getDependencyIssues().stream() + .filter(i -> i.getPackageId().startsWith(packageIdPrefix)) + .findFirst() + .orElse(null); + } + + /** + * Gets the first JDK issue if present. + */ + @Nullable + public JavaUpgradeIssue getJdkIssue() { + List issues = getJdkIssues(); + return issues.isEmpty() ? null : issues.get(0); + } + + /** + * Checks if the cache has been initialized. + */ + public boolean isInitialized() { + return initialized.get(); + } + + /** + * Refreshes the cache by re-scanning the project. + * This should be called at project startup and when the project model changes. + */ + public void refresh() { + if (project.isDisposed()) { + return; + } + + final JavaUpgradeIssuesDetectionService detectionService = JavaUpgradeIssuesDetectionService.getInstance(); + + // Scan for issues + List jdkIssues = detectionService.getJavaIssues(project); + List dependencyIssues = detectionService.getDependencyIssues(project); + + // Update cache + jdkIssuesCache.set(Collections.unmodifiableList(jdkIssues)); + dependencyIssuesCache.set(Collections.unmodifiableList(dependencyIssues)); + initialized.set(true); + } + + /** + * Invalidates the cache, forcing a refresh on next access. + */ + public void invalidate() { + jdkIssuesCache.set(null); + dependencyIssuesCache.set(null); + initialized.set(false); + } + + @Override + public void dispose() { + invalidate(); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java new file mode 100644 index 00000000000..895b08d0362 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -0,0 +1,675 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.common.utils.JdkUtils; +import com.microsoft.intellij.util.GradleUtils; +import com.microsoft.intellij.util.MavenUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.maven.artifact.versioning.ComparableVersion; +import org.jetbrains.idea.maven.model.MavenArtifact; +import org.jetbrains.idea.maven.model.MavenArtifactNode; +import org.jetbrains.idea.maven.project.MavenProject; +import org.jetbrains.idea.maven.project.MavenProjectsManager; +import org.jetbrains.plugins.gradle.model.ExternalDependency; +import org.jetbrains.plugins.gradle.model.ExternalProject; +import org.jetbrains.plugins.gradle.model.ExternalSourceSet; +import org.jetbrains.plugins.gradle.model.UnresolvedExternalDependency; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; + +/** + * Service to detect JDK version and framework dependency versions in Java projects. + * This service analyzes the project to identify outdated versions that may need upgrading. + * + * This implementation is aligned with the TypeScript version in vscode-java-dependency. + * @see assessmentManager.ts + */ +public class JavaUpgradeIssuesDetectionService { + + /** + * The mature LTS version of Java that is recommended. + * Aligned with MATURE_JAVA_LTS_VERSION from vscode-java-dependency. + */ + public static final int MATURE_JAVA_LTS_VERSION = 21; + + // Group ID constants for Spring dependencies + public static final String GROUP_ID_SPRING_BOOT = "org.springframework.boot"; + public static final String GROUP_ID_SPRING_FRAMEWORK = "org.springframework"; + public static final String GROUP_ID_SPRING_SECURITY = "org.springframework.security"; + + // Artifact ID constants + public static final String ARTIFACT_ID_SPRING_BOOT_STARTER_PARENT = "spring-boot-starter-parent"; + public static final String ARTIFACT_ID_MAVEN_COMPILER_PLUGIN = "maven-compiler-plugin"; + + // Package ID constants (used for cache lookups) + public static final String PACKAGE_ID_JDK = "jdk"; + + /** + * Metadata for dependencies to scan. + * Aligned with DEPENDENCIES_TO_SCAN from vscode-java-dependency. + */ + public static class DependencyCheckItem { + @Nonnull public final String groupId; + @Nonnull public final String artifactId; + @Nonnull public final String displayName; + @Nonnull public final String supportedVersion; + @Nonnull public final String suggestedVersion; + @Nonnull public final String learnMoreUrl; + @Nonnull public final Map eolDate; + + public DependencyCheckItem(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String displayName, + @Nonnull String supportedVersion, @Nonnull String suggestedVersion, @Nonnull String learnMoreUrl, + @Nonnull Map eolDate) { + this.groupId = groupId; + this.artifactId = artifactId; + this.displayName = displayName; + this.supportedVersion = supportedVersion; + this.suggestedVersion = suggestedVersion; + this.learnMoreUrl = learnMoreUrl; + this.eolDate = eolDate; + } + + public String getPackageId() { + return groupId + ":" + artifactId; + } + + /** + * Gets the EOL date for a specific version. + * @param version The version to check (e.g., "2.7.x", "3.5.x") + * @return The EOL date string (e.g., "2029-06") or null if not found + */ + @Nullable + public String getEolDateForVersion(@Nonnull String version) { + // Try exact match first + if (eolDate.containsKey(version)) { + return eolDate.get(version); + } + // Try to match major.minor.x pattern + String[] parts = version.split("\\."); + if (parts.length >= 2) { + String pattern = parts[0] + "." + parts[1] + ".x"; + return eolDate.get(pattern); + } + return null; + } + } + + /** + * Dependencies to scan for upgrade issues. + * Aligned with DEPENDENCIES_TO_SCAN from dependency.metadata.ts in vscode-java-dependency. + */ + private static final List DEPENDENCIES_TO_SCAN = List.of( + // Spring Boot - supported versions: 2.7.x or >=3.2.x + new DependencyCheckItem( + GROUP_ID_SPRING_BOOT, + "*", + "Spring Boot", + "2.7.x || >=3.2.x", + "3.5", + "https://spring.io/projects/spring-boot#support", + Map.ofEntries( + Map.entry("4.0.x", "2027-12"), + Map.entry("3.5.x", "2032-06"), + Map.entry("3.4.x", "2026-12"), + Map.entry("3.3.x", "2026-06"), + Map.entry("3.2.x", "2025-12"), + Map.entry("3.1.x", "2025-06"), + Map.entry("3.0.x", "2024-12"), + Map.entry("2.7.x", "2029-06"), + Map.entry("2.6.x", "2024-02"), + Map.entry("2.5.x", "2023-08"), + Map.entry("2.4.x", "2023-02"), + Map.entry("2.3.x", "2022-08"), + Map.entry("2.2.x", "2022-01"), + Map.entry("2.1.x", "2021-01"), + Map.entry("2.0.x", "2020-06"), + Map.entry("1.5.x", "2020-11") + ) + ), + // Spring Framework - supported versions: 5.3.x or >=6.2.x + new DependencyCheckItem( + GROUP_ID_SPRING_FRAMEWORK, + "*", + "Spring Framework", + "5.3.x || >=6.2.x", + "6.2", + "https://spring.io/projects/spring-framework#support", + Map.ofEntries( + Map.entry("7.0.x", "2028-06"), + Map.entry("6.2.x", "2032-06"), + Map.entry("6.1.x", "2026-06"), + Map.entry("6.0.x", "2025-08"), + Map.entry("5.3.x", "2029-06"), + Map.entry("5.2.x", "2023-12"), + Map.entry("5.1.x", "2022-12"), + Map.entry("5.0.x", "2022-12"), + Map.entry("4.3.x", "2020-12") + ) + ), + // Spring Security - supported versions: 5.7.x || 5.8.x || >=6.2.x + new DependencyCheckItem( + GROUP_ID_SPRING_SECURITY, + "*", + "Spring Security", + "5.7.x || 5.8.x || >=6.2.x", + "6.5", + "https://spring.io/projects/spring-security#support", + Map.ofEntries( + Map.entry("7.0.x", "2027-12"), + Map.entry("6.5.x", "2032-06"), + Map.entry("6.4.x", "2026-12"), + Map.entry("6.3.x", "2026-06"), + Map.entry("6.2.x", "2025-12"), + Map.entry("6.1.x", "2025-06"), + Map.entry("6.0.x", "2024-12"), + Map.entry("5.8.x", "2029-06"), + Map.entry("5.7.x", "2029-06"), + Map.entry("5.6.x", "2024-02"), + Map.entry("5.5.x", "2023-08"), + Map.entry("5.4.x", "2023-02"), + Map.entry("5.3.x", "2022-08"), + Map.entry("5.2.x", "2022-01"), + Map.entry("5.1.x", "2021-01"), + Map.entry("5.0.x", "2020-06"), + Map.entry("4.2.x", "2020-11") + ) + ) + ); + + private static final String JDK_LEARN_MORE_URL = + "https://learn.microsoft.com/azure/developer/java/fundamentals/java-support-on-azure"; + + private static final Logger LOG = Logger.getInstance(JavaUpgradeIssuesDetectionService.class); + + private static JavaUpgradeIssuesDetectionService instance; + + private JavaUpgradeIssuesDetectionService() { + } + + public static synchronized JavaUpgradeIssuesDetectionService getInstance() { + if (instance == null) { + instance = new JavaUpgradeIssuesDetectionService(); + } + return instance; + } + + /** + * Analyzes the given project and returns a list of detected outdated version issues. + * + * @param project The IntelliJ project to analyze + * @return List of detected outdated version issues + */ + @Nonnull + public List analyzeProject(@Nonnull Project project) { + final List issues = new ArrayList<>(); + + try { + // Get JDK issues + issues.addAll(getJavaIssues(project)); + + // Get dependency issues + issues.addAll(getDependencyIssues(project)); + + // Get CVE issues + issues.addAll(getCVEIssues(project)); + + } catch (Exception e) { + // Error analyzing project for upgrade issues + } + + return issues; + } + + /** + * Gets JDK/JRE version issues. + * Aligned with getJavaIssues() from assessmentManager.ts. + */ + @Nonnull + public List getJavaIssues(@Nonnull Project project) { + final List issues = new ArrayList<>(); + + try { + final Integer jdkVersion = JdkUtils.getJdkLanguageLevel(project); + if (jdkVersion == null) { + return issues; + } + + // Skip versions below 8 - out of scope + if (jdkVersion < 8) { + return issues; + } + + // Check against MATURE_JAVA_LTS_VERSION (21) + if (jdkVersion < MATURE_JAVA_LTS_VERSION) { + issues.add(JavaUpgradeIssue.builder() + .packageId("jdk") + .packageDisplayName("JDK") + .upgradeReason(JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) + .severity(JavaUpgradeIssue.Severity.WARNING) + .currentVersion(String.valueOf(jdkVersion)) + .supportedVersion(">=" + MATURE_JAVA_LTS_VERSION) + .suggestedVersion(String.valueOf(MATURE_JAVA_LTS_VERSION)) + .message(String.format("This project is using an older Java runtime (%d). Would you like to upgrade it to %d (LTS)?", + jdkVersion, MATURE_JAVA_LTS_VERSION)) + .learnMoreUrl(JDK_LEARN_MORE_URL) + .build()); + } + } catch (Exception e) { + // Error checking JDK version + } + + return issues; + } + + /** + * Gets dependency issues by checking against DEPENDENCIES_TO_SCAN metadata. + * Aligned with getDependencyIssue() from assessmentManager.ts. + */ + @Nonnull + public List getDependencyIssues(@Nonnull Project project) { + final List issues = new ArrayList<>(); + + try { + final Set checkedPackages = new HashSet<>(); + + if (MavenUtils.isMavenProject(project)) { + final MavenProjectsManager mavenProjectsManager = MavenProjectsManager.getInstanceIfCreated(project); + if (mavenProjectsManager != null && mavenProjectsManager.isMavenizedProject()) { + final List mavenProjects = mavenProjectsManager.getProjects(); + + for (MavenProject mavenProject : mavenProjects) { + for (DependencyCheckItem checkItem : DEPENDENCIES_TO_SCAN) { + if (checkedPackages.contains(checkItem.getPackageId())) { + continue; + } + + final JavaUpgradeIssue issue = checkDependency(mavenProject, checkItem, checkedPackages); + if (issue != null) { + issues.add(issue); + } + } + } + } + } else if (GradleUtils.isGradleProject(project)) { + final List gradleProjects = GradleUtils.listGradleProjects(project); + for (ExternalProject gradleProject : gradleProjects) { + for (DependencyCheckItem checkItem : DEPENDENCIES_TO_SCAN) { + if (checkedPackages.contains(checkItem.getPackageId())) { + continue; + } + final JavaUpgradeIssue issue = checkGradleDependency(gradleProject, checkItem, checkedPackages); + if (issue != null) { + issues.add(issue); + } + } + } + } + + } catch (Exception e) { + // Error checking dependencies + } + + return issues; + } + + /** + * Gets CVE (Common Vulnerabilities and Exposures) issues for project dependencies. + * Aligned with getCVEIssues() from assessmentManager.ts. + * + * @param project The IntelliJ project to analyze + * @return List of CVE-related upgrade issues + */ + @Nonnull + public List getCVEIssues(@Nonnull Project project) { + try { + final Set coordinateSet = new HashSet<>(); + + if (MavenUtils.isMavenProject(project)) { + final MavenProjectsManager mavenProjectsManager = MavenProjectsManager.getInstanceIfCreated(project); + if (mavenProjectsManager != null && mavenProjectsManager.isMavenizedProject()) { + final List mavenProjects = mavenProjectsManager.getProjects(); + + for (MavenProject mavenProject : mavenProjects) { + // Get direct dependencies only (root level of dependency tree) + mavenProject.getDependencyTree().stream() + .map(MavenArtifactNode::getArtifact) + .filter(dep -> StringUtils.isNotBlank(dep.getVersion())) + .forEach(dep -> coordinateSet.add( + dep.getGroupId() + ":" + dep.getArtifactId() + ":" + dep.getVersion() + )); + } + } + } else if (GradleUtils.isGradleProject(project)) { + final List gradleProjects = GradleUtils.listGradleProjects(project); + for (ExternalProject gradleProject : gradleProjects) { + final ExternalSourceSet main = gradleProject.getSourceSets().get("main"); + if (main != null) { + main.getDependencies().stream() + .filter(dep -> !(dep instanceof UnresolvedExternalDependency)) + .filter(dep -> StringUtils.isNotBlank(dep.getVersion())) + .forEach(dep -> coordinateSet.add( + dep.getGroup() + ":" + dep.getName() + ":" + dep.getVersion() + )); + } + } + } + + if (coordinateSet.isEmpty()) { + return Collections.emptyList(); + } + + // Check CVEs for all collected dependencies + final List coordinates = new ArrayList<>(coordinateSet); + return CVECheckService.getInstance().batchGetCVEIssues(coordinates); + + } catch (Exception e) { + // Error checking CVE issues + return Collections.emptyList(); + } + } + + /** + * Checks a single dependency against its metadata. + * Aligned with the logic in getDependencyIssue() from assessmentManager.ts. + */ + @Nullable + private JavaUpgradeIssue checkDependency(@Nonnull MavenProject mavenProject, + @Nonnull DependencyCheckItem checkItem, + @Nonnull Set checkedPackages) { + String version = null; + + // Special handling for Spring Boot parent POM + if (GROUP_ID_SPRING_BOOT.equals(checkItem.groupId)) { + version = getParentVersion(mavenProject, checkItem.groupId, ARTIFACT_ID_SPRING_BOOT_STARTER_PARENT); + } + + // If not found in parent, check direct dependencies + if (version == null) { + String targetArtifactId = "*".equals(checkItem.artifactId) ? null : checkItem.artifactId; + final MavenArtifact dependency = findDirectDependency(mavenProject, checkItem.groupId, targetArtifactId); + if (dependency != null) { + version = dependency.getVersion(); + } + } + + if (version == null || StringUtils.isBlank(version)) { + return null; + } + + checkedPackages.add(checkItem.getPackageId()); + + // Check if version satisfies the supported version range + if (!satisfiesVersionRange(version, checkItem.supportedVersion)) { + return JavaUpgradeIssue.builder() + .packageId(checkItem.getPackageId()) + .packageDisplayName(checkItem.displayName) + .upgradeReason(determineUpgradeReason(version, checkItem)) + .severity(determineSeverity(version, checkItem)) + .currentVersion(version) + .supportedVersion(checkItem.supportedVersion) + .suggestedVersion(checkItem.suggestedVersion) + .message(buildUpgradeMessage(checkItem.displayName, version, checkItem)) + .learnMoreUrl(checkItem.learnMoreUrl) + .build(); + } + + return null; + } + + /** + * Gets the version from parent POM. + */ + @Nullable + private String getParentVersion(@Nonnull MavenProject mavenProject, + @Nonnull String groupId, + @Nonnull String artifactId) { + try { + final var parentId = mavenProject.getParentId(); + LOG.info("getParentVersion: parentId=" + (parentId != null ? + parentId.getGroupId() + ":" + parentId.getArtifactId() + ":" + parentId.getVersion() : "null") + + ", looking for " + groupId + ":" + artifactId); + if (parentId != null && + groupId.equals(parentId.getGroupId()) && + artifactId.equals(parentId.getArtifactId())) { + LOG.info("getParentVersion: Found matching parent version: " + parentId.getVersion()); + return parentId.getVersion(); + } + } catch (Exception e) { + LOG.warn("Error getting parent version", e); + } + return null; + } + + /** + * Checks if a version satisfies a version range. + * Supports ranges like: "2.7.x || >=3.2", ">=10", "5.3.x || >=6.1" + * Aligned with semver logic from assessmentManager.ts. + */ + private boolean satisfiesVersionRange(@Nonnull String version, @Nonnull String range) { + // Split by "||" for OR conditions + final String[] orConditions = range.split("\\|\\|"); + + for (String condition : orConditions) { + condition = condition.trim(); + + if (satisfiesSingleCondition(version, condition)) { + return true; + } + } + + return false; + } + + /** + * Checks if a version satisfies a single version condition. + */ + private boolean satisfiesSingleCondition(@Nonnull String version, @Nonnull String condition) { + try { + // Handle "x.y.z" pattern (e.g., "2.7.x" means any 2.7.*) + if (condition.endsWith(".x")) { + final String prefix = condition.substring(0, condition.length() - 2); + return version.startsWith(prefix + "."); + } + + // Handle ">=" pattern + if (condition.startsWith(">=")) { + String minVersion = condition.substring(2).trim(); + // Handle version with wildcard, e.g. ">=3.2.x" -> "3.2" + if (minVersion.endsWith(".x")) { + minVersion = minVersion.substring(0, minVersion.length() - 2); + } + final ComparableVersion current = new ComparableVersion(version); + final ComparableVersion min = new ComparableVersion(minVersion); + return current.compareTo(min) >= 0; + } + + // Handle ">" pattern + if (condition.startsWith(">")) { + String minVersion = condition.substring(1).trim(); + // Handle version with wildcard, e.g. ">3.2.x" -> "3.2" + if (minVersion.endsWith(".x")) { + minVersion = minVersion.substring(0, minVersion.length() - 2); + } + final ComparableVersion current = new ComparableVersion(version); + final ComparableVersion min = new ComparableVersion(minVersion); + return current.compareTo(min) > 0; + } + + // Handle exact version match + return version.equals(condition); + + } catch (Exception e) { + // Error checking version range + return false; + } + } + + /** + * Checks if a version has reached its end-of-life date based on the EOL map. + * @param version The version to check (e.g., "2.0.1.RELEASE") + * @param checkItem The dependency check item containing EOL dates + * @return true if the version is past its EOL date + */ + private boolean isVersionEndOfLife(@Nonnull String version, @Nonnull DependencyCheckItem checkItem) { + String eolDateStr = checkItem.getEolDateForVersion(version); + if (eolDateStr == null) { + return false; + } + + try { + // Parse EOL date (format: "YYYY-MM") + java.time.YearMonth eolDate = java.time.YearMonth.parse(eolDateStr); + java.time.YearMonth currentDate = java.time.YearMonth.now(); + return currentDate.isAfter(eolDate); + } catch (Exception e) { + return false; + } + } + + /** + * Gets the EOL date string for a version if available. + */ + @Nullable + private String getEolDateString(@Nonnull String version, @Nonnull DependencyCheckItem checkItem) { + return checkItem.getEolDateForVersion(version); + } + + /** + * Determines the upgrade reason based on version and EOL status. + */ + @Nonnull + private JavaUpgradeIssue.UpgradeReason determineUpgradeReason(@Nonnull String version, + @Nonnull DependencyCheckItem checkItem) { + // Check if version has reached EOL based on the EOL date map + if (isVersionEndOfLife(version, checkItem)) { + return JavaUpgradeIssue.UpgradeReason.END_OF_LIFE; + } + + // For deprecated but still maintained versions + return JavaUpgradeIssue.UpgradeReason.DEPRECATED; + } + + /** + * Determines the severity based on version and EOL status. + */ + @Nonnull + private JavaUpgradeIssue.Severity determineSeverity(@Nonnull String version, + @Nonnull DependencyCheckItem checkItem) { + // If version has reached EOL, mark as critical + if (isVersionEndOfLife(version, checkItem)) { + return JavaUpgradeIssue.Severity.INFO; + } + + // For other unsupported versions (not yet EOL but outside supported range) + return JavaUpgradeIssue.Severity.INFO; + } + + /** + * Builds a human-readable upgrade message. + */ + @Nonnull + private String buildUpgradeMessage(@Nonnull String displayName, + @Nonnull String currentVersion, + @Nonnull DependencyCheckItem checkItem) { + String eolDateStr = getEolDateString(currentVersion, checkItem); + boolean isEol = isVersionEndOfLife(currentVersion, checkItem); + if (isEol && eolDateStr != null) { + return String.format( + "This project is using %s %s, which has reached end of life in %s. " + + "Would you like to upgrade it to %s?", + displayName, currentVersion, eolDateStr, checkItem.suggestedVersion + ); + } else if (eolDateStr != null) { + return String.format( + "This project is using %s %s, which will reach end of life in %s. " + + "Would you like to upgrade it to %s?", + displayName, currentVersion, eolDateStr, checkItem.suggestedVersion + ); + } else { + return String.format( + "This project is using %s %s, which is outside the supported version range (%s). " + + "Would you like to upgrade it to %s?", + displayName, currentVersion, checkItem.supportedVersion, checkItem.suggestedVersion + ); + } + } + + /** + * Finds a direct dependency in the Maven project (excludes transitive dependencies). + * Uses getDependencyTree() to identify only dependencies explicitly declared in pom.xml. + * This aligns with the TypeScript implementation which parses pom.xml directly. + */ + @Nullable + private MavenArtifact findDirectDependency(@Nonnull MavenProject mavenProject, + @Nonnull String groupId, + @Nullable String artifactId) { + // getDependencyTree() returns the root-level nodes which are direct dependencies + // (transitive dependencies are children of these nodes) + List dependencyTree = mavenProject.getDependencyTree(); + return dependencyTree.stream() + .map(MavenArtifactNode::getArtifact) + .filter(dep -> groupId.equals(dep.getGroupId())) + .filter(dep -> artifactId == null || artifactId.equals(dep.getArtifactId())) + .findFirst() + .orElse(null); + } + + /** + * Checks a single dependency against its metadata for Gradle projects. + */ + @Nullable + private JavaUpgradeIssue checkGradleDependency(@Nonnull ExternalProject gradleProject, + @Nonnull DependencyCheckItem checkItem, + @Nonnull Set checkedPackages) { + final ExternalSourceSet main = gradleProject.getSourceSets().get("main"); + if (main == null) { + return null; + } + + // Find direct dependency + final ExternalDependency dependency = main.getDependencies().stream() + .filter(dep -> StringUtils.equalsIgnoreCase(checkItem.groupId, dep.getGroup()) && + ("*".equals(checkItem.artifactId) || StringUtils.equalsIgnoreCase(checkItem.artifactId, dep.getName()))) + .filter(dep -> !(dep instanceof UnresolvedExternalDependency)) + .findFirst() + .orElse(null); + + if (dependency == null) { + return null; + } + + final String version = dependency.getVersion(); + + if (version == null || StringUtils.isBlank(version)) { + return null; + } + + checkedPackages.add(checkItem.getPackageId()); + + // Check if version satisfies the supported version range + if (!satisfiesVersionRange(version, checkItem.supportedVersion)) { + return JavaUpgradeIssue.builder() + .packageId(checkItem.getPackageId()) + .packageDisplayName(checkItem.displayName) + .upgradeReason(determineUpgradeReason(version, checkItem)) + .severity(determineSeverity(version, checkItem)) + .currentVersion(version) + .supportedVersion(checkItem.supportedVersion) + .suggestedVersion(checkItem.suggestedVersion) + .message(buildUpgradeMessage(checkItem.displayName, version, checkItem)) + .learnMoreUrl(checkItem.learnMoreUrl) + .build(); + } + + return null; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java new file mode 100644 index 00000000000..50a7449bfff --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -0,0 +1,555 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; + +import com.intellij.ide.plugins.IdeaPluginDescriptor; +import com.intellij.ide.plugins.PluginManagerCore; +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationAction; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.extensions.PluginId; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; + +import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.installPlugin; +import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.isAppModPluginInstalled; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; +import java.lang.reflect.Method; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import org.jetbrains.annotations.NotNull; +import com.github.copilot.api.CopilotChatService; +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Objects; + +/** + * Service to display notifications about outdated Java project versions. + * Notifications appear in the bottom-right corner of the IDE. + * Only shows one notification at a time (the first detected issue). + */ +public class JavaVersionNotificationService { + + private static final String NOTIFICATION_GROUP_ID = "Azure Toolkit - Java Version Check"; + private static final String NOTIFICATIONS_ENABLED_KEY = "azure.toolkit.java.version.notifications.enabled"; + private static final String DEFERRED_UNTIL_KEY = "azure.toolkit.java.version.deferred_until"; + private static final long DEFER_INTERVAL_MS = 10 * 24 * 60 * 60 * 1000L; // 10 days in milliseconds + private static final String DEFAULT_MODEL_NAME = "Claude Sonnet 4.5"; + // GitHub Copilot app modernization plugin ID + private static final String COPILOT_APPMOD_PLUGIN_ID = "com.github.copilot.appmod"; + // GitHub Copilot plugin ID + private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; + + private static JavaVersionNotificationService instance; + + private JavaVersionNotificationService() { + } + + public static synchronized JavaVersionNotificationService getInstance() { + if (instance == null) { + instance = new JavaVersionNotificationService(); + } + return instance; + } + + /** + * Shows a notification for the first outdated version issue. + * Only shows if notifications are enabled for the Java upgrade feature. + * + * @param project The project context + * @param issues List of detected issues + */ + public void showNotifications(@Nonnull Project project, @Nonnull List issues) { + if (project.isDisposed() || issues.isEmpty()) { + return; + } + + // Check if notifications are enabled for this feature + if (!isNotificationsEnabled(project)) { + return; + } + + // Check if we should skip based on timing (deferred) + if (!shouldCheckNow(project)) { + return; + } + + // Only show notification for the first issue + final JavaUpgradeIssue firstIssue = issues.get(0); + showNotification(project, firstIssue); + } + + /** + * Shows a single notification for an outdated version issue. + */ + private void showNotification(@Nonnull Project project, + @Nonnull JavaUpgradeIssue issue) { + final NotificationType notificationType = getNotificationType(issue.getSeverity()); + + final Notification notification = new Notification( + NOTIFICATION_GROUP_ID, + issue.getTitle(), + formatMessage(issue), + notificationType + ); + + // Add upgrade action based on whether GitHub Copilot app modernization plugin is installed + if (isUpgradeSupported(issue)) { + if (isAppModPluginInstalled()) { + // Plugin is installed - show "Upgrade" action + notification.addAction(new NotificationAction("Upgrade") { + @Override + public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { + openCopilotChatWithUpgradePrompt(project, issue); + notification.expire(); + } + }); + } else { + // Plugin is not installed - show "Install and Upgrade" action + notification.addAction(new NotificationAction("Install and Upgrade") { + @Override + public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { + // installCopilotAppModPluginAndUpgrade(project, issue); + installPlugin(project); + notification.expire(); + } + }); + } + } + + // Add "Not Now" action - defers the notification for 10 days + notification.addAction(new NotificationAction("Not Now") { + @Override + public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { + deferNotifications(); + notification.expire(); + } + }); + + // Add "Don't Show Again" action - disables the entire Java upgrade notification feature + notification.addAction(new NotificationAction("Don't Show Again") { + @Override + public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { + setNotificationsEnabled(false); + notification.expire(); + } + }); + + // Show the notification + Notifications.Bus.notify(notification, project); + } + + /** + * Formats the notification message with HTML for better display. + */ + private String formatMessage(@Nonnull JavaUpgradeIssue issue) { + final StringBuilder sb = new StringBuilder(); + sb.append(""); + sb.append(issue.getMessage()); + + if (issue.getCurrentVersion() != null && issue.getSuggestedVersion() != null) { + sb.append("

"); + sb.append("Current: ").append(issue.getCurrentVersion()); + sb.append(" → Suggested: ").append(issue.getSuggestedVersion()); + } + + sb.append(""); + return sb.toString(); + } + + /** + * Gets the notification type based on issue severity. + */ + private NotificationType getNotificationType(@Nonnull JavaUpgradeIssue.Severity severity) { + return switch (severity) { + case CRITICAL -> NotificationType.ERROR; + case WARNING -> NotificationType.WARNING; + case INFO -> NotificationType.INFORMATION; + }; + } + + /** + * Generates a unique key for an issue to track dismissals. + */ + private String getIssueKey(@Nonnull JavaUpgradeIssue issue) { + return issue.getPackageId() + ":" + + issue.getUpgradeReason().name() + ":" + + Objects.requireNonNullElse(issue.getCurrentVersion(), "unknown"); + } + + /** + * Checks if Java upgrade notifications are enabled globally. + * @return true if notifications are enabled (default), false otherwise + */ + public boolean isNotificationsEnabled() { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + return properties.getBoolean(NOTIFICATIONS_ENABLED_KEY, true); + } + + /** + * Checks if Java upgrade notifications are enabled for the project. + * Uses application-level setting. + * @param project The project (for backwards compatibility, not used) + * @return true if notifications are enabled (default), false otherwise + */ + public boolean isNotificationsEnabled(@Nonnull Project project) { + return isNotificationsEnabled(); + } + + /** + * Enables or disables Java upgrade notifications globally. + * @param enabled true to enable notifications, false to disable + */ + public void setNotificationsEnabled(boolean enabled) { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + properties.setValue(NOTIFICATIONS_ENABLED_KEY, enabled, true); + } + + /** + * Enables or disables Java upgrade notifications. + * Uses application-level setting. + * @param project The project (for backwards compatibility, not used) + * @param enabled true to enable notifications, false to disable + */ + public void setNotificationsEnabled(@Nonnull Project project, boolean enabled) { + setNotificationsEnabled(enabled); + } + + /** + * Checks if the notification should be shown now. + * Returns false if the user has clicked "Not Now" and the defer period hasn't passed. + */ + private boolean shouldCheckNow(@Nonnull Project project) { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + final long deferredUntil = properties.getLong(DEFERRED_UNTIL_KEY, 0); + final long now = System.currentTimeMillis(); + + // If we're still in the deferred period, don't show notification + return now >= deferredUntil; + } + + /** + * Defers notifications for 10 days. + * Called when user clicks "Not Now". + */ + public void deferNotifications() { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + final long deferUntil = System.currentTimeMillis() + DEFER_INTERVAL_MS; + properties.setValue(DEFERRED_UNTIL_KEY, String.valueOf(deferUntil)); + } + + /** + * Gets the timestamp until which notifications are deferred. + * @return The deferred-until timestamp in milliseconds, or 0 if not deferred + */ + public long getDeferredUntil() { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + return properties.getLong(DEFERRED_UNTIL_KEY, 0); + } + + /** + * Clears the deferred notification state. + */ + public void clearDeferral() { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + properties.unsetValue(DEFERRED_UNTIL_KEY); + } + + /** + * Checks if upgrade is supported for this issue type. + */ + private boolean isUpgradeSupported(@Nonnull JavaUpgradeIssue issue) { + // Upgrade support for JDK and Spring Boot + return issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD || + issue.getPackageId().startsWith(GROUP_ID_SPRING_BOOT + ":"); + } + +// /** +// * Checks if the GitHub Copilot app modernization plugin is installed. +// * @return true if the plugin is installed, false otherwise +// */ +// private boolean isCopilotAppModPluginInstalled() { +// return PluginManagerCore.isPluginInstalled(PluginId.getId(COPILOT_APPMOD_PLUGIN_ID)); +// } + + /** + * Checks if the GitHub Copilot plugin is installed. + * @return true if the plugin is installed, false otherwise + */ + private boolean isCopilotPluginInstalled() { + return PluginManagerCore.isPluginInstalled(PluginId.getId(COPILOT_PLUGIN_ID)); + } + + /** + * Opens GitHub Copilot chat in agent mode with an upgrade prompt. + * @param project The project context + * @param issue The upgrade issue to address + */ + private void openCopilotChatWithUpgradePrompt(@Nonnull Project project, @Nonnull JavaUpgradeIssue issue) { + final String prompt = buildUpgradePrompt(issue); + openCopilotChatWithPrompt(project, prompt); + } + + /** + * Opens GitHub Copilot chat in agent mode with a given prompt. + * Tries direct API first, falls back to reflection for cross-version compatibility. + * @param project The project context + * @param prompt The prompt to send to Copilot + */ + public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String prompt) { + AzureTaskManager.getInstance().runLater(() -> { + if (!isAppModPluginInstalled()) { + // showGenericUpgradeGuidance(project, prompt); + installPlugin(project); + return; + } + + // Try direct API call first (works when plugin versions match) + if (tryDirectCopilotCall(project, prompt)) { + System.out.println("Direct Copilot call succeeded."); + return; // Success, no need for reflection + } + + // Fallback to reflection for cross-version compatibility + if (tryReflectionCopilotCall(project, prompt)) { + System.out.println("Reflection Copilot call succeeded."); + return; // Success via reflection + } + + // Both approaches failed + System.out.println("Both direct and reflection Copilot calls failed."); + showGenericUpgradeGuidance(project, prompt); + }); + } + + /** + * Tries to call CopilotChatService directly (works when compile-time and runtime versions match). + * @return true if successful, false if an error occurred + */ + private boolean tryDirectCopilotCall(@Nonnull Project project, @Nonnull String prompt) { + try { + CopilotChatService service = project.getService(CopilotChatService.class); + if (service != null) { + service.query(DataContext.EMPTY_CONTEXT, builder -> { + builder.withInput(prompt); + builder.withAgentMode(); + builder.withNewSession(); + withModelCompatibility(builder, DEFAULT_MODEL_NAME); + builder.withSessionIdReceiver(sessionId -> null); + return null; + }); + System.out.println("Direct Copilot call succeeded."); + return true; + } + } catch (Error | Exception e) { + // Direct call failed (version mismatch, class not found, etc.) - will try reflection + System.out.println("Direct Copilot call failed: " + e.getMessage()); + } + return false; + } + + /** + * Tries to call CopilotChatService via reflection for cross-version compatibility. + * @return true if successful, false if an error occurred + */ + private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull String prompt) { + try { + // Get the Copilot plugin's classloader to load its classes + final IdeaPluginDescriptor copilotPlugin = PluginManagerCore.getPlugin(PluginId.getId(COPILOT_PLUGIN_ID)); + if (copilotPlugin == null || !copilotPlugin.isEnabled()) { + return false; + } + + final ClassLoader copilotClassLoader = copilotPlugin.getPluginClassLoader(); + if (copilotClassLoader == null) { + return false; + } + + // Use reflection to load CopilotChatService from the Copilot plugin's classloader + Class copilotChatServiceClass = copilotClassLoader.loadClass("com.github.copilot.api.CopilotChatService"); + Object service = project.getService(copilotChatServiceClass); + + if (service != null) { + // Find the query method dynamically - signature may vary across Copilot versions + Method queryMethod = findQueryMethod(copilotChatServiceClass); + if (queryMethod == null) { + return false; + } + + // Use Kotlin Function1 since the Copilot API is written in Kotlin + Function1 queryBuilder = builder -> { + try { + builder.getClass().getMethod("withInput", String.class).invoke(builder, prompt); + builder.getClass().getMethod("withAgentMode").invoke(builder); + builder.getClass().getMethod("withNewSession").invoke(builder); + withModelCompatibility(builder, DEFAULT_MODEL_NAME); + Method withSessionIdReceiverMethod = findMethodByName(builder.getClass(), "withSessionIdReceiver"); + if (withSessionIdReceiverMethod != null) { + Function1 sessionIdReceiver = sessionId -> Unit.INSTANCE; + withSessionIdReceiverMethod.invoke(builder, sessionIdReceiver); + } + } catch (Exception ex) { + // Error configuring query builder via reflection + } + return Unit.INSTANCE; + }; + queryMethod.invoke(service, DataContext.EMPTY_CONTEXT, queryBuilder); + return true; + } + } catch (Exception e) { + // Reflection call failed + } + return false; + } + + /** + * Shows generic guidance for upgrading when Copilot is not available. + * @param project The project context + * @param prompt The upgrade prompt that would be used + */ + private void showGenericUpgradeGuidance(@Nonnull Project project, @Nonnull String prompt) { + final String guidance = String.format( + "Open GitHub Copilot chat and use the following prompt in agent mode:\n\n%s", prompt); + + final Notification guidanceNotification = new Notification( + NOTIFICATION_GROUP_ID, + "Upgrade Guidance", + guidance, + NotificationType.INFORMATION + ); + + Notifications.Bus.notify(guidanceNotification, project); + } + + /** + * Sets the model for the query builder using reflection for compatibility with older versions of GitHub Copilot. + * Note: The API 'withModel' is supported starting from Copilot version '1.5.63'. + * @param builder the query option builder + * @param modelName the name of the model to set + */ + private static void withModelCompatibility(Object builder, String modelName) { + try { + builder.getClass().getMethod("withModel", String.class).invoke(builder, modelName); + } catch (NoSuchMethodException ex) { + // Method withModel not found in QueryOptionBuilder, skipping + } catch (Exception ex) { + // Error calling withModel via reflection, can be ignored + } + } + + /** + * Finds the 'query' method in CopilotChatService dynamically. + * The method signature may vary across Copilot versions. + * @param serviceClass The CopilotChatService class + * @return The query method, or null if not found + */ + private Method findQueryMethod(Class serviceClass) { + for (Method method : serviceClass.getMethods()) { + if ("query".equals(method.getName()) && method.getParameterCount() == 2) { + Class[] paramTypes = method.getParameterTypes(); + // Look for query(DataContext, Function/Consumer/etc) + if (DataContext.class.isAssignableFrom(paramTypes[0])) { + return method; + } + } + } + return null; + } + + /** + * Finds a method by name in a class (first match). + * @param clazz The class to search + * @param methodName The method name to find + * @return The method, or null if not found + */ + private Method findMethodByName(Class clazz, String methodName) { + for (Method method : clazz.getMethods()) { + if (methodName.equals(method.getName())) { + return method; + } + } + return null; + } + + /** + * Builds the upgrade prompt for Copilot based on the issue type. + * @param issue The upgrade issue + * @return The prompt string for Copilot + */ + private String buildUpgradePrompt(@Nonnull JavaUpgradeIssue issue) { + if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) { + return String.format("upgrade java runtime to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", MATURE_JAVA_LTS_VERSION); + } else if (issue.getPackageId().startsWith(GROUP_ID_SPRING_BOOT + ":")) { + return String.format("upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"); + } + return "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + } + + /** + * Installs the GitHub Copilot app modernization plugin and then opens Copilot chat. + * @param project The project context + * @param issue The upgrade issue to address + */ + private void installCopilotAppModPluginAndUpgrade(@Nonnull Project project, @Nonnull JavaUpgradeIssue issue) { +// final Set pluginIds = new HashSet<>(); +// pluginIds.add(COPILOT_APPMOD_PLUGIN_ID); +// +// AzureTaskManager.getInstance().runLater(() -> { +// PluginsAdvertiser.installAndEnablePlugins(pluginIds, () -> { +// PluginInstaller.addStateListener(new PluginStateListener() { +// @Override +// public void install(@NotNull IdeaPluginDescriptor descriptor) { +// if (COPILOT_APPMOD_PLUGIN_ID.equals(descriptor.getPluginId().getIdString())) { +// // Plugin installed successfully, now open Copilot chat +// ApplicationManager.getApplication().invokeLater(() -> { +// openCopilotChatWithUpgradePrompt(project, issue); +// }); +// } +// } +// +// @Override +// public void uninstall(@NotNull IdeaPluginDescriptor descriptor) { +// // Not needed +// } +// }); +// }); +// }); + } + + /** + * Shows guidance for upgrading the project when Copilot is not available. + * @param project The project context + * @param issue The upgrade issue + * @param prompt The upgrade prompt that would be used + */ + private void showUpgradeGuidance(@Nonnull Project project, @Nonnull JavaUpgradeIssue issue, @Nonnull String prompt) { + showGenericUpgradeGuidance(project, prompt); + } + + /** + * Resets notification settings (useful for testing or reset). + * Re-enables notifications and clears the deferral. + */ + public void resetNotificationSettings() { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + properties.unsetValue(NOTIFICATIONS_ENABLED_KEY); + properties.unsetValue(DEFERRED_UNTIL_KEY); + } + + /** + * Resets notification settings for a project (for backwards compatibility). + * @param project The project (not used, settings are application-level) + */ + public void resetNotificationSettings(@Nonnull Project project) { + resetNotificationSettings(); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java new file mode 100644 index 00000000000..392c7ccbc87 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.settings; + +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.util.NlsContexts; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Settings page for Java Upgrade feature. + * Allows users to enable/disable notifications and reset deferral settings. + * + * Accessible via: Settings → Tools → Azure Toolkit → Java Upgrade + */ +public class JavaUpgradeConfigurable implements Configurable { + + private JPanel mainPanel; + private JCheckBox enableNotificationsCheckBox; + private JButton resetDeferralButton; + private JLabel deferralStatusLabel; + + @Override + public @NlsContexts.ConfigurableName String getDisplayName() { + return "Java Upgrade"; + } + + @Override + public @Nullable JComponent createComponent() { + mainPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.insets = new Insets(2, 0, 2, 5); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + + // Title label + gbc.gridx = 0; + gbc.gridy = 0; + JLabel titleLabel = new JLabel("Java Upgrade Notifications"); + mainPanel.add(titleLabel, gbc); + + // Description + gbc.gridy = 1; + JLabel descriptionLabel = new JLabel("Configure notifications for outdated JDK versions, " + + "framework versions, and security vulnerabilities."); + mainPanel.add(descriptionLabel, gbc); + + // Enable notifications checkbox + gbc.gridy = 2; + enableNotificationsCheckBox = new JCheckBox("Show notifications for Java upgrade issues"); + enableNotificationsCheckBox.setToolTipText( + "When enabled, balloon notifications will appear when outdated JDK or framework versions are detected."); + mainPanel.add(enableNotificationsCheckBox, gbc); + + // Deferral section subtitle (indented) + gbc.gridy = 3; + gbc.insets = new Insets(2, 20, 2, 5); + JLabel deferralTitle = new JLabel("Notification Deferral"); + mainPanel.add(deferralTitle, gbc); + + // Deferral status (indented) + gbc.gridy = 4; + deferralStatusLabel = new JLabel(); + updateDeferralStatusLabel(); + mainPanel.add(deferralStatusLabel, gbc); + + // Reset deferral button (indented) + gbc.gridy = 5; + gbc.fill = GridBagConstraints.NONE; + gbc.anchor = GridBagConstraints.WEST; + resetDeferralButton = new JButton("Reset Deferral"); + resetDeferralButton.setToolTipText("Clear the \"Not Now\" deferral and allow notifications to show immediately."); + resetDeferralButton.addActionListener(e -> { + JavaVersionNotificationService.getInstance().clearDeferral(); + updateDeferralStatusLabel(); + }); + mainPanel.add(resetDeferralButton, gbc); + + // Spacer to push content to the top + gbc.gridy = 6; + gbc.insets = new Insets(2, 0, 2, 5); + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + mainPanel.add(new JPanel(), gbc); + + return mainPanel; + } + + private void updateDeferralStatusLabel() { + if (deferralStatusLabel == null || resetDeferralButton == null) { + return; + } + final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); + final long deferredUntil = service.getDeferredUntil(); + final long now = System.currentTimeMillis(); + + if (deferredUntil > now) { + // Notifications are deferred + SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy HH:mm"); + String dateStr = dateFormat.format(new Date(deferredUntil)); + deferralStatusLabel.setText("Notifications deferred until: " + dateStr + ""); + resetDeferralButton.setEnabled(true); + } else { + deferralStatusLabel.setText("Notifications are not deferred."); + resetDeferralButton.setEnabled(false); + } + } + + @Override + public boolean isModified() { + if (enableNotificationsCheckBox == null) { + return false; + } + final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); + return enableNotificationsCheckBox.isSelected() != service.isNotificationsEnabled(); + } + + @Override + public void apply() throws ConfigurationException { + if (enableNotificationsCheckBox == null) { + return; + } + final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); + service.setNotificationsEnabled(enableNotificationsCheckBox.isSelected()); + } + + @Override + public void reset() { + if (enableNotificationsCheckBox == null) { + return; + } + final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); + enableNotificationsCheckBox.setSelected(service.isNotificationsEnabled()); + updateDeferralStatusLabel(); + } + + @Override + public void disposeUIResources() { + mainPanel = null; + enableNotificationsCheckBox = null; + resetDeferralButton = null; + deferralStatusLabel = null; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index e8647c39271..44574fea17b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -3,11 +3,49 @@ - + + + + + + XML + com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.UpgradeInQuickFixIntentionAction + Azure Toolkit + + + + + + + + + + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index f28053e83d6..ef99b223ea4 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -17,7 +17,7 @@ import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; import com.microsoft.azure.toolkit.intellij.appmod.IMigrateOptionProvider; import com.microsoft.azure.toolkit.intellij.appmod.MigrateNodeData; -import com.microsoft.azure.toolkit.intellij.appmod.MigratePluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; import com.microsoft.azure.toolkit.lib.common.action.Action; import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; @@ -63,7 +63,7 @@ private List getMigrationNodes() { * Computes migration nodes from extension point providers. */ private List computeMigrationNodes() { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { return List.of(); } final List nodes = migrationProviders.getExtensionList().stream() @@ -123,8 +123,8 @@ public Collection> buildChildren() { protected void buildView(@Nonnull PresentationData presentation) { presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); - if (!MigratePluginInstaller.isAppModPluginInstalled()) { - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + final boolean copilotInstalled = AppModPluginInstaller.isCopilotInstalled(); final String text = copilotInstalled ? "Migrate to Azure (Install Github Copilot app modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"; @@ -139,11 +139,11 @@ protected void buildView(@Nonnull PresentationData presentation) { @Override public void navigate(boolean requestFocus) { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { // Plugin not installed - trigger install on double-click AppModUtils.logTelemetryEvent("facet.click-install"); - MigratePluginInstaller.showInstallConfirmation(getProject(), - () -> MigratePluginInstaller.installPlugin(getProject())); + AppModPluginInstaller.showInstallConfirmation(getProject(), + () -> AppModPluginInstaller.installPlugin(getProject())); } else if (!hasMigrationOptions()) { // No migration options - open App Modernization Panel AppModPanelHelper.openAppModPanel(getProject(), "facet"); @@ -153,14 +153,14 @@ public void navigate(boolean requestFocus) { @Override public boolean canNavigate() { // Enable navigation when plugin is not installed OR when no migration options - return !MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions(); + return !AppModPluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions(); } @Override public @Nonnull LeafState getLeafState() { // Use ASYNC to avoid triggering extension point loading synchronously // The actual leaf state will be determined when buildChildren() is called - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { return LeafState.ALWAYS; } // ASYNC means IntelliJ will call buildChildren() to determine if there are children From a2f78128d1694d5c18e5156abaf165fac627aefe Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Mon, 26 Jan 2026 15:12:12 +0800 Subject: [PATCH 13/24] move migration code to javamigration package and add utils package --- .../docs/architecture.md | 255 ------------------ .../appmod/common/AppModPluginInstaller.java | 2 +- .../IMigrateOptionProvider.java | 2 +- .../{ => javamigration}/MigrateNodeData.java | 2 +- .../MigrateToAzureAction.java | 4 +- .../MigrateToAzureNode.java | 5 +- .../appmod/{ => utils}/AppModPanelHelper.java | 2 +- .../appmod/{ => utils}/AppModUtils.java | 2 +- .../appmod/{ => utils}/Constants.java | 2 +- .../META-INF/azure-intellij-plugin-appmod.xml | 4 +- .../intellij/explorer/AzureExplorer.java | 4 +- .../MigrateToAzureFacetNode.java | 10 +- 12 files changed, 21 insertions(+), 273 deletions(-) delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => javamigration}/IMigrateOptionProvider.java (97%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => javamigration}/MigrateNodeData.java (99%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => javamigration}/MigrateToAzureAction.java (97%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => javamigration}/MigrateToAzureNode.java (96%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => utils}/AppModPanelHelper.java (96%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => utils}/AppModUtils.java (97%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => utils}/Constants.java (63%) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md deleted file mode 100644 index 9ca4897f4e6..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md +++ /dev/null @@ -1,255 +0,0 @@ -# App Modernization Module Architecture - -## Overview - -The `azure-intellij-plugin-appmod` module provides the "Migrate to Azure" functionality in Azure Toolkit for IntelliJ. It serves as a bridge to integrate GitHub Copilot App Modernization plugin with Azure Toolkit. - -## Plugin Relationship Diagram - -``` -┌──────────────────────────────────────────────────────────────────────────────────┐ -│ IntelliJ IDEA │ -│ │ -│ ┌────────────────────────────────────────────────────────────────────────────┐ │ -│ │ Azure Toolkit for IntelliJ │ │ -│ │ │ │ -│ │ ┌────────────────────────┐ ┌────────────────────────────────────────┐ │ │ -│ │ │ service-explorer │ │ resource-connector-lib │ │ │ -│ │ │ │ │ │ │ │ -│ │ │ ┌──────────────────┐ │ │ ┌──────────────────────────────────┐ │ │ │ -│ │ │ │MigrateToAzureNode│ │ │ │ MigrateToAzureFacetNode │ │ │ │ -│ │ │ └────────┬─────────┘ │ │ └───────────────┬──────────────────┘ │ │ │ -│ │ └───────────┼────────────┘ └──────────────────┼─────────────────────┘ │ │ -│ │ │ │ │ │ -│ │ └─────────────────┬──────────────────┘ │ │ -│ │ ▼ │ │ -│ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ -│ │ │ azure-intellij-plugin-appmod │ │ │ -│ │ │ │ │ │ -│ │ │ • IMigrateOptionProvider (Extension Point Interface) │ │ │ -│ │ │ • MigrateNodeData (Data Model) │ │ │ -│ │ │ • MigratePluginInstaller (Plugin Detection/Installation) │ │ │ -│ │ │ • MigrateToAzureAction (Context Menu) │ │ │ -│ │ │ │ │ │ -│ │ └──────────────────────────────────┬───────────────────────────────────┘ │ │ -│ │ │ │ │ -│ │ │ Extension Point │ │ -│ │ ▼ │ │ -│ └────────────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ implements │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────────────────────────┐ │ -│ │ GitHub Copilot App Modernization Plugin │ │ -│ │ (appmod-intellij) │ │ -│ │ │ │ -│ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ -│ │ │ MyMigrationProvider implements IMigrateOptionProvider │ │ │ -│ │ │ │ │ │ -│ │ │ • createNodeData() → Returns migration options │ │ │ -│ │ │ • isApplicable() → Check project compatibility │ │ │ -│ │ └──────────────────────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ Depends on: com.github.copilot (GitHub Copilot) │ │ -│ └────────────────────────────────────────────────────────────────────────────┘ │ -│ │ -└──────────────────────────────────────────────────────────────────────────────────┘ -``` - -## Data Flow - -``` -User Action (click/expand) - │ - ▼ -┌─────────────────────┐ -│ Entry Point │ (MigrateToAzureNode / MigrateToAzureFacetNode / MigrateToAzureAction) -└─────────┬───────────┘ - │ - ▼ -┌─────────────────────┐ No ┌─────────────────────┐ -│ Plugin Installed? │────────────▶│ Show Install Dialog │ -└─────────┬───────────┘ └─────────────────────┘ - │ Yes - ▼ -┌─────────────────────┐ -│ Load Extension │ -│ Providers │ -└─────────┬───────────┘ - │ - ▼ -┌─────────────────────┐ -│ Filter by │ -│ isApplicable() │ -└─────────┬───────────┘ - │ - ▼ -┌─────────────────────┐ -│ Sort by Priority │ -└─────────┬───────────┘ - │ - ▼ -┌─────────────────────┐ -│ Call createNodeData │ -│ for each provider │ -└─────────┬───────────┘ - │ - ▼ -┌─────────────────────┐ -│ Display Nodes │ -│ in UI │ -└─────────────────────┘ -``` - -## Module Structure - -``` -azure-intellij-plugin-appmod/ -├── build.gradle.kts -├── docs/ -│ └── architecture.md -└── src/main/ - ├── java/com/microsoft/azure/toolkit/intellij/appmod/ - │ ├── IMigrateOptionProvider.java # Extension Point interface - │ ├── MigrateNodeData.java # Node data model - │ ├── MigratePluginInstaller.java # Plugin detection & installation - │ ├── MigrateToAzureNode.java # Service Explorer entry point - │ ├── MigrateToAzureAction.java # Context menu entry point - │ ├── InstallPluginDialog.java # Installation confirmation dialog - │ └── RestartIdeDialog.java # Restart prompt dialog - └── resources/ - ├── META-INF/azure-intellij-plugin-appmod.xml - └── icons/app_mod.svg -``` - -## Entry Points - -The module provides **three entry points** for users to access migration functionality: - -### 1. Service Explorer Node (`MigrateToAzureNode`) -- **Location**: Azure Explorer panel → "Migrate to Azure" node -- **Behavior**: - - If plugins installed → Shows child nodes from extension providers - - If plugins not installed → Double-click triggers installation dialog - -### 2. Project Explorer Node (`MigrateToAzureFacetNode`) -- **Location**: Project Explorer → Azure facet → "Migrate to Azure" node -- **Note**: Located in `azure-intellij-resource-connector-lib` module (due to `AbstractAzureFacetNode` inheritance) -- **Behavior**: Same as Service Explorer Node - -### 3. Context Menu Action (`MigrateToAzureAction`) -- **Location**: Right-click on project/module → "Migrate to Azure" submenu -- **Behavior**: - - If plugins installed → Shows child actions from extension providers - - If plugins not installed → Single "Install Plugins" action - -## Extension Point - -### Definition -```xml - -``` - -Full ID: `com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider` - -### Interface: `IMigrateOptionProvider` -```java -public interface IMigrateOptionProvider { - // Check if this provider applies to the given project - boolean isApplicable(@Nonnull Project project); - - // Create node data for display (can return multiple nodes) - @Nonnull List createNodeData(@Nonnull Project project); - - // Priority for ordering (lower = first) - default int getPriority() { return 100; } -} -``` - -### Data Model: `MigrateNodeData` -```java -MigrateNodeData.builder() - .label("Node Label") // Required: display text - .description("Optional description") // Shown as location string - .tooltip("Hover tooltip") // Tooltip text - .iconPath("/icons/my_icon.svg") // Icon path (falls back to app_mod.svg) - .visible(true) // Visibility control - .onDoubleClick(anActionEvent -> {...}) // Double-click handler - .children(childList) // Static children - .childrenLoader(() -> loadChildren()) // OR lazy-loaded children - .build(); -``` - -## Plugin Detection & Installation - -### `MigratePluginInstaller` -Central utility class for plugin management: - -```java -// Check if plugins are installed -MigratePluginInstaller.isAppModPluginInstalled(); // com.github.copilot.appmod -MigratePluginInstaller.isCopilotInstalled(); // com.github.copilot - -// Show installation confirmation dialog -MigratePluginInstaller.showInstallConfirmation(project, onConfirmCallback); - -// Trigger installation (IntelliJ handles the rest) -MigratePluginInstaller.installPlugin(project); - -// Dev mode detection (runIde task) -MigratePluginInstaller.isRunningInDevMode(); -``` - -### Installation Flow -1. User triggers install (double-click node or context menu) -2. `showInstallConfirmation()` shows confirmation dialog -3. On confirm, `installPlugin()` calls `PluginsAdvertiser.installAndEnable()` -4. IntelliJ platform handles: - - Plugin selection dialog (with all plugins pre-selected) - - Download and installation - - Restart prompt -5. In dev mode: Special message shown (don't click IDE restart, re-run `./gradlew runIde`) - -## Module Dependencies - -``` -azure-intellij-plugin-appmod (base module) - ↑ - ├── azure-intellij-plugin-service-explorer - │ └── Uses: MigrateToAzureNode, Extension Point - │ - └── azure-intellij-resource-connector-lib - └── Contains: MigrateToAzureFacetNode (due to inheritance constraint) -``` - -### Why `MigrateToAzureFacetNode` is in connector-lib? -- Must extend `AbstractAzureFacetNode` from connector-lib -- Moving `AbstractAzureFacetNode` to appmod would require moving many other classes -- Current design minimizes code changes while maintaining clean architecture - -## External Plugin Integration - -The `appmod-intellij` plugin (GitHub Copilot App Modernization) should: - -1. Add dependency on `azure-intellij-plugin-appmod` -2. Implement `IMigrateOptionProvider` extension -3. Register in its `plugin.xml`: -```xml - - - -``` - -## UI Behavior Summary - -| State | Service Explorer | Project Explorer | Context Menu | -|-------|-----------------|------------------|--------------| -| Plugins NOT installed | Node shows "(Install...)" suffix, double-click triggers install | Same as Service Explorer | Shows "Install Plugins" action | -| Plugins installed | Expand to show child nodes from providers | Same as Service Explorer | Shows submenu with actions from providers | - -## Icon - -- **Path**: `/icons/app_mod.svg` -- **Location**: `azure-intellij-plugin-appmod/src/main/resources/icons/` -- **Usage**: Centralized icon for all migrate-related nodes and actions diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 51c0c60a008..150f3c755a9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -11,7 +11,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; -import com.microsoft.azure.toolkit.intellij.appmod.AppModUtils; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import javax.annotation.Nonnull; import java.util.LinkedHashSet; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/IMigrateOptionProvider.java similarity index 97% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/IMigrateOptionProvider.java index df7befbf960..a82fb4277f7 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/IMigrateOptionProvider.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.javamigration; import com.intellij.openapi.project.Project; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateNodeData.java similarity index 99% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateNodeData.java index 49541eb262a..1733fddd1aa 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateNodeData.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.javamigration; import lombok.Getter; import lombok.Setter; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java similarity index 97% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java index f2c5945e469..1e5d6fa5143 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.javamigration; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.ActionGroup; @@ -15,6 +15,8 @@ import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModPanelHelper; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java similarity index 96% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java index a10e4598ff5..cd4c43dea06 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java @@ -3,13 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.javamigration; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.ide.common.component.Node; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModPanelHelper; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; +import com.microsoft.azure.toolkit.intellij.appmod.utils.Constants; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.lib.common.action.Action; import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModPanelHelper.java similarity index 96% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModPanelHelper.java index 5cd329c3192..b84084136c5 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModPanelHelper.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.utils; import com.intellij.openapi.project.Project; import com.intellij.openapi.wm.ToolWindow; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModUtils.java similarity index 97% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModUtils.java index 49ace1fa9bd..b114b7f3f85 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModUtils.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.utils; import com.intellij.openapi.diagnostic.Logger; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/Constants.java similarity index 63% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/Constants.java index 00b7b605325..8d1bf611621 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/Constants.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.utils; public class Constants { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index 44574fea17b..5cbc896ed4b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -1,7 +1,7 @@ + interface="com.microsoft.azure.toolkit.intellij.appmod.javamigration.IMigrateOptionProvider"/> @@ -30,7 +30,7 @@ - Date: Mon, 26 Jan 2026 23:35:28 +0800 Subject: [PATCH 14/24] Enhance the feature function and remove unused code --- .../appmod/common/AppModPluginInstaller.java | 2 +- .../intellij/appmod/javaupgrade/Contants.java | 15 + .../JavaUpgradeCheckStartupActivity.java | 2 +- .../action/CveFixInProblemsViewAction.java | 126 ++++ ...Action.java => CveFixIntentionAction.java} | 90 +-- ...java => JavaUpgradeContextMenuAction.java} | 19 +- .../action/JavaUpgradeQuickFix.java | 73 +++ .../action/UpgradeActionRegistrar.java | 17 +- .../action/UpgradeInProblemsViewAction.java | 602 ------------------ .../JavaUpgradeIssuesInspection.java | 196 ++---- .../javaupgrade/service/CVECheckService.java | 4 + .../service/JavaUpgradeIssuesCache.java | 39 +- .../JavaUpgradeIssuesDetectionService.java | 34 - .../JavaVersionNotificationService.java | 116 +--- .../META-INF/azure-intellij-plugin-appmod.xml | 15 +- 15 files changed, 365 insertions(+), 985 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/{UpgradeInQuickFixIntentionAction.java => CveFixIntentionAction.java} (60%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/{UpgradeProjectAction.java => JavaUpgradeContextMenuAction.java} (82%) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 150f3c755a9..198890e7137 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -26,7 +26,7 @@ public class AppModPluginInstaller { private static final String PLUGIN_ID = "com.github.copilot.appmod"; private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; - + public static final String TO_INSTALL_APP_MODE_PLUGIN = " (Install Github Copilot app modernization)"; private AppModPluginInstaller() { // Utility class - prevent instantiation } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java new file mode 100644 index 00000000000..019092aff81 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java @@ -0,0 +1,15 @@ +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.MATURE_JAVA_LTS_VERSION; + +public class Contants { + public static final String UPGRADE_JAVA_AND_FRAMEWORK_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + public static final String UPGRADE_JAVA_VERSION_PROMPT = "upgrade java runtime to Java " + MATURE_JAVA_LTS_VERSION + " (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; + public static final String UPGRADE_JAVA_FRAMEWORK_PROMPT = "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + public static final String SCAN_AND_RESOLVE_CVES_PROMPT = "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; + public static final String UPGRADE_JDK_WITH_COPILOT_DISPLAY_NAME = "Upgrade JDK with Copilot"; + public static final String UPGRADE_SPRING_BOOT_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Boot with Copilot"; + public static final String UPGRADE_SPRING_FRAMEWORK_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Framework with Copilot"; + public static final String UPGRADE_SPRING_SECURITY_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Security with Copilot"; + public static final String SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME = "Scan and Resolve CVEs with Copilot"; +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java index b0cc134fb81..5eb1606dad9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java @@ -72,7 +72,7 @@ private void performJavaUpgradeCheck(@Nonnull Project project) { final List allIssues = new java.util.ArrayList<>(); allIssues.addAll(cache.getJdkIssues()); allIssues.addAll(cache.getDependencyIssues()); - allIssues.addAll(detectionService.getCVEIssues(project)); + allIssues.addAll(cache.getCveIssues()); // Update UI on the main thread AzureTaskManager.getInstance().runLater(() -> { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java new file mode 100644 index 00000000000..ee97e00cfa1 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; + +/** + * Action to fix vulnerable dependencies by opening GitHub Copilot chat with an upgrade prompt. + * This action appears in the Problems View context menu for vulnerable dependency issues. + */ +public class CveFixInProblemsViewAction extends AnAction implements DumbAware { + + private static final String CVE_MARKER = "CVE-"; + + public CveFixInProblemsViewAction() { + super(); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + return; + } + + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + SCAN_AND_RESOLVE_CVES_PROMPT + ); + } + + @Override + public void update(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Check if we're in the Problems View context with a vulnerability + final String description = extractProblemDescription(e); + if (description == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Also check if the file is pom.xml or build.gradle (common for dependency issues) + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + final boolean isBuildFile = isBuildFile(file); + + if (!isBuildFile || !isCVEIssue(description)) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + e.getPresentation().setEnabledAndVisible(true); + e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME); + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); + } + } + + private boolean isBuildFile(VirtualFile file){ + return file != null && + (file.getName().equals("pom.xml") || file.getName().endsWith(".gradle") || file.getName().endsWith(".gradle.kts")); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + /** + * Extracts the problem description from the action event context. + */ + @Nullable + private String extractProblemDescription(@NotNull AnActionEvent e) { + + // Approach 3: Try to get selected items from the tree + try { + final Object[] selectedItems = e.getData(PlatformDataKeys.SELECTED_ITEMS); + if (selectedItems != null && selectedItems.length > 0) { + // Concatenate all selected items' string representations + final StringBuilder sb = new StringBuilder(); + for (Object item : selectedItems) { + if (item != null) { + sb.append(item.toString()).append(" "); + } + } + final String result = sb.toString().trim(); + if (!result.isEmpty()) { + return result; + } + } + } catch (Exception ignored) { + } + return null; + } + + /** + * Extracts CVE ID from the problem description. + */ + private boolean isCVEIssue(@NotNull String description) { + // Pattern: CVE-YYYY-NNNNN + final int cveIndex = description.toUpperCase().indexOf(CVE_MARKER); + if (cveIndex >= 0) { + return true; + } + return false; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java similarity index 60% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java index ff3db6287ef..b22f817b71b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java @@ -13,18 +13,21 @@ import com.intellij.openapi.project.Project; import com.intellij.psi.PsiFile; import com.intellij.util.IncorrectOperationException; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import org.jetbrains.annotations.NotNull; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.SCAN_AND_RESOLVE_CVES_PROMPT; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME; + /** - * Intention action to upgrade vulnerable dependencies using GitHub Copilot. - * This action appears in the editor's quick-fix popup (More actions...) for pom.xml files - * when a vulnerable dependency is detected. + * Intention action to fix CVE vulnerabilities in dependencies using GitHub Copilot. + * This action appears in the editor's quick-fix popup (More actions...) for pom.xml files(yellow wavy dependency with CVE issue) + * when a vulnerable dependency with CVE issues is detected. */ -public class UpgradeInQuickFixIntentionAction implements IntentionAction, PriorityAction { - - private static final String DEFAULT_TEXT = "Scan and Resolve CVEs by Copilot"; +public class CveFixIntentionAction implements IntentionAction, PriorityAction { // Cached dependency info from isAvailable() for use in getText() private String cachedGroupId; @@ -32,40 +35,10 @@ public class UpgradeInQuickFixIntentionAction implements IntentionAction, Priori @Override public @IntentionName @NotNull String getText() { - // Return dynamic text based on cached dependency info - if (cachedGroupId != null) { - // Use a friendly display name for known dependencies - String displayName = getDisplayName(cachedGroupId, cachedArtifactId); - return displayName; + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + return SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; } - return DEFAULT_TEXT; - } - - /** - * Gets a friendly display name for a dependency. - */ - private String getDisplayName(String groupId, String artifactId) { - return "Scan and Resolve CVEs by Copilot"; -// if (groupId == null) { -// return "Dependency"; -// } -// -// // Map known groupIds to friendly names -// if (groupId.equals("org.springframework.boot")) { -// return "Upgrade Spring Boot with Copilot"; -// } else if (groupId.equals("org.springframework.security")) { -// return "Upgrade Spring Security with Copilot"; -// } else if (groupId.equals("org.springframework")) { -// return "Spring Framework"; -// } else if (groupId.startsWith("org.springframework")) { -// return "Spring " + (artifactId != null ? artifactId : "dependency"); -// } -// -// // For other dependencies, use groupId:artifactId or just groupId -// if (artifactId != null) { -// return groupId + ":" + artifactId; -// } -// return groupId; + return SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME; } @Override @@ -103,7 +76,11 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file cachedArtifactId = extractXmlValue(dependencyBlock, "artifactId"); // Only show if we have valid dependency info (not for parent/plugin sections) - return cachedGroupId != null && cachedArtifactId != null; + if(cachedGroupId != null && cachedArtifactId != null) { + //if the artifact is in the cached cve issues, show the intention + final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(cachedGroupId + ":" + cachedArtifactId); + return issue != null; + } } } catch (Exception e) { // Ignore and return false @@ -127,38 +104,7 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws * Builds a prompt based on the current editor context. */ private String buildPromptFromContext(@NotNull Editor editor, @NotNull PsiFile file) { -// try { -// final int offset = editor.getCaretModel().getOffset(); -// final String documentText = editor.getDocument().getText(); -// -// // Find the dependency block around the cursor -// final int dependencyStart = findDependencyStart(documentText, offset); -// final int dependencyEnd = findDependencyEnd(documentText, offset); -// -// if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { -// final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); -// -// // Extract groupId and artifactId -// final String groupId = extractXmlValue(dependencyBlock, "groupId"); -// final String artifactId = extractXmlValue(dependencyBlock, "artifactId"); -// final String version = extractXmlValue(dependencyBlock, "version"); -// -// if (groupId != null && artifactId != null) { -// final StringBuilder prompt = new StringBuilder(); -// prompt.append("Fix security vulnerabilities in "); -// prompt.append(groupId).append(":").append(artifactId); -// if (version != null) { -// prompt.append(":").append(version); -// } -// prompt.append(" by using #validate_cves_for_java"); -// return prompt.toString(); -// } -// } -// } catch (Exception e) { -// // Fall back to generic prompt -// } - - return "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; + return SCAN_AND_RESOLVE_CVES_PROMPT; } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java similarity index 82% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java index 890848e1d94..c398b14252f 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java @@ -15,19 +15,20 @@ import org.jetbrains.annotations.NotNull; +import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; +import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.isAppModPluginInstalled; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; + /** - * Action to upgrade a Java project using GitHub Copilot. + * Context menu action to upgrade a Java project using GitHub Copilot. * This action appears in the GitHub Copilot submenu when right-clicking on: * - Project root folder * - pom.xml (Maven projects) * - build.gradle or build.gradle.kts (Gradle projects) */ -public class UpgradeProjectAction extends AnAction { - - private static final String UPGRADE_JAVA_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; - +public class JavaUpgradeContextMenuAction extends AnAction { // text, description, and icon are defined in azure-intellij-plugin-appmod.xml - public UpgradeProjectAction() { + public JavaUpgradeContextMenuAction() { super(); } @@ -49,7 +50,9 @@ public void update(@NotNull AnActionEvent e) { isMavenBuildFile(file) || isGradleBuildFile(file); } - + if (!isAppModPluginInstalled()) { + e.getPresentation().setText(e.getPresentation().getText() + TO_INSTALL_APP_MODE_PLUGIN); + } e.getPresentation().setEnabledAndVisible(visible); } @@ -71,7 +74,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { * Builds the upgrade prompt based on the selected file. */ private String buildUpgradePrompt(Project project, VirtualFile file) { - return UPGRADE_JAVA_PROMPT; + return UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java new file mode 100644 index 00000000000..5841a552c8b --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.PACKAGE_ID_JDK; + +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +/** + * Quick fix for Java/Spring version upgrade issues detected in pom.xml files. + * This action is triggered from Java upgrade issue inspections and opens Copilot + * to assist with the upgrade process. + */ +public class JavaUpgradeQuickFix implements LocalQuickFix { + private final JavaUpgradeIssue issue; + + public JavaUpgradeQuickFix(@NotNull JavaUpgradeIssue issue) { + this.issue = issue; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getFamilyName() { + return "Azure Toolkit"; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getName() { + String name = "Upgrade " + issue.getPackageDisplayName() + " with Copilot"; + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + return name + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; + } + return name; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + String prompt = buildPromptForIssue(issue); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } + + private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { + String packageId = issue.getPackageId(); + + // JDK upgrade + if (PACKAGE_ID_JDK.equals(packageId)) { + return String.format( + "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", + issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } + + // Framework upgrade (Spring Boot, Spring Framework, etc.) + return String.format( + "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", + issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java index fbb892636d8..1b8ca3fbfc6 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java @@ -10,7 +10,6 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.actionSystem.Separator; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.ProjectActivity; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; @@ -26,8 +25,7 @@ */ public class UpgradeActionRegistrar implements ProjectActivity { - private static final Logger LOG = Logger.getInstance(UpgradeActionRegistrar.class); - private static final String UPGRADE_ACTION_ID = "AzureToolkit.UpgradeProject"; + private static final String UPGRADE_ACTION_ID = "AzureToolkit.JavaUpgradeContextMenu"; private static final String PROJECT_VIEW_POPUP_MENU = "ProjectViewPopupMenu"; @Nullable @@ -45,8 +43,6 @@ private void discoverAndRegisterAction() { ActionManager actionManager = ActionManager.getInstance(); - LOG.info("=== Searching for GitHub Copilot submenu in ProjectViewPopupMenu ==="); - // Get the ProjectViewPopupMenu group AnAction projectViewPopup = actionManager.getAction(PROJECT_VIEW_POPUP_MENU); if (projectViewPopup instanceof DefaultActionGroup) { @@ -57,14 +53,8 @@ private void discoverAndRegisterAction() { if (copilotGroup != null) { tryAddToGroup(actionManager, copilotGroup, "GitHub Copilot submenu"); - } else { - LOG.info("GitHub Copilot submenu not found in ProjectViewPopupMenu"); } - } else { - LOG.warn("ProjectViewPopupMenu not found or not a DefaultActionGroup"); } - - LOG.info("=== End Copilot Action Discovery ==="); } /** @@ -81,7 +71,6 @@ private DefaultActionGroup findCopilotSubmenu(DefaultActionGroup parentGroup, Ac // Match exactly "GitHub Copilot" to avoid false positives if ("GitHub Copilot".equals(text)) { - LOG.info("Found Copilot submenu by exact text match: " + text + ", id=" + actionId); return childGroup; } } @@ -92,7 +81,6 @@ private DefaultActionGroup findCopilotSubmenu(DefaultActionGroup parentGroup, Ac private void tryAddToGroup(ActionManager actionManager, DefaultActionGroup group, String groupId) { AnAction upgradeAction = actionManager.getAction(UPGRADE_ACTION_ID); if (upgradeAction == null) { - LOG.warn("Upgrade action not found: " + UPGRADE_ACTION_ID); return; } @@ -101,9 +89,6 @@ private void tryAddToGroup(ActionManager actionManager, DefaultActionGroup group // Add a separator before the upgrade action to visually group it group.add(Separator.create()); group.add(upgradeAction); - LOG.info("Successfully added upgrade action to group: " + groupId); - } else { - LOG.info("Upgrade action already exists in group: " + groupId); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java deleted file mode 100644 index 6746e560406..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java +++ /dev/null @@ -1,602 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; - -import com.intellij.analysis.problemsView.Problem; -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.actionSystem.PlatformDataKeys; -import com.intellij.openapi.project.DumbAware; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiManager; -import com.intellij.psi.xml.XmlFile; -import com.intellij.psi.xml.XmlTag; -import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; -import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; -import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; - -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - - -/** - * Action to fix vulnerable dependencies by opening GitHub Copilot chat with an upgrade prompt. - * This action appears in the Problems View context menu for vulnerable dependency issues. - */ -public class UpgradeInProblemsViewAction extends AnAction implements DumbAware { - - private static final String CVE_MARKER = "CVE-"; - - // Data key for problems in the Problems View - private static final String PROBLEMS_VIEW_PROBLEM_KEY = "Problem"; - - public UpgradeInProblemsViewAction() { - super(); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - final Project project = e.getData(CommonDataKeys.PROJECT); - if (project == null || project.isDisposed()) { - return; - } - - // Try to get issue from cache using PsiElement context - final JavaUpgradeIssue cachedIssue = findIssueFromContext(e, project); - if (cachedIssue != null) { - final String prompt = buildPromptFromIssue(cachedIssue); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - return; - } - - // Fallback: Get the problem description from the context - final String problemDescription = extractProblemDescription(e); - - if (problemDescription != null && !problemDescription.isEmpty()) { - // Extract dependency info and CVE from the problem description - final String prompt = buildUpgradePrompt(problemDescription); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - } else { - // Fallback: generic CVE fix prompt - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( - project, - "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java" - ); - } - } - - /** - * Tries to find the JavaUpgradeIssue from the context by examining the PsiElement. - */ - @Nullable - private JavaUpgradeIssue findIssueFromContext(@NotNull AnActionEvent e, @NotNull Project project) { - final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); - if (!cache.isInitialized()) { - return null; - } - - // Try to get PsiElement directly - PsiElement element = e.getData(CommonDataKeys.PSI_ELEMENT); - - // If no direct element, try to get from file + offset - if (element == null) { - element = findElementFromProblem(e, project); - } - - if (element == null) { - return null; - } - - // Navigate to find dependency/parent context - return findIssueFromElement(element, cache); - } - - /** - * Finds the PsiElement from a Problem in the Problems View. - * Uses reflection for cross-version compatibility as Problem API varies. - */ - @Nullable - private PsiElement findElementFromProblem(@NotNull AnActionEvent e, @NotNull Project project) { - try { - final Object problemData = e.getDataContext().getData(PROBLEMS_VIEW_PROBLEM_KEY); - if (problemData instanceof Problem) { - final Problem problem = (Problem) problemData; - - // Use reflection to get file - API varies across IntelliJ versions - VirtualFile file = null; - try { - java.lang.reflect.Method getFileMethod = problem.getClass().getMethod("getFile"); - Object fileObj = getFileMethod.invoke(problem); - if (fileObj instanceof VirtualFile) { - file = (VirtualFile) fileObj; - } - } catch (Exception ignored) { - // getFile method might not exist in this version - System.out.println("error" + ignored.getMessage()); - } - - if (file != null && file.getName().equals("pom.xml")) { - final PsiFile psiFile = PsiManager.getInstance(project).findFile(file); - if (psiFile instanceof XmlFile) { - // Try to get offset from problem and find element - try { - java.lang.reflect.Method getOffsetMethod = problem.getClass().getMethod("getOffset"); - Object offsetObj = getOffsetMethod.invoke(problem); - if (offsetObj instanceof Integer) { - int offset = (Integer) offsetObj; - if (offset >= 0) { - return psiFile.findElementAt(offset); - } - } - } catch (Exception ignored) { - // getOffset method might not exist - System.out.println("error" + ignored.getMessage()); - } - } - } - } - } catch (Exception ignored) { - System.out.println("error" + ignored.getMessage()); - - } - return null; - } - - /** - * Finds the JavaUpgradeIssue based on the XML element context. - */ - @Nullable - private JavaUpgradeIssue findIssueFromElement(@NotNull PsiElement element, @NotNull JavaUpgradeIssuesCache cache) { - // Find the containing XmlTag - XmlTag tag = findParentTag(element); - if (tag == null) { - return null; - } - - // Check if this is a Java version property - if (isJavaVersionContext(tag)) { - return cache.getJdkIssue(); - } - - // Check if this is a dependency or parent version - final String groupId = extractGroupId(tag); - if (groupId != null) { - if (groupId.equals(GROUP_ID_SPRING_BOOT)) { - return cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); - } else if (groupId.equals(GROUP_ID_SPRING_SECURITY)) { - return cache.findDependencyIssue(GROUP_ID_SPRING_SECURITY); - } else if (groupId.equals(GROUP_ID_SPRING_FRAMEWORK)) { - return cache.findDependencyIssue(GROUP_ID_SPRING_FRAMEWORK + ":"); - } - } - - return null; - } - - /** - * Finds the parent XmlTag of an element. - */ - @Nullable - private XmlTag findParentTag(@NotNull PsiElement element) { - PsiElement current = element; - while (current != null) { - if (current instanceof XmlTag) { - return (XmlTag) current; - } - current = current.getParent(); - } - return null; - } - - /** - * Checks if the tag is in a Java version context. - */ - private boolean isJavaVersionContext(@NotNull XmlTag tag) { - String tagName = tag.getName(); - - // Check for properties like java.version, maven.compiler.source, etc. - if ("java.version".equals(tagName) || - "maven.compiler.source".equals(tagName) || - "maven.compiler.target".equals(tagName) || - "maven.compiler.release".equals(tagName)) { - XmlTag parent = tag.getParentTag(); - return parent != null && "properties".equals(parent.getName()); - } - - // Check for maven-compiler-plugin source/target/release - if ("source".equals(tagName) || "target".equals(tagName) || "release".equals(tagName)) { - XmlTag current = tag.getParentTag(); - while (current != null) { - if ("plugin".equals(current.getName())) { - XmlTag artifactIdTag = current.findFirstSubTag("artifactId"); - if (artifactIdTag != null && ARTIFACT_ID_MAVEN_COMPILER_PLUGIN.equals(artifactIdTag.getValue().getText())) { - return true; - } - } - current = current.getParentTag(); - } - } - - return false; - } - - /** - * Extracts the groupId from a dependency or parent tag context. - */ - @Nullable - private String extractGroupId(@NotNull XmlTag tag) { - // If we're on a version tag, look at parent (dependency or parent) - XmlTag container = tag; - if ("version".equals(tag.getName())) { - container = tag.getParentTag(); - } - - if (container == null) { - return null; - } - - // Check if it's a dependency or parent tag - String containerName = container.getName(); - if ("dependency".equals(containerName) || "parent".equals(containerName)) { - XmlTag groupIdTag = container.findFirstSubTag("groupId"); - if (groupIdTag != null) { - return groupIdTag.getValue().getText(); - } - } - - return null; - } - - /** - * Builds a prompt from a cached JavaUpgradeIssue. - */ - @NotNull - private String buildPromptFromIssue(@NotNull JavaUpgradeIssue issue) { - String packageId = issue.getPackageId(); - - // JDK upgrade - if (PACKAGE_ID_JDK.equals(packageId)) { - return String.format( - "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", - issue.getCurrentVersion(), issue.getSuggestedVersion() - ); - } - - // Framework upgrade (Spring Boot, Spring Framework, etc.) - return String.format( - "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", - issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() - ); - } - - @Override - public void update(@NotNull AnActionEvent e) { - final Project project = e.getData(CommonDataKeys.PROJECT); - if (project == null || project.isDisposed()) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - - // Check if we're in the Problems View context with a vulnerability - final String description = extractProblemDescription(e); - if (description == null) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - - // Also check if the file is pom.xml or build.gradle (common for dependency issues) - final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); - final boolean isBuildFile = file != null && - (file.getName().equals("pom.xml") || file.getName().endsWith(".gradle") || file.getName().endsWith(".gradle.kts")); - - final boolean isVulnerability = isVulnerabilityDescription(description); - - if (!isBuildFile && !isVulnerability) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - - // Set dynamic text based on issue type detected from description - String actionText = getDynamicActionText(description, project); - e.getPresentation().setText(actionText); - e.getPresentation().setEnabledAndVisible(true); - } - - /** - * Gets dynamic action text based on the problem description. - */ - private String getDynamicActionText(@NotNull String description, @NotNull Project project) { - // Try to detect JDK issue - if (description.toLowerCase().contains("jdk") || - description.toLowerCase().contains("java runtime") || - description.toLowerCase().contains("java version")) { - return "Upgrade JDK with Copilot"; - } - - // Try to detect Spring Boot - if (description.toLowerCase().contains("spring boot")) { - return "Upgrade Spring Boot with Copilot"; - } - - // Try to detect Spring Framework - if (description.toLowerCase().contains("spring framework")) { - return "Upgrade Spring Framework with Copilot"; - } - - // Try to detect Spring Security - if (description.toLowerCase().contains("spring security")) { - return "Upgrade Spring Security with Copilot"; - } - - // Try to get from cache if available - final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); - if (cache.isInitialized()) { - // Check for JDK issue - if (cache.getJdkIssue() != null && description.contains(cache.getJdkIssue().getMessage())) { - return "Upgrade JDK with Copilot"; - } - // Check for Spring Boot - JavaUpgradeIssue springBootIssue = cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); - if (springBootIssue != null && description.contains(springBootIssue.getMessage())) { - return "Upgrade Spring Boot with Copilot"; - } - } - - // Default text - return "Scan and Resolve CVEs with Copilot"; - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } - - /** - * Checks if the description indicates a vulnerability. - */ - private boolean isVulnerabilityDescription(@NotNull String description) { - final String lowerDescription = description.toLowerCase(); - return lowerDescription.contains("vulnerable") || - lowerDescription.contains("cve-") || - lowerDescription.contains("security") || - lowerDescription.contains("vulnerability"); - } - - /** - * Extracts the problem description from the action event context. - */ - @Nullable - private String extractProblemDescription(@NotNull AnActionEvent e) { - // Try multiple approaches to get the problem description - - // Approach 1: Try to get Problem object directly from Problems View - try { - final Object problemData = e.getDataContext().getData(PROBLEMS_VIEW_PROBLEM_KEY); - if (problemData instanceof Problem) { - final Problem problem = (Problem) problemData; - final String text = problem.getText(); - if (text != null && !text.isEmpty()) { - return text; - } - } - } catch (Exception ignored) { - // Problem class might not be available - } - - // Approach 2: Try "problem.description" data key - try { - @SuppressWarnings("deprecation") - final Object data = e.getDataContext().getData("problem.description"); - if (data instanceof String && !((String) data).isEmpty()) { - return (String) data; - } - } catch (Exception ignored) { - } - - // Approach 3: Try to get selected items from the tree - try { - final Object[] selectedItems = e.getData(PlatformDataKeys.SELECTED_ITEMS); - if (selectedItems != null && selectedItems.length > 0) { - // Concatenate all selected items' string representations - final StringBuilder sb = new StringBuilder(); - for (Object item : selectedItems) { - if (item != null) { - sb.append(item.toString()).append(" "); - } - } - final String result = sb.toString().trim(); - if (!result.isEmpty()) { - return result; - } - } - } catch (Exception ignored) { - } - - // Approach 4: Try SELECTED_ITEM - try { - final Object selectedItem = e.getData(PlatformDataKeys.SELECTED_ITEM); - if (selectedItem != null) { - final String text = selectedItem.toString(); - if (!text.isEmpty()) { - return text; - } - } - } catch (Exception ignored) { - } - - // Approach 5: Try getting from context component - try { - final java.awt.Component component = e.getData(PlatformDataKeys.CONTEXT_COMPONENT); - if (component instanceof javax.swing.JTree) { - final javax.swing.JTree tree = (javax.swing.JTree) component; - final javax.swing.tree.TreePath[] paths = tree.getSelectionPaths(); - if (paths != null && paths.length > 0) { - final StringBuilder sb = new StringBuilder(); - for (javax.swing.tree.TreePath path : paths) { - final Object lastComponent = path.getLastPathComponent(); - if (lastComponent != null) { - sb.append(lastComponent.toString()).append(" "); - } - } - final String result = sb.toString().trim(); - if (!result.isEmpty()) { - return result; - } - } - } - } catch (Exception ignored) { - } - - // Approach 6: Try getting selected text from editor - try { - final com.intellij.openapi.editor.Editor editor = e.getData(CommonDataKeys.EDITOR); - if (editor != null && editor.getSelectionModel().hasSelection()) { - final String selectedText = editor.getSelectionModel().getSelectedText(); - if (selectedText != null && !selectedText.isEmpty()) { - return selectedText; - } - } - } catch (Exception ignored) { - } - - return null; - } - - /** - * Builds an upgrade prompt based on the problem description. - */ - private String buildUpgradePrompt(@NotNull String problemDescription) { - - // Extract dependency coordinates if present - final String dependency = extractDependencyCoordinates(problemDescription); - - if (problemDescription != null) { - // Try to detect JDK issue - if (problemDescription.toLowerCase().contains("jdk") || - problemDescription.toLowerCase().contains("java runtime") || - problemDescription.toLowerCase().contains("java version")) { - return "upgrade java runtime to Java " + MATURE_JAVA_LTS_VERSION + " (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; - } - - // Try to detect Spring Boot - if (problemDescription.toLowerCase().contains("spring boot") || problemDescription.toLowerCase().contains("spring framework") || problemDescription.toLowerCase().contains("spring security")) { - return "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; - } - - } - // Default text - return "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; - } - - /** - * Extracts CVE ID from the problem description. - */ - private String extractCVEId(@NotNull String description) { - // Pattern: CVE-YYYY-NNNNN - final int cveIndex = description.toUpperCase().indexOf(CVE_MARKER); - if (cveIndex >= 0) { - final int endIndex = findCVEEndIndex(description, cveIndex); - if (endIndex > cveIndex) { - return description.substring(cveIndex, endIndex); - } - } - return null; - } - - /** - * Finds the end index of a CVE ID in the description. - */ - private int findCVEEndIndex(@NotNull String description, int startIndex) { - int index = startIndex + CVE_MARKER.length(); - // Skip year (4 digits) - while (index < description.length() && Character.isDigit(description.charAt(index))) { - index++; - } - // Skip separator - if (index < description.length() && description.charAt(index) == '-') { - index++; - } - // Skip ID number - while (index < description.length() && Character.isDigit(description.charAt(index))) { - index++; - } - return index; - } - - /** - * Extracts Maven dependency coordinates from the problem description. - * Looks for patterns like groupId:artifactId:version - */ - private String extractDependencyCoordinates(@NotNull String description) { - // Look for Maven coordinate pattern: groupId:artifactId:version - // Common patterns in vulnerability reports - final String[] patterns = { - "maven:", // maven:groupId:artifactId:version - "dependency " // dependency groupId:artifactId - }; - - for (String pattern : patterns) { - final int index = description.toLowerCase().indexOf(pattern); - if (index >= 0) { - return extractCoordinatesAfterPattern(description, index + pattern.length()); - } - } - - // Try to find standalone coordinate pattern (e.g., org.example:artifact:1.0.0) - return findStandaloneCoordinates(description); - } - - /** - * Extracts coordinates after a known pattern. - */ - private String extractCoordinatesAfterPattern(@NotNull String description, int startIndex) { - final StringBuilder coords = new StringBuilder(); - int colonCount = 0; - - for (int i = startIndex; i < description.length(); i++) { - final char c = description.charAt(i); - if (Character.isLetterOrDigit(c) || c == '.' || c == '-' || c == '_') { - coords.append(c); - } else if (c == ':' && colonCount < 2) { - coords.append(c); - colonCount++; - } else if (!coords.isEmpty()) { - break; - } - } - - final String result = coords.toString(); - return result.contains(":") ? result : null; - } - - /** - * Finds standalone Maven coordinates in the description. - */ - private String findStandaloneCoordinates(@NotNull String description) { - // Simple heuristic: look for pattern like "org.xxx:xxx" or "com.xxx:xxx" - final String[] prefixes = {"org.", "com.", "io.", "net."}; - - for (String prefix : prefixes) { - int index = description.indexOf(prefix); - while (index >= 0) { - final String coords = extractCoordinatesAfterPattern(description, index); - if (coords != null && coords.contains(":")) { - return coords; - } - index = description.indexOf(prefix, index + 1); - } - } - - return null; - } -} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java index 41222922e10..999dd32d36c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java @@ -6,26 +6,24 @@ package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.inspection; import com.intellij.codeInspection.LocalInspectionTool; -import com.intellij.codeInspection.LocalQuickFix; -import com.intellij.codeInspection.ProblemDescriptor; import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.codeInspection.ProblemsHolder; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.PsiFile; import com.intellij.psi.XmlElementVisitor; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.JavaUpgradeQuickFix; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; -import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; -import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; +import java.util.List; + /** * Inspection that displays Java upgrade issues detected by JavaUpgradeDetectionService. * Shows JDK version and framework version issues in pom.xml files with wavy underlines. @@ -35,8 +33,6 @@ */ public class JavaUpgradeIssuesInspection extends LocalInspectionTool { - private static final Logger LOG = Logger.getInstance(JavaUpgradeIssuesInspection.class); - @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { @@ -57,15 +53,7 @@ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean is // Get cached issues (computed once at project startup) final JavaUpgradeIssue jdkIssue = cache.getJdkIssue(); - final JavaUpgradeIssue springBootIssue = cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); - final JavaUpgradeIssue springFrameworkIssue = cache.findDependencyIssue(GROUP_ID_SPRING_FRAMEWORK + ":"); - final JavaUpgradeIssue springSecurityIssue = cache.findDependencyIssue(GROUP_ID_SPRING_SECURITY); - - // Debug logging - LOG.info("JavaUpgradeIssuesInspection: Cache initialized=" + cache.isInitialized() + - ", jdkIssue=" + (jdkIssue != null) + - ", springBootIssue=" + (springBootIssue != null ? springBootIssue.getCurrentVersion() : "null") + - ", dependencyIssues=" + cache.getDependencyIssues().size()); + final List dependencyIssues = cache.getDependencyIssues(); return new XmlElementVisitor() { @Override @@ -79,42 +67,52 @@ public void visitXmlTag(@NotNull XmlTag tag) { } } - // Check for Spring Boot parent version - if (springBootIssue != null && isSpringBootParentVersion(tag)) { - registerProblem(holder, tag, springBootIssue); - } - - // Check for Spring Framework dependency version - if (springFrameworkIssue != null && isSpringFrameworkDependencyVersion(tag)) { - registerProblem(holder, tag, springFrameworkIssue); - } - - // Check for Spring Boot dependency version (when not using parent) - if (springBootIssue != null && isSpringBootDependencyVersion(tag)) { - registerProblem(holder, tag, springBootIssue); - } - - // Check for Spring Security dependency version - if (springSecurityIssue != null && isSpringSecurityDependencyVersion(tag)) { - registerProblem(holder, tag, springSecurityIssue); + // Check for dependency/parent version tags and register all matching issues + if ("version".equals(tag.getName())) { + XmlTag parentElement = tag.getParentTag(); + if (parentElement != null) { + String parentTagName = parentElement.getName(); + if ("dependency".equals(parentTagName) || "parent".equals(parentTagName)) { + XmlTag groupIdTag = parentElement.findFirstSubTag("groupId"); + XmlTag artifactIdTag = parentElement.findFirstSubTag("artifactId"); + if (groupIdTag != null && artifactIdTag != null) { + String packageId = groupIdTag.getValue().getText() + ":" + artifactIdTag.getValue().getText(); + // Register all issues that match this package + for (JavaUpgradeIssue issue : dependencyIssues) { + if (matchesPackageId(packageId, issue.getPackageId())) { + registerProblem(holder, tag, issue); + } + } + } + } + } } } }; } private void registerProblem(@NotNull ProblemsHolder holder, @NotNull XmlTag tag, @NotNull JavaUpgradeIssue issue) { - // ProblemHighlightType highlightType = issue.getSeverity() == JavaUpgradeIssue.Severity.CRITICAL - // ? ProblemHighlightType.ERROR - // : ProblemHighlightType.WARNING; - holder.registerProblem( tag, issue.getMessage(), ProblemHighlightType.WARNING, - new UpgradeWithCopilotQuickFix(issue) + new JavaUpgradeQuickFix(issue) ); } + /** + * Checks if the packageId matches the issue's packageId pattern. + * Supports wildcard patterns like "org.springframework.boot:*" to match any artifact in a group. + */ + private boolean matchesPackageId(@NotNull String packageId, @NotNull String issuePackageId) { + if (issuePackageId.endsWith(":*")) { + // Wildcard match: check if packageId starts with the group prefix + String groupPrefix = issuePackageId.substring(0, issuePackageId.length() - 1); // "org.springframework.boot:" + return packageId.startsWith(groupPrefix); + } + return packageId.equals(issuePackageId); + } + /** * Checks if the tag is a Java version property (java.version, maven.compiler.source, maven.compiler.target). */ @@ -154,124 +152,4 @@ private boolean isCompilerPluginVersionTag(@NotNull XmlTag tag) { } return false; } - - /** - * Checks if the tag is the version tag inside a Spring Boot parent declaration. - */ - private boolean isSpringBootParentVersion(@NotNull XmlTag tag) { - if (!"version".equals(tag.getName())) { - return false; - } - - XmlTag parent = tag.getParentTag(); - if (parent == null || !"parent".equals(parent.getName())) { - return false; - } - - XmlTag groupIdTag = parent.findFirstSubTag("groupId"); - XmlTag artifactIdTag = parent.findFirstSubTag("artifactId"); - - return groupIdTag != null && GROUP_ID_SPRING_BOOT.equals(groupIdTag.getValue().getText()) && - artifactIdTag != null && ARTIFACT_ID_SPRING_BOOT_STARTER_PARENT.equals(artifactIdTag.getValue().getText()); - } - - /** - * Checks if the tag is a version tag inside a Spring Boot dependency. - */ - private boolean isSpringBootDependencyVersion(@NotNull XmlTag tag) { - if (!"version".equals(tag.getName())) { - return false; - } - - XmlTag dependency = tag.getParentTag(); - if (dependency == null || !"dependency".equals(dependency.getName())) { - return false; - } - - XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); - return groupIdTag != null && GROUP_ID_SPRING_BOOT.equals(groupIdTag.getValue().getText()); - } - - /** - * Checks if the tag is a version tag inside a Spring Framework dependency. - */ - private boolean isSpringFrameworkDependencyVersion(@NotNull XmlTag tag) { - if (!"version".equals(tag.getName())) { - return false; - } - - XmlTag dependency = tag.getParentTag(); - if (dependency == null || !"dependency".equals(dependency.getName())) { - return false; - } - - XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); - return groupIdTag != null && GROUP_ID_SPRING_FRAMEWORK.equals(groupIdTag.getValue().getText()); - } - - /** - * Checks if the tag is a version tag inside a Spring Security dependency. - */ - private boolean isSpringSecurityDependencyVersion(@NotNull XmlTag tag) { - if (!"version".equals(tag.getName())) { - return false; - } - - XmlTag dependency = tag.getParentTag(); - if (dependency == null || !"dependency".equals(dependency.getName())) { - return false; - } - - XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); - return groupIdTag != null && GROUP_ID_SPRING_SECURITY.equals(groupIdTag.getValue().getText()); - } - - /** - * Quick fix to upgrade using Copilot based on the issue type. - */ - private static class UpgradeWithCopilotQuickFix implements LocalQuickFix { - private final JavaUpgradeIssue issue; - - public UpgradeWithCopilotQuickFix(@NotNull JavaUpgradeIssue issue) { - this.issue = issue; - } - - @Nls(capitalization = Nls.Capitalization.Sentence) - @NotNull - @Override - public String getFamilyName() { - return "Azure Toolkit"; - } - - @Nls(capitalization = Nls.Capitalization.Sentence) - @NotNull - @Override - public String getName() { - return "Upgrade " + issue.getPackageDisplayName() + " with Copilot"; - } - - @Override - public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { - String prompt = buildPromptForIssue(issue); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - } - - private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { - String packageId = issue.getPackageId(); - - // JDK upgrade - if (PACKAGE_ID_JDK.equals(packageId)) { - return String.format( - "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", - issue.getCurrentVersion(), issue.getSuggestedVersion() - ); - } - - // Framework upgrade (Spring Boot, Spring Framework, etc.) - return String.format( - "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", - issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() - ); - } - } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java index d6380a38f6c..87fd952036d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java @@ -5,6 +5,7 @@ package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; +import com.intellij.util.net.JdkProxyProvider; import com.intellij.util.net.ssl.CertificateManager; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.google.gson.Gson; @@ -127,6 +128,9 @@ private CVECheckService() { final HttpClient.Builder builder = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS)); try { + // Configure proxy using IntelliJ's JdkProxyProvider + // This respects the IDE's proxy settings (Settings → HTTP Proxy) + builder.proxy(JdkProxyProvider.getInstance().getProxySelector()); builder.sslContext(CertificateManager.getInstance().getSslContext()); } catch (Throwable e) { // Failed to get IntelliJ SSL context, using default diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java index 71fabe6fea0..87364af5f19 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java @@ -30,6 +30,7 @@ public final class JavaUpgradeIssuesCache implements Disposable { private final Project project; private final AtomicReference> jdkIssuesCache = new AtomicReference<>(); private final AtomicReference> dependencyIssuesCache = new AtomicReference<>(); + private final AtomicReference> cvesIssuesCache = new AtomicReference<>(); private final AtomicBoolean initialized = new AtomicBoolean(false); public JavaUpgradeIssuesCache(@NotNull Project project) { @@ -59,9 +60,30 @@ public List getDependencyIssues() { } /** - * Finds a specific issue by package ID prefix. + * Gets cached CVE issues. Returns empty list if not yet initialized. + */ + @Nonnull + public List getCveIssues() { + List cached = cvesIssuesCache.get(); + return cached != null ? cached : Collections.emptyList(); + } + + /** + * Finds a specific CVE issue by package ID prefix. */ @Nullable + public JavaUpgradeIssue findCveIssue(@Nonnull String packageIdPrefix) { + return getCveIssues().stream() + .filter(i -> i.getPackageId().startsWith(packageIdPrefix)) + .findFirst() + .orElse(null); + } + /** + * Finds the first issue matching a package ID prefix. + * @deprecated Use {@link #findDependencyIssues(String)} to handle multiple dependencies with the same groupId. + */ + @Deprecated + @Nullable public JavaUpgradeIssue findDependencyIssue(@Nonnull String packageIdPrefix) { return getDependencyIssues().stream() .filter(i -> i.getPackageId().startsWith(packageIdPrefix)) @@ -69,6 +91,17 @@ public JavaUpgradeIssue findDependencyIssue(@Nonnull String packageIdPrefix) { .orElse(null); } + /** + * Finds all issues matching a package ID prefix (groupId). + * This handles the case where multiple dependencies share the same groupId. + */ + @Nonnull + public List findDependencyIssues(@Nonnull String packageIdPrefix) { + return getDependencyIssues().stream() + .filter(i -> i.getPackageId().startsWith(packageIdPrefix)) + .toList(); + } + /** * Gets the first JDK issue if present. */ @@ -99,10 +132,11 @@ public void refresh() { // Scan for issues List jdkIssues = detectionService.getJavaIssues(project); List dependencyIssues = detectionService.getDependencyIssues(project); - + List cveIssues = detectionService.getCVEIssues(project); // Update cache jdkIssuesCache.set(Collections.unmodifiableList(jdkIssues)); dependencyIssuesCache.set(Collections.unmodifiableList(dependencyIssues)); + cvesIssuesCache.set(Collections.unmodifiableList(cveIssues)); initialized.set(true); } @@ -112,6 +146,7 @@ public void refresh() { public void invalidate() { jdkIssuesCache.set(null); dependencyIssuesCache.set(null); + cvesIssuesCache.set(null); initialized.set(false); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java index 895b08d0362..9389d20d734 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -188,8 +188,6 @@ public String getEolDateForVersion(@Nonnull String version) { private static final String JDK_LEARN_MORE_URL = "https://learn.microsoft.com/azure/developer/java/fundamentals/java-support-on-azure"; - private static final Logger LOG = Logger.getInstance(JavaUpgradeIssuesDetectionService.class); - private static JavaUpgradeIssuesDetectionService instance; private JavaUpgradeIssuesDetectionService() { @@ -202,33 +200,6 @@ public static synchronized JavaUpgradeIssuesDetectionService getInstance() { return instance; } - /** - * Analyzes the given project and returns a list of detected outdated version issues. - * - * @param project The IntelliJ project to analyze - * @return List of detected outdated version issues - */ - @Nonnull - public List analyzeProject(@Nonnull Project project) { - final List issues = new ArrayList<>(); - - try { - // Get JDK issues - issues.addAll(getJavaIssues(project)); - - // Get dependency issues - issues.addAll(getDependencyIssues(project)); - - // Get CVE issues - issues.addAll(getCVEIssues(project)); - - } catch (Exception e) { - // Error analyzing project for upgrade issues - } - - return issues; - } - /** * Gets JDK/JRE version issues. * Aligned with getJavaIssues() from assessmentManager.ts. @@ -434,17 +405,12 @@ private String getParentVersion(@Nonnull MavenProject mavenProject, @Nonnull String artifactId) { try { final var parentId = mavenProject.getParentId(); - LOG.info("getParentVersion: parentId=" + (parentId != null ? - parentId.getGroupId() + ":" + parentId.getArtifactId() + ":" + parentId.getVersion() : "null") + - ", looking for " + groupId + ":" + artifactId); if (parentId != null && groupId.equals(parentId.getGroupId()) && artifactId.equals(parentId.getArtifactId())) { - LOG.info("getParentVersion: Found matching parent version: " + parentId.getVersion()); return parentId.getVersion(); } } catch (Exception e) { - LOG.warn("Error getting parent version", e); } return null; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index 50a7449bfff..af1bc385e28 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -21,6 +21,7 @@ import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.installPlugin; import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.isAppModPluginInstalled; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; import java.lang.reflect.Method; @@ -44,8 +45,7 @@ public class JavaVersionNotificationService { private static final String DEFERRED_UNTIL_KEY = "azure.toolkit.java.version.deferred_until"; private static final long DEFER_INTERVAL_MS = 10 * 24 * 60 * 60 * 1000L; // 10 days in milliseconds private static final String DEFAULT_MODEL_NAME = "Claude Sonnet 4.5"; - // GitHub Copilot app modernization plugin ID - private static final String COPILOT_APPMOD_PLUGIN_ID = "com.github.copilot.appmod"; + // GitHub Copilot plugin ID private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; @@ -94,38 +94,35 @@ public void showNotifications(@Nonnull Project project, @Nonnull List clazz, String methodName) { */ private String buildUpgradePrompt(@Nonnull JavaUpgradeIssue issue) { if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) { - return String.format("upgrade java runtime to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", MATURE_JAVA_LTS_VERSION); - } else if (issue.getPackageId().startsWith(GROUP_ID_SPRING_BOOT + ":")) { - return String.format("upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"); + return UPGRADE_JAVA_VERSION_PROMPT; + } else if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.CVE){ + return SCAN_AND_RESOLVE_CVES_PROMPT; + }else if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.DEPRECATED || issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.END_OF_LIFE) { + return UPGRADE_JAVA_FRAMEWORK_PROMPT; } - return "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; - } - - /** - * Installs the GitHub Copilot app modernization plugin and then opens Copilot chat. - * @param project The project context - * @param issue The upgrade issue to address - */ - private void installCopilotAppModPluginAndUpgrade(@Nonnull Project project, @Nonnull JavaUpgradeIssue issue) { -// final Set pluginIds = new HashSet<>(); -// pluginIds.add(COPILOT_APPMOD_PLUGIN_ID); -// -// AzureTaskManager.getInstance().runLater(() -> { -// PluginsAdvertiser.installAndEnablePlugins(pluginIds, () -> { -// PluginInstaller.addStateListener(new PluginStateListener() { -// @Override -// public void install(@NotNull IdeaPluginDescriptor descriptor) { -// if (COPILOT_APPMOD_PLUGIN_ID.equals(descriptor.getPluginId().getIdString())) { -// // Plugin installed successfully, now open Copilot chat -// ApplicationManager.getApplication().invokeLater(() -> { -// openCopilotChatWithUpgradePrompt(project, issue); -// }); -// } -// } -// -// @Override -// public void uninstall(@NotNull IdeaPluginDescriptor descriptor) { -// // Not needed -// } -// }); -// }); -// }); + return UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index 5cbc896ed4b..bf394c89e59 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -9,7 +9,7 @@ XML - com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.UpgradeInQuickFixIntentionAction + com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.CveFixIntentionAction Azure Toolkit @@ -34,17 +34,16 @@ id="Actions.MigrateToAzure" text="Migrate to Azure" description="Migrate application to Azure" icon="/icons/appmod.svg"/> - + From bf4c5ae7c8232082db62adb9b696acbc42c79fc5 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Tue, 27 Jan 2026 11:09:22 +0800 Subject: [PATCH 15/24] revert .idea file --- .../azure-toolkit-for-intellij/.idea/gradle.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml index d604fd81745..25aace38038 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml @@ -14,7 +14,6 @@ - - \ No newline at end of file From 3ee458a441b2211dca659a25880132de74fc142b Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Tue, 27 Jan 2026 14:12:21 +0800 Subject: [PATCH 16/24] remove InstallPluginDialog and add built-in dialog --- .../appmod/common/AppModPluginInstaller.java | 9 +- .../appmod/common/InstallPluginDialog.java | 82 ------------------- 2 files changed, 4 insertions(+), 87 deletions(-) delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 198890e7137..3db9cc9aea0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -9,6 +9,7 @@ import com.intellij.ide.plugins.PluginManagerCore; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; @@ -77,7 +78,6 @@ public static boolean isRunningInDevMode() { /** * Shows a confirmation dialog for plugin installation. - * Uses a modal dialog similar to AzdNode's ConfirmAndRunDialog. * * @param project The current project * @param onConfirm Callback to execute when user confirms installation @@ -93,10 +93,9 @@ public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Ru ? "Install this plugin to automate migrating your apps to Azure with Copilot." : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; - new InstallPluginDialog(project, title) - .setLabel(message) - .setOnOkAction(onConfirm) - .show(); + if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { + onConfirm.run(); + } } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java deleted file mode 100644 index 0989aa160fa..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -package com.microsoft.azure.toolkit.intellij.appmod.common; - -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.DialogWrapper; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.awt.*; -import java.util.Objects; - -/** - * Dialog for confirming plugin installation. - * Similar to ConfirmAndRunDialog used in AzdNode. - */ -public class InstallPluginDialog extends DialogWrapper { - - private final Project project; - private String label; - private Runnable onOkAction; - - public InstallPluginDialog(Project project, String title) { - super(project, true); - this.project = project; - setSize(400, 150); - setTitle(Objects.requireNonNull(title, "Title must not be null")); - setOKButtonText("Install"); - } - - public InstallPluginDialog setLabel(String label) { - this.label = Objects.requireNonNull(label, "Label must not be null"); - return this; - } - - public InstallPluginDialog setOnOkAction(Runnable onOkAction) { - this.onOkAction = onOkAction; - return this; - } - - @Override - public void show() { - init(); - super.show(); - } - - @Override - protected @Nullable JComponent createCenterPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - // Support HTML for multi-line labels - JLabel labelComponent; - if (label != null && label.contains("\n")) { - // Convert newlines to HTML breaks - String htmlLabel = "" + label.replace("\n", "
") + ""; - labelComponent = new JLabel(htmlLabel); - } else { - labelComponent = new JLabel(label); - } - labelComponent.setHorizontalAlignment(SwingConstants.LEFT); - panel.add(labelComponent, BorderLayout.CENTER); - return panel; - } - - @Override - protected Action @NotNull [] createActions() { - return new Action[]{getOKAction(), getCancelAction()}; - } - - @Override - protected void doOKAction() { - super.doOKAction(); - if (onOkAction != null) { - onOkAction.run(); - } - } -} From 9445e3fdcb0b92e901680505c70637be5e39c70b Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 28 Jan 2026 09:52:54 +0800 Subject: [PATCH 17/24] revert gradle.properties --- .../azure-toolkit-for-intellij/gradle.properties | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index f5051491877..43485d7ea98 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -3,7 +3,7 @@ intellijDisplayVersion=2025.3 intellij_version=253-EAP-SNAPSHOT platformVersion=253-EAP-SNAPSHOT # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html -pluginSinceBuild=251 +pluginSinceBuild=253 pluginUntilBuild=253.* # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP platformPlugins=org.intellij.scala:2025.3.12 @@ -31,10 +31,7 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.console=rich org.gradle.configureondemand=true -org.gradle.daemon=true -org.gradle.workers.max=8 -org.gradle.vfs.watch=true -org.gradle.jvmargs=-Xmx4096m -Xms1024m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -Dfile.encoding=UTF-8 -Duser.language=en +org.gradle.jvmargs='-Duser.language=en' org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false From 0e1cc290828cae9ffd9abf05cd618985d9c97307 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 28 Jan 2026 09:53:33 +0800 Subject: [PATCH 18/24] revert gradle.properties --- PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index 43485d7ea98..c608c642f3c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -31,6 +31,7 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.console=rich org.gradle.configureondemand=true +org.gradle.daemon=true org.gradle.jvmargs='-Duser.language=en' org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false From 407a07ed263d606401010b86ca1b99fce3ea7194 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 28 Jan 2026 15:49:02 +0800 Subject: [PATCH 19/24] telemetry update click-option to click-task --- .../intellij/appmod/javamigration/MigrateToAzureAction.java | 2 +- .../intellij/appmod/javamigration/MigrateToAzureNode.java | 2 +- .../connector/projectexplorer/MigrateToAzureFacetNode.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java index 1e5d6fa5143..79147560b67 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java @@ -202,7 +202,7 @@ public void update(@NotNull AnActionEvent e) { @Override public void actionPerformed(@NotNull AnActionEvent e) { - AppModUtils.logTelemetryEvent("action.click-option", Map.of("label", nodeData.getLabel())); + AppModUtils.logTelemetryEvent("action.click-task", Map.of("label", nodeData.getLabel())); nodeData.click(e); } }; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java index cd4c43dea06..67b377a4111 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java @@ -157,7 +157,7 @@ private Node convertToNode(MigrateNodeData data) { // Set click handler if (data.hasClickHandler()) { node.onClicked(d -> { - AppModUtils.logTelemetryEvent("node.click-option", Map.of("label", data.getLabel())); + AppModUtils.logTelemetryEvent("node.click-task", Map.of("label", data.getLabel())); data.click(null); }); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index d7d89596215..e8f57a21866 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -212,7 +212,7 @@ protected void buildView(@Nonnull PresentationData presentation) { @Override public void navigate(boolean requestFocus) { // Trigger click handler - AppModUtils.logTelemetryEvent("facet.click-option", java.util.Map.of("label", nodeData.getLabel())); + AppModUtils.logTelemetryEvent("facet.click-task", java.util.Map.of("label", nodeData.getLabel())); nodeData.doubleClick(null); } From 38b2625291f5bf7cdec43f30b03d77dfcd8424d2 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 28 Jan 2026 15:54:28 +0800 Subject: [PATCH 20/24] telemetry update no-options to no-tasks --- .../intellij/appmod/javamigration/MigrateToAzureAction.java | 2 +- .../intellij/appmod/javamigration/MigrateToAzureNode.java | 2 +- .../connector/projectexplorer/MigrateToAzureFacetNode.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java index 79147560b67..9ff70b001a5 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java @@ -86,7 +86,7 @@ private MigrationState computeState(Project project) { .collect(Collectors.toList()); if (nodes.isEmpty()) { - AppModUtils.logTelemetryEvent("action.no-options"); + AppModUtils.logTelemetryEvent("action.no-tasks"); } return new MigrationState( diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java index 67b377a4111..41eae1252bc 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java @@ -108,7 +108,7 @@ private List loadMigrationNodeData() { .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); if (nodes.isEmpty()) { - AppModUtils.logTelemetryEvent("node.no-options"); + AppModUtils.logTelemetryEvent("node.no-tasks"); } return nodes; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index e8f57a21866..583ebd14a94 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -73,7 +73,7 @@ private List computeMigrationNodes() { .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); if (nodes.isEmpty()) { - AppModUtils.logTelemetryEvent("facet.no-options"); + AppModUtils.logTelemetryEvent("facet.no-tasks"); } return nodes; } From 26818ea60868078617cb45530b0eb27a013a9056 Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Wed, 28 Jan 2026 22:31:25 +0800 Subject: [PATCH 21/24] Add fix for single CVE dependency; Polish the UI text --- .../appmod/common/AppModPluginInstaller.java | 15 ++ .../intellij/appmod/javaupgrade/Contants.java | 7 +- .../CveFixDependencyInProblemsViewAction.java | 145 +++++++++++++++ .../CveFixDependencyIntentionAction.java | 165 ++++++++++++++++++ .../action/CveFixIntentionAction.java | 11 +- .../action/JavaUpgradeQuickFix.java | 14 +- .../javaupgrade/dao/JavaUpgradeIssue.java | 5 +- .../javaupgrade/dao/VulnerabilityInfo.java | 88 ++++++++++ .../JavaUpgradeIssuesInspection.java | 2 +- .../JavaUpgradeIssuesDetectionService.java | 63 ++++--- .../JavaVersionNotificationService.java | 47 ++--- .../META-INF/azure-intellij-plugin-appmod.xml | 13 +- .../after.xml.template | 5 + .../before.xml.template | 5 + .../description.html | 13 ++ .../CveFixIntentionAction/after.xml.template | 5 + .../CveFixIntentionAction/before.xml.template | 5 + .../CveFixIntentionAction/description.html | 13 ++ 18 files changed, 549 insertions(+), 72 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/after.xml.template create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/before.xml.template create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/description.html create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/after.xml.template create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/before.xml.template create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/description.html diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 3db9cc9aea0..011624edc29 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -98,6 +98,21 @@ public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Ru } } + public static void showAppModInstallationConfirmation(@Nonnull Project project) { + final boolean copilotInstalled = isCopilotInstalled(); + + final String title = copilotInstalled + ? "Install Github Copilot app modernization" + : "Install GitHub Copilot and app modernization"; + + final String message = copilotInstalled + ? "Install this plugin to upgrade your apps with Copilot." + : "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization."; + + if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { + installPlugin(project); + } + } /** * Installs the App Modernization plugin. * IntelliJ platform will automatically install Copilot as a dependency if AppMod declares it. diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java index 019092aff81..f5f37d64e60 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java @@ -4,12 +4,15 @@ public class Contants { public static final String UPGRADE_JAVA_AND_FRAMEWORK_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; - public static final String UPGRADE_JAVA_VERSION_PROMPT = "upgrade java runtime to Java " + MATURE_JAVA_LTS_VERSION + " (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; - public static final String UPGRADE_JAVA_FRAMEWORK_PROMPT = "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + public static final String UPGRADE_JAVA_VERSION_PROMPT = "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; + public static final String UPGRADE_JAVA_FRAMEWORK_PROMPT = "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan"; public static final String SCAN_AND_RESOLVE_CVES_PROMPT = "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; public static final String UPGRADE_JDK_WITH_COPILOT_DISPLAY_NAME = "Upgrade JDK with Copilot"; public static final String UPGRADE_SPRING_BOOT_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Boot with Copilot"; public static final String UPGRADE_SPRING_FRAMEWORK_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Framework with Copilot"; public static final String UPGRADE_SPRING_SECURITY_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Security with Copilot"; public static final String SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME = "Scan and Resolve CVEs with Copilot"; + public static final String FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT = "Fix the vulnerable dependency %s by using #validate_cves_for_java"; + public static final String FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME = "Fix the vulnerable dependency with Copilot"; + public static final String ISSUE_DISPLAY_NAME = "Your project uses %s %s. Consider upgrading to %s %s or higher for better performance and support"; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java new file mode 100644 index 00000000000..5cc25fe80d9 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.VulnerabilityInfo; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.VulnerabilityInfo.parseVulnerabilityDescription; + +/** + * Action to fix vulnerable dependencies by opening GitHub Copilot chat with an upgrade prompt. + * This action appears in the Problems View context menu for vulnerable dependency issues. + */ +public class CveFixDependencyInProblemsViewAction extends AnAction implements DumbAware { + + private static final String CVE_MARKER = "CVE-"; + + private VulnerabilityInfo vulnerabilityInfo; + public CveFixDependencyInProblemsViewAction() { + super(); + } + + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + return; + } + if (vulnerabilityInfo == null) { + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + SCAN_AND_RESOLVE_CVES_PROMPT + ); + } else { + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, + vulnerabilityInfo.getDependencyCoordinate()) + ); + } + + } + + @Override + public void update(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Check if we're in the Problems View context with a vulnerability + final String description = extractProblemDescription(e); + if (description == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + vulnerabilityInfo = parseVulnerabilityDescription(description); + // Also check if the file is pom.xml or build.gradle (common for dependency issues) + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + final boolean isBuildFile = isBuildFile(file); + + if (!isBuildFile || !isCVEIssue(description)) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(vulnerabilityInfo.getGroupId() + ":" + vulnerabilityInfo.getArtifactId()); + if (issue == null){ + e.getPresentation().setEnabledAndVisible(false); + return; + } + e.getPresentation().setEnabledAndVisible(true); + // e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME); + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setText(e.getPresentation().getText() + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); + } + } + + private boolean isBuildFile(VirtualFile file){ + return file != null && + (file.getName().equals("pom.xml") || file.getName().endsWith(".gradle") || file.getName().endsWith(".gradle.kts")); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + /** + * Extracts the problem description from the action event context. + */ + @Nullable + private String extractProblemDescription(@NotNull AnActionEvent e) { + + // Approach 3: Try to get selected items from the tree + try { + final Object[] selectedItems = e.getData(PlatformDataKeys.SELECTED_ITEMS); + if (selectedItems != null && selectedItems.length > 0) { + // Concatenate all selected items' string representations + final StringBuilder sb = new StringBuilder(); + for (Object item : selectedItems) { + if (item != null) { + sb.append(item.toString()).append(" "); + } + } + final String result = sb.toString().trim(); + if (!result.isEmpty()) { + return result; + } + } + } catch (Exception ignored) { + } + return null; + } + + /** + * Extracts CVE ID from the problem description. + */ + private boolean isCVEIssue(@NotNull String description) { + // Pattern: CVE-YYYY-NNNNN + final int cveIndex = description.toUpperCase().indexOf(CVE_MARKER); + if (cveIndex >= 0) { + return true; + } + return false; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java new file mode 100644 index 00000000000..41244535d3a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.codeInsight.intention.HighPriorityAction; +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInspection.util.IntentionFamilyName; +import com.intellij.codeInspection.util.IntentionName; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiFile; +import com.intellij.util.IncorrectOperationException; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.VulnerabilityInfo; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import org.jetbrains.annotations.NotNull; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; + +/** + * Intention action to fix CVE vulnerabilities in dependencies using GitHub Copilot. + * This action appears in the editor's quick-fix popup (More actions...) for pom.xml files(yellow wavy dependency with CVE issue) + * when a vulnerable dependency with CVE issues is detected. + * + * Implements HighPriorityAction to appear at the top of the quick-fix list. + */ +public class CveFixDependencyIntentionAction implements IntentionAction, HighPriorityAction { + + // Cached dependency info from isAvailable() for use in getText() + private VulnerabilityInfo vulnerabilityInfo; + + @Override + public @IntentionName @NotNull String getText() { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + return FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; + } + return FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME; + } + + @Override + public @IntentionFamilyName @NotNull String getFamilyName() { + return "Azure Toolkit"; + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + // Reset cached values + vulnerabilityInfo = null; + + if (file == null || editor == null) { + return false; + } + + // Only available for pom.xml files + final String fileName = file.getName(); + if (!fileName.equals("pom.xml")) { + return false; + } + + try { + final int offset = editor.getCaretModel().getOffset(); + final String documentText = editor.getDocument().getText(); + + // Try to extract dependency info - only show if cursor is within a block + final int dependencyStart = findDependencyStart(documentText, offset); + final int dependencyEnd = findDependencyEnd(documentText, offset); + + if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { + final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); + String cachedGroupId = extractXmlValue(dependencyBlock, "groupId"); + String cachedArtifactId = extractXmlValue(dependencyBlock, "artifactId"); + String cachedVersion = extractXmlValue(dependencyBlock, "version"); + vulnerabilityInfo = VulnerabilityInfo.builder().groupId(cachedGroupId).artifactId(cachedArtifactId).version(cachedVersion).build(); + // Only show if we have valid dependency info (not for parent/plugin sections) + if(cachedGroupId != null && cachedArtifactId != null) { + //if the artifact is in the cached cve issues, show the intention + final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(cachedGroupId + ":" + cachedArtifactId); + return issue != null; + } + } + } catch (Exception e) { + // Ignore and return false + } + + return false; + } + + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + if (file == null || editor == null) { + return; + } + + // Try to extract dependency information from the current context + final String prompt = buildPromptFromContext(editor, file); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } + + /** + * Builds a prompt based on the current editor context. + */ + private String buildPromptFromContext(@NotNull Editor editor, @NotNull PsiFile file) { + return String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, + vulnerabilityInfo.getDependencyCoordinate()); + } + + /** + * Finds the start of the dependency block containing the given offset. + */ + private int findDependencyStart(@NotNull String text, int offset) { + // Look for tag before the offset + int searchStart = Math.max(0, offset - 500); + String searchArea = text.substring(searchStart, offset); + int lastDependency = searchArea.lastIndexOf(""); + if (lastDependency >= 0) { + return searchStart + lastDependency; + } + return -1; + } + + /** + * Finds the end of the dependency block containing the given offset. + */ + private int findDependencyEnd(@NotNull String text, int offset) { + // Look for tag after the offset + int searchEnd = Math.min(text.length(), offset + 500); + String searchArea = text.substring(offset, searchEnd); + int endDependency = searchArea.indexOf(""); + if (endDependency >= 0) { + return offset + endDependency + "".length(); + } + return -1; + } + + /** + * Extracts a value from an XML tag. + */ + private String extractXmlValue(@NotNull String xml, @NotNull String tagName) { + final String startTag = "<" + tagName + ">"; + final String endTag = ""; + + int start = xml.indexOf(startTag); + if (start < 0) { + return null; + } + start += startTag.length(); + + int end = xml.indexOf(endTag, start); + if (end < 0) { + return null; + } + + return xml.substring(start, end).trim(); + } + + @Override + public boolean startInWriteAction() { + return false; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java index b22f817b71b..febf7850e28 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java @@ -5,8 +5,8 @@ package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; +import com.intellij.codeInsight.intention.HighPriorityAction; import com.intellij.codeInsight.intention.IntentionAction; -import com.intellij.codeInsight.intention.PriorityAction; import com.intellij.codeInspection.util.IntentionFamilyName; import com.intellij.codeInspection.util.IntentionName; import com.intellij.openapi.editor.Editor; @@ -26,8 +26,10 @@ * Intention action to fix CVE vulnerabilities in dependencies using GitHub Copilot. * This action appears in the editor's quick-fix popup (More actions...) for pom.xml files(yellow wavy dependency with CVE issue) * when a vulnerable dependency with CVE issues is detected. + * + * Implements HighPriorityAction to appear at the top of the quick-fix list. */ -public class CveFixIntentionAction implements IntentionAction, PriorityAction { +public class CveFixIntentionAction implements IntentionAction, HighPriorityAction { // Cached dependency info from isAvailable() for use in getText() private String cachedGroupId; @@ -160,9 +162,4 @@ private String extractXmlValue(@NotNull String xml, @NotNull String tagName) { public boolean startInWriteAction() { return false; } - - @Override - public @NotNull Priority getPriority() { - return Priority.NORMAL; - } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java index 5841a552c8b..711a28b92a0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java @@ -12,7 +12,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.PACKAGE_ID_JDK; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.UPGRADE_JAVA_FRAMEWORK_PROMPT; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -54,19 +54,9 @@ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descri } private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { - String packageId = issue.getPackageId(); - // JDK upgrade - if (PACKAGE_ID_JDK.equals(packageId)) { - return String.format( - "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", - issue.getCurrentVersion(), issue.getSuggestedVersion() - ); - } - - // Framework upgrade (Spring Boot, Spring Framework, etc.) return String.format( - "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", + UPGRADE_JAVA_FRAMEWORK_PROMPT, issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() ); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java index 8224dc19887..b4ddceedd89 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java @@ -121,13 +121,16 @@ public enum Severity { */ @Nullable private String cveId; + + @Nullable + private String eofDate; /** * Gets a formatted title for the notification. */ public String getTitle() { return switch (upgradeReason) { - case JRE_TOO_OLD -> "Outdated JDK Version Detected"; + case JRE_TOO_OLD -> "Outdated JDK Detected"; case END_OF_LIFE -> packageDisplayName + " End of Life"; case DEPRECATED -> packageDisplayName + " Deprecated"; case CVE -> "Security Vulnerability in " + packageDisplayName; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java new file mode 100644 index 00000000000..0910b62f7a5 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java @@ -0,0 +1,88 @@ +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao; + + +import lombok.Builder; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents a parsed vulnerability with dependency information and CVE IDs. + */ +@Data +@Builder +public class VulnerabilityInfo { + // Pattern to match Maven dependency: maven:groupId:artifactId:version + private static final Pattern MAVEN_DEPENDENCY_PATTERN = + Pattern.compile("maven:([^:]+):([^:]+):([^\\s]+)"); + + // Pattern to match CVE IDs: CVE-YYYY-NNNNN (year and number) + private static final Pattern CVE_PATTERN = + Pattern.compile("CVE-\\d{4}-\\d{4,}"); + @NotNull + private String groupId; + @NotNull + private String artifactId; + @Nullable + private String version; + @Nullable + private List cveIds; + + /** + * Returns the full Maven coordinate in format groupId:artifactId:version + */ + public String getDependencyCoordinate() { + if (version == null){ + return groupId + ":" + artifactId; + } + return groupId + ":" + artifactId + ":" + version; + } + + @Override + public String toString() { + return "VulnerabilityInfo{" + + "dependency='" + getDependencyCoordinate() + '\'' + + ", cveIds=" + cveIds + + '}'; + } + + + /** + * Parses a vulnerability description to extract the dependency coordinate and CVE IDs. + *

+ * Expected format: "Provides transitive vulnerable dependency maven:groupId:artifactId:version CVE-YYYY-NNNNN score ..." + * + * @param description The vulnerability description from the Problems View + * @return VulnerabilityInfo containing the parsed dependency and CVE IDs, or null if parsing fails + */ + @Nullable + public static VulnerabilityInfo parseVulnerabilityDescription(@Nullable String description) { + if (description == null || description.isEmpty()) { + return null; + } + + // Extract Maven dependency coordinate + final Matcher dependencyMatcher = MAVEN_DEPENDENCY_PATTERN.matcher(description); + if (!dependencyMatcher.find()) { + return null; + } + + final String groupId = dependencyMatcher.group(1); + final String artifactId = dependencyMatcher.group(2); + final String version = dependencyMatcher.group(3); + + // Extract all CVE IDs + final List cveIds = new ArrayList<>(); + final Matcher cveMatcher = CVE_PATTERN.matcher(description.toUpperCase()); + while (cveMatcher.find()) { + cveIds.add(cveMatcher.group()); + } + + return new VulnerabilityInfo(groupId, artifactId, version, cveIds); + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java index 999dd32d36c..8725a9b5812 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java @@ -95,7 +95,7 @@ private void registerProblem(@NotNull ProblemsHolder holder, @NotNull XmlTag tag holder.registerProblem( tag, issue.getMessage(), - ProblemHighlightType.WARNING, + ProblemHighlightType.WEAK_WARNING, new JavaUpgradeQuickFix(issue) ); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java index 9389d20d734..80372993f6b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -24,8 +24,12 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; import java.util.*; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.ISSUE_DISPLAY_NAME; + /** * Service to detect JDK version and framework dependency versions in Java projects. * This service analyzes the project to identify outdated versions that may need upgrading. @@ -52,6 +56,7 @@ public class JavaUpgradeIssuesDetectionService { // Package ID constants (used for cache lookups) public static final String PACKAGE_ID_JDK = "jdk"; + public static final String JDK_DISPLAY_NAME = "JDK"; /** * Metadata for dependencies to scan. @@ -190,6 +195,12 @@ public String getEolDateForVersion(@Nonnull String version) { private static JavaUpgradeIssuesDetectionService instance; + /** Formatter for parsing EOL dates in "yyyy-MM" format */ + private static final DateTimeFormatter EOL_DATE_PARSER = DateTimeFormatter.ofPattern("yyyy-MM"); + + /** Formatter for displaying EOL dates in "MMMM yyyy" format (e.g., "June 2020") */ + private static final DateTimeFormatter EOL_DATE_DISPLAY = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH); + private JavaUpgradeIssuesDetectionService() { } @@ -200,6 +211,24 @@ public static synchronized JavaUpgradeIssuesDetectionService getInstance() { return instance; } + /** + * Formats an EOL date from "yyyy-MM" format to "Month yyyy" format. + * For example, "2020-06" becomes "June 2020". + * + * @param eofDate The EOL date string in "yyyy-MM" format (e.g., "2020-06") + * @return The formatted date string (e.g., "June 2020"), or the original string if parsing fails + */ + @Nonnull + public static String formatEofDate(@Nonnull String eofDate) { + try { + YearMonth yearMonth = YearMonth.parse(eofDate, EOL_DATE_PARSER); + return yearMonth.format(EOL_DATE_DISPLAY); + } catch (Exception e) { + // If parsing fails, return the original string + return eofDate; + } + } + /** * Gets JDK/JRE version issues. * Aligned with getJavaIssues() from assessmentManager.ts. @@ -222,15 +251,14 @@ public List getJavaIssues(@Nonnull Project project) { // Check against MATURE_JAVA_LTS_VERSION (21) if (jdkVersion < MATURE_JAVA_LTS_VERSION) { issues.add(JavaUpgradeIssue.builder() - .packageId("jdk") - .packageDisplayName("JDK") + .packageId(PACKAGE_ID_JDK) + .packageDisplayName(JDK_DISPLAY_NAME) .upgradeReason(JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) .severity(JavaUpgradeIssue.Severity.WARNING) .currentVersion(String.valueOf(jdkVersion)) .supportedVersion(">=" + MATURE_JAVA_LTS_VERSION) .suggestedVersion(String.valueOf(MATURE_JAVA_LTS_VERSION)) - .message(String.format("This project is using an older Java runtime (%d). Would you like to upgrade it to %d (LTS)?", - jdkVersion, MATURE_JAVA_LTS_VERSION)) + .message(String.format(ISSUE_DISPLAY_NAME, JDK_DISPLAY_NAME, jdkVersion, JDK_DISPLAY_NAME, MATURE_JAVA_LTS_VERSION)) .learnMoreUrl(JDK_LEARN_MORE_URL) .build()); } @@ -390,6 +418,7 @@ private JavaUpgradeIssue checkDependency(@Nonnull MavenProject mavenProject, .suggestedVersion(checkItem.suggestedVersion) .message(buildUpgradeMessage(checkItem.displayName, version, checkItem)) .learnMoreUrl(checkItem.learnMoreUrl) + .eofDate(checkItem.getEolDateForVersion(version)) .build(); } @@ -546,27 +575,10 @@ private JavaUpgradeIssue.Severity determineSeverity(@Nonnull String version, private String buildUpgradeMessage(@Nonnull String displayName, @Nonnull String currentVersion, @Nonnull DependencyCheckItem checkItem) { - String eolDateStr = getEolDateString(currentVersion, checkItem); - boolean isEol = isVersionEndOfLife(currentVersion, checkItem); - if (isEol && eolDateStr != null) { - return String.format( - "This project is using %s %s, which has reached end of life in %s. " + - "Would you like to upgrade it to %s?", - displayName, currentVersion, eolDateStr, checkItem.suggestedVersion - ); - } else if (eolDateStr != null) { - return String.format( - "This project is using %s %s, which will reach end of life in %s. " + - "Would you like to upgrade it to %s?", - displayName, currentVersion, eolDateStr, checkItem.suggestedVersion - ); - } else { - return String.format( - "This project is using %s %s, which is outside the supported version range (%s). " + - "Would you like to upgrade it to %s?", - displayName, currentVersion, checkItem.supportedVersion, checkItem.suggestedVersion - ); - } + return String.format( + ISSUE_DISPLAY_NAME, + displayName, currentVersion, displayName, checkItem.suggestedVersion + ); } /** @@ -633,6 +645,7 @@ private JavaUpgradeIssue checkGradleDependency(@Nonnull ExternalProject gradlePr .suggestedVersion(checkItem.suggestedVersion) .message(buildUpgradeMessage(checkItem.displayName, version, checkItem)) .learnMoreUrl(checkItem.learnMoreUrl) + .eofDate(checkItem.getEolDateForVersion(version)) .build(); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index af1bc385e28..adebff00933 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -19,8 +19,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; -import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.installPlugin; -import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.isAppModPluginInstalled; +import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.*; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; import java.lang.reflect.Method; @@ -94,10 +93,15 @@ public void showNotifications(@Nonnull Project project, @Nonnull List"); - sb.append(issue.getMessage()); - - if (issue.getCurrentVersion() != null && issue.getSuggestedVersion() != null) { - sb.append("

"); - sb.append("Current: ").append(issue.getCurrentVersion()); - sb.append(" → Suggested: ").append(issue.getSuggestedVersion()); + if (isAppModPluginInstalled()){ + sb.append(issue.getMessage()); + } else { + sb.append(issue.getMessage() + TO_INSTALL_APP_MODE_PLUGIN); } + sb.append("."); + +// if (issue.getCurrentVersion() != null && issue.getSuggestedVersion() != null) { +// sb.append("

"); +// sb.append("Current: ").append(issue.getCurrentVersion()); +// sb.append(" → Suggested: ").append(issue.getSuggestedVersion()); +// } sb.append(""); return sb.toString(); @@ -290,24 +299,21 @@ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String AzureTaskManager.getInstance().runLater(() -> { if (!isAppModPluginInstalled()) { // showGenericUpgradeGuidance(project, prompt); - installPlugin(project); + showAppModInstallationConfirmation(project); return; } // Try direct API call first (works when plugin versions match) if (tryDirectCopilotCall(project, prompt)) { - System.out.println("Direct Copilot call succeeded."); return; // Success, no need for reflection } // Fallback to reflection for cross-version compatibility if (tryReflectionCopilotCall(project, prompt)) { - System.out.println("Reflection Copilot call succeeded."); return; // Success via reflection } // Both approaches failed - System.out.println("Both direct and reflection Copilot calls failed."); showGenericUpgradeGuidance(project, prompt); }); } @@ -328,7 +334,6 @@ private boolean tryDirectCopilotCall(@Nonnull Project project, @Nonnull String p builder.withSessionIdReceiver(sessionId -> null); return null; }); - System.out.println("Direct Copilot call succeeded."); return true; } } catch (Error | Exception e) { @@ -467,14 +472,10 @@ private Method findMethodByName(Class clazz, String methodName) { * @return The prompt string for Copilot */ private String buildUpgradePrompt(@Nonnull JavaUpgradeIssue issue) { - if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) { - return UPGRADE_JAVA_VERSION_PROMPT; - } else if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.CVE){ - return SCAN_AND_RESOLVE_CVES_PROMPT; - }else if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.DEPRECATED || issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.END_OF_LIFE) { - return UPGRADE_JAVA_FRAMEWORK_PROMPT; + if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.CVE){ + return String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, issue.getPackageId()); } - return UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; + return String.format(UPGRADE_JAVA_FRAMEWORK_PROMPT, issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion()); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index bf394c89e59..52ef298c447 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -9,9 +9,14 @@ XML - com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.CveFixIntentionAction + com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.CveFixDependencyIntentionAction Azure Toolkit + + XML + com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.CveFixIntentionAction + Azure Toolkit + + + + + org.example + vulnerable-library + 2.0.0 + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/before.xml.template b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/before.xml.template new file mode 100644 index 00000000000..b1bef2c076a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/before.xml.template @@ -0,0 +1,5 @@ + + org.example + vulnerable-library + 1.0.0 + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/description.html b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/description.html new file mode 100644 index 00000000000..9225dc45325 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/description.html @@ -0,0 +1,13 @@ + + +Fixes CVE vulnerabilities in Maven dependencies using GitHub Copilot. +

+When the cursor is positioned on a dependency in pom.xml that has known CVE issues, +this intention action opens GitHub Copilot Chat with a predefined prompt to help identify and fix +the security vulnerabilities in that specific dependency. +

+

+This action is available only for Maven pom.xml files and requires GitHub Copilot and GitHub Copilot app modernization to be installed. +

+ + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/after.xml.template b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/after.xml.template new file mode 100644 index 00000000000..47d971d72b9 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/after.xml.template @@ -0,0 +1,5 @@ + + org.example + vulnerable-library + 2.0.0 + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/before.xml.template b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/before.xml.template new file mode 100644 index 00000000000..b1bef2c076a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/before.xml.template @@ -0,0 +1,5 @@ + + org.example + vulnerable-library + 1.0.0 + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/description.html b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/description.html new file mode 100644 index 00000000000..67067b85919 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/description.html @@ -0,0 +1,13 @@ + + +Scans and resolves CVE vulnerabilities in Maven dependencies using java upgrade tools. +

+When the cursor is positioned on a dependency in pom.xml that has known CVE issues, +this intention action opens GitHub Copilot Chat with a predefined prompt to help identify and fix +the security vulnerabilities. +

+

+This action is available only for Maven pom.xml files and requires GitHub Copilot and Github Copilot app modernization to be installed. +

+ + From 6cc112a66dc401a8def135286e35b5f40092360b Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Thu, 29 Jan 2026 17:04:01 +0800 Subject: [PATCH 22/24] Add telemetry --- .../intellij/appmod/common/AppModPluginInstaller.java | 3 +++ .../action/CveFixDependencyInProblemsViewAction.java | 3 ++- .../action/CveFixDependencyIntentionAction.java | 2 ++ .../javaupgrade/action/CveFixInProblemsViewAction.java | 2 ++ .../javaupgrade/action/CveFixIntentionAction.java | 2 ++ .../action/JavaUpgradeContextMenuAction.java | 2 ++ .../appmod/javaupgrade/action/JavaUpgradeQuickFix.java | 2 ++ .../javaupgrade/action/UpgradeActionRegistrar.java | 2 ++ .../service/JavaUpgradeIssuesDetectionService.java | 3 ++- .../service/JavaVersionNotificationService.java | 10 +++++++--- .../javaupgrade/settings/JavaUpgradeConfigurable.java | 4 ++++ 11 files changed, 30 insertions(+), 5 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 011624edc29..e14725d4b16 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -110,7 +110,10 @@ public static void showAppModInstallationConfirmation(@Nonnull Project project) : "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization."; if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { + AppModUtils.logTelemetryEvent("plugin.install-upgrade-confirmed"); installPlugin(project); + } else { + AppModUtils.logTelemetryEvent("plugin.install-upgrade-cancelled"); } } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java index 5cc25fe80d9..8119be9392a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java @@ -13,6 +13,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.VulnerabilityInfo; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -56,7 +57,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { vulnerabilityInfo.getDependencyCoordinate()) ); } - + AppModUtils.logTelemetryEvent("openCopilotChatForCveFixDependencyInProblemsViewAction"); } @Override diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java index 41244535d3a..a6b638e4761 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java @@ -18,6 +18,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.NotNull; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; @@ -99,6 +100,7 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws // Try to extract dependency information from the current context final String prompt = buildPromptFromContext(editor, file); JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openCveFixDependencyCopilotChatFromIntentionAction"); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java index ee97e00cfa1..5996a3de197 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java @@ -15,6 +15,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; @@ -42,6 +43,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { project, SCAN_AND_RESOLVE_CVES_PROMPT ); + AppModUtils.logTelemetryEvent("openCopilotChatForCveFixInProblemsViewAction"); } @Override diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java index febf7850e28..7ed7215af14 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java @@ -17,6 +17,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.NotNull; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.SCAN_AND_RESOLVE_CVES_PROMPT; @@ -100,6 +101,7 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws // Try to extract dependency information from the current context final String prompt = buildPromptFromContext(editor, file); JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openCveFixCopilotChatFromIntentionAction"); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java index c398b14252f..36da8242dc4 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java @@ -13,6 +13,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.NotNull; import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; @@ -68,6 +69,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { // Open Copilot chat with the upgrade prompt JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openJavaUpgradeCopilotChatFromContextMenu"); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java index 711a28b92a0..077007d8ee0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java @@ -14,6 +14,7 @@ import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.UPGRADE_JAVA_FRAMEWORK_PROMPT; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -51,6 +52,7 @@ public String getName() { public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { String prompt = buildPromptForIssue(issue); JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openCopilotChatForJavaUpgradeQuickFix"); } private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java index 1b8ca3fbfc6..986df78c58b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java @@ -14,6 +14,7 @@ import com.intellij.openapi.startup.ProjectActivity; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import kotlin.Unit; import kotlin.coroutines.Continuation; import org.jetbrains.annotations.NotNull; @@ -88,6 +89,7 @@ private void tryAddToGroup(ActionManager actionManager, DefaultActionGroup group if (!containsAction(group, UPGRADE_ACTION_ID, actionManager)) { // Add a separator before the upgrade action to visually group it group.add(Separator.create()); + AppModUtils.logTelemetryEvent("java-upgrade.contextmenu.action.registered"); group.add(upgradeAction); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java index 80372993f6b..001321702db 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -5,9 +5,9 @@ package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import com.microsoft.azure.toolkit.intellij.common.utils.JdkUtils; import com.microsoft.intellij.util.GradleUtils; import com.microsoft.intellij.util.MavenUtils; @@ -239,6 +239,7 @@ public List getJavaIssues(@Nonnull Project project) { try { final Integer jdkVersion = JdkUtils.getJdkLanguageLevel(project); + AppModUtils.logTelemetryEvent("getJavaVersion", Map.of("jdkVersion", String.valueOf(jdkVersion))); if (jdkVersion == null) { return issues; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index adebff00933..3499b2c2c99 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -17,6 +17,7 @@ import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.*; @@ -30,6 +31,7 @@ import com.github.copilot.api.CopilotChatService; import javax.annotation.Nonnull; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -105,23 +107,25 @@ private void showNotification(@Nonnull Project project, formatMessage(issue), notificationType ); - + String issueStr = issue.toString(); if (isAppModPluginInstalled()) { // Plugin is installed - show "Upgrade" action + AppModUtils.logTelemetryEvent("showNotification.install.appmod", Map.of("javaupgrade.issue", issueStr)); notification.addAction(new NotificationAction("Upgrade") { @Override public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { openCopilotChatWithUpgradePrompt(project, issue); - notification.expire(); + // notification.expire(); } }); } else { // Plugin is not installed - show "Install and Upgrade" action + AppModUtils.logTelemetryEvent("showNotification.upgrade", Map.of("javaupgrade.issue", issueStr)); notification.addAction(new NotificationAction("Install and Upgrade") { @Override public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { showAppModInstallationConfirmation(project); - notification.expire(); + // notification.expire(); } }); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java index 392c7ccbc87..2a9cbacf37f 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java @@ -10,12 +10,14 @@ import com.intellij.openapi.util.NlsContexts; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Map; /** * Settings page for Java Upgrade feature. @@ -133,6 +135,7 @@ public void apply() throws ConfigurationException { } final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); service.setNotificationsEnabled(enableNotificationsCheckBox.isSelected()); + AppModUtils.logTelemetryEvent("applyJavaUpgradeNotificationSettings", Map.of("notificationsEnabled", String.valueOf(enableNotificationsCheckBox.isSelected()))); } @Override @@ -143,6 +146,7 @@ public void reset() { final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); enableNotificationsCheckBox.setSelected(service.isNotificationsEnabled()); updateDeferralStatusLabel(); + AppModUtils.logTelemetryEvent("resetJavaUpgradeNotificationDeferralSettings"); } @Override From 46b410deb529041215176450d11907dec3c768ae Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 29 Jan 2026 17:48:02 +0800 Subject: [PATCH 23/24] remove duplicate and unuseful code --- .../appmod/common/AppModPluginInstaller.java | 63 +++++++------------ .../javamigration/MigrateToAzureAction.java | 2 +- .../javamigration/MigrateToAzureNode.java | 2 +- .../JavaVersionNotificationService.java | 5 +- .../MigrateToAzureFacetNode.java | 2 +- 5 files changed, 29 insertions(+), 45 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 011624edc29..f195c062a3c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -11,7 +11,6 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; -import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import javax.annotation.Nonnull; @@ -80,39 +79,33 @@ public static boolean isRunningInDevMode() { * Shows a confirmation dialog for plugin installation. * * @param project The current project - * @param onConfirm Callback to execute when user confirms installation + * @param forUpgrade true for "upgrade" scenario, false for "migrate to Azure" scenario + * @param onConfirm Callback to execute when user confirms installation (if null, calls installPlugin directly) */ - public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Runnable onConfirm) { + public static void showInstallConfirmation(@Nonnull Project project, boolean forUpgrade, @Nonnull Runnable onConfirm) { final boolean copilotInstalled = isCopilotInstalled(); final String title = copilotInstalled ? "Install Github Copilot app modernization" : "Install GitHub Copilot and app modernization"; - final String message = copilotInstalled - ? "Install this plugin to automate migrating your apps to Azure with Copilot." - : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; + final String message; + if (copilotInstalled) { + message = forUpgrade + ? "Install this plugin to upgrade your apps with Copilot." + : "Install this plugin to automate migrating your apps to Azure with Copilot."; + } else { + message = forUpgrade + ? "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization." + : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; + } if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { onConfirm.run(); } } - public static void showAppModInstallationConfirmation(@Nonnull Project project) { - final boolean copilotInstalled = isCopilotInstalled(); - - final String title = copilotInstalled - ? "Install Github Copilot app modernization" - : "Install GitHub Copilot and app modernization"; - - final String message = copilotInstalled - ? "Install this plugin to upgrade your apps with Copilot." - : "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization."; - - if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { - installPlugin(project); - } - } + /** * Installs the App Modernization plugin. * IntelliJ platform will automatically install Copilot as a dependency if AppMod declares it. @@ -120,32 +113,22 @@ public static void showAppModInstallationConfirmation(@Nonnull Project project) * @param project The current project */ public static void installPlugin(@Nonnull Project project) { - final boolean appModInstalled = isAppModPluginInstalled(); - - // If already installed, nothing to do - if (appModInstalled) { + if (isAppModPluginInstalled()) { AppModUtils.logTelemetryEvent("plugin.install-skipped", Map.of("reason", "already-installed")); return; } // Only pass AppMod ID - IntelliJ will automatically install Copilot as dependency - // (AppMod's plugin.xml should declare com.github.copilot) final Set pluginsToInstall = new LinkedHashSet<>(); pluginsToInstall.add(PluginId.getId(PLUGIN_ID)); - // Use PluginsAdvertiser.installAndEnable - IntelliJ handles the rest - // The platform will show plugin selection dialog, download, install, and prompt for restart - AzureTaskManager.getInstance().runAndWait(() -> { - PluginsAdvertiser.installAndEnable( - project, - pluginsToInstall, - true, // showDialog - true, // selectAllInDialog - pre-select all plugins - null, // modalityState - () -> { - AppModUtils.logTelemetryEvent("plugin.install-complete"); - } - ); - }); + PluginsAdvertiser.installAndEnable( + project, + pluginsToInstall, + true, // showDialog + true, // selectAllInDialog - pre-select all plugins + null, // modalityState + () -> AppModUtils.logTelemetryEvent("plugin.install-complete") + ); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java index 9ff70b001a5..30b54e89fb9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java @@ -144,7 +144,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { switch (migrationState.state) { case NOT_INSTALLED: AppModUtils.logTelemetryEvent("action.click-install"); - AppModPluginInstaller.showInstallConfirmation(project, + AppModPluginInstaller.showInstallConfirmation(project, false, () -> AppModPluginInstaller.installPlugin(project)); break; case NO_OPTIONS: diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java index 41eae1252bc..6b917bf9ea8 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java @@ -93,7 +93,7 @@ private void showNotInstalled() { onClicked(e -> { AppModUtils.logTelemetryEvent("node.click-install"); - AppModPluginInstaller.showInstallConfirmation(project, () -> AppModPluginInstaller.installPlugin(project)); + AppModPluginInstaller.showInstallConfirmation(project, false, () -> AppModPluginInstaller.installPlugin(project)); }); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index adebff00933..8b3e5d6a07d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -16,6 +16,7 @@ import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; @@ -120,7 +121,7 @@ public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification noti notification.addAction(new NotificationAction("Install and Upgrade") { @Override public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { - showAppModInstallationConfirmation(project); + AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project)); notification.expire(); } }); @@ -299,7 +300,7 @@ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String AzureTaskManager.getInstance().runLater(() -> { if (!isAppModPluginInstalled()) { // showGenericUpgradeGuidance(project, prompt); - showAppModInstallationConfirmation(project); + AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project)); return; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 583ebd14a94..dbe18f444bf 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -142,7 +142,7 @@ public void navigate(boolean requestFocus) { if (!AppModPluginInstaller.isAppModPluginInstalled()) { // Plugin not installed - trigger install on double-click AppModUtils.logTelemetryEvent("facet.click-install"); - AppModPluginInstaller.showInstallConfirmation(getProject(), + AppModPluginInstaller.showInstallConfirmation(getProject(), false, () -> AppModPluginInstaller.installPlugin(getProject())); } else if (!hasMigrationOptions()) { // No migration options - open App Modernization Panel From def8daca3fb7eb58d14fc876276a4d657d462070 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 29 Jan 2026 18:02:40 +0800 Subject: [PATCH 24/24] text update --- .../appmod/common/AppModPluginInstaller.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index e0975285f58..ced3a618228 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -84,14 +84,21 @@ public static boolean isRunningInDevMode() { */ public static void showInstallConfirmation(@Nonnull Project project, boolean forUpgrade, @Nonnull Runnable onConfirm) { final boolean copilotInstalled = isCopilotInstalled(); - - final String title = copilotInstalled - ? "Install Github Copilot app modernization" - : "Install GitHub Copilot and app modernization"; - - final String message = copilotInstalled - ? "Install this plugin to automate migrating your apps to Azure with Copilot." - : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; + + final String title = copilotInstalled + ? "Install Github Copilot app modernization" + : "Install GitHub Copilot and app modernization"; + + final String message; + if (copilotInstalled) { + message = forUpgrade + ? "Install this plugin to upgrade your apps with Copilot." + : "Install this plugin to automate migrating your apps to Azure with Copilot."; + } else { + message = forUpgrade + ? "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization." + : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; + } final String action = forUpgrade ? "upgrade" : "migration";