diff --git a/.vscode/launch.json b/.vscode/launch.json index 3d316e1927ef..6075e276b7a0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -274,6 +274,17 @@ }, "cwd": "${workspaceFolder}/test-integration/samples/app-sample-dev/", "console": "integratedTerminal" - } + }, + { + "type": "node", + "request": "launch", + "name": "jhipster openapi-client", + "program": "${workspaceFolder}/cli/jhipster.js", + "args": [ + "openapi-client" + ], + "cwd": "${workspaceFolder}/test-integration/samples/app-sample-dev/", + "console": "integratedTerminal" + }, ] } diff --git a/cli/commands.js b/cli/commands.js index cd8952375e36..d4b21efe0744 100644 --- a/cli/commands.js +++ b/cli/commands.js @@ -135,6 +135,9 @@ Example: argument: ['name'], desc: 'Create a new Spring controller' }, + 'openapi-client': { + desc: 'Generates java client code from an OpenAPI/Swagger definition' + }, upgrade: { desc: 'Upgrade the JHipster version, and upgrade the generated application' } diff --git a/generators/client/templates/angular/package.json.ejs b/generators/client/templates/angular/package.json.ejs index 8cb75015905b..0dfd72fc1247 100644 --- a/generators/client/templates/angular/package.json.ejs +++ b/generators/client/templates/angular/package.json.ejs @@ -147,6 +147,7 @@ <%_ if (protractorTests) { _%> "webdriver-manager": "12.1.6", <%_ } _%> + "@openapitools/openapi-generator-cli": "0.0.14-4.0.2", "webpack": "4.39.3", "webpack-cli": "3.3.7", "webpack-dev-server": "3.8.0", diff --git a/generators/client/templates/react/package.json.ejs b/generators/client/templates/react/package.json.ejs index 2c17084f2cd0..daad7a78385d 100644 --- a/generators/client/templates/react/package.json.ejs +++ b/generators/client/templates/react/package.json.ejs @@ -166,6 +166,7 @@ limitations under the License. <%_ if (protractorTests) { _%> "webdriver-manager": "12.1.5", <%_ } _%> + "@openapitools/openapi-generator-cli": "0.0.14-4.0.2", "webpack": "4.28.4", "webpack-cli": "3.3.0", "webpack-dev-server": "3.2.1", diff --git a/generators/openapi-client/USAGE b/generators/openapi-client/USAGE new file mode 100644 index 000000000000..53935d5a365a --- /dev/null +++ b/generators/openapi-client/USAGE @@ -0,0 +1,5 @@ +Description: + Generates java client code from an OpenAPI/Swagger definition + +Example: + jhipster openapi-client diff --git a/generators/openapi-client/files.js b/generators/openapi-client/files.js new file mode 100644 index 000000000000..3ae5fdc687cd --- /dev/null +++ b/generators/openapi-client/files.js @@ -0,0 +1,173 @@ +/** + * Copyright 2013-2019 the original author or authors from the JHipster project. + * + * This file is part of the JHipster project, see https://www.jhipster.tech/ + * for more information. + * + * Licensed 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. + */ + +const path = require('path'); +const shelljs = require('shelljs'); +const _ = require('lodash'); +const chalk = require('chalk'); +const jhipsterConstants = require('../generator-constants'); + +module.exports = { + writeFiles +}; + +function writeFiles() { + return { + callOpenApiGenerator() { + this.baseName = this.config.get('baseName'); + this.authenticationType = this.config.get('authenticationType'); + this.packageName = this.config.get('packageName'); + this.clientPackageManager = this.config.get('clientPackageManager'); + this.packageFolder = this.config.get('packageFolder'); + this.buildTool = this.config.get('buildTool'); + + this.javaDir = `${jhipsterConstants.SERVER_MAIN_SRC_DIR + this.packageFolder}/`; + + if (Object.keys(this.clientsToGenerate).length === 0) { + this.log('No openapi client configured. Please run "jhipster openapi-client" to generate your first OpenAPI client.'); + return; + } + + Object.keys(this.clientsToGenerate).forEach(cliName => { + const inputSpec = this.clientsToGenerate[cliName].spec; + const generatorName = this.clientsToGenerate[cliName].generatorName; + + // using openapi jar file since so this section can be tested + const jarPath = path.resolve('node_modules', '@openapitools', 'openapi-generator-cli', 'bin', 'openapi-generator.jar'); + let JAVA_OPTS; + let command; + if (generatorName === 'spring') { + this.log(chalk.green(`\n\nGenerating java client code for client ${cliName} (${inputSpec})`)); + const cliPackage = `${this.packageName}.client.${_.snakeCase(cliName)}`; + const clientPackageLocation = path.resolve('src', 'main', 'java', ...cliPackage.split('.')); + if (shelljs.test('-d', clientPackageLocation)) { + this.log(`cleanup generated java code for client ${cliName} in directory ${clientPackageLocation}`); + shelljs.rm('-rf', clientPackageLocation); + } + + JAVA_OPTS = ' -Dmodels -Dapis -DsupportingFiles=ApiKeyRequestInterceptor.java,ClientConfiguration.java '; + + let params = + ' generate -g spring ' + + ` -t ${path.resolve(__dirname, 'templates/swagger-codegen/libraries/spring-cloud')} ` + + ' --library spring-cloud ' + + ` -i ${inputSpec} --artifact-id ${_.camelCase(cliName)} --api-package ${cliPackage}.api` + + ` --model-package ${cliPackage}.model` + + ' --type-mappings DateTime=OffsetDateTime,Date=LocalDate ' + + ' --import-mappings OffsetDateTime=java.time.OffsetDateTime,LocalDate=java.time.LocalDate' + + ` -DdateLibrary=custom,basePackage=${this.packageName}.client,configPackage=${cliPackage},` + + `title=${_.camelCase(cliName)}`; + + if (this.clientsToGenerate[cliName].useServiceDiscovery) { + params += ' --additional-properties ribbon=true'; + } + + command = `java ${JAVA_OPTS} -jar ${jarPath} ${params}`; + } + this.log(`\n${command}`); + + const done = this.async(); + shelljs.exec(command, { silent: this.silent }, (code, msg, err) => { + if (code === 0) { + this.success(`Succesfully generated ${cliName} ${generatorName} client`); + done(); + } else { + this.error(`Something went wrong while generating ${cliName} ${generatorName} client: ${msg} ${err}`); + done(); + } + }); + }); + }, + + addBackendDependencies() { + if (!_.map(this.clientsToGenerate, 'generatorName').includes('spring')) { + return; + } + + if (this.buildTool === 'maven') { + if (!['microservice', 'gateway', 'uaa'].includes(this.applicationType)) { + let exclusions; + if (this.authenticationType === 'session') { + exclusions = + ' \n' + + ' \n' + + ' org.springframework.cloud\n' + + ' spring-cloud-starter-ribbon\n' + + ' \n' + + ' '; + } + this.addMavenDependency('org.springframework.cloud', 'spring-cloud-starter-openfeign', null, exclusions); + } + this.addMavenDependency('org.springframework.cloud', 'spring-cloud-starter-oauth2'); + } else if (this.buildTool === 'gradle') { + if (!['microservice', 'gateway', 'uaa'].includes(this.applicationType)) { + if (this.authenticationType === 'session') { + const content = + "compile 'org.springframework.cloud:spring-cloud-starter-openfeign', { exclude group: 'org.springframework.cloud', module: 'spring-cloud-starter-ribbon' }"; + this.rewriteFile('./build.gradle', 'jhipster-needle-gradle-dependency', content); + } else { + this.addGradleDependency('compile', 'org.springframework.cloud', 'spring-cloud-starter-openfeign'); + } + } + this.addGradleDependency('compile', 'org.springframework.cloud', 'spring-cloud-starter-oauth2'); + } + }, + + enableFeignClients() { + if (!_.map(this.clientsToGenerate, 'generatorName').includes('spring')) { + return; + } + + const mainClassFile = `${this.javaDir + this.getMainClassName()}.java`; + + if (this.applicationType !== 'microservice' || !['uaa', 'jwt'].includes(this.authenticationType)) { + this.rewriteFile( + mainClassFile, + 'import org.springframework.core.env.Environment;', + 'import org.springframework.cloud.openfeign.EnableFeignClients;' + ); + this.rewriteFile(mainClassFile, '@SpringBootApplication', '@EnableFeignClients'); + } + }, + + handleComponentScanExclusion() { + if (!_.map(this.clientsToGenerate, 'generatorName').includes('spring')) { + return; + } + + const mainClassFile = `${this.javaDir + this.getMainClassName()}.java`; + + this.rewriteFile( + mainClassFile, + 'import org.springframework.core.env.Environment;', + 'import org.springframework.context.annotation.ComponentScan;' + ); + + const componentScan = + `${'@ComponentScan( excludeFilters = {\n @ComponentScan.Filter('}${this.packageName}` + + '.client.ExcludeFromComponentScan.class)\n})'; + this.rewriteFile(mainClassFile, '@SpringBootApplication', componentScan); + + this.template( + 'src/main/java/package/client/_ExcludeFromComponentScan.java', + `${this.javaDir}/client/ExcludeFromComponentScan.java` + ); + } + }; +} diff --git a/generators/openapi-client/index.js b/generators/openapi-client/index.js new file mode 100644 index 000000000000..9de41025054a --- /dev/null +++ b/generators/openapi-client/index.js @@ -0,0 +1,94 @@ +/** + * Copyright 2013-2019 the original author or authors from the JHipster project. + * + * This file is part of the JHipster project, see https://www.jhipster.tech/ + * for more information. + * + * Licensed 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. + */ + +const chalk = require('chalk'); +const BaseGenerator = require('../generator-base'); +const prompts = require('./prompts'); +const writeFiles = require('./files').writeFiles; + +module.exports = class extends BaseGenerator { + constructor(args, opts) { + super(args, opts); + this.option('regen', { + desc: 'Regenerates all saved clients', + type: Boolean, + defaults: false + }); + this.registerPrettierTransform(); + } + + get initializing() { + return { + validateFromCli() { + this.checkInvocationFromCLI(); + }, + sayHello() { + // Have Yeoman greet the user. + this.log(chalk.white('Welcome to the JHipster OpenApi client Sub-Generator')); + }, + getConfig() { + this.openApiClients = this.config.get('openApiClients') || {}; + } + }; + } + + get prompting() { + return { + askActionType: prompts.askActionType, + askExistingAvailableDocs: prompts.askExistingAvailableDocs, + askGenerationInfos: prompts.askGenerationInfos + }; + } + + get configuring() { + return { + determineApisToGenerate() { + this.clientsToGenerate = {}; + if (this.options.regen || this.props.action === 'all') { + this.clientsToGenerate = this.openApiClients; + } else if (this.props.action === 'new' || this.props.action === undefined) { + this.clientsToGenerate[this.props.cliName] = { + spec: this.props.inputSpec, + useServiceDiscovery: this.props.useServiceDiscovery, + generatorName: this.props.generatorName + }; + } else if (this.props.action === 'select') { + this.props.selected.forEach(selection => { + this.clientsToGenerate[selection.cliName] = selection.spec; + }); + } + }, + + saveConfig() { + if (!this.options.regen && this.props.saveConfig) { + this.openApiClients[this.props.cliName] = this.clientsToGenerate[this.props.cliName]; + this.config.set('openApiClients', this.openApiClients); + } + } + }; + } + + get writing() { + return writeFiles(); + } + + end() { + this.log('End of openapi-client generator'); + } +}; diff --git a/generators/openapi-client/prompts.js b/generators/openapi-client/prompts.js new file mode 100644 index 000000000000..ec4c4409dd55 --- /dev/null +++ b/generators/openapi-client/prompts.js @@ -0,0 +1,266 @@ +/** + * Copyright 2013-2019 the original author or authors from the JHipster project. + * + * This file is part of the JHipster project, see https://www.jhipster.tech/ + * for more information. + * + * Licensed 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. + */ +const path = require('path'); +const shelljs = require('shelljs'); +const request = require('sync-request'); + +module.exports = { + askActionType, + askExistingAvailableDocs, + askGenerationInfos +}; + +function fetchSwaggerResources(input) { + const availableDocs = []; + + const baseUrl = input.replace(/\/$/, ''); + const swaggerResources = request('GET', `${baseUrl}/swagger-resources`, { + // This header is needed to use the custom /swagger-resources controller + // and not the default one that has only the gateway's swagger resource + headers: { Accept: 'application/json, text/javascript;' } + }); + + JSON.parse(swaggerResources.getBody()).forEach(swaggerResource => { + const specPath = swaggerResource.location.replace(/^\/+/g, ''); + availableDocs.push({ + value: { url: `${baseUrl}/${specPath}`, name: swaggerResource.name }, + name: `${swaggerResource.name} (${swaggerResource.location})` + }); + }); + + return availableDocs; +} + +function askActionType() { + if (this.options.regen) { + return; + } + + const done = this.async(); + + const hasExistingApis = Object.keys(this.openApiClients).length !== 0; + + const actionList = [ + { + value: 'new', + name: 'Generate a new API client' + } + ]; + + if (hasExistingApis) { + actionList.push({ value: 'all', name: 'Generate all stored API clients' }); + actionList.push({ value: 'select', name: 'Select stored API clients to generate' }); + } + + const newClient = actionList.length === 1; + + const prompts = [ + { + when: !newClient, + type: 'list', + name: 'action', + message: 'What do you want to do ?', + choices: actionList + }, + { + when: response => response.action === 'new' || newClient, + type: 'list', + name: 'specOrigin', + message: 'Where do you want to import your OpenAPI/Swagger specification from ?', + choices: [ + { value: 'jhipster-endpoint', name: 'From a Jhipster /swagger-resources live doc endpoint' }, + { value: 'jhipster-directory', name: 'From the api.yml spec of an existing Jhipster project' }, + { value: 'custom-endpoint', name: 'From a custom specification file or endpoint' } + ] + }, + { + when: response => response.specOrigin === 'jhipster-endpoint', + type: 'input', + name: 'jhipsterEndpoint', + message: 'Enter the URL of the running Jhipster instance', + default: 'http://localhost:8080', + validate: input => { + try { + const availableDocs = fetchSwaggerResources(input); + + if (availableDocs.length === 0) { + return `No live doc found at ${input}`; + } + return true; + } catch (err) { + return `Error while fetching live doc from '${input}'. "${err.message}"`; + } + } + }, + { + when: response => response.specOrigin === 'jhipster-directory', + type: 'input', + name: 'jhipsterDirectory', + message: 'Enter the path to the jhipster project root directory', + default: '../', + validate: input => { + let fromPath; + if (path.isAbsolute(input)) { + fromPath = `${input}/src/main/resources/swagger/api.yml`; + } else { + fromPath = this.destinationPath(`${input}/src/main/resources/swagger/api.yml`); + } + + if (shelljs.test('-f', fromPath)) { + return true; + } + return `api.yml not found in ${input}/`; + } + }, + { + when: response => response.specOrigin === 'custom-endpoint', + type: 'input', + name: 'customEndpoint', + message: 'Where is your Swagger/OpenAPI spec (URL or path) ?', + default: 'http://petstore.swagger.io/v2/swagger.json', + store: true, + validate: input => { + try { + if (/^((http|https):\/\/)/.test(input)) { + request('GET', `${input}`, { + // headers: { Accept: 'application/json, text/javascript;' } + }); + } else if (!shelljs.test('-f', input)) { + return `file '${input}' not found`; + } + return true; + } catch (err) { + return `Cannot read from ${input}. ${err.message}`; + } + } + } + ]; + + this.prompt(prompts).then(props => { + if (props.jhipsterEndpoint !== undefined) { + props.availableDocs = fetchSwaggerResources(props.jhipsterEndpoint); + } else if (props.jhipsterDirectory !== undefined) { + props.inputSpec = `${props.jhipsterDirectory}/src/main/resources/swagger/api.yml`; + } else if (props.customEndpoint !== undefined) { + props.inputSpec = props.customEndpoint; + } + + if (newClient) { + props.action = 'new'; + } + + props.generatorName = 'spring'; + + this.props = props; + done(); + }); +} + +function askExistingAvailableDocs() { + if (this.options.regen) { + return; + } + const done = this.async(); + + const prompts = [ + { + when: !this.options.regen && this.props.availableDocs !== undefined, + type: 'list', + name: 'availableDoc', + message: 'Select the doc for which you want to create a client', + choices: this.props.availableDocs + } + ]; + + this.prompt(prompts).then(props => { + if (props.availableDoc !== undefined) { + this.props.inputSpec = props.availableDoc.url; + this.props.cliName = props.availableDoc.name; + } + done(); + }); +} + +function askGenerationInfos() { + if (this.options.regen) { + return; + } + + const done = this.async(); + const prompts = [ + { + when: + this.props.specOrigin === 'jhipster-endpoint' && + this.config.get('serviceDiscoveryType') === 'eureka' && + this.props.generatorName === 'spring', + type: 'confirm', + name: 'useServiceDiscovery', + message: 'Do you want to use Eureka service discovery ?', + default: true + }, + { + when: response => this.props.action === 'new' && !response.useServiceDiscovery, + type: 'input', + name: 'cliName', + message: 'What is the unique name for your API client ?', + default: this.props.cliName || 'petstore', + validate: input => { + if (!/^([a-zA-Z0-9_]*)$/.test(input)) { + return 'Your API client name cannot contain special characters or a blank space'; + } + if (input === '') { + return 'Your API client name cannot be empty'; + } + return true; + } + }, + { + when: this.props.action === 'new', + type: 'confirm', + name: 'saveConfig', + message: 'Do you want to save this config for future reuse ?', + default: false + }, + { + when: this.props.action === 'select', + type: 'checkbox', + name: 'selected', + message: 'Select which APIs you want to generate', + choices: () => { + const choices = []; + Object.keys(this.openApiClients).forEach(cliName => { + choices.push({ + name: `${cliName} (${this.openApiClients[cliName].spec})`, + value: { cliName, spec: this.openApiClients[cliName] } + }); + }); + return choices; + } + } + ]; + + this.prompt(prompts).then(props => { + if (props.cliName !== undefined) { + this.props.cliName = props.cliName; + } + this.props.saveConfig = props.saveConfig; + this.props.selected = props.selected; + done(); + }); +} diff --git a/generators/openapi-client/templates/src/main/java/package/client/_ExcludeFromComponentScan.java b/generators/openapi-client/templates/src/main/java/package/client/_ExcludeFromComponentScan.java new file mode 100644 index 000000000000..c47bdad39fa1 --- /dev/null +++ b/generators/openapi-client/templates/src/main/java/package/client/_ExcludeFromComponentScan.java @@ -0,0 +1,11 @@ +package <%=packageName%>.client; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ExcludeFromComponentScan { +} \ No newline at end of file diff --git a/generators/openapi-client/templates/swagger-codegen/libraries/spring-cloud/apiClient.mustache b/generators/openapi-client/templates/swagger-codegen/libraries/spring-cloud/apiClient.mustache new file mode 100644 index 000000000000..75e906bbefcf --- /dev/null +++ b/generators/openapi-client/templates/swagger-codegen/libraries/spring-cloud/apiClient.mustache @@ -0,0 +1,10 @@ +package {{package}}; + +import org.springframework.cloud.openfeign.FeignClient; +import {{configPackage}}.ClientConfiguration; + +{{=<% %>=}} +@FeignClient(name="${<%title%>.name:<%title%>}", url="${<%title%>.url:<%^ribbon%><%basePath%><%/ribbon%>}", configuration = ClientConfiguration.class) +<%={{ }}=%> +public interface {{classname}}Client extends {{classname}} { +} \ No newline at end of file diff --git a/generators/openapi-client/templates/swagger-codegen/libraries/spring-cloud/clientConfiguration.mustache b/generators/openapi-client/templates/swagger-codegen/libraries/spring-cloud/clientConfiguration.mustache new file mode 100644 index 000000000000..4d98f276f75b --- /dev/null +++ b/generators/openapi-client/templates/swagger-codegen/libraries/spring-cloud/clientConfiguration.mustache @@ -0,0 +1,106 @@ +package {{configPackage}}; + +import {{basePackage}}.ExcludeFromComponentScan; +import feign.auth.BasicAuthRequestInterceptor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor; +import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; +import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitResourceDetails; +import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails; + +@Configuration +@ExcludeFromComponentScan +@EnableConfigurationProperties +public class ClientConfiguration { + +{{#authMethods}} + {{#isBasic}} + {{=<% %>=}} + @Value("${<%title%>.security.<%name%>.username:}") + private String <%name%>Username; + + @Value("${<%title%>.security.<%name%>.password:}") + private String <%name%>Password; + <%={{ }}=%> + + @Bean + @ConditionalOnProperty(name = "{{{title}}}.security.{{{name}}}.username") + public BasicAuthRequestInterceptor {{{name}}}RequestInterceptor() { + return new BasicAuthRequestInterceptor(this.{{{name}}}Username, this.{{{name}}}Password); + } + + {{/isBasic}} + {{#isApiKey}} + @Value("${ {{{title}}}.security.{{{name}}}.key:}") + private String {{{name}}}Key; + + @Bean + @ConditionalOnProperty(name = "{{{title}}}.security.{{{name}}}.key") + public ApiKeyRequestInterceptor {{{name}}}RequestInterceptor() { + return new ApiKeyRequestInterceptor({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{{keyParamName}}}", this.{{{name}}}Key); + } + + {{/isApiKey}} + {{#isOAuth}} + @Bean + @ConditionalOnProperty("{{{title}}}.security.{{{name}}}.client-id") + public OAuth2FeignRequestInterceptor {{{name}}}RequestInterceptor() { + return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), {{{name}}}ResourceDetails()); + } + + {{#isCode}} + @Bean + @ConditionalOnProperty("{{{title}}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{{title}}}.security.{{{name}}}") + public AuthorizationCodeResourceDetails {{{name}}}ResourceDetails() { + AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails(); + details.setAccessTokenUri("{{{tokenUrl}}}"); + details.setUserAuthorizationUri("{{{authorizationUrl}}}"); + return details; + } + + {{/isCode}} + {{#isPassword}} + @Bean + @ConditionalOnProperty("{{{title}}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{{title}}}.security.{{{name}}}") + public ResourceOwnerPasswordResourceDetails {{{name}}}ResourceDetails() { + ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails(); + details.setAccessTokenUri("{{{tokenUrl}}}"); + return details; + } + + {{/isPassword}} + {{#isApplication}} + @Bean + @ConditionalOnProperty("{{{title}}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{{title}}}.security.{{{name}}}") + public ClientCredentialsResourceDetails {{{name}}}ResourceDetails() { + ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails(); + details.setAccessTokenUri("{{{tokenUrl}}}"); + return details; + } + + {{/isApplication}} + {{#isImplicit}} + @Bean + @ConditionalOnProperty("{{{title}}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{{title}}}.security.{{{name}}}") + public ImplicitResourceDetails {{{name}}}ResourceDetails() { + ImplicitResourceDetails details = new ImplicitResourceDetails(); + details.setUserAuthorizationUri("{{{authorizationUrl}}}"); + return details; + } + + {{/isImplicit}} + {{/isOAuth}} +{{/authMethods}} +} diff --git a/generators/server/templates/package.json.ejs b/generators/server/templates/package.json.ejs index 94c656a8a48b..c02951e2394f 100644 --- a/generators/server/templates/package.json.ejs +++ b/generators/server/templates/package.json.ejs @@ -29,7 +29,8 @@ <%_ otherModules.forEach(module => { _%> "<%= module.name %>": "<%= module.version %>", <%_ }); _%> - "generator-jhipster": "<%= jhipsterVersion %>" + "generator-jhipster": "<%= jhipsterVersion %>", + "@openapitools/openapi-generator-cli": "0.0.14-4.0.2" }, "engines": { "node": ">=8.9.0" diff --git a/package-lock.json b/package-lock.json index 7a6ed8f7f73f..e101cc34b95d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,12 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "@openapitools/openapi-generator-cli": { + "version": "0.0.14-4.0.2", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-0.0.14-4.0.2.tgz", + "integrity": "sha512-x4gKaGNgG5Eqt43B4V814Bp0zUBT5h9/K0Tz4oTi5wGy+5sqU5/gElKys0jmM6wvEio2OXJm728BIFuKPuFi5w==", + "dev": true + }, "@sindresorhus/is": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", @@ -85,6 +91,32 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@types/concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=", + "requires": { + "@types/node": "*" + } + }, + "@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "10.14.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.17.tgz", + "integrity": "sha512-p/sGgiPaathCfOtqu2fx5Mu1bcjuP8ALFg4xpGgNkcin7LwRyzUKniEHBKdcE1RPsenq5JVPIpMTJSygLboygQ==" + }, + "@types/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-Jugo5V/1bS0fRhy2z8+cUAHEyWOATaz4rbyLVvcFs7+dXp5HfwpEwzF1Q11bB10ApUqHf+yTauxI0UXQDwGrbA==" + }, "acorn": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", @@ -289,6 +321,11 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -2418,6 +2455,11 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" + }, "get-stdin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", @@ -2733,11 +2775,30 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" }, + "http-basic": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz", + "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==", + "requires": { + "caseless": "^0.12.0", + "concat-stream": "^1.6.2", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + } + }, "http-cache-semantics": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" }, + "http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "requires": { + "@types/node": "^10.0.3" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -4604,6 +4665,11 @@ "callsites": "^3.0.0" } }, + "parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=" + }, "parse-gitignore": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-1.0.1.tgz", @@ -4837,6 +4903,14 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.3.tgz", + "integrity": "sha512-HeRDUL1RJiLhyA0/grn+PTShlBAcLuh/1BJGtrvjwbvRDCTLLMEz9rOGCV+R3vHY4MixIuoMEd9Yq/XvsTPcjw==", + "requires": { + "asap": "~2.0.6" + } + }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -5829,6 +5903,24 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" }, + "sync-request": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.0.0.tgz", + "integrity": "sha512-jGNIAlCi9iU4X3Dm4oQnNQshDD3h0/1A7r79LyqjbjUnj69sX6mShAXlhRXgImsfVKtTcnra1jfzabdZvp+Lmw==", + "requires": { + "http-response-object": "^3.0.1", + "sync-rpc": "^1.2.1", + "then-request": "^6.0.0" + } + }, + "sync-rpc": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", + "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", + "requires": { + "get-port": "^3.1.0" + } + }, "syntax-error": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", @@ -6145,6 +6237,31 @@ "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-2.4.0.tgz", "integrity": "sha512-qftQXnX1DzpSV8EddtHIT0eDDEiBF8ywhFYR2lI9xrGtxqKN+CvLXhACeCIGbCpQfxxERbrkZEFb8cZcDKbVZA==" }, + "then-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", + "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==", + "requires": { + "@types/concat-stream": "^1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "^8.0.0", + "@types/qs": "^6.2.31", + "caseless": "~0.12.0", + "concat-stream": "^1.6.0", + "form-data": "^2.2.0", + "http-basic": "^8.1.1", + "http-response-object": "^3.0.1", + "promise": "^8.0.0", + "qs": "^6.4.0" + }, + "dependencies": { + "@types/node": { + "version": "8.10.53", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.53.tgz", + "integrity": "sha512-aOmXdv1a1/vYUn1OT1CED8ftbkmmYbKhKGSyMDeJiidLvKRKvZUQOdXwG/wcNY7T1Qb0XTlVdiYjIq00U7pLrQ==" + } + } + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index b878748e91fc..ba1d6092675b 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "tabtab": "2.2.2", "through2": "3.0.1", "uuid": "3.3.2", + "sync-request": "6.0.0", "yeoman-environment": "2.3.4", "yeoman-generator": "3.2.0", "yo": "3.1.0" @@ -92,7 +93,8 @@ "mocha": "6.1.4", "sinon": "7.2.5", "yeoman-assert": "3.1.1", - "yeoman-test": "1.9.1" + "yeoman-test": "1.9.1", + "@openapitools/openapi-generator-cli": "0.0.14-4.0.2" }, "resolutions": { "inquirer": "6.3.1" diff --git a/test/openapi-client.spec.js b/test/openapi-client.spec.js new file mode 100644 index 000000000000..123bfd9f2c6c --- /dev/null +++ b/test/openapi-client.spec.js @@ -0,0 +1,89 @@ +const path = require('path'); +const assert = require('yeoman-assert'); +const helpers = require('yeoman-test'); +const fse = require('fs-extra'); + +const basePackage = 'src/main/java/com/mycompany/myapp'; +const expectedFiles = { + petstoreClientFiles: [ + `${basePackage}/client/petstore/ApiKeyRequestInterceptor.java`, + `${basePackage}/client/petstore/ClientConfiguration.java`, + `${basePackage}/client/petstore/api/PetsApi.java`, + `${basePackage}/client/petstore/api/PetsApiClient.java`, + `${basePackage}/client/petstore/model/Error.java`, + `${basePackage}/client/petstore/model/Pet.java` + ] +}; + +describe('JHipster OpenAPI Client Sub Generator', () => { + //-------------------------------------------------- + // Spring Cloud Client tests + //-------------------------------------------------- + describe('Spring: microservice petstore custom endpoint ', () => { + before(done => { + helpers + .run(require.resolve('../generators/openapi-client')) + .inTmpDir(dir => { + fse.copySync(path.join(__dirname, './templates/openapi-client/microservice-simple'), dir); + fse.copySync(path.join(__dirname, './templates/openapi-client'), dir); + fse.copySync(path.join(__dirname, '../node_modules/@openapitools'), `${dir}/node_modules/@openapitools`); + }) + .withOptions({ skipChecks: true }) + .withPrompts({ + action: 'new', + specOrigin: 'custom-endpoint', + customEndpoint: 'petstore-openapi-3.yml', + cliName: 'petstore' + }) + .on('end', done); + }); + it('creates java client files', () => { + assert.file(expectedFiles.petstoreClientFiles); + }); + it('generates file for component scan exclusion', () => { + assert.file(`${basePackage}/client/ExcludeFromComponentScan.java`); + }); + it('does not override Jhipster files ', () => { + assert.noFile('README.md'); + assert.noFile('pom.xml'); + }); + }); + + describe('Spring: microservice petstore regenerate ', () => { + before(done => { + helpers + .run(require.resolve('../generators/openapi-client')) + .inTmpDir(dir => { + fse.copySync(path.join(__dirname, './templates/openapi-client/microservice-with-client'), dir); + fse.copySync(path.join(__dirname, './templates/openapi-client'), dir); + fse.copySync(path.join(__dirname, '../node_modules/@openapitools'), `${dir}/node_modules/@openapitools`); + }) + .withOptions({ skipChecks: true, regen: true }) + .on('end', done); + }); + it('regenerates java client files', () => { + assert.file(expectedFiles.petstoreClientFiles); + }); + + it('has removed old client file', () => { + assert.noFile(`${basePackage}/client/petstore/api/PetsApiClientOld.java`); + }); + }); + + describe('Spring: microservice regenerate no clients ', () => { + before(done => { + helpers + .run(require.resolve('../generators/openapi-client')) + .inTmpDir(dir => { + fse.copySync(path.join(__dirname, './templates/openapi-client/microservice-simple'), dir); + fse.copySync(path.join(__dirname, './templates/openapi-client'), dir); + fse.copySync(path.join(__dirname, '../node_modules/@openapitools'), `${dir}/node_modules/@openapitools`); + }) + .withOptions({ skipChecks: true, regen: true }) + .on('end', done); + }); + it('does not generate java client if no client configured', () => { + assert.noFile(expectedFiles.petstoreClientFiles); + }); + }); +}); diff --git a/test/templates/openapi-client/microservice-simple/.yo-rc.json b/test/templates/openapi-client/microservice-simple/.yo-rc.json new file mode 100644 index 000000000000..9a01d7dc2924 --- /dev/null +++ b/test/templates/openapi-client/microservice-simple/.yo-rc.json @@ -0,0 +1,26 @@ +{ + "generator-jhipster": { + "applicationType": "microservice", + "baseName": "sampleOpenApiClient", + "packageName": "com.mycompany.myapp", + "packageFolder": "com/mycompany/myapp", + "serverPort": "8081", + "authenticationType": "jwt", + "cacheProvider": "ehcache", + "enableHibernateCache": true, + "websocket": false, + "databaseType": "sql", + "devDatabaseType": "h2Memory", + "prodDatabaseType": "mysql", + "searchEngine": false, + "messageBroker": false, + "serviceDiscoveryType": "consul", + "buildTool": "maven", + "enableSwaggerCodegen": true, + "jwtSecretKey": "Y2Q4NzYwMzEwMzNiZTExYzI1Y2E2Yjg2MGViNjRjMGE2YjcwNTEzY2ZkMDgxNTYwNTUxZWJjMzJhYjUwY2JiOTBiNjk3YWExMWRiNWM0MDljOTJhN2M2ODBkZDY5MjQ4MTVhYmUyZGUwYjFiODE1YmFhMTE0MjY2NDRiZDI5NDI=", + "testFrameworks": [], + "enableTranslation": false, + "skipClient": true, + "skipUserManagement": true + } +} \ No newline at end of file diff --git a/test/templates/openapi-client/microservice-with-client/.yo-rc.json b/test/templates/openapi-client/microservice-with-client/.yo-rc.json new file mode 100644 index 000000000000..5fe675bc5e9e --- /dev/null +++ b/test/templates/openapi-client/microservice-with-client/.yo-rc.json @@ -0,0 +1,32 @@ +{ + "generator-jhipster": { + "applicationType": "microservice", + "baseName": "sampleOpenApiClient", + "packageName": "com.mycompany.myapp", + "packageFolder": "com/mycompany/myapp", + "serverPort": "8081", + "authenticationType": "jwt", + "cacheProvider": "ehcache", + "enableHibernateCache": true, + "websocket": false, + "databaseType": "sql", + "devDatabaseType": "h2Memory", + "prodDatabaseType": "mysql", + "searchEngine": false, + "messageBroker": false, + "serviceDiscoveryType": "consul", + "buildTool": "maven", + "enableSwaggerCodegen": true, + "jwtSecretKey": "Y2Q4NzYwMzEwMzNiZTExYzI1Y2E2Yjg2MGViNjRjMGE2YjcwNTEzY2ZkMDgxNTYwNTUxZWJjMzJhYjUwY2JiOTBiNjk3YWExMWRiNWM0MDljOTJhN2M2ODBkZDY5MjQ4MTVhYmUyZGUwYjFiODE1YmFhMTE0MjY2NDRiZDI5NDI=", + "testFrameworks": [], + "enableTranslation": false, + "skipClient": true, + "skipUserManagement": true, + "openApiClients": { + "petstore": { + "spec": "petstore-openapi-3.yml", + "generatorName": "spring" + } + } + } +} \ No newline at end of file diff --git a/test/templates/openapi-client/microservice-with-client/src/main/java/com/mycompany/myapp/client/petstore/PetsApiClientOld.java b/test/templates/openapi-client/microservice-with-client/src/main/java/com/mycompany/myapp/client/petstore/PetsApiClientOld.java new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/templates/openapi-client/monolith-simple/.yo-rc.json b/test/templates/openapi-client/monolith-simple/.yo-rc.json new file mode 100644 index 000000000000..45bbec1f8c58 --- /dev/null +++ b/test/templates/openapi-client/monolith-simple/.yo-rc.json @@ -0,0 +1,26 @@ +{ + "generator-jhipster": { + "applicationType": "monolith", + "baseName": "sampleOpenApiClient", + "packageName": "com.mycompany.myapp", + "packageFolder": "com/mycompany/myapp", + "serverPort": "8081", + "authenticationType": "jwt", + "cacheProvider": "ehcache", + "enableHibernateCache": true, + "websocket": false, + "databaseType": "sql", + "devDatabaseType": "h2Memory", + "prodDatabaseType": "mysql", + "searchEngine": false, + "messageBroker": false, + "serviceDiscoveryType": "consul", + "buildTool": "maven", + "enableSwaggerCodegen": true, + "jwtSecretKey": "Y2Q4NzYwMzEwMzNiZTExYzI1Y2E2Yjg2MGViNjRjMGE2YjcwNTEzY2ZkMDgxNTYwNTUxZWJjMzJhYjUwY2JiOTBiNjk3YWExMWRiNWM0MDljOTJhN2M2ODBkZDY5MjQ4MTVhYmUyZGUwYjFiODE1YmFhMTE0MjY2NDRiZDI5NDI=", + "testFrameworks": [], + "enableTranslation": false, + "skipClient": true, + "skipUserManagement": true + } +} \ No newline at end of file diff --git a/test/templates/openapi-client/petstore-openapi-3.yml b/test/templates/openapi-client/petstore-openapi-3.yml new file mode 100644 index 000000000000..5b2c589608b2 --- /dev/null +++ b/test/templates/openapi-client/petstore-openapi-3.yml @@ -0,0 +1,109 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string \ No newline at end of file