diff --git a/package.json b/package.json index df286b18..6003c338 100644 --- a/package.json +++ b/package.json @@ -288,6 +288,16 @@ "command": "java.view.package.renameFile", "title": "%contributes.commands.java.view.package.renameFile%", "category": "Java" + }, + { + "command": "_java.view.modernizeJavaProject", + "title": "%contributes.commands.java.view.modernizeJavaProject%", + "category": "Java" + }, + { + "command": "_java.upgradeWithCopilot", + "title": "%contributes.commands.java.upgradeWithCopilot%", + "category": "Java" } ], "configuration": { @@ -323,6 +333,11 @@ "description": "%configuration.java.dependency.packagePresentation%", "default": "flat" }, + "java.dependency.enableDependencyCheckup": { + "type": "boolean", + "description": "%configuration.java.dependency.enableDependencyCheckup%", + "default": true + }, "java.project.exportJar.targetPath": { "type": "string", "anyOf": [ @@ -550,6 +565,14 @@ { "command": "_java.project.create.from.javaprojectexplorer", "when": "false" + }, + { + "command": "_java.view.modernizeJavaProject", + "when": "false" + }, + { + "command": "_java.upgradeWithCopilot", + "when": "false" } ], "explorer/context": [ @@ -568,6 +591,11 @@ "when": "explorerResourceIsFolder", "group": "1_javaactions@30" }, + { + "command": "_java.view.modernizeJavaProject", + "when": "explorerResourceIsFolder && java:serverMode && isModernizationExtensionInstalled", + "group": "1_javaactions@40" + }, { "command": "java.view.package.revealInProjectExplorer", "when": "resourceFilename =~ /(.*\\.gradle)|(.*\\.gradle\\.kts)|(pom\\.xml)$/ && java:serverMode == Standard", @@ -1092,4 +1120,4 @@ "vscode-extension-telemetry-wrapper": "^0.14.0", "vscode-tas-client": "^0.1.75" } -} +} \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index 8feb8343..95052e18 100644 --- a/package.nls.json +++ b/package.nls.json @@ -24,6 +24,7 @@ "contributes.commands.java.view.package.copyRelativeFilePath": "Copy Relative Path", "contributes.commands.java.view.package.new": "New...", "contributes.commands.java.view.package.newJavaClass": "Class...", + "contributes.commands.java.view.modernizeJavaProject": "Modernize Java project", "contributes.commands.java.view.package.newJavaInterface": "Interface...", "contributes.commands.java.view.package.newJavaEnum": "Enum...", "contributes.commands.java.view.package.newJavaRecord": "Record...", @@ -38,11 +39,13 @@ "contributes.commands.java.view.fileExplorer.newPackage": "New Java Package...", "contributes.submenus.javaProject.new": "New", "contributes.commands.java.view.menus.file.newJavaClass": "New Java File", + "contributes.commands.java.upgradeWithCopilot": "Upgrade dependencies", "configuration.java.dependency.showMembers": "Show the members in the explorer", "configuration.java.dependency.syncWithFolderExplorer": "Link Java Projects Explorer with the active editor", "configuration.java.dependency.autoRefresh": "Synchronize Java Projects explorer with changes", "configuration.java.dependency.refreshDelay": "The delay time (ms) the auto refresh is invoked when changes are detected", "configuration.java.dependency.packagePresentation": "Package presentation mode: flat or hierarchical", + "configuration.java.dependency.enableDependencyCheckup": "Show reminders when your Java runtimes or dependencies need an upgrade.", "configuration.java.project.explorer.showNonJavaResources": "When enabled, the explorer shows non-Java resources.", "configuration.java.project.exportJar.targetPath.customization": "The output path of the exported jar. Leave it empty if you want to manually pick the output location.", "configuration.java.project.exportJar.targetPath.workspaceFolder": "Export the jar file into the workspace folder. Its name is the same as the folder's.", diff --git a/src/commands.ts b/src/commands.ts index a2564835..f3887ef7 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -42,6 +42,8 @@ export namespace Commands { export const VIEW_PACKAGE_NEW_JAVA_CLASS = "java.view.package.newJavaClass"; + export const VIEW_MODERNIZE_JAVA_PROJECT = "_java.view.modernizeJavaProject"; + export const VIEW_PACKAGE_NEW_JAVA_INTERFACE = "java.view.package.newJavaInterface"; export const VIEW_PACKAGE_NEW_JAVA_ENUM = "java.view.package.newJavaEnum"; @@ -132,6 +134,8 @@ export namespace Commands { export const JAVA_PROJECT_CHECK_IMPORT_STATUS = "java.project.checkImportStatus"; + export const JAVA_UPGRADE_WITH_COPILOT = "_java.upgradeWithCopilot"; + /** * Commands from Visual Studio Code */ @@ -156,6 +160,11 @@ export namespace Commands { export const BUILD_PROJECT = "java.project.build"; + /** + * Commands from Java Upgrade Tool + */ + export const GOTO_AGENT_MODE = "javaupgrade.gotoAgentMode"; + /** * Get the project settings */ diff --git a/src/constants.ts b/src/constants.ts index df01101c..659e67ca 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -33,6 +33,12 @@ export namespace Explorer { export namespace ExtensionName { export const JAVA_LANGUAGE_SUPPORT: string = "redhat.java"; + export const APP_MODERNIZATION_FOR_JAVA = "vscjava.migrate-java-to-azure"; + export const APP_MODERNIZATION_UPGRADE_FOR_JAVA = "vscjava.vscode-java-upgrade"; +} + +export namespace Upgrade { + export const PACKAGE_ID_FOR_JAVA_RUNTIME = "java:*"; } /** diff --git a/src/extension.ts b/src/extension.ts index 531b5ac3..d2fe424b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,9 +20,11 @@ import { DiagnosticProvider } from "./tasks/buildArtifact/migration/DiagnosticPr import { setContextForDeprecatedTasks, updateExportTaskType } from "./tasks/buildArtifact/migration/utils"; import { CodeActionProvider } from "./tasks/buildArtifact/migration/CodeActionProvider"; import { newJavaFile } from "./explorerCommands/new"; +import upgradeManager from "./upgrade/upgradeManager"; export async function activate(context: ExtensionContext): Promise { contextManager.initialize(context); + upgradeManager.initialize(context); await initializeFromJsonFile(context.asAbsolutePath("./package.json")); await initExpService(context); await instrumentOperation("activation", activateExtension)(context); diff --git a/src/settings.ts b/src/settings.ts index bea8e7c2..60bde619 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -108,6 +108,10 @@ export class Settings { return workspace.getConfiguration("java.dependency").get("refreshDelay", 2000); } + public static getEnableDependencyCheckup() { + return workspace.getConfiguration("java.dependency").get("enableDependencyCheckup", true); + } + public static getExportJarTargetPath(): string { // tslint:disable-next-line: no-invalid-template-strings return workspace.getConfiguration("java.project.exportJar").get("targetPath", "${workspaceFolder}/${workspaceFolderBasename}.jar"); diff --git a/src/syncHandler.ts b/src/syncHandler.ts index d6d247c0..40315fa0 100644 --- a/src/syncHandler.ts +++ b/src/syncHandler.ts @@ -13,6 +13,7 @@ import { DataNode } from "./views/dataNode"; import { ExplorerNode } from "./views/explorerNode"; import { explorerNodeCache } from "./views/nodeCache/explorerNodeCache"; import { Jdtls } from "./java/jdtls"; +import upgradeManager from "./upgrade/upgradeManager"; const ENABLE_AUTO_REFRESH: string = "java.view.package.enableAutoRefresh"; const DISABLE_AUTO_REFRESH: string = "java.view.package.disableAutoRefresh"; @@ -46,6 +47,7 @@ class SyncHandler implements Disposable { this.disposables.push(workspace.onDidChangeWorkspaceFolders(() => { this.refresh(); + upgradeManager.scan(); })); try { diff --git a/src/upgrade/assessmentManager.ts b/src/upgrade/assessmentManager.ts new file mode 100644 index 00000000..1a361460 --- /dev/null +++ b/src/upgrade/assessmentManager.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as semver from 'semver'; +import { Jdtls } from "../java/jdtls"; +import { NodeKind, type INodeData } from "../java/nodeData"; +import { type DependencyCheckItem, UpgradeReason, type UpgradeIssue } from "./type"; +import { DEPENDENCY_JAVA_RUNTIME } from "./dependency.metadata"; +import { Upgrade } from '../constants'; +import { buildPackageId } from './utility'; +import metadataManager from './metadataManager'; +import { sendInfo } from 'vscode-extension-telemetry-wrapper'; + +function getJavaIssues(data: INodeData): UpgradeIssue[] { + const javaVersion = data.metaData?.MaxSourceVersion as number | undefined; + const { name, supportedVersion } = DEPENDENCY_JAVA_RUNTIME; + if (!javaVersion) { + return []; + } + const currentSemVer = semver.coerce(javaVersion); + if (currentSemVer && !semver.satisfies(currentSemVer, supportedVersion)) { + return [{ + ...DEPENDENCY_JAVA_RUNTIME, + packageId: Upgrade.PACKAGE_ID_FOR_JAVA_RUNTIME, + packageDisplayName: name, + currentVersion: String(javaVersion), + }]; + } + + return []; +} + +function getUpgradeForDependency(versionString: string, supportedVersionDefinition: DependencyCheckItem, packageId: string): UpgradeIssue | null { + const { reason } = supportedVersionDefinition; + switch (reason) { + case UpgradeReason.DEPRECATED: { + return { + ...supportedVersionDefinition, + packageDisplayName: supportedVersionDefinition.name, + reason, + currentVersion: versionString, + packageId, + }; + } + case UpgradeReason.END_OF_LIFE: { + const currentSemVer = semver.coerce(versionString); + if (currentSemVer && !semver.satisfies(currentSemVer, supportedVersionDefinition.supportedVersion)) { + return { + ...supportedVersionDefinition, + packageDisplayName: supportedVersionDefinition.name, + reason, + currentVersion: versionString, + packageId, + }; + } + } + } + + return null; +} + +function getDependencyIssue(data: INodeData): UpgradeIssue | null { + const versionString = data.metaData?.["maven.version"]; + const groupId = data.metaData?.["maven.groupId"]; + const artifactId = data.metaData?.["maven.artifactId"]; + const packageId = buildPackageId(groupId, artifactId); + const supportedVersionDefinition = metadataManager.getMetadataById(packageId); + if (!versionString || !groupId || !supportedVersionDefinition) { + return null; + } + + return getUpgradeForDependency(versionString, supportedVersionDefinition, packageId); +} + +async function getDependencyIssues(projectNode: INodeData): Promise { + const projectStructureData = await Jdtls.getPackageData({ kind: NodeKind.Project, projectUri: projectNode.uri }); + const packageContainerIssues = await Promise.allSettled( + projectStructureData + .filter(x => x.kind === NodeKind.Container) + .map(async (packageContainer) => { + const packages = await Jdtls.getPackageData({ + kind: NodeKind.Container, + projectUri: projectNode.uri, + path: packageContainer.path, + }); + + return packages.map(getDependencyIssue).filter((x): x is UpgradeIssue => Boolean(x)); + }) + ); + + return packageContainerIssues + .map(x => { + if (x.status === "fulfilled") { + return x.value; + } + + sendInfo("", { + operationName: "java.dependency.assessmentManager.getDependencyIssues", + }); + return []; + }) + .reduce((a, b) => [...a, ...b]); +} + +async function getProjectIssues(projectNode: INodeData): Promise { + const issues: UpgradeIssue[] = []; + issues.push(...getJavaIssues(projectNode)); + issues.push(...(await getDependencyIssues(projectNode))); + return issues; +} + +async function getWorkspaceIssues(workspaceFolderUri: string): Promise { + const projects = await Jdtls.getProjects(workspaceFolderUri); + const projectsIssues = await Promise.allSettled(projects.map(async (projectNode) => { + const issues = await getProjectIssues(projectNode); + sendInfo("", { + operationName: "java.dependency.assessmentManager.getWorkspaceIssues", + issuesFoundForPackageId: JSON.stringify(issues.map(x => `${x.packageId}:${x.currentVersion}`)), + }); + return issues; + })); + + const workspaceIssues = projectsIssues.map(x => { + if (x.status === "fulfilled") { + return x.value; + } + + sendInfo("", { + operationName: "java.dependency.assessmentManager.getWorkspaceIssues", + }); + return []; + }).reduce((a, b) => [...a, ...b]); + + return workspaceIssues; +} + +export default { + getWorkspaceIssues, +}; \ No newline at end of file diff --git a/src/upgrade/dependency.metadata.ts b/src/upgrade/dependency.metadata.ts new file mode 100644 index 00000000..8b31b04f --- /dev/null +++ b/src/upgrade/dependency.metadata.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { Upgrade } from "../constants"; +import { UpgradeReason, type DependencyCheckMetadata } from "./type"; + +const LATEST_JAVA_LTS_VESRION = 21; + +export const DEPENDENCY_JAVA_RUNTIME = { + "name": "Java Runtime", + "reason": UpgradeReason.JRE_TOO_OLD, + "supportedVersion": `>=${LATEST_JAVA_LTS_VESRION}`, + "suggestedVersion": { + "name": String(LATEST_JAVA_LTS_VESRION), + "description": "latest LTS version", + }, +} as const; + +const DEPENDENCIES_TO_SCAN: DependencyCheckMetadata = { + "org.springframework.boot:*": { + "reason": UpgradeReason.END_OF_LIFE, + "name": "Spring Boot", + "supportedVersion": "2.7.x || >=3.2.x", + "eolDate": { + "4.0.x": "2027-12", + "3.5.x": "2032-06", + "3.4.x": "2026-12", + "3.3.x": "2026-06", + "3.2.x": "2025-12", + "3.1.x": "2025-06", + "3.0.x": "2024-12", + "2.7.x": "2029-06", + "2.6.x": "2024-02", + "2.5.x": "2023-08", + "2.4.x": "2023-02", + "2.3.x": "2022-08", + "2.2.x": "2022-01", + "2.1.x": "2021-01", + "2.0.x": "2020-06", + "1.5.x": "2020-11", + }, + "suggestedVersion": { + "name": "3.5", + "description": "latest stable release", + }, + }, + "org.springframework:*": { + "reason": UpgradeReason.END_OF_LIFE, + "name": "Spring Framework", + "supportedVersion": "5.3.x || >=6.2.x", + "eolDate": { + "7.0.x": "2028-06", + "6.2.x": "2032-06", + "6.1.x": "2026-06", + "6.0.x": "2025-08", + "5.3.x": "2029-06", + "5.2.x": "2023-12", + "5.1.x": "2022-12", + "5.0.x": "2022-12", + "4.3.x": "2020-12", + }, + "suggestedVersion": { + "name": "3.5", + "description": "latest stable release", + }, + }, + "org.springframework.security:*": { + "reason": UpgradeReason.END_OF_LIFE, + "name": "Spring Security", + "supportedVersion": "5.7.x || 5.8.x || >=6.2.x", + "eolDate": { + "7.0.x": "2027-12", + "6.5.x": "2032-06", + "6.4.x": "2026-12", + "6.3.x": "2026-06", + "6.2.x": "2025-12", + "6.1.x": "2025-06", + "6.0.x": "2024-12", + "5.8.x": "2029-06", + "5.7.x": "2029-06", + "5.6.x": "2024-02", + "5.5.x": "2023-08", + "5.4.x": "2023-02", + "5.3.x": "2022-08", + "5.2.x": "2022-01", + "5.1.x": "2021-01", + "5.0.x": "2020-06", + "4.2.x": "2020-11", + }, + "suggestedVersion": { + "name": "3.5", + "description": "latest stable release", + }, + }, + "javax:*": { + "reason": UpgradeReason.DEPRECATED, + "name": "Java EE", + "suggestedVersion": { + "name": "Jakarta EE 10", + "description": "latest release with wide Java runtime version support", + + }, + }, + [Upgrade.PACKAGE_ID_FOR_JAVA_RUNTIME]: DEPENDENCY_JAVA_RUNTIME, +}; + +export default DEPENDENCIES_TO_SCAN; \ No newline at end of file diff --git a/src/upgrade/display/notificationManager.ts b/src/upgrade/display/notificationManager.ts new file mode 100644 index 00000000..1b6c98d0 --- /dev/null +++ b/src/upgrade/display/notificationManager.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { commands, ExtensionContext, window } from "vscode"; +import type { IUpgradeIssuesRenderer, UpgradeIssue } from "../type"; +import { buildFixPrompt, buildNotificationMessage } from "../utility"; +import { Commands } from "../../commands"; +import { Settings } from "../../settings"; +import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper"; + +const KEY_PREFIX = 'javaupgrade.notificationManager'; +const NEXT_SHOW_TS_KEY = `${KEY_PREFIX}.nextShowTs`; + +const BUTTON_TEXT_UPGRADE = "Upgrade Now"; +const BUTTON_TEXT_NOT_NOW = "Not Now"; + +const SECONDS_IN_A_DAY = 24 * 60 * 60; +const SECONDS_COUNT_BEFORE_NOTIFICATION_RESHOW = 10 * SECONDS_IN_A_DAY; + +function getNowTs() { + return Number(new Date()) / 1000; +} + +class NotificationManager implements IUpgradeIssuesRenderer { + private hasShown = false; + private context?: ExtensionContext; + + initialize(context: ExtensionContext) { + this.context = context; + } + + async render(issues: UpgradeIssue[]) { + return (instrumentOperation( + "java.dependency.showUpgradeNotification", + async (operationId: string) => { + if (issues.length === 0) { + return; + } + const issue = issues[0]; + + if (!this.shouldShow()) { + return; + } + + if (this.hasShown) { + return; + } + this.hasShown = true; + + const prompt = buildFixPrompt(issue); + const notificationMessage = buildNotificationMessage(issue); + const selection = await window.showInformationMessage( + notificationMessage, + BUTTON_TEXT_UPGRADE, + BUTTON_TEXT_NOT_NOW); + sendInfo(operationId, { + operationName: "java.dependency.upgradeNotification.runUpgrade", + choice: selection ?? "", + }); + + switch (selection) { + case BUTTON_TEXT_UPGRADE: { + commands.executeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, prompt); + break; + } + case BUTTON_TEXT_NOT_NOW: { + this.setNextShowTs(getNowTs() + SECONDS_COUNT_BEFORE_NOTIFICATION_RESHOW); + break; + } + } + } + ))(); + } + + private shouldShow() { + return Settings.getEnableDependencyCheckup() + && ((this.getNextShowTs() ?? 0) <= getNowTs()); + } + + private getNextShowTs() { + return this.context?.globalState.get(NEXT_SHOW_TS_KEY); + } + + private setNextShowTs(num: number) { + return this.context?.globalState.update(NEXT_SHOW_TS_KEY, num); + } +} + +const notificationManager = new NotificationManager(); +export default notificationManager; \ No newline at end of file diff --git a/src/upgrade/metadataManager.ts b/src/upgrade/metadataManager.ts new file mode 100644 index 00000000..f9f79dfd --- /dev/null +++ b/src/upgrade/metadataManager.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { type DependencyCheckMetadata, type DependencyCheckItem } from "./type"; +import { buildPackageId } from "./utility"; +import DEPENDENCIES_TO_SCAN from "./dependency.metadata"; + +class MetadataManager { + private static dependencyCheckMetadata: DependencyCheckMetadata = DEPENDENCIES_TO_SCAN; + + public static getMetadataById(givenPackageId: string): DependencyCheckItem | undefined { + const splits = givenPackageId.split(":", 2); + const groupId = splits[0]; + const artifactId = splits[1] ?? ""; + + const packageId = buildPackageId(groupId, artifactId); + const packageIdWithWildcardArtifactId = buildPackageId(groupId, "*"); + return this.getMetadata(packageId) ?? this.getMetadata(packageIdWithWildcardArtifactId); + } + + private static getMetadata(packageRuleUsed: string) { + return this.dependencyCheckMetadata[packageRuleUsed] ? { + ...this.dependencyCheckMetadata[packageRuleUsed], packageRuleUsed + } : undefined; + } +} + +export default MetadataManager; \ No newline at end of file diff --git a/src/upgrade/type.ts b/src/upgrade/type.ts new file mode 100644 index 00000000..82105fb0 --- /dev/null +++ b/src/upgrade/type.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +export type UpgradeTarget = { name: string; description: string }; +export type DependencyCheckItemBase = { name: string, reason: UpgradeReason, suggestedVersion: UpgradeTarget }; +export type DependencyCheckItemEol = DependencyCheckItemBase & { + reason: UpgradeReason.END_OF_LIFE, + supportedVersion: string, + eolDate: Record +}; +export type DependencyCheckItemJreTooOld = DependencyCheckItemBase & { reason: UpgradeReason.JRE_TOO_OLD }; +export type DependencyCheckItemDeprecated = DependencyCheckItemBase & { reason: UpgradeReason.DEPRECATED }; +export type DependencyCheckItem = (DependencyCheckItemEol | DependencyCheckItemJreTooOld | DependencyCheckItemDeprecated); +export type DependencyCheckMetadata = Record; + +export enum UpgradeReason { + END_OF_LIFE, + DEPRECATED, + CVE, + JRE_TOO_OLD, +} + +export type UpgradeIssue = { + packageId: string; + packageDisplayName: string; + currentVersion: string; +} & DependencyCheckItem; + +export interface IUpgradeIssuesRenderer { + render(issues: UpgradeIssue[]): void; +} \ No newline at end of file diff --git a/src/upgrade/upgradeManager.ts b/src/upgrade/upgradeManager.ts new file mode 100644 index 00000000..60e270e4 --- /dev/null +++ b/src/upgrade/upgradeManager.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { commands, type ExtensionContext, extensions, workspace, type WorkspaceFolder } from "vscode"; + +import { Jdtls } from "../java/jdtls"; +import { languageServerApiManager } from "../languageServerApi/languageServerApiManager"; +import { ExtensionName } from "../constants"; +import { instrumentOperation, instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper"; +import { Commands } from "../commands"; +import notificationManager from "./display/notificationManager"; +import { Settings } from "../settings"; +import assessmentManager from "./assessmentManager"; + +const DEFAULT_UPGRADE_PROMPT = "Upgrade Java project dependency to latest version."; + + +function shouldRunCheckup() { + return Settings.getEnableDependencyCheckup() + && !!extensions.getExtension(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA); +} + +class UpgradeManager { + public static initialize(context: ExtensionContext) { + notificationManager.initialize(context); + + // Commands to be used + context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, async (promptText?: string) => { + const promptToUse = promptText ?? DEFAULT_UPGRADE_PROMPT; + await commands.executeCommand(Commands.GOTO_AGENT_MODE, { prompt: promptToUse }); + })); + commands.executeCommand('setContext', 'isModernizationExtensionInstalled', + !!extensions.getExtension(ExtensionName.APP_MODERNIZATION_FOR_JAVA)); + context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_MODERNIZE_JAVA_PROJECT, () => { + commands.executeCommand("workbench.view.extension.azureJavaMigrationExplorer"); + })); + + UpgradeManager.scan(); + } + + public static scan() { + if (!shouldRunCheckup()) { + return; + } + workspace.workspaceFolders?.forEach((folder) => + UpgradeManager.runDependencyCheckup(folder) + ); + } + + private static async runDependencyCheckup(folder: WorkspaceFolder) { + return (instrumentOperation("java.dependency.runDependencyCheckup", + async (_operationId: string) => { + if (!await languageServerApiManager.ready()) { + return; + } + const hasJavaError: boolean = await Jdtls.checkImportStatus(); + if (hasJavaError) { + return; + } + + const uri = folder.uri.toString(); + const workspaceIssues = await assessmentManager.getWorkspaceIssues(uri); + + if (workspaceIssues.length > 0) { + // only show one issue in notifications + notificationManager.render(workspaceIssues); + } + } + ))(); + } +} + +export default UpgradeManager; \ No newline at end of file diff --git a/src/upgrade/utility.ts b/src/upgrade/utility.ts new file mode 100644 index 00000000..1735f599 --- /dev/null +++ b/src/upgrade/utility.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { Uri } from "vscode"; +import * as semver from "semver"; +import { UpgradeReason, type UpgradeIssue } from "./type"; +import { Upgrade } from "../constants"; + + +function findEolDate(currentVersion: string, eolDate: Record): string | null { + const currentVersionSemVer = semver.coerce(currentVersion); + if (!currentVersionSemVer) { + return null; + } + for (const [versionRange, date] of Object.entries(eolDate)) { + if (semver.satisfies(currentVersionSemVer, versionRange)) { + return date; + } + } + return null; +} + +export function buildNotificationMessage(issue: UpgradeIssue): string { + const { + packageId, + currentVersion, + reason, + suggestedVersion: { name: suggestedVersionName, description: suggestedVersionDescription }, + packageDisplayName + } = issue; + + + if (packageId === Upgrade.PACKAGE_ID_FOR_JAVA_RUNTIME) { + return `The current project is using an older Java runtime (${currentVersion}). Do you want to upgrade to the latest LTS version ${suggestedVersionName}?`; + } + + switch (reason) { + case UpgradeReason.END_OF_LIFE: { + const { eolDate } = issue; + const versionEolDate = findEolDate(currentVersion, eolDate); + return `The current project is using ${packageDisplayName} ${currentVersion}, which has reached end of life${versionEolDate ? ` in ${versionEolDate}` : "" + }. Do you want to upgrade to ${suggestedVersionName} (${suggestedVersionDescription})?`; + } + case UpgradeReason.DEPRECATED: + default: { + return `The current project is using ${packageDisplayName} ${currentVersion}, which has been deprecated. Do you want to upgrade to ${suggestedVersionName} (${suggestedVersionDescription})?`; + } + } +} + + +export function buildFixPrompt(issue: UpgradeIssue): string { + const { packageDisplayName, reason } = issue; + + switch (reason) { + case UpgradeReason.JRE_TOO_OLD: { + const { suggestedVersion: { name: suggestedVersionName } } = issue; + return `upgrade java runtime to the latest LTS version ${suggestedVersionName} using java upgrade tools`; + } + case UpgradeReason.END_OF_LIFE: + case UpgradeReason.DEPRECATED: { + const { suggestedVersion: { name: suggestedVersionName } } = issue; + return `upgrade ${packageDisplayName} to ${suggestedVersionName} using java upgrade tools`; + } + } +} + +export function buildPackageId(groupId: string, artifactId: string): string { + return `${groupId}:${artifactId}`; +} + +export function normalizePath(path: string): string { + return Uri.parse(path).toString(); +} +