Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support create java function project #51

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/IUserInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface IUserInterface {
showQuickPick<T>(items: PickWithData<T>[] | Thenable<PickWithData<T>[]>, placeHolder: string, ignoreFocusOut?: boolean): Promise<PickWithData<T>>;
showQuickPick(items: Pick[] | Thenable<Pick[]>, placeHolder: string, ignoreFocusOut?: boolean): Promise<Pick>;

showInputBox(placeHolder: string, prompt: string, ignoreFocusOut?: boolean, validateInput?: (s: string) => string | undefined | null, value?: string): Promise<string>;
showInputBox(placeHolder: string, prompt: string, ignoreFocusOut?: boolean, validateInput?: (s: string) => string | undefined | null, defaultValue?: string): Promise<string>;

showFolderDialog(): Promise<string>;
}
Expand Down
66 changes: 39 additions & 27 deletions src/commands/createFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ import * as path from 'path';
import * as vscode from 'vscode';
import { AzureAccount } from '../azure-account.api';
import * as errors from '../errors';
import * as FunctionsCli from '../functions-cli';
import { IUserInterface, Pick, PickWithData } from '../IUserInterface';
import { LocalAppSettings } from '../LocalAppSettings';
import { localize } from '../localize';
import { ConfigSetting, ValueType } from '../templates/ConfigSetting';
import { EnumValue } from '../templates/EnumValue';
import { Template } from '../templates/Template';
import { TemplateLanguage } from '../templates/Template';
import { TemplateData } from '../templates/TemplateData';
import * as fsUtil from '../utils/fs';
import { runCommandInTerminal } from '../utils/terminal';
import * as workspaceUtil from '../utils/workspace';
import { VSCodeUI } from '../VSCodeUI';
import * as CreateNewProject from './createNewProject';

const expectedFunctionAppFiles: string[] = [
'host.json',
Expand Down Expand Up @@ -48,7 +50,7 @@ async function validateIsFunctionApp(outputChannel: vscode.OutputChannel, functi
const message: string = localize('azFunc.missingFuncAppFiles', 'The current folder is missing the following function app files: \'{0}\'. Add the missing files?', missingFiles.join(','));
const result: string | undefined = await vscode.window.showWarningMessage(message, yes, no);
if (result === yes) {
await FunctionsCli.createNewProject(outputChannel, functionAppPath);
await CreateNewProject.createNewProject(outputChannel, functionAppPath);
} else {
throw new errors.UserCancelledError();
}
Expand Down Expand Up @@ -108,31 +110,41 @@ export async function createFunction(
const functionAppPath: string = await workspaceUtil.selectWorkspaceFolder(ui, folderPlaceholder);
await validateIsFunctionApp(outputChannel, functionAppPath);

const localAppSettings: LocalAppSettings = new LocalAppSettings(ui, azureAccount, functionAppPath);

const templatePicks: PickWithData<Template>[] = (await templateData.getTemplates()).map((t: Template) => new PickWithData<Template>(t, t.name));
const templatePlaceHolder: string = localize('azFunc.selectFuncTemplate', 'Select a function template');
const template: Template = (await ui.showQuickPick<Template>(templatePicks, templatePlaceHolder)).data;

if (template.bindingType !== 'httpTrigger') {
await localAppSettings.validateAzureWebJobsStorage();
}

const name: string = await promptForFunctionName(ui, functionAppPath, template);

for (const settingName of template.userPromptedSettings) {
const setting: ConfigSetting | undefined = await templateData.getSetting(template.bindingType, settingName);
if (setting) {
const defaultValue: string | undefined = template.getSetting(settingName);
const settingValue: string | undefined = await promptForSetting(ui, localAppSettings, setting, defaultValue);

template.setSetting(settingName, settingValue);
}
const languageType: string = workspaceUtil.getProjectType(functionAppPath);
switch (languageType) {
case TemplateLanguage.Java:
// For Java function, using Maven for now.
runCommandInTerminal(`mvn azure-functions:add -f ${path.join(functionAppPath, 'pom.xml')} -B`);
break;
default:
const localAppSettings: LocalAppSettings = new LocalAppSettings(ui, azureAccount, functionAppPath);

const templatePicks: PickWithData<Template>[] = (await templateData.getTemplates()).map((t: Template) => new PickWithData<Template>(t, t.name));
const templatePlaceHolder: string = localize('azFunc.selectFuncTemplate', 'Select a function template');
const template: Template = (await ui.showQuickPick<Template>(templatePicks, templatePlaceHolder)).data;

if (template.bindingType !== 'httpTrigger') {
await localAppSettings.validateAzureWebJobsStorage();
}

const name: string = await promptForFunctionName(ui, functionAppPath, template);

for (const settingName of template.userPromptedSettings) {
const setting: ConfigSetting | undefined = await templateData.getSetting(template.bindingType, settingName);
if (setting) {
const defaultValue: string | undefined = template.getSetting(settingName);
const settingValue: string | undefined = await promptForSetting(ui, localAppSettings, setting, defaultValue);

template.setSetting(settingName, settingValue);
}
}

const functionPath: string = path.join(functionAppPath, name);
await template.writeTemplateFiles(functionPath);

const newFileUri: vscode.Uri = vscode.Uri.file(path.join(functionPath, 'index.js'));
vscode.window.showTextDocument(await vscode.workspace.openTextDocument(newFileUri));
break;
}

const functionPath: string = path.join(functionAppPath, name);
await template.writeTemplateFiles(functionPath);

const newFileUri: vscode.Uri = vscode.Uri.file(path.join(functionPath, 'index.js'));
vscode.window.showTextDocument(await vscode.workspace.openTextDocument(newFileUri));
}
92 changes: 85 additions & 7 deletions src/commands/createNewProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,107 @@ import * as path from 'path';
import * as vscode from 'vscode';
import * as FunctionsCli from '../functions-cli';
import { IUserInterface } from '../IUserInterface';
import { Pick } from '../IUserInterface';
import { localize } from '../localize';
import * as TemplateFiles from '../template-files';
import { TemplateLanguage } from '../templates/Template';
import * as fsUtil from '../utils/fs';
import * as workspaceUtil from '../utils/workspace';
import { VSCodeUI } from '../VSCodeUI';

export async function createNewProject(outputChannel: vscode.OutputChannel, ui: IUserInterface = new VSCodeUI()): Promise<void> {
const functionAppPath: string = await workspaceUtil.selectWorkspaceFolder(ui, localize('azFunc.selectFunctionAppFolderNew', 'Select the folder that will contain your function app'));
export async function createNewProject(outputChannel: vscode.OutputChannel, functionAppPath?: string, ui: IUserInterface = new VSCodeUI()): Promise<void> {
if (!functionAppPath) {
functionAppPath = await workspaceUtil.selectWorkspaceFolder(ui, localize('azFunc.selectFunctionAppFolderNew', 'Select the folder that will contain your function app'));
}

const languages: Pick[] = [
new Pick(TemplateLanguage.JavaScript),
new Pick(TemplateLanguage.Java)
];
const language: Pick = await ui.showQuickPick(languages, localize('azFunc.selectFuncTemplate', 'Select a language for your function project'));

let javaTargetPath: string = '';
switch (language.label) {
case TemplateLanguage.Java:
// Get parameters for Maven command
const { groupId, artifactId, version, packageName, appName } = await promotForMavenParameters(ui, functionAppPath);
// Use maven command to init Java function project.
await FunctionsCli.createNewProject(
outputChannel,
functionAppPath,
language.label,
'archetype:generate',
'-DarchetypeGroupId="com.microsoft.azure"',
'-DarchetypeArtifactId="azure-functions-archetype"',
`-DgroupId="${groupId}"`,
`-DartifactId="${artifactId}"`,
`-Dversion="${version}"`,
`-Dpackage="${packageName}"`,
`-DappName="${appName}"`,
'-B' // in Batch Mode
);

functionAppPath = path.join(functionAppPath, artifactId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user explicitly selected the functionAppPath above. I don't think we should change it after the fact

Copy link
Member Author

@jdneo jdneo Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because java function project that generated by the Maven archetype is different from JS function in terms of the folder structure.

For example, this is what it looks like when we use func init to create a function project
image

And this is what it looks like to use maven function archetype:
image

So the reason why join the path when it's a java function project is that we want to open the folder that contains the function related files (host.json, local.settings.json, etc.) also the pom.xml which is java specific.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above examples look like the same folder structure to me. It's just that in the top one the user selected D:\ as the functionAppPath and in the bottom one the user selected D:\myfunction as the functionAppPath. (I understand that func init and mvn deal with this slightly differently, but the user doesn't care about that internal detail. They just want the same behavior in both scenarios)

With your current code, I end up with an extra folder every time:
screen shot 2017-11-06 at 6 13 59 pm

I'm never going to put anything else in 'myFunctionProject'. It seems like rather than asking for the artifactId, you should just infer it from the parent directory name

Copy link
Member Author

@jdneo jdneo Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, what I mean is that the myfunction is the extra folder generated by the maven archetype. (In your case it's the folder: artifactId). The functionAppPath user selected in both senario is the same. Unfortunately, the maven archetype will always create the extra folder for the user.

How about treat it as you said, infer artifactId from the parent directory name, without asking user for it. And for Java function project, we still keep the path join logic here to ensure that the extension will open the right base folder for function project?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if you run the maven command from the parent folder - with the folder name as the artifactId? Wouldn't that solve the issue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail with what error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the screenshot:
image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, the error msg ouput by Maven is not that meaningful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright - please file an issue for this. I don't see an obvious solution, but I still think it's worth further investigation later

Copy link
Member Author

@jdneo jdneo Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I agree with you. I'll file an issue later.

javaTargetPath = `target/azure-functions/${appName}/`;
break;
default:
await FunctionsCli.createNewProject(outputChannel, functionAppPath, language.label, 'init');
break;
}

const tasksJsonPath: string = path.join(functionAppPath, '.vscode', 'tasks.json');
const tasksJsonExists: boolean = await fse.pathExists(tasksJsonPath);
const launchJsonPath: string = path.join(functionAppPath, '.vscode', 'launch.json');
const launchJsonExists: boolean = await fse.pathExists(launchJsonPath);

await FunctionsCli.createNewProject(outputChannel, functionAppPath);

if (!tasksJsonExists && !launchJsonExists) {
await fsUtil.writeFormattedJson(tasksJsonPath, TemplateFiles.tasksJson);
await fsUtil.writeFormattedJson(launchJsonPath, TemplateFiles.launchJson);
if (!tasksJsonExists || !launchJsonExists) {
await fse.ensureDir(path.join(functionAppPath, '.vscode'));
await Promise.all([
fsUtil.writeFormattedJson(tasksJsonPath, TemplateFiles.getTasksJson(language.label, javaTargetPath)),
fsUtil.writeFormattedJson(launchJsonPath, TemplateFiles.getLaunchJson(language.label))
]);
}

if (!workspaceUtil.isFolderOpenInWorkspace(functionAppPath)) {
// If the selected folder is not open in a workspace, open it now. NOTE: This may restart the extension host
await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(functionAppPath), false);
}
}

async function promotForMavenParameters(ui: IUserInterface, functionAppPath: string): Promise<IMavenParameters> {
const groupIdPlaceHolder: string = localize('azFunc.java.groupIdPlaceholder', 'Group ID');
const groupIdPrompt: string = localize('azFunc.java.groupIdPrompt', 'Provide value for groupId');
const groupId: string = await ui.showInputBox(groupIdPlaceHolder, groupIdPrompt, false, undefined, 'com.function');

const artifactIdPlaceHolder: string = localize('azFunc.java.artifactIdPlaceholder', 'Artifact ID');
const artifactIdprompt: string = localize('azFunc.java.artifactIdPrompt', 'Provide value for artifactId');
const artifactId: string = await ui.showInputBox(artifactIdPlaceHolder, artifactIdprompt, false, undefined, path.basename(functionAppPath));

const versionPlaceHolder: string = localize('azFunc.java.versionPlaceHolder', 'Version');
const versionPrompt: string = localize('azFunc.java.versionPrompt', 'Provide value for version');
const version: string = await ui.showInputBox(versionPlaceHolder, versionPrompt, false, undefined, '1.0-SNAPSHOT');

const packagePlaceHolder: string = localize('azFunc.java.packagePlaceHolder', 'Package');
const packagePrompt: string = localize('azFunc.java.packagePrompt', 'Provide value for package');
const packageName: string = await ui.showInputBox(packagePlaceHolder, packagePrompt, false, undefined, groupId);

const appNamePlaceHolder: string = localize('azFunc.java.appNamePlaceHolder', 'App Name');
const appNamePrompt: string = localize('azFunc.java.appNamePrompt', 'Provide value for appName');
const appName: string = await ui.showInputBox(appNamePlaceHolder, appNamePrompt, false, undefined, `${artifactId}-${Date.now()}`);

return {
groupId: groupId,
artifactId: artifactId,
version: version,
packageName: packageName,
appName: appName
};
}

interface IMavenParameters {
groupId: string;
artifactId: string;
version: string;
packageName: string;
appName: string;
}
10 changes: 6 additions & 4 deletions src/functions-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@
import * as cp from 'child_process';
import * as vscode from 'vscode';
import { localize } from './localize';
import { TemplateLanguage } from './templates/Template';

// tslint:disable-next-line:export-name
export async function createNewProject(outputChannel: vscode.OutputChannel, workingDirectory: string): Promise<void> {
await executeCommand(outputChannel, workingDirectory, 'init');
export async function createNewProject(outputChannel: vscode.OutputChannel, workingDirectory: string, languageName: string, ...args: string[]): Promise<void> {
const command: string = languageName === TemplateLanguage.Java ? 'mvn' : 'func';
await executeCommand(outputChannel, workingDirectory, command, ...args);
}

async function executeCommand(outputChannel: vscode.OutputChannel, workingDirectory: string, ...args: string[]): Promise<void> {
async function executeCommand(outputChannel: vscode.OutputChannel, workingDirectory: string, command: string, ...args: string[]): Promise<void> {
await new Promise((resolve: () => void, reject: (e: Error) => void): void => {
const options: cp.SpawnOptions = {
cwd: workingDirectory,
shell: true
};
const childProc: cp.ChildProcess = cp.spawn('func', args, options);
const childProc: cp.ChildProcess = cp.spawn(command, args, options);
let stderr: string = '';
childProc.stdout.on('data', (data: string | Buffer) => outputChannel.append(data.toString()));
childProc.stderr.on('data', (data: string | Buffer) => stderr = stderr.concat(data.toString()));
Expand Down
51 changes: 49 additions & 2 deletions src/template-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
*--------------------------------------------------------------------------------------------*/

import { localize } from './localize';
import { TemplateLanguage } from './templates/Template';

const taskId: string = 'launchFunctionApp';
export const tasksJson: {} = {

const tasksJson: {} = {
version: '2.0.0',
tasks: [
{
Expand Down Expand Up @@ -40,7 +42,7 @@ export const tasksJson: {} = {
]
};

export const launchJson: {} = {
const launchJsonForJavascript: {} = {
version: '0.2.0',
configurations: [
{
Expand All @@ -53,3 +55,48 @@ export const launchJson: {} = {
}
]
};

const launchJsonForJava: {} = {
version: '0.2.0',
configurations: [
{
name: localize('azFunc.attachToFunc', 'Attach to Azure Functions'),
type: 'java',
request: 'attach',
hostName: 'localhost',
port: 5005,
preLaunchTask: taskId
}
]
};

const mavenCleanPackageTask: {} = {
taskName: 'Maven Clean Package',
type: 'shell',
command: 'mvn clean package',
isBackground: true
};

export function getLaunchJson(language: string): object {
switch (language) {
case TemplateLanguage.Java:
return launchJsonForJava;
default:
return launchJsonForJavascript;
}
}

export function getTasksJson(language: string, args: string): object {
switch (language) {
case TemplateLanguage.Java:
/* tslint:disable:no-string-literal no-unsafe-any */
const taskJsonForJava: {} = JSON.parse(JSON.stringify(tasksJson)); // deep clone
taskJsonForJava['tasks'][0].command += ` --script-root ${args}`;
taskJsonForJava['tasks'][0].dependsOn = ['Maven Clean Package'];
taskJsonForJava['tasks'].push(mavenCleanPackageTask);

return taskJsonForJava;
default:
return tasksJson;
}
}
3 changes: 2 additions & 1 deletion src/templates/Template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ interface ITemplateMetadata {
}

export enum TemplateLanguage {
JavaScript = 'JavaScript'
JavaScript = 'JavaScript',
Java = 'Java'
}

export enum TemplateCategory {
Expand Down
14 changes: 14 additions & 0 deletions src/utils/terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';

const terminal: vscode.Terminal = vscode.window.createTerminal('Azure Functions');

// tslint:disable-next-line:export-name
export function runCommandInTerminal(command: string, addNewLine: boolean = true): void {
terminal.show();
terminal.sendText(command, addNewLine);
}
19 changes: 15 additions & 4 deletions src/utils/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as fse from 'fs-extra';
import * as path from 'path';
import * as vscode from 'vscode';
import { IUserInterface, PickWithData } from '../IUserInterface';
import { localize } from '../localize';
import { TemplateLanguage } from '../templates/Template';

export async function selectWorkspaceFolder(ui: IUserInterface, placeholder: string): Promise<string> {
const browse: string = ':browse';
Expand All @@ -23,16 +25,25 @@ export async function selectWorkspaceFolder(ui: IUserInterface, placeholder: str

export function isFolderOpenInWorkspace(fsPath: string): boolean {
if (vscode.workspace.workspaceFolders) {
if (!fsPath.endsWith(path.sep)) {
fsPath = fsPath + path.sep;
}

const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.workspaceFolders.find((f: vscode.WorkspaceFolder): boolean => {
return fsPath.startsWith(f.uri.fsPath);
return path.relative(fsPath, f.uri.fsPath) === '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm - this definitely fixes some edge cases, but it's not quite what I originally intended the behavior to be. This function should return true even if the path is a subfolder of a workspace. Maybe something like this would work?:

const relativePath: string = path.relative(fsPath, f.uri.fsPath);
return relativePath === '' || !relativePath.startsWith('..');

This logic can be tricky with lots of edge cases, so I'd prefer to add some unit tests when we fix this. You can do that in this PR or file an issue to fix this separately (I don't mind fixing this myself)

Copy link
Member Author

@jdneo jdneo Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is if the subfolder is not opened, the user might not able to launch it since the cwd might not have .vscode folder which contains launch.json and task.json.

Another concern is that as what we discussed above. The maven archetype will always create a artifactId folder first, and put function things into the folder. For Java, there is another important file: pom.xml, will also be put into the artifactId folder. The maven command can be execute only if the cwd contains pom.xml or specify its location in parameter. That's why we prefer to open the folder which contains pom.xml (that is the artifactId folder).

I quite agree that always open the project base path might not be the best solution for this. While I think dong this it not that harmful. What about keeping this change for now and file a issue and we can do more further discussion in detail?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah filing an issue is fine

Copy link
Member Author

@jdneo jdneo Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great

});

return folder !== undefined;
} else {
return false;
}
}

export function getProjectType(projectPath: string): string {
let language: string = TemplateLanguage.JavaScript;
fse.readdirSync(projectPath).forEach((file: string) => {
const stat: fse.Stats = fse.statSync(path.join(projectPath, file));
// Currently checking the existing pom.xml to determine whether the function project is Java language based.
if (stat.isFile() && file === 'pom.xml') {
language = TemplateLanguage.Java;
}
});
return language;
}