diff --git a/src/liberty/devCommands.ts b/src/liberty/devCommands.ts index 01702fec..7f49ae3e 100644 --- a/src/liberty/devCommands.ts +++ b/src/liberty/devCommands.ts @@ -1,6 +1,6 @@ /* * IBM Confidential - * Copyright IBM Corp. 2020, 2024 + * Copyright IBM Corp. 2020, 2026 */ import * as fs from "fs"; import * as fse from "fs-extra"; @@ -91,20 +91,44 @@ export async function listAllCommands(): Promise { // start dev mode export async function startDevMode(libProject?: LibertyProject | undefined): Promise { if (libProject !== undefined) { - console.log(localize("starting.liberty.dev.on", libProject.getLabel())); - let terminal = libProject.getTerminal(); + // Resolve the target project (handles aggregators with multiple Liberty children) + const projectProvider: ProjectProvider = ProjectProvider.getInstance(); + const targetProject = await projectProvider.resolveCommandTarget(libProject, localize("command.start.dev.mode")); + + if (targetProject === undefined) { + // User cancelled or no valid target found + return; + } + + // Check if user clicked directly on a child (not via aggregator) + const clickedDirectlyOnChild = (libProject === targetProject) && targetProject.parent; + + console.log(localize("starting.liberty.dev.on", targetProject.getLabel())); + let terminal = targetProject.getTerminal(); if (terminal === undefined) { + // For multi-module projects, create terminal in parent aggregator directory + const terminalPath = targetProject.parent + ? Path.dirname(targetProject.parent.getPath()) + : Path.dirname(targetProject.getPath()); //function call to create new terminal for LTV - terminal = createTerminalforLiberty(libProject, terminal); + terminal = createTerminalforLiberty(targetProject, terminal, terminalPath); } if (terminal !== undefined) { terminal.show(); - libProject.setTerminal(terminal); - if (libProject.getContextValue() === LIBERTY_MAVEN_PROJECT || libProject.getContextValue() === LIBERTY_MAVEN_PROJECT_CONTAINER) { - const cmd = await getCommandForMaven(libProject.getPath(), "io.openliberty.tools:liberty-maven-plugin:dev", libProject.getTerminalType()); + targetProject.setTerminal(terminal); + if (targetProject.getContextValue() === LIBERTY_MAVEN_PROJECT || targetProject.getContextValue() === LIBERTY_MAVEN_PROJECT_CONTAINER) { + // For multi-module projects, use parent aggregator path + const pomPath = targetProject.parent ? targetProject.parent.getPath() : targetProject.getPath(); + // Use module selector (-pl) if: + // 1. User clicked directly on child module, OR + // 2. Parent has multiple Liberty children + const artifactId = (targetProject.parent && (clickedDirectlyOnChild || projectProvider.findLibertyDescendants(targetProject.parent).length > 1)) + ? targetProject.artifactId + : undefined; + const cmd = await getCommandForMaven(pomPath, "io.openliberty.tools:liberty-maven-plugin:dev", targetProject.getTerminalType(), undefined, artifactId); terminal.sendText(cmd); // start dev mode on current project - } else if (libProject.getContextValue() === LIBERTY_GRADLE_PROJECT || libProject.getContextValue() === LIBERTY_GRADLE_PROJECT_CONTAINER) { - const cmd = await getCommandForGradle(libProject.getPath(), "libertyDev", libProject.getTerminalType()); + } else if (targetProject.getContextValue() === LIBERTY_GRADLE_PROJECT || targetProject.getContextValue() === LIBERTY_GRADLE_PROJECT_CONTAINER) { + const cmd = await getCommandForGradle(targetProject.getPath(), "libertyDev", targetProject.getTerminalType()); terminal.sendText(cmd); // start dev mode on current project } } @@ -224,13 +248,22 @@ export async function addProject(uri: vscode.Uri): Promise { // stop dev mode export async function stopDevMode(libProject?: LibertyProject | undefined): Promise { if (libProject !== undefined) { - console.log(localize("stopping.liverty.dev.on", libProject.getLabel())); - const terminal = libProject.getTerminal(); + // Resolve the target project (handles aggregators with multiple Liberty children) + const projectProvider: ProjectProvider = ProjectProvider.getInstance(); + const targetProject = await projectProvider.resolveCommandTarget(libProject, localize("command.stop.dev.mode")); + + if (targetProject === undefined) { + // User cancelled or no valid target found + return; + } + + console.log(localize("stopping.liverty.dev.on", targetProject.getLabel())); + const terminal = targetProject.getTerminal(); if (terminal !== undefined) { terminal.show(); terminal.sendText("exit"); // stop dev mode on current project } else { - const message = localize("liberty.dev.not.started.on", libProject.getLabel()); + const message = localize("liberty.dev.not.started.on", targetProject.getLabel()); vscode.window.showWarningMessage(message); } } else if (ProjectProvider.getInstance()) { @@ -243,24 +276,33 @@ export async function stopDevMode(libProject?: LibertyProject | undefined): Prom } } -// stop dev mode +// attach debugger export async function attachDebugger(libProject?: LibertyProject | undefined): Promise { if (libProject !== undefined) { + // Resolve the target project (handles aggregators with multiple Liberty children) + const projectProvider: ProjectProvider = ProjectProvider.getInstance(); + const targetProject = await projectProvider.resolveCommandTarget(libProject, localize("command.attach.debugger")); + + if (targetProject === undefined) { + // User cancelled or no valid target found + return; + } + const EXCLUDED_DIR_PATTERN = "**/{bin,classes}/**"; let pathPrefix = ""; - if (libProject.getContextValue() === LIBERTY_MAVEN_PROJECT || libProject.getContextValue() === LIBERTY_MAVEN_PROJECT_CONTAINER) { + if (targetProject.getContextValue() === LIBERTY_MAVEN_PROJECT || targetProject.getContextValue() === LIBERTY_MAVEN_PROJECT_CONTAINER) { pathPrefix = "target"; - } else if (libProject.getContextValue() === LIBERTY_GRADLE_PROJECT || libProject.getContextValue() === LIBERTY_GRADLE_PROJECT_CONTAINER) { + } else if (targetProject.getContextValue() === LIBERTY_GRADLE_PROJECT || targetProject.getContextValue() === LIBERTY_GRADLE_PROJECT_CONTAINER) { pathPrefix = "build"; } let paths: string[] = []; if (pathPrefix !== "") { - const serverEnvPattern = new vscode.RelativePattern(Path.dirname(libProject.getPath()), pathPrefix + "/**/server.env"); + const serverEnvPattern = new vscode.RelativePattern(Path.dirname(targetProject.getPath()), pathPrefix + "/**/server.env"); paths = (await vscode.workspace.findFiles(serverEnvPattern, EXCLUDED_DIR_PATTERN)).map(uri => uri.fsPath); } if (paths.length === 1) { - console.log(localize("attach.debugger.liverty.dev.in", libProject.getLabel())); + console.log(localize("attach.debugger.liverty.dev.in", targetProject.getLabel())); const file = Path.resolve(paths[0]); const lines = await fse.readFileSync(file, "utf8").split("\n"); let port = ""; @@ -272,16 +314,16 @@ export async function attachDebugger(libProject?: LibertyProject | undefined): P } } if (port !== "") { - const path = Path.dirname(libProject.getPath()); - const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(libProject.getPath())); + const path = Path.dirname(targetProject.getPath()); + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(targetProject.getPath())); vscode.debug.startDebugging(workspaceFolder, { "type": "java", - "name": localize("liberty.dev.debug.label", Path.dirname(libProject.getPath())), + "name": localize("liberty.dev.debug.label", Path.dirname(targetProject.getPath())), "request": "attach", "hostName": "localhost", "port": port, "cwd": path, - "projectName": libProject.getLabel() + "projectName": targetProject.getLabel() }).then(() => { // do not show any message }, err => { @@ -355,20 +397,36 @@ export async function customDevModeWithHistory(libProject?: LibertyProject | und export async function customDevMode(libProject?: LibertyProject | undefined, params?: string | undefined): Promise { const _customParameters = (params === undefined) ? "" : params.trim(); if (libProject !== undefined) { - let terminal = libProject.getTerminal(); + // Resolve the target project (handles aggregators with multiple Liberty children) + const projectProvider: ProjectProvider = ProjectProvider.getInstance(); + const targetProject = await projectProvider.resolveCommandTarget(libProject, localize("command.start.custom.dev.mode")); + + if (targetProject === undefined) { + // User cancelled or no valid target found + return; + } + + // Check if user clicked directly on a child (not via aggregator) + const clickedDirectlyOnChild = (libProject === targetProject) && targetProject.parent; + + let terminal = targetProject.getTerminal(); if (terminal === undefined) { + // For multi-module projects, create terminal in parent aggregator directory + const terminalPath = targetProject.parent + ? Path.dirname(targetProject.parent.getPath()) + : Path.dirname(targetProject.getPath()); //function call to create new terminal for LTV - terminal = createTerminalforLiberty(libProject, terminal); + terminal = createTerminalforLiberty(targetProject, terminal, terminalPath); } if (terminal !== undefined) { terminal.show(); - libProject.setTerminal(terminal); + targetProject.setTerminal(terminal); let placeHolderStr = ""; let promptString = localize("specify.custom.parms.maven"); - if (libProject.getContextValue() === LIBERTY_MAVEN_PROJECT || libProject.getContextValue() === LIBERTY_MAVEN_PROJECT_CONTAINER) { + if (targetProject.getContextValue() === LIBERTY_MAVEN_PROJECT || targetProject.getContextValue() === LIBERTY_MAVEN_PROJECT_CONTAINER) { placeHolderStr = "e.g. -DhotTests=true"; - } else if (libProject.getContextValue() === LIBERTY_GRADLE_PROJECT || libProject.getContextValue() === LIBERTY_GRADLE_PROJECT_CONTAINER) { + } else if (targetProject.getContextValue() === LIBERTY_GRADLE_PROJECT || targetProject.getContextValue() === LIBERTY_GRADLE_PROJECT_CONTAINER) { placeHolderStr = "e.g. --hotTests"; promptString = localize("specify.custom.parms.gradle"); } @@ -396,18 +454,25 @@ export async function customDevMode(libProject?: LibertyProject | undefined, par // save command customCommand = customCommand.trim(); if ( customCommand.length > 0 ) { - const projectStartCmdParam: ProjectStartCmdParam = new ProjectStartCmdParam(libProject.getPath(), customCommand); - const projectProvider: ProjectProvider = ProjectProvider.getInstance(); + const projectStartCmdParam: ProjectStartCmdParam = new ProjectStartCmdParam(targetProject.getPath(), customCommand); const dashboardData: DashboardData = helperUtil.getStorageData(projectProvider.getContext()); dashboardData.addStartCmdParams(projectStartCmdParam); await helperUtil.saveStorageData(projectProvider.getContext(), dashboardData); } - if (libProject.getContextValue() === LIBERTY_MAVEN_PROJECT || libProject.getContextValue() === LIBERTY_MAVEN_PROJECT_CONTAINER) { - const cmd = await getCommandForMaven(libProject.getPath(), "io.openliberty.tools:liberty-maven-plugin:dev", libProject.getTerminalType(), customCommand); + if (targetProject.getContextValue() === LIBERTY_MAVEN_PROJECT || targetProject.getContextValue() === LIBERTY_MAVEN_PROJECT_CONTAINER) { + // For multi-module projects, use parent aggregator path + const pomPath = targetProject.parent ? targetProject.parent.getPath() : targetProject.getPath(); + // Use module selector (-pl) if: + // 1. User clicked directly on child module, OR + // 2. Parent has multiple Liberty children + const artifactId = (targetProject.parent && (clickedDirectlyOnChild || projectProvider.findLibertyDescendants(targetProject.parent).length > 1)) + ? targetProject.artifactId + : undefined; + const cmd = await getCommandForMaven(pomPath, "io.openliberty.tools:liberty-maven-plugin:dev", targetProject.getTerminalType(), customCommand, artifactId); terminal.sendText(cmd); - } else if (libProject.getContextValue() === LIBERTY_GRADLE_PROJECT || libProject.getContextValue() === LIBERTY_GRADLE_PROJECT_CONTAINER) { - const cmd = await getCommandForGradle(libProject.getPath(), "libertyDev", libProject.getTerminalType(), customCommand); + } else if (targetProject.getContextValue() === LIBERTY_GRADLE_PROJECT || targetProject.getContextValue() === LIBERTY_GRADLE_PROJECT_CONTAINER) { + const cmd = await getCommandForGradle(targetProject.getPath(), "libertyDev", targetProject.getTerminalType(), customCommand); terminal.sendText(cmd); } } @@ -425,19 +490,43 @@ export async function customDevMode(libProject?: LibertyProject | undefined, par // start dev mode in a container export async function startContainerDevMode(libProject?: LibertyProject | undefined): Promise { if (libProject !== undefined) { - let terminal = libProject.getTerminal(); + // Resolve the target project (handles aggregators with multiple Liberty children) + const projectProvider: ProjectProvider = ProjectProvider.getInstance(); + const targetProject = await projectProvider.resolveCommandTarget(libProject, localize("command.start.container.dev.mode")); + + if (targetProject === undefined) { + // User cancelled or no valid target found + return; + } + + // Check if user clicked directly on a child (not via aggregator) + const clickedDirectlyOnChild = (libProject === targetProject) && targetProject.parent; + + let terminal = targetProject.getTerminal(); if (terminal === undefined) { + // For multi-module projects, create terminal in parent aggregator directory + const terminalPath = targetProject.parent + ? Path.dirname(targetProject.parent.getPath()) + : Path.dirname(targetProject.getPath()); //function call to create new terminal for LTV - terminal = createTerminalforLiberty(libProject, terminal); + terminal = createTerminalforLiberty(targetProject, terminal, terminalPath); } if (terminal !== undefined) { terminal.show(); - libProject.setTerminal(terminal); - if (libProject.getContextValue() === LIBERTY_MAVEN_PROJECT_CONTAINER) { - const cmd = await getCommandForMaven(libProject.getPath(), "io.openliberty.tools:liberty-maven-plugin:devc", libProject.getTerminalType()); + targetProject.setTerminal(terminal); + if (targetProject.getContextValue() === LIBERTY_MAVEN_PROJECT_CONTAINER) { + // For multi-module projects, use parent aggregator path + const pomPath = targetProject.parent ? targetProject.parent.getPath() : targetProject.getPath(); + // Use module selector (-pl) if: + // 1. User clicked directly on child module, OR + // 2. Parent has multiple Liberty children + const artifactId = (targetProject.parent && (clickedDirectlyOnChild || projectProvider.findLibertyDescendants(targetProject.parent).length > 1)) + ? targetProject.artifactId + : undefined; + const cmd = await getCommandForMaven(pomPath, "io.openliberty.tools:liberty-maven-plugin:devc", targetProject.getTerminalType(), undefined, artifactId); terminal.sendText(cmd); - } else if (libProject.getContextValue() === LIBERTY_GRADLE_PROJECT_CONTAINER) { - const cmd = await getCommandForGradle(libProject.getPath(), "libertyDevc", libProject.getTerminalType()); + } else if (targetProject.getContextValue() === LIBERTY_GRADLE_PROJECT_CONTAINER) { + const cmd = await getCommandForGradle(targetProject.getPath(), "libertyDevc", targetProject.getTerminalType()); terminal.sendText(cmd); } } @@ -454,13 +543,22 @@ export async function startContainerDevMode(libProject?: LibertyProject | undefi // run tests on dev mode export async function runTests(libProject?: LibertyProject | undefined): Promise { if (libProject !== undefined) { - console.log(localize("running.liberty.dev.tests.on", libProject.getLabel())); - const terminal = libProject.getTerminal(); + // Resolve the target project (handles aggregators with multiple Liberty children) + const projectProvider: ProjectProvider = ProjectProvider.getInstance(); + const targetProject = await projectProvider.resolveCommandTarget(libProject, localize("command.run.tests")); + + if (targetProject === undefined) { + // User cancelled or no valid target found + return; + } + + console.log(localize("running.liberty.dev.tests.on", targetProject.getLabel())); + const terminal = targetProject.getTerminal(); if (terminal !== undefined) { terminal.show(); terminal.sendText(" "); // sends Enter to run tests in terminal } else { - vscode.window.showWarningMessage(localize("liberty.dev.has.not.been.started.on", libProject.getLabel())); + vscode.window.showWarningMessage(localize("liberty.dev.has.not.been.started.on", targetProject.getLabel())); } } else if (ProjectProvider.getInstance()) { showProjects("liberty.dev.run.tests", runTests); @@ -519,11 +617,14 @@ export function deleteTerminal(terminal: vscode.Terminal): void { } } /** - * function to create new terminal of default type + * function to create new terminal of default type + * @param libProject The Liberty project + * @param terminal The existing terminal (if any) + * @param terminalPath Optional path to create terminal in (defaults to project directory) */ -function createTerminalforLiberty(libProject: LibertyProject, terminal: vscode.Terminal | undefined) { - const path = Path.dirname(libProject.getPath()); - //fetch the default terminal details and store it in LibertyProject object +function createTerminalforLiberty(libProject: LibertyProject, terminal: vscode.Terminal | undefined, terminalPath?: string) { + const path = terminalPath || Path.dirname(libProject.getPath()); + //fetch the default terminal details and store it in LibertyProject object const terminalType = defaultWindowsShell(); libProject.setTerminalType(terminalType); terminal = libProject.createTerminal(path); diff --git a/src/liberty/libertyProject.ts b/src/liberty/libertyProject.ts index e54643c6..716bf1b3 100644 --- a/src/liberty/libertyProject.ts +++ b/src/liberty/libertyProject.ts @@ -28,6 +28,9 @@ export class ProjectProvider implements vscode.TreeDataProvider // Map of buildFilePath -> LibertyProject private projects: Map = new Map(); + + // Root projects for hierarchical tree view + private rootProjects: LibertyProject[] = []; private _context: vscode.ExtensionContext; @@ -253,7 +256,104 @@ export class ProjectProvider implements vscode.TreeDataProvider } public getTreeItem(element: LibertyProject): vscode.TreeItem { - return element; + // Set collapsible state based on whether project has children + const collapsibleState = (element.isAggregator && element.children.length > 0) + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None; + + // Update the element's collapsible state by creating a new TreeItem + const treeItem = new vscode.TreeItem(element.label, collapsibleState); + treeItem.tooltip = element.tooltip; + treeItem.iconPath = element.iconPath; + treeItem.contextValue = element.contextValue; + treeItem.command = element.command; + + return treeItem; + } + + /** + * Check if a project has any Liberty-enabled descendants + * @param project The project to check + * @returns true if project or any descendant is Liberty-enabled + */ + private hasLibertyDescendants(project: LibertyProject): boolean { + if (project.isLibertyEnabled) { + return true; + } + return project.children.some(child => this.hasLibertyDescendants(child)); + } + + /** + * Find all Liberty-enabled descendants of a project + * @param project The project to search + * @returns Array of Liberty-enabled descendant projects + */ + public findLibertyDescendants(project: LibertyProject): LibertyProject[] { + const descendants: LibertyProject[] = []; + + // Only search children - don't include the project itself + // This ensures aggregators aren't included in their own descendant list + for (const child of project.children) { + // Add the child if it's Liberty-enabled + if (child.isLibertyEnabled) { + descendants.push(child); + } + // Recursively search the child's descendants + descendants.push(...this.findLibertyDescendants(child)); + } + + return descendants; + } + + /** + * Execute a command on a project, with delegation to children if it's an aggregator + * @param project The project to execute the command on + * @param commandName The command name for display purposes + * @returns The target project to execute on, or undefined if cancelled + */ + public async resolveCommandTarget(project: LibertyProject, commandName: string): Promise { + // Check if project is an aggregator FIRST (before checking if Liberty-enabled) + // An aggregator with children should delegate to children, not execute directly + if (project.isAggregator && project.children.length > 0) { + const libertyChildren = this.findLibertyDescendants(project); + + if (libertyChildren.length === 0) { + vscode.window.showWarningMessage( + localize("no.liberty.modules.found", `No Liberty-enabled modules found in ${project.label}`) + ); + return undefined; + } + + if (libertyChildren.length === 1) { + // Only one Liberty child, execute automatically + return libertyChildren[0]; + } + + // Multiple children - show notification with buttons + const buttonLabels = libertyChildren.map(c => c.label); + const selected = await vscode.window.showInformationMessage( + localize("select.module.for.command", commandName), + ...buttonLabels + ); + + if (!selected) { + return undefined; + } + + // Find the project that matches the selected button + return libertyChildren.find(c => c.label === selected); + } + + // If we get here, check if it's a Liberty-enabled leaf project (not an aggregator) + if (project.isLibertyEnabled) { + return project; + } + + // Not Liberty-enabled and not an aggregator + vscode.window.showWarningMessage( + localize("project.not.liberty.enabled", `${project.label} is not Liberty-enabled`) + ); + return undefined; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -264,12 +364,17 @@ export class ProjectProvider implements vscode.TreeDataProvider } // if element is null, vscode is asking for the root node if (element === undefined) { - // projects is a map of buildFilePath -> LibertyProjects - // Need to return an array of just the LibertyProject + // Return root projects for hierarchical view + // If no hierarchy is built, fall back to flat list + if (this.rootProjects.length > 0) { + return this.rootProjects; + } return [... this.projects.values()]; } - // else it is asking for a child node - return []; + // Return children that are Liberty-enabled OR have Liberty descendants + return element.children.filter(child => + child.isLibertyEnabled || this.hasLibertyDescendants(child) + ); } @@ -401,6 +506,94 @@ export class ProjectProvider implements vscode.TreeDataProvider } return added; } + /** + * Build hierarchical relationships between projects based on metadata + * @param projectsMap Map of all projects + */ + private async buildHierarchy(projectsMap: Map): Promise { + const mavenProjectsByArtifactId = new Map(); + const gradleProjectsByName = new Map(); + const mavenMetadataMap = new Map(); + + // First pass: populate metadata for all projects + for (const [buildFilePath, project] of projectsMap.entries()) { + try { + if (buildFilePath.endsWith("pom.xml")) { + const xmlString = await fse.readFile(buildFilePath, "utf8"); + const metadata = await mavenUtil.extractMavenMetadata(buildFilePath, xmlString); + project.artifactId = metadata.artifactId; + project.parentArtifactId = metadata.parentArtifactId; + project.isAggregator = metadata.isAggregator; + project.isLibertyEnabled = metadata.isLibertyEnabled; + mavenProjectsByArtifactId.set(metadata.artifactId, project); + mavenMetadataMap.set(buildFilePath, metadata); + } else if (buildFilePath.endsWith("build.gradle")) { + const metadata = await gradleUtil.extractGradleMetadata(buildFilePath); + project.artifactId = metadata.projectName; + project.parentArtifactId = metadata.parentProjectName; + project.isAggregator = metadata.isAggregator; + project.isLibertyEnabled = metadata.isLibertyEnabled; + gradleProjectsByName.set(metadata.projectName, project); + } + } catch (error) { + console.error(`Error extracting metadata for ${buildFilePath}:`, error); + // Set defaults if metadata extraction fails + project.isLibertyEnabled = true; // Assume Liberty-enabled if in the map + } + } + + // Second pass: build parent-child relationships + // Strategy 1: Use parentArtifactId if available (standard Maven with element) + for (const project of projectsMap.values()) { + if (project.parentArtifactId) { + // Look up parent in the appropriate map based on build tool + const parent = project.path.endsWith("pom.xml") + ? mavenProjectsByArtifactId.get(project.parentArtifactId) + : gradleProjectsByName.get(project.parentArtifactId); + + if (parent) { + project.parent = parent; + if (!parent.children.includes(project)) { + parent.children.push(project); + } + } + } + } + + // Strategy 2: For Maven projects without parent links, use filesystem paths + module declarations + // This handles multimodule projects where child POMs don't have elements + for (const [parentPath, parentMetadata] of mavenMetadataMap.entries()) { + if (parentMetadata.isAggregator && parentMetadata.modules.length > 0) { + const parentProject = projectsMap.get(parentPath); + if (!parentProject) continue; + + const parentDir = vscodePath.dirname(parentPath); + + // For each module declared in parent's section + for (const moduleName of parentMetadata.modules) { + // Resolve the module path relative to parent + const modulePomPath = vscodePath.resolve(parentDir, moduleName, "pom.xml"); + + // Check if this module exists in our discovered projects + const childProject = projectsMap.get(modulePomPath); + if (childProject && !childProject.parent) { + // Link child to parent + childProject.parent = parentProject; + if (!parentProject.children.includes(childProject)) { + parentProject.children.push(childProject); + } + console.debug(`Linked module ${moduleName} to parent ${parentMetadata.artifactId} via filesystem path`); + } + } + } + } + + // Third pass: identify root projects (no parent or parent not in workspace) + this.rootProjects = Array.from(projectsMap.values()) + .filter(p => !p.parent) + .filter(p => p.isLibertyEnabled || this.hasLibertyDescendants(p)); + } + private async updateProjects(): Promise { // find all build files in the open workspace and find all the ones that are valid for dev-mode @@ -425,12 +618,14 @@ export class ProjectProvider implements vscode.TreeDataProvider } for (const gradleBuild of validGradleBuilds) { + console.log(`[DEBUG] Processing Gradle build: ${gradleBuild.getBuildFilePath()}, type: ${gradleBuild.getProjectType()}`); // if a LibertyProject for this build.gradle has already been created // we want to re-use it if ( !await this.addExistingProjectToNewProjectsMap(gradleBuild.getBuildFilePath(), gradleBuild.getProjectType(), newProjectsMap) ) { const project = await createProject(this._context, gradleBuild.getBuildFilePath(), gradleBuild.getProjectType()); newProjectsMap.set(gradleBuild.getBuildFilePath(), project); + console.log(`[DEBUG] Added Gradle project to map: ${gradleBuild.getBuildFilePath()}`); } } @@ -471,11 +666,32 @@ export class ProjectProvider implements vscode.TreeDataProvider } } } + console.log(`[DEBUG] Total projects in map: ${newProjectsMap.size}`); + for (const [path, proj] of newProjectsMap.entries()) { + console.log(`[DEBUG] Project: ${proj.label} at ${path}`); + } + this.projects = newProjectsMap; + + // Build hierarchical relationships between projects + await this.buildHierarchy(newProjectsMap); + + console.log(`[DEBUG] Root projects after hierarchy: ${this.rootProjects.length}`); + for (const root of this.rootProjects) { + console.log(`[DEBUG] Root: ${root.label}, isAggregator: ${root.isAggregator}, isLibertyEnabled: ${root.isLibertyEnabled}, children: ${root.children.length}`); + } } } export class LibertyProject extends vscode.TreeItem { + // New fields for multi-module hierarchy support + public parent?: LibertyProject; + public children: LibertyProject[] = []; + public isAggregator: boolean = false; + public isLibertyEnabled: boolean = false; + public artifactId: string = ""; + public parentArtifactId?: string; + constructor( private _context: vscode.ExtensionContext, public label: string, @@ -491,6 +707,7 @@ export class LibertyProject extends vscode.TreeItem { ) { super(label, collapsibleState); this.tooltip = this.path; + this.children = []; } private EXPLORER_ICON = this.setExplorerIcon(); diff --git a/src/locales/en.json b/src/locales/en.json index 849984ed..f2b39ade 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -72,5 +72,14 @@ "hotkey.commands.title.add.project": "Liberty: Add Project to Liberty Tools", "hotkey.commands.title.remove.project": "Liberty: Remove Project from Liberty Tools", "hotkey.commands.title.show.commands": "Liberty: Show Liberty commands", - "workspace.not.saved.projects.may.not.persist": "Save the workspace first. Projects that you manually add to Liberty Tools might not persist in the next VS Code session if you don't save the workspace before you add the project." + "workspace.not.saved.projects.may.not.persist": "Save the workspace first. Projects that you manually add to Liberty Tools might not persist in the next VS Code session if you don't save the workspace before you add the project.", + "no.liberty.modules.found": "No Liberty-enabled modules found in {0}", + "select.module.for.command": "Select which module to {0}", + "project.not.liberty.enabled": "{0} is not Liberty-enabled", + "command.start.dev.mode": "start dev mode", + "command.start.container.dev.mode": "start container dev mode", + "command.start.custom.dev.mode": "start custom dev mode", + "command.stop.dev.mode": "stop dev mode", + "command.run.tests": "run tests", + "command.attach.debugger": "attach debugger" } diff --git a/src/util/commandUtils.ts b/src/util/commandUtils.ts index f32caeab..4380ea7b 100644 --- a/src/util/commandUtils.ts +++ b/src/util/commandUtils.ts @@ -21,9 +21,17 @@ enum ShellType { /** * Return the maven command based on the OS and Terminal for start, start in container, start.. + * @param pomPath Path to the pom.xml file (or aggregator pom for multi-module) + * @param command Maven command to execute + * @param terminalType Type of terminal + * @param customCommand Custom parameters + * @param artifactId Optional artifact ID for multi-module projects (uses -pl :artifactId -am) */ -export async function getCommandForMaven(pomPath: string, command: string, terminalType?: string, customCommand?: string): Promise { +export async function getCommandForMaven(pomPath: string, command: string, terminalType?: string, customCommand?: string, artifactId?: string): Promise { + // For multi-module projects, add -pl :artifactId -am + const moduleSelector = artifactId ? ` -pl :${artifactId} -am` : ""; + // attempt to use the Maven executable path, if empty try using mvn or mvnw according to the preferMavenWrapper setting const mavenExecutablePath: string | undefined = vscode.workspace.getConfiguration("maven").get("executable.path"); if (mavenExecutablePath) { @@ -31,19 +39,19 @@ export async function getCommandForMaven(pomPath: string, command: string, termi /** * Function call to get the command for powershell in Windows OS */ - return formPowershellCommand(mavenExecutablePath, pomPath, command, "-f ", customCommand); + return formPowershellCommand(mavenExecutablePath, pomPath, command, "-f ", customCommand, moduleSelector); } - return formDefaultCommandWithPath(mavenExecutablePath, pomPath, command, "-f ", customCommand); + return formDefaultCommandWithPath(mavenExecutablePath, pomPath, command, "-f ", customCommand, moduleSelector); } let mvnCmdStart = await mvnCmd(pomPath); if (mvnCmdStart === "mvn") { - return formDefaultCommand(mvnCmdStart, pomPath, command, "-f ", customCommand); + return formDefaultCommand(mvnCmdStart, pomPath, command, "-f ", customCommand, moduleSelector); } //checking the OS type for command customization if (isWin()) { - return getMavenCommandForWin(mvnCmdStart, pomPath, command, terminalType, customCommand); + return getMavenCommandForWin(mvnCmdStart, pomPath, command, terminalType, customCommand, moduleSelector); } else { - return formLinuxBasedCommand(mvnCmdStart, command, "./mvnw ", customCommand); + return formLinuxBasedCommand(mvnCmdStart, command, "./mvnw ", customCommand, moduleSelector); } } @@ -175,61 +183,65 @@ function getGradleCommandForWin(gradleCmdStart: string, projectDir: string, comm /** * Returns the maven command for windows OS based on the terminal configured */ -function getMavenCommandForWin(mvnCmdStart: string, pomPath: string, command: string, terminalType?: String, customCommand?: string): string { +function getMavenCommandForWin(mvnCmdStart: string, pomPath: string, command: string, terminalType?: String, customCommand?: string, moduleSelector?: string): string { switch (terminalType) { case ShellType.GIT_BASH: - return formLinuxBasedCommand(mvnCmdStart, command, "./mvnw ", customCommand); + return formLinuxBasedCommand(mvnCmdStart, command, "./mvnw ", customCommand, moduleSelector); case ShellType.POWERSHELL: mvnCmdStart = Path.join(mvnCmdStart, "mvnw.cmd"); - return formPowershellCommand(mvnCmdStart, pomPath, command, "-f ", customCommand); + return formPowershellCommand(mvnCmdStart, pomPath, command, "-f ", customCommand, moduleSelector); case ShellType.WSL: mvnCmdStart = toDefaultWslPath(mvnCmdStart); - return formLinuxBasedCommand(mvnCmdStart, command, "./mvnw ", customCommand); + return formLinuxBasedCommand(mvnCmdStart, command, "./mvnw ", customCommand, moduleSelector); default: // The default case is ShellType CMD or OTHERS mvnCmdStart = Path.join(mvnCmdStart, "mvnw.cmd"); - return formDefaultCommandWithPath(mvnCmdStart, pomPath, command, "-f ", customCommand); + return formDefaultCommandWithPath(mvnCmdStart, pomPath, command, "-f ", customCommand, moduleSelector); } } /** * Returns the Powershell based command for windows OS */ -function formPowershellCommand(cmdStart: string, projectPath: string, command: string, cmdOption: String, customCommand?: string): string { +function formPowershellCommand(cmdStart: string, projectPath: string, command: string, cmdOption: String, customCommand?: string, moduleSelector?: string): string { + const modulePart = moduleSelector || ""; if (customCommand) { - return "& \"" + cmdStart + "\" " + `${command}` + ` ${customCommand}` + ` ${cmdOption}"${projectPath}"`; //Powershell for start.. + return "& \"" + cmdStart + "\" " + `${command}` + ` ${customCommand}` + `${modulePart}` + ` ${cmdOption}"${projectPath}"`; //Powershell for start.. } - return "& \"" + cmdStart + "\" " + `${command}` + ` ${cmdOption}"${projectPath}"`; //PowerShell + return "& \"" + cmdStart + "\" " + `${command}` + `${modulePart}` + ` ${cmdOption}"${projectPath}"`; //PowerShell } /** * Returns the Linux based command */ -function formLinuxBasedCommand(cmdStart: string, command: string, wrapperType: String, customCommand?: string): string { +function formLinuxBasedCommand(cmdStart: string, command: string, wrapperType: String, customCommand?: string, moduleSelector?: string): string { + const modulePart = moduleSelector || ""; if (customCommand) { - return "cd \"" + cmdStart + "\" && " + `${wrapperType}` + `${command}` + ` ${customCommand}`; //Bash or WSL for start.. + return "cd \"" + cmdStart + "\" && " + `${wrapperType}` + `${command}` + ` ${customCommand}` + `${modulePart}`; //Bash or WSL for start.. } - return "cd \"" + cmdStart + "\" && " + `${wrapperType}` + `${command}`; //Bash or WSL command + return "cd \"" + cmdStart + "\" && " + `${wrapperType}` + `${command}` + `${modulePart}`; //Bash or WSL command } /** * Returns default command */ -function formDefaultCommand(projectPath: string, buildFilePath: String, command: string, cmdOption: String, customCommand?: string): string { +function formDefaultCommand(projectPath: string, buildFilePath: String, command: string, cmdOption: String, customCommand?: string, moduleSelector?: string): string { + const modulePart = moduleSelector || ""; if (customCommand) { - return `${projectPath} ` + `${command}` + ` ${customCommand}` + ` ${cmdOption}"${buildFilePath}"`; + return `${projectPath} ` + `${command}` + ` ${customCommand}` + `${modulePart}` + ` ${cmdOption}"${buildFilePath}"`; } - return `${projectPath} ` + `${command}` + ` ${cmdOption}"${buildFilePath}"`; + return `${projectPath} ` + `${command}` + `${modulePart}` + ` ${cmdOption}"${buildFilePath}"`; } /** - * Returns default format for the command with Path + * Returns default format for the command with Path */ -function formDefaultCommandWithPath(projectPath: string, buildFilePath: String, command: string, cmdOption: String, customCommand?: string): string { +function formDefaultCommandWithPath(projectPath: string, buildFilePath: String, command: string, cmdOption: String, customCommand?: string, moduleSelector?: string): string { + const modulePart = moduleSelector || ""; if (customCommand) { - return "\"" + projectPath + "\" " + `${command}` + ` ${customCommand}` + ` ${cmdOption}"${buildFilePath}"`; + return "\"" + projectPath + "\" " + `${command}` + ` ${customCommand}` + `${modulePart}` + ` ${cmdOption}"${buildFilePath}"`; } - return "\"" + projectPath + "\" " + `${command}` + ` ${cmdOption}"${buildFilePath}"`; + return "\"" + projectPath + "\" " + `${command}` + `${modulePart}` + ` ${cmdOption}"${buildFilePath}"`; } /** diff --git a/src/util/gradleUtil.ts b/src/util/gradleUtil.ts index 2dc93162..2212d53a 100644 --- a/src/util/gradleUtil.ts +++ b/src/util/gradleUtil.ts @@ -1,6 +1,6 @@ /* * IBM Confidential - * Copyright IBM Corp. 2020, 2022 + * Copyright IBM Corp. 2020, 2026 */ import * as fse from "fs-extra"; import * as path from "path"; @@ -95,15 +95,15 @@ export function getGradleSettings(gradlePath: string): string { /** * Given a settings.gradle file, determine if there are valid child gradle projects * The parent build.gradle must have subprojects in the `include` section and - * apply the liberty-gradle-plugin to the subprojects + * apply the liberty-gradle-plugin to the subprojects, OR simply be an aggregator with modules * Return GradleBuildFile object - * + * * @param settingsFile settings.gradle file */ export function findChildGradleProjects(buildFile: any, settingsFile: any): GradleBuildFile { let projectType: string = LIBERTY_GRADLE_PROJECT; let gradleChildren: string[] = []; - const gradleBuildFile: GradleBuildFile = new GradleBuildFile(false, ""); + if (settingsFile !== undefined) { // look for a valid "include" section in the settingsFile if (settingsFile.include !== undefined) { @@ -119,16 +119,20 @@ export function findChildGradleProjects(buildFile: any, settingsFile: any): Grad } } - // check if the liberty-gradle-plugin is applied to any/all of the subprojects + // If there are children in settings.gradle, this is a valid aggregator if (gradleChildren.length !== 0) { - const parent: GradleBuildFile = validParent(buildFile); + // Check if the parent build.gradle has Liberty plugin + const parent: GradleBuildFile = validGradleBuild(buildFile); if (parent.isValidBuildFile()) { projectType = parent.getProjectType(); - gradleBuildFile.setProjectType(projectType); - gradleBuildFile.setChildren(gradleChildren); } + // Always mark as valid if there are children - aggregators are valid even without Liberty plugin + const result = new GradleBuildFile(true, projectType); + result.setChildren(gradleChildren); + return result; } - return gradleBuildFile; + + return new GradleBuildFile(false, ""); } /** @@ -158,17 +162,6 @@ export async function getGradleTestReport(gradlePath: any, projectRootPath: stri return testReport; } -function validParent(buildFile: any): GradleBuildFile { - // every subproject listed in the include section of the parent is supported by the liberty-gradle-plugin - const gradleBuildSub: GradleBuildFile = validGradleBuild(buildFile.subprojects); - const gradleBuildAll: GradleBuildFile = validGradleBuild(buildFile.allprojects); - if (gradleBuildSub.isValidBuildFile()) { - return gradleBuildSub; - } else if (gradleBuildAll.isValidBuildFile()) { - return gradleBuildAll; - } - return (new GradleBuildFile(false, "")); -} /** * Return true if the liberty-gradle-plugin version is compatible @@ -209,4 +202,122 @@ async function findCustomTestReport(projectRootPath: string, testReport: string) testReport = lastModifiedPath; } return testReport; +} + +/** + * Interface for Gradle project metadata used in multi-module hierarchy + */ +export interface GradleProjectMetadata { + projectName: string; + parentProjectName?: string; + subprojects: string[]; + hasLibertyPlugin: boolean; + isAggregator: boolean; + isLibertyEnabled: boolean; + buildFilePath: string; + contextValue: string; +} + +/** + * Extract metadata from a Gradle build file for multi-module support + * @param buildGradlePath Path to the build.gradle file + * @param buildFileContent Optional parsed build file content + * @param settingsContent Optional parsed settings.gradle content + * @returns GradleProjectMetadata object + */ +export async function extractGradleMetadata( + buildGradlePath: string, + buildFileContent?: any, + settingsContent?: any +): Promise { + const g2js = require("gradle-to-js/lib/parser"); + + // Parse build.gradle if not provided + const buildFile = buildFileContent || await g2js.parseFile(buildGradlePath); + + // Parse settings.gradle if exists and not provided + const settingsPath = getGradleSettings(buildGradlePath); + let settingsFile = settingsContent; + if (!settingsFile && settingsPath && fse.existsSync(settingsPath)) { + try { + settingsFile = await g2js.parseFile(settingsPath); + } catch (err) { + console.error(localize("unable.to.parse.settings.gradle", settingsPath, err)); + } + } + + // Extract project name + const projectName = await getGradleProjectName(buildGradlePath); + + // Extract subprojects + const subprojects = extractSubprojectsFromSettings(settingsFile); + + // Determine parent project name + let parentProjectName: string | undefined; + const buildDir = path.dirname(buildGradlePath); + const currentDirName = path.basename(buildDir); + const parentDir = path.dirname(buildDir); + const parentSettings = path.join(parentDir, "settings.gradle"); + + // Check if parent directory has settings.gradle + if (fse.existsSync(parentSettings)) { + try { + const parentSettingsFile = await g2js.parseFile(parentSettings); + const parentSubprojects = extractSubprojectsFromSettings(parentSettingsFile); + + // If current directory is in parent's includes, this is a child module + if (parentSubprojects.includes(currentDirName)) { + parentProjectName = parentSettingsFile["rootProject.name"] || path.basename(parentDir); + } + } catch (err) { + console.error(localize("unable.to.parse.settings.gradle", parentSettings, err)); + } + } + + // Check for Liberty plugin + const gradleBuildFile = validGradleBuild(buildFile); + const hasLibertyPlugin = gradleBuildFile.isValidBuildFile(); + + // Check if this is an aggregator (has subprojects) + const isAggregator = subprojects.length > 0; + + // Determine context value + let contextValue = ""; + if (hasLibertyPlugin) { + contextValue = gradleBuildFile.getProjectType(); + } + + return { + projectName, + parentProjectName, + subprojects, + hasLibertyPlugin, + isAggregator, + isLibertyEnabled: hasLibertyPlugin, + buildFilePath: buildGradlePath, + contextValue + }; +} + +/** + * Extract subproject names from settings.gradle + * @param settingsFile Parsed settings.gradle content + * @returns Array of subproject names + */ +function extractSubprojectsFromSettings(settingsFile: any): string[] { + if (!settingsFile || !settingsFile.include) { + return []; + } + + if (typeof settingsFile.include === "string") { + // Handle string format: "'module1', 'module2'" + const cleaned = settingsFile.include.replace(/['" ]+/g, ""); + return cleaned.split(",").filter((s: string) => s.length > 0); + } + + if (Array.isArray(settingsFile.include)) { + return settingsFile.include; + } + + return []; } \ No newline at end of file diff --git a/src/util/mavenUtil.ts b/src/util/mavenUtil.ts index 2abe971e..b90e751d 100644 --- a/src/util/mavenUtil.ts +++ b/src/util/mavenUtil.ts @@ -6,6 +6,11 @@ import { LIBERTY_MAVEN_PLUGIN_CONTAINER_VERSION, LIBERTY_MAVEN_PROJECT_CONTAINER import { BuildFileImpl } from "./buildFile"; import { localize } from "../util/i18nUtil"; import * as semver from "semver"; +import * as vscode from "vscode"; +import * as Path from "path"; +import { pathExists } from "fs-extra"; +import { exec } from "child_process"; +import { promisify } from "util"; /** * Look for a valid parent pom.xml @@ -52,6 +57,20 @@ export function validParentPom(xmlString: string): BuildFileImpl { } } } + + // If no Liberty plugin found, check if this is an aggregator with modules + // This allows intermediate aggregators to appear in the hierarchy + if (!parentPom.isValidBuildFile() && result.project.modules !== undefined) { + const modules = result.project.modules; + for (let i = 0; i < modules.length; i++) { + const module = modules[i].module; + if (module !== undefined && module.length > 0) { + console.debug("Found aggregator pom.xml with modules"); + parentPom = new BuildFileImpl(true, LIBERTY_MAVEN_PROJECT); + return; + } + } + } if (err) { console.error(localize("error.parsing.pom","Error parsing the pom " + err, err)); @@ -213,3 +232,239 @@ function containerVersion(plugin: any): boolean { } return false; } + +/** + * Resolve effective POM using Maven help:effective-pom goal + * @param pomPath Path to pom.xml file + * @returns Effective POM XML string + * @throws Error if Maven command fails + */ +export async function resolveEffectivePom(pomPath: string): Promise { + const execAsync = promisify(exec); + + // Determine Maven executable + const mavenConfig = vscode.workspace.getConfiguration("maven"); + const mavenExecutablePath = mavenConfig.get("executable.path") as string | undefined; + let mvnCmd = "mvn"; + + if (mavenExecutablePath) { + mvnCmd = mavenExecutablePath; + } else { + // Check for Maven wrapper + const preferMavenWrapper = mavenConfig.get("executable.preferMavenWrapper") as boolean | undefined; + if (preferMavenWrapper) { + const pomDir = Path.dirname(pomPath); + const mvnw = process.platform.startsWith("win") ? "mvnw.cmd" : "mvnw"; + let currentDir = pomDir; + + // Walk up parent folders to find mvnw + while (Path.basename(currentDir)) { + const potentialMvnwPath = Path.join(currentDir, mvnw); + if (await pathExists(potentialMvnwPath)) { + mvnCmd = potentialMvnwPath; + break; + } + currentDir = Path.dirname(currentDir); + } + } + } + + // Build the command + const command = `"${mvnCmd}" help:effective-pom -f "${pomPath}" -q`; + + try { + // Execute the Maven command and capture stdout + const { stdout, stderr } = await execAsync(command, { + maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large POMs + cwd: Path.dirname(pomPath) + }); + + if (stderr && stderr.trim().length > 0) { + console.warn(`Maven effective-pom stderr: ${stderr}`); + } + + return stdout; + } catch (error: any) { + throw new Error(`Failed to resolve effective POM for ${pomPath}: ${error.message}`); + } +} + +/** + * Interface for Maven project metadata used in multi-module hierarchy + */ +export interface MavenProjectMetadata { + artifactId: string; + parentArtifactId?: string; + modules: string[]; + hasLibertyPlugin: boolean; + isAggregator: boolean; + isLibertyEnabled: boolean; + buildFilePath: string; + contextValue: string; + xmlString?: string; +} + +/** + * Extract metadata from a Maven POM file for multi-module support + * Uses effective-pom for accurate metadata extraction with fallback to direct XML + * @param pomPath Path to the pom.xml file + * @param xmlString Optional XML string content (if already read) + * @returns MavenProjectMetadata object + */ +export async function extractMavenMetadata(pomPath: string, xmlString?: string): Promise { + const fse = require("fs-extra"); + const xml = xmlString || await fse.readFile(pomPath, "utf8"); + + // Try to use effective-pom for accurate metadata extraction + try { + const effectivePomXml = await resolveEffectivePom(pomPath); + const metadata = parsePomXml(effectivePomXml); + metadata.buildFilePath = pomPath; + metadata.xmlString = xml; // Keep original XML for reference + return metadata; + } catch (error: any) { + // Fallback: Use direct XML parsing if effective-pom fails + console.warn(`Could not resolve effective POM for ${pomPath}, using direct XML parsing: ${error.message}`); + const metadata = parsePomXml(xml); + metadata.buildFilePath = pomPath; + metadata.xmlString = xml; + return metadata; + } +} + +/** + * Parse POM XML to extract metadata + * @param xmlString XML content of pom.xml + * @returns MavenProjectMetadata object + */ +function parsePomXml(xmlString: string): MavenProjectMetadata { + const parseString = require("xml2js").parseString; + let metadata: MavenProjectMetadata = { + artifactId: "", + modules: [], + hasLibertyPlugin: false, + isAggregator: false, + isLibertyEnabled: false, + buildFilePath: "", + contextValue: LIBERTY_MAVEN_PROJECT + }; + + parseString(xmlString, (err: any, result: any) => { + if (err) { + console.error(localize("error.parsing.pom", "Error parsing the pom " + err, err)); + return; + } + + // Extract artifactId + if (result.project.artifactId && result.project.artifactId[0] !== undefined) { + metadata.artifactId = result.project.artifactId[0]; + } + + // Extract parent artifactId + if (result.project.parent && result.project.parent[0].artifactId) { + metadata.parentArtifactId = result.project.parent[0].artifactId[0]; + } + + // Extract modules + if (result.project.modules) { + metadata.modules = extractModulesFromPom(result.project.modules); + if (metadata.modules.length > 0) { + metadata.isAggregator = true; + } + } + + // Check for packaging type "pom" + if (result.project.packaging && result.project.packaging[0] === "pom") { + metadata.isAggregator = true; + } + + // Check for Liberty Maven plugin + metadata.hasLibertyPlugin = checkForLibertyMavenPlugin(result); + metadata.isLibertyEnabled = metadata.hasLibertyPlugin; + + // Set context value based on plugin detection + if (metadata.hasLibertyPlugin) { + const buildFile = mavenPluginDetected(result.project.build); + if (buildFile.isValidBuildFile()) { + metadata.contextValue = buildFile.getProjectType(); + } + } + }); + + return metadata; +} + +/** + * Extract module names from POM modules section + * @param modules Modules section from parsed POM + * @returns Array of module names + */ +function extractModulesFromPom(modules: any[]): string[] { + const moduleNames: string[] = []; + + for (let i = 0; i < modules.length; i++) { + const module = modules[i].module; + if (module !== undefined) { + for (let k = 0; k < module.length; k++) { + moduleNames.push(module[k]); + } + } + } + + return moduleNames; +} + +/** + * Check if POM contains Liberty Maven plugin + * @param result Parsed POM object + * @returns true if Liberty plugin is found + */ +function checkForLibertyMavenPlugin(result: any): boolean { + // Check in build section + if (result.project.build !== undefined) { + const buildFile = mavenPluginDetected(result.project.build); + if (buildFile.isValidBuildFile()) { + return true; + } + } + + // Check in profiles + if (result.project.profiles !== undefined) { + for (let i = 0; i < result.project.profiles.length; i++) { + const profile = result.project.profiles[i].profile; + if (profile !== undefined) { + for (let j = 0; j < profile.length; j++) { + const buildFile = mavenPluginDetected(profile[j].build); + if (buildFile.isValidBuildFile()) { + return true; + } + } + } + } + } + + // Check in pluginManagement + if (result.project.build !== undefined) { + for (let i = 0; i < result.project.build.length; i++) { + const pluginManagement = result.project.build[i].pluginManagement; + if (pluginManagement !== undefined) { + const plugins = pluginManagement[0].plugins; + if (plugins !== undefined) { + for (let j = 0; j < plugins.length; j++) { + const plugin = plugins[j].plugin; + if (plugin !== undefined) { + for (let k = 0; k < plugin.length; k++) { + if (plugin[k].artifactId[0] === "liberty-maven-plugin" && + plugin[k].groupId[0] === "io.openliberty.tools") { + return true; + } + } + } + } + } + } + } + } + + return false; +}