diff --git a/java/java.lsp.server/vscode/package-lock.json b/java/java.lsp.server/vscode/package-lock.json index e2f2b8b89997..069cd9c289ed 100644 --- a/java/java.lsp.server/vscode/package-lock.json +++ b/java/java.lsp.server/vscode/package-lock.json @@ -13,7 +13,8 @@ "@vscode/webview-ui-toolkit": "^1.2.2", "jdk-utils": "^0.4.4", "jsonc-parser": "3.0.0", - "vscode-languageclient": "8.0.1" + "vscode-languageclient": "8.0.1", + "xml2js": "^0.6.2" }, "devDependencies": { "@types/glob": "^7.1.1", @@ -22,6 +23,7 @@ "@types/ps-node": "^0.1.0", "@types/vscode": "^1.76.0", "@types/vscode-webview": "^1.57.1", + "@types/xml2js": "^0.4.14", "@vscode/codicons": "0.0.29", "esbuild": "^0.16.17", "glob": "^7.1.6", @@ -463,6 +465,15 @@ "integrity": "sha512-ghW5SfuDmsGDS2A4xkvGsLwDRNc3Vj5rS6rPOyPm/IryZuf3wceZKxgYaUoW+k9f0f/CB7y2c1rRsdOWZWn0PQ==", "dev": true }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "dev": true, @@ -1372,6 +1383,11 @@ ], "license": "MIT" }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -1574,6 +1590,26 @@ "dev": true, "license": "ISC" }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "dev": true, @@ -1861,6 +1897,15 @@ "integrity": "sha512-ghW5SfuDmsGDS2A4xkvGsLwDRNc3Vj5rS6rPOyPm/IryZuf3wceZKxgYaUoW+k9f0f/CB7y2c1rRsdOWZWn0PQ==", "dev": true }, + "@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@ungap/promise-all-settled": { "version": "1.1.2", "dev": true @@ -2445,6 +2490,11 @@ "version": "5.2.1", "dev": true }, + "sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -2581,6 +2631,20 @@ "version": "1.0.2", "dev": true }, + "xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, "y18n": { "version": "5.0.8", "dev": true diff --git a/java/java.lsp.server/vscode/package.json b/java/java.lsp.server/vscode/package.json index 625521ed55c4..f82349e990f5 100644 --- a/java/java.lsp.server/vscode/package.json +++ b/java/java.lsp.server/vscode/package.json @@ -1256,6 +1256,7 @@ "@types/ps-node": "^0.1.0", "@types/vscode": "^1.76.0", "@types/vscode-webview": "^1.57.1", + "@types/xml2js": "^0.4.14", "@vscode/codicons": "0.0.29", "esbuild": "^0.16.17", "glob": "^7.1.6", @@ -1269,7 +1270,8 @@ "@vscode/webview-ui-toolkit": "^1.2.2", "jdk-utils": "^0.4.4", "jsonc-parser": "3.0.0", - "vscode-languageclient": "8.0.1" + "vscode-languageclient": "8.0.1", + "xml2js": "^0.6.2" }, "__metadata": { "id": "66c7d7dc-934c-499b-94af-5375e8234fdd", diff --git a/java/java.lsp.server/vscode/src/extension.ts b/java/java.lsp.server/vscode/src/extension.ts index 149ae4894cbb..1852cfa9cdb1 100644 --- a/java/java.lsp.server/vscode/src/extension.ts +++ b/java/java.lsp.server/vscode/src/extension.ts @@ -62,12 +62,13 @@ import { InputStep, MultiStepInput } from './utils'; import { PropertiesView } from './propertiesView/propertiesView'; import * as configuration from './jdk/configuration'; import * as jdk from './jdk/jdk'; +import { validateJDKCompatibility } from './jdk/validation/validation'; const API_VERSION : string = "1.0"; export const COMMAND_PREFIX : string = "nbls"; const DATABASE: string = 'Database'; const listeners = new Map(); -let client: Promise; +export let client: Promise; let testAdapter: NbTestAdapter | undefined; let nbProcess : ChildProcess | null = null; let debugPort: number = -1; @@ -184,6 +185,7 @@ function findJDK(onChange: (path : string | null) => void): void { } let currentJdk = find(); + validateJDKCompatibility(currentJdk); let timeout: NodeJS.Timeout | undefined = undefined; workspace.onDidChangeConfiguration(params => { if (timeout) { diff --git a/java/java.lsp.server/vscode/src/jdk/validation/extensionUtils.ts b/java/java.lsp.server/vscode/src/jdk/validation/extensionUtils.ts new file mode 100644 index 000000000000..39d0f56ade0c --- /dev/null +++ b/java/java.lsp.server/vscode/src/jdk/validation/extensionUtils.ts @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as vscode from 'vscode'; +import { client as nblsClient } from '../../extension'; + +const RH_EXTENSION_ID = 'redhat.java'; + +export function isRHExtensionActive(): boolean { + const rh = vscode.extensions.getExtension(RH_EXTENSION_ID); + return rh ? true : false; +} + +export async function waitForNblsCommandToBeAvailable() { + await nblsClient; +} diff --git a/java/java.lsp.server/vscode/src/jdk/validation/javaUtil.ts b/java/java.lsp.server/vscode/src/jdk/validation/javaUtil.ts new file mode 100644 index 000000000000..af34a5270720 --- /dev/null +++ b/java/java.lsp.server/vscode/src/jdk/validation/javaUtil.ts @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as cp from 'child_process'; + +const JAVA_VERSION_REGEX = /version\s+"(\S+)"/; + +function findExecutable(program: string, home: string): string | undefined { + if (home) { + let executablePath = path.join(home, 'bin', program); + if (process.platform === 'win32') { + if (fs.existsSync(executablePath + '.cmd')) { + return executablePath + '.cmd'; + } + if (fs.existsSync(executablePath + '.exe')) { + return executablePath + '.exe'; + } + } else if (fs.existsSync(executablePath)) { + return executablePath; + } + } + return undefined; +} + +export function normalizeJavaVersion(version: string): string { + return version.startsWith("1.") ? version.substring(2) : version; +} + +export async function getJavaVersion(homeFolder: string): Promise { + return new Promise(resolve => { + if (homeFolder && fs.existsSync(homeFolder)) { + const executable: string | undefined = findExecutable('java', homeFolder); + if (executable) { + cp.execFile(executable, ['-version'], { encoding: 'utf8' }, (_error, _stdout, stderr) => { + if (stderr) { + let javaVersion: string | undefined; + stderr.split('\n').forEach((line: string) => { + const javaInfo: string[] | null = line.match(JAVA_VERSION_REGEX); + if (javaInfo && javaInfo.length > 1) { + javaVersion = javaInfo[1]; + } + }); + if (javaVersion) { + let majorVersion = normalizeJavaVersion(javaVersion); + let i = majorVersion.indexOf('.'); + if (i > -1) { + majorVersion = majorVersion.slice(0, i); + } + resolve(majorVersion); + return; + } + } + resolve(undefined); + }); + } + } else { + resolve(undefined); + } + }); +} \ No newline at end of file diff --git a/java/java.lsp.server/vscode/src/jdk/validation/project.ts b/java/java.lsp.server/vscode/src/jdk/validation/project.ts new file mode 100644 index 000000000000..57419e2e5eb5 --- /dev/null +++ b/java/java.lsp.server/vscode/src/jdk/validation/project.ts @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as xml2js from 'xml2js'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as os from 'os'; +import { normalizeJavaVersion } from './javaUtil'; + +const GET_PROJECT_INFO = 'nbls.project.info'; +const GRADLE_TARGET_COMPATIBILITY_REGEX = /targetCompatibility\s*=\s*(?:JavaVersion\s*\.\s*toVersion\s*\(\s*['"](\d+(\.\d+)?)['"]\s*\)|['"](\d+(\.\d+)?)['"])/ +const GRADLE_SOURCE_COMPATIBILITY_REGEX = /sourceCompatibility\s*=\s*(?:JavaVersion\s*\.\s*toVersion\s*\(\s*['"](\d+(\.\d+)?)['"]\s*\)|['"](\d+(\.\d+)?)['"])/ + +export enum BuildSystemType { + MAVEN = 'Maven', + GRADLE = 'Gradle', + UNKNOWN = 'Unknown' +} + +export async function getProjectFrom(projectUri: vscode.Uri): Promise { + const projectInfos: any[] = await vscode.commands.executeCommand(GET_PROJECT_INFO, projectUri.toString(), { recursive: true, projectStructure: true }); + if (projectInfos?.length && projectInfos[0]) { + const projectDirectory = projectInfos[0].projectDirectory.toString(); + const buildSystem: BuildSystemType = resolveBuildSystemType(projectUri, projectInfos[0].projectType); + + switch (buildSystem) { + case BuildSystemType.MAVEN: + const mavenSubprojects: Project[] = projectInfos[0].subprojects + .map((subproject: string) => new MavenProject(subproject, [])) + return new MavenProject(projectDirectory, mavenSubprojects); + case BuildSystemType.GRADLE: + const gradleSubprojects: Project[] = projectInfos[0].subprojects + .map((subproject: string) => new GradleProject(subproject, [])) + return new GradleProject(projectDirectory, gradleSubprojects); + default: + break; + } + } + return Promise.resolve(undefined); +} + +function resolveBuildSystemType(uri: vscode.Uri, projectType?: string): BuildSystemType { + if (projectType?.includes('gradle')) { + return BuildSystemType.GRADLE; + } + if (projectType?.includes('maven')) { + return BuildSystemType.MAVEN; + } + if (fs.existsSync(path.join(uri.fsPath, 'build.gradle'))) { + return BuildSystemType.GRADLE; + } + if (fs.existsSync(path.join(uri.fsPath, 'pom.xml'))) { + return BuildSystemType.MAVEN; + } + return BuildSystemType.UNKNOWN; +} + +export abstract class Project { + + readonly directory: string; + readonly subprojects: Project[]; + + constructor(directory: string, subprojects: any[]) { + this.directory = vscode.Uri.parse(directory).fsPath; + this.subprojects = subprojects; + } + + // Whether the project contains subprojects + containsSubprojects(): boolean { + return this.subprojects.length > 0; + } + + async getJavaVersion(): Promise { + if (!this.containsSubprojects()) { + return this.extractJavaVersion(); + } + + let maxJavaVersion: number | undefined; + + for (const subproject of this.subprojects) { + const projectDirectory: string = vscode.Uri.file(subproject.directory).toString(); + const subInfos: any[] = await vscode.commands.executeCommand(GET_PROJECT_INFO, projectDirectory); + if (subInfos?.length && subInfos[0]) { + const javaVersion = subproject.extractJavaVersion(); + + if (!maxJavaVersion || (javaVersion && javaVersion > maxJavaVersion)) { + maxJavaVersion = javaVersion; + } + } + } + return maxJavaVersion; + } + + // Extracts project java version + // Note: update when this feature becomes available: https://github.com/apache/netbeans/issues/7557 + protected abstract extractJavaVersion(): number | undefined +} + +export class MavenProject extends Project { + + constructor(directory: string, subprojects: any[]) { + super(directory, subprojects); + } + + extractJavaVersion(): number | undefined { + const buildscript = path.resolve(this.directory, 'pom.xml'); + let version: string | undefined; + if (fs.existsSync(buildscript)) { + const parser: xml2js.Parser = new xml2js.Parser({ async: false }); + parser.parseString(fs.readFileSync(buildscript)?.toString() || '', (err, result) => { + if (!err && result) { + const properties = result['project']?.['properties']; + if (properties?.[0]) { + const mavenCompilerTarget = properties[0]['maven.compiler.target']; + if (mavenCompilerTarget?.[0]) { + version = mavenCompilerTarget[0]; + return; + } + + const mavenCompilerSource = properties[0]['maven.compiler.source']; + if (mavenCompilerSource?.[0]) { + version = mavenCompilerSource[0]; + return; + } + + const jdkVersion = properties[0]['jdk.version']; + if (jdkVersion?.[0]) { + version = jdkVersion[0]; + return; + } + } + } + }) + } + return version ? Number(normalizeJavaVersion(version)) : undefined; + } +} + +export class GradleProject extends Project { + + constructor(directory: string, subprojects: any[]) { + super(directory, subprojects); + } + + getJavaCompatibilityFrom(buildscript: string, from: 'target' | 'source'): string | undefined { + const res = from === 'target' ? GRADLE_TARGET_COMPATIBILITY_REGEX.exec(buildscript) : GRADLE_SOURCE_COMPATIBILITY_REGEX.exec(buildscript) + if (res?.[3]) { + return res[3]; // Get the version number directly + } else if (res?.[1]) { + return res[1]; // Get the version number from JavaVersion.toVersion + } + return undefined; + } + + extractJavaVersion(): number | undefined { + let version: number | undefined; + const buildscript = path.resolve(this.directory, 'build.gradle'); + if (fs.existsSync(buildscript)) { + fs.readFileSync(buildscript)?.toString().split(os.EOL).find(l => { + let tempVersion: string | undefined = this.getJavaCompatibilityFrom(l, 'target'); + + if (!tempVersion) { + tempVersion = this.getJavaCompatibilityFrom(l, 'source'); + } + + if (tempVersion) { + version = Number(normalizeJavaVersion(tempVersion)); + return true; + } + + return false; + }); + } + return version; + } +} \ No newline at end of file diff --git a/java/java.lsp.server/vscode/src/jdk/validation/validation.ts b/java/java.lsp.server/vscode/src/jdk/validation/validation.ts new file mode 100644 index 000000000000..fbebedc1555b --- /dev/null +++ b/java/java.lsp.server/vscode/src/jdk/validation/validation.ts @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as vscode from 'vscode'; +import * as jdkUtils from 'jdk-utils'; + +import { isRHExtensionActive, waitForNblsCommandToBeAvailable } from './extensionUtils'; +import { getJavaVersion } from './javaUtil'; +import { getProjectFrom } from './project'; + +const CONFIGURE_JDK_COMMAND = 'nbls.jdk.configuration' +const CONFIGURE_JDK = 'Configure JDK'; + +export async function validateJDKCompatibility(javaPath: string | null) { + // In this case RH will try it's best to validate Java versions + if (isRHExtensionActive()) return; + + const projectJavaVersion = await getProjectJavaVersion(); + const ideJavaVersion = await parseJavaVersion(javaPath); + if (projectJavaVersion && ideJavaVersion && ideJavaVersion < projectJavaVersion) { + const value = await vscode.window.showWarningMessage(`Source level (JDK ${projectJavaVersion}) not compatible with current JDK installation (JDK ${ideJavaVersion})`, CONFIGURE_JDK); + if (value === CONFIGURE_JDK) { + vscode.commands.executeCommand(CONFIGURE_JDK_COMMAND); + } + } +} + +async function parseJavaVersion(javaPath: string | null): Promise { + if (!javaPath) return undefined; + + const javaRuntime = await jdkUtils.getRuntime(javaPath, { checkJavac: true }); + if (!javaRuntime?.hasJavac) { + return undefined; + } + const version = await getJavaVersion(javaRuntime.homedir); + return version ? Number(version) : undefined; +} + +async function getProjectJavaVersion(): Promise { + const folder = vscode.workspace.workspaceFolders?.[0]; + if (!folder) return undefined; + + await waitForNblsCommandToBeAvailable(); + + const project = await getProjectFrom(folder.uri); + const javaVersion = await project?.getJavaVersion(); + + return javaVersion; +}