Skip to content

Commit

Permalink
Support creating a new maven module to existing multi-module project …
Browse files Browse the repository at this point in the history
…via 'New Module...' command (#1018)

* Support creating a new maven module to existing multi-module project via 'New Module...' command
  • Loading branch information
testforstephen authored Jan 19, 2024
1 parent 4e5e65d commit 8d5fe37
Show file tree
Hide file tree
Showing 20 changed files with 405 additions and 36 deletions.
2 changes: 1 addition & 1 deletion ThirdPartyNotices.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2354,7 +2354,7 @@ THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI

---------------------------------------------------------

vscode-tas-client 0.1.63 - MIT
vscode-tas-client 0.1.75 - MIT


Copyright (c) Microsoft Corporation
Expand Down
41 changes: 30 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@
"title": "Deselect",
"icon": "$(remove)",
"category": "Maven"
},
{
"command": "maven.new.module",
"title": "%contributes.commands.maven.module.new%",
"category": "Maven"
}
],
"views": {
Expand All @@ -261,6 +266,10 @@
{
"id": "maven/run",
"label": "Run Maven Commands..."
},
{
"id": "explorer/maven",
"label": "Maven"
}
],
"menus": {
Expand Down Expand Up @@ -392,14 +401,8 @@
],
"explorer/context": [
{
"command": "maven.archetype.generate",
"when": "config.maven.showInExplorerContextMenu && explorerResourceIsFolder",
"group": "maven@3"
},
{
"command": "maven.project.effectivePom",
"when": "resourceFilename == pom.xml",
"group": "maven@1"
"submenu": "explorer/maven",
"group": "1_javaactions@100"
}
],
"view/title": [
Expand Down Expand Up @@ -550,6 +553,22 @@
"group": "maven@30"
}
],
"explorer/maven": [
{
"command": "maven.archetype.generate",
"when": "config.maven.showInExplorerContextMenu && explorerResourceIsFolder",
"group": "maven@1"
},
{
"command": "maven.new.module",
"group": "maven@2"
},
{
"command": "maven.project.effectivePom",
"when": "resourceFilename == pom.xml",
"group": "maven@3"
}
],
"maven/run": [
{
"command": "maven.goal.custom",
Expand Down Expand Up @@ -802,15 +821,15 @@
"@types/vscode": "1.75.0",
"@types/which": "^1.3.2",
"@types/xml2js": "^0.4.11",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"@vscode/test-electron": "^2.3.8",
"eslint": "^8.32.0",
"glob": "^7.2.3",
"mocha": "^9.2.2",
"ts-loader": "^9.4.1",
"typescript": "^4.8.4",
"webpack": "^5.76.0",
"eslint": "^8.32.0",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"webpack-cli": "^4.10.0"
},
"dependencies": {
Expand Down
3 changes: 2 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"contributes.commands.maven.goal.custom": "Custom... ",
"contributes.commands.maven.project.effectivePom": "Show Effective POM",
"contributes.commands.maven.project.openPom": "Open POM file",
"contributes.commands.maven.archetype.generate": "Create Maven Project",
"contributes.commands.maven.archetype.generate": "New Project...",
"contributes.commands.maven.module.new": "New Module...",
"contributes.commands.maven.archetype.update": "Update Maven Archetype Catalog",
"contributes.commands.maven.favorites": "Favorites...",
"contributes.commands.maven.history": "History...",
Expand Down
1 change: 1 addition & 0 deletions package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"contributes.commands.maven.project.effectivePom": "显示 Effective POM",
"contributes.commands.maven.project.openPom": "打开 POM 文件",
"contributes.commands.maven.archetype.generate": "从 Maven 原型创建新项目",
"contributes.commands.maven.module.new": "新建 Module...",
"contributes.commands.maven.archetype.update": "更新 Maven 原型目录",
"contributes.commands.maven.favorites": "收藏夹…",
"contributes.commands.maven.history": "历史…",
Expand Down
6 changes: 3 additions & 3 deletions resources/projectTemplate/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

${parentPom}
<groupId>${groupId}</groupId>
<artifactId>${artifactId}</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>${javaSourceVersion}</maven.compiler.source>
<maven.compiler.target>${javaTargetVersion}</maven.compiler.target>
</properties>

</project>
173 changes: 167 additions & 6 deletions src/archetype/ArchetypeModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,47 @@ import { getPathToExtensionRoot } from "../utils/contextUtils";
import { getEmbeddedMavenWrapper, getMaven } from "../utils/mavenUtils";
import { Utils } from "../utils/Utils";
import { Archetype } from "./Archetype";
import { runSteps, selectArchetypeStep, specifyArchetypeVersionStep, specifyArtifactIdStep, specifyGroupIdStep, specifyTargetFolderStep } from "./createProject";
import { runSteps, selectArchetypeStep, selectParentPomStep, specifyArchetypeVersionStep, specifyArtifactIdStep, specifyGroupIdStep, specifyTargetFolderStep } from "./createProject";
import { IProjectCreationMetadata, IProjectCreationStep } from "./createProject/types";
import { promptOnDidProjectCreated } from "./utils";
import { importProjectOnDemand, promptOnDidProjectCreated } from "./utils";
import { XmlTagName, detectDocumentIndent, getChildrenByTags, getTextFromNode, parseDocument } from "../utils/lexerUtils";

const REMOTE_ARCHETYPE_CATALOG_URL = "https://repo.maven.apache.org/maven2/archetype-catalog.xml";

export class ArchetypeModule {

public static async createMavenModule(entry: Uri | IProjectCreationMetadata | undefined): Promise<void> {
const metadata: IProjectCreationMetadata = {
title: "New Maven Module",
targetFolderHint: workspace.workspaceFolders?.[0]?.uri.fsPath
};
if (entry instanceof Uri) {
metadata.targetFolderHint = entry.fsPath;
} else if (typeof entry === 'object') {
Object.assign(metadata, entry);
}

const steps: IProjectCreationStep[] = [];
if (!metadata.parentProject) {
steps.push(selectParentPomStep);
}

if (!metadata.groupId) {
steps.push(specifyGroupIdStep);
}
if (!metadata.artifactId) {
steps.push(specifyArtifactIdStep);
}
if (!metadata.targetFolder) {
steps.push(specifyTargetFolderStep);
}

await ArchetypeModule.scaffoldMavenProject(steps, metadata);
}

public static async createMavenProject(entry: Uri | IProjectCreationMetadata | undefined): Promise<void> {
const metadata: IProjectCreationMetadata = {
title: "New Maven Project",
targetFolderHint: workspace.workspaceFolders?.[0]?.uri.fsPath
};
if (entry instanceof Uri) {
Expand All @@ -45,11 +76,25 @@ export class ArchetypeModule {
steps.push(specifyTargetFolderStep);
}

await ArchetypeModule.scaffoldMavenProject(steps, metadata);
}

private static async scaffoldMavenProject(steps: IProjectCreationStep[], metadata: IProjectCreationMetadata): Promise<void> {
const success: boolean = await runSteps(steps, metadata);
if (success) {
if (metadata.archetypeArtifactId && metadata.archetypeGroupId && metadata.archetypeVersion) {
sendInfo("", {
archetypeArtifactId: metadata.archetypeArtifactId,
archetypeGroupId: metadata.archetypeGroupId,
archetypeVersion: metadata.archetypeVersion,
triggerfrom: metadata.title,
});
await executeInTerminalHandler(metadata);
} else {
sendInfo("", {
archetypeArtifactId: "No Archetype",
triggerfrom: metadata.title,
});
await createBasicMavenProject(metadata);
}
}
Expand Down Expand Up @@ -106,7 +151,6 @@ async function executeInTerminalHandler(metadata: IProjectCreationMetadata): Pro
if (archetypeArtifactId === undefined || archetypeGroupId === undefined || archetypeVersion === undefined) {
throw new Error("Archetype information is incomplete.");
}
sendInfo("", { archetypeArtifactId, archetypeGroupId, archetypeVersion });
const cmdArgs: string[] = [
// explicitly using 3.1.2 as maven-archetype-plugin:3.0.1 ignores -DoutputDirectory
// see https://github.com/microsoft/vscode-maven/issues/478
Expand Down Expand Up @@ -158,17 +202,30 @@ async function createBasicMavenProject(metadata: IProjectCreationMetadata): Prom

const task = async (p: vscode.Progress<{ message?: string; increment?: number }>) => {
// copy from template
p.report({ message: "Generating project from template...", increment: 20 });
p.report({ message: "Generating project from template...", increment: 10 });
const templateUri = vscode.Uri.file(getPathToExtensionRoot("resources", "projectTemplate"));
const targetUri = vscode.Uri.joinPath(vscode.Uri.file(targetFolder), artifactId);
await workspace.fs.copy(templateUri, targetUri, { overwrite: true });

// update groupId/artifactId in pom.xml
p.report({ message: "Updating pom.xml file...", increment: 20 });
p.report({ message: "Updating pom.xml file...", increment: 10 });
const pomUri = vscode.Uri.joinPath(targetUri, "pom.xml");
let pomContent = (await workspace.fs.readFile(pomUri)).toString();
let parentPom = "";
const compilerSource = metadata.parentProject?.getProperty("maven.compiler.source") || "17";
const compilerTarget = metadata.parentProject?.getProperty("maven.compiler.target") || "17";
if (metadata.parentProject) {
parentPom = ` <parent>\n`
+ ` <groupId>${metadata.parentProject.groupId}</groupId>\n`
+ ` <artifactId>${metadata.parentProject.artifactId}</artifactId>\n`
+ ` <version>${metadata.parentProject.version}</version>\n`
+ ` </parent>\n`;
}
pomContent = pomContent.replace("${parentPom}", parentPom);
pomContent = pomContent.replace("${groupId}", groupId);
pomContent = pomContent.replace("${artifactId}", artifactId);
pomContent = pomContent.replace("${javaSourceVersion}", compilerSource);
pomContent = pomContent.replace("${javaTargetVersion}", compilerTarget);
await workspace.fs.writeFile(pomUri, Buffer.from(pomContent));

// create source files
Expand All @@ -190,7 +247,15 @@ async function createBasicMavenProject(metadata: IProjectCreationMetadata): Prom
].join("\n");
await vscode.workspace.fs.writeFile(mainUri, Buffer.from(content));

// TODO: update modules of parent project, on demand
// Update parent pom on demand
if (metadata.parentProject) {
p.report({ message: "Update parent pom.xml...", increment: 20 });
await updateParentPom(metadata.parentProject.pomPath, artifactId);
}

// Import the new module as a Java project
p.report({ message: "Import the new module as Java project...", increment: 20 });
importProjectOnDemand(targetFolder);
};

await vscode.window.withProgress({
Expand All @@ -200,6 +265,102 @@ async function createBasicMavenProject(metadata: IProjectCreationMetadata): Prom
await promptOnDidProjectCreated(artifactId, targetFolder);
}

async function updateParentPom(parentPomPath: string, subModuleName: string): Promise<void> {
if (!await fse.pathExists(parentPomPath)) {
return;
}

const pomDocument = await workspace.openTextDocument(parentPomPath);
const documentText = pomDocument.getText();
const xmlDocument = await parseDocument(documentText);
if (!xmlDocument) {
return;
}

const projectNodes = getChildrenByTags(xmlDocument, [XmlTagName.Project]);
if (!projectNodes.length) {
return;
}

const indentInfo = detectDocumentIndent(xmlDocument, documentText) || {
indent: 2,
indentChar: " ",
};
const basicNodes = getChildrenByTags(projectNodes[0], [XmlTagName.GroupId, XmlTagName.ArtifactId, XmlTagName.Version]);
let nextInsertOffset = -1;
basicNodes.forEach(node => {
if (node.endIndex) {
if (nextInsertOffset == -1) {
nextInsertOffset = node.endIndex;
} else {
nextInsertOffset = Math.max(nextInsertOffset, node.endIndex);
}
}
});
nextInsertOffset++;

const parentPomUri = Uri.file(parentPomPath);
const packagingNodes = getChildrenByTags(projectNodes[0], [XmlTagName.Packaging]);
const workspaceEdit = new vscode.WorkspaceEdit();
// Update the packaging mode to pom.
if (packagingNodes.length) {
const node = packagingNodes[0].firstChild;
if (node && getTextFromNode(node) === "pom") {
// it's already packaging as pom, do nothing
} else {
workspaceEdit.replace(parentPomUri,
new vscode.Range(
pomDocument.positionAt(packagingNodes[0].startIndex ?? 0),
pomDocument.positionAt(packagingNodes[0].endIndex ? packagingNodes[0].endIndex + 1 : 0)
),
"<packaging>pom</packaging>");
}
nextInsertOffset = packagingNodes[0].endIndex ? packagingNodes[0].endIndex + 1 : nextInsertOffset;
} else {
workspaceEdit.insert(parentPomUri,
pomDocument.positionAt(nextInsertOffset),
`\n${genIndent(indentInfo.indentChar, indentInfo.indent)}<packaging>pom</packaging>`);
}

// Add new module as a child module of parent pom.
const moduleNodes = getChildrenByTags(projectNodes[0], [XmlTagName.Modules]);
if (moduleNodes.length) {
const modules = getChildrenByTags(moduleNodes[0], [XmlTagName.Module]);
if (modules.length) {
const lastModule = modules[modules.length - 1];
nextInsertOffset = lastModule.endIndex ? (lastModule.endIndex || 0) + 1 : nextInsertOffset;
workspaceEdit.insert(parentPomUri,
pomDocument.positionAt(nextInsertOffset),
`\n${genIndent(indentInfo.indentChar, indentInfo.indent * 2)}<module>${subModuleName}</module>`);
} else {
workspaceEdit.replace(parentPomUri,
new vscode.Range(
pomDocument.positionAt(moduleNodes[0].startIndex ?? 0),
pomDocument.positionAt(moduleNodes[0].endIndex ? moduleNodes[0].endIndex + 1 : 0)
),
`<modules>\n` +
`${genIndent(indentInfo.indentChar, indentInfo.indent * 2)}<module>${subModuleName}</module>\n` +
`${genIndent(indentInfo.indentChar, indentInfo.indent)}</modules>`);
}
} else {
workspaceEdit.insert(parentPomUri, pomDocument.positionAt(nextInsertOffset),
`\n${genIndent(indentInfo.indentChar, indentInfo.indent)}<modules>\n` +
`${genIndent(indentInfo.indentChar, indentInfo.indent * 2)}<module>${subModuleName}</module>\n` +
`${genIndent(indentInfo.indentChar, indentInfo.indent)}</modules>`);
}

await vscode.workspace.applyEdit(workspaceEdit);
await pomDocument?.save();
}

function genIndent(indentChar: string, indentSize: number): string {
let ret = "";
for (let i = 0; i < indentSize; i++) {
ret += indentChar;
}
return ret;
}

export class ArchetypeMetadata {
public groupId: string;
public artifactId: string;
Expand Down
2 changes: 1 addition & 1 deletion src/archetype/createProject/SelectArchetypeStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class SelectArchetypeStep implements IProjectCreationStep {
const disposables: Disposable[] = [];
const specifyAchetypePromise = (items: IArchetypePickItem[]) => new Promise<StepResult>((resolve) => {
const pickBox: QuickPick<IArchetypePickItem> = window.createQuickPick<IArchetypePickItem>();
pickBox.title = "Create Maven Project";
pickBox.title = metadata.title;
pickBox.placeholder = "Select an archetype ...";
pickBox.matchOnDescription = true;
pickBox.ignoreFocusOut = true;
Expand Down
Loading

0 comments on commit 8d5fe37

Please sign in to comment.