gen-jhipster
Version:
Spring Boot + Angular/React/Vue in one handy generator
552 lines (550 loc) • 29.1 kB
JavaScript
/**
* Copyright 2013-2024 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import os from 'node:os';
import chalk from 'chalk';
import { sortedUniqBy } from 'lodash-es';
import BaseApplicationGenerator from '../base-application/index.js';
import { GENERATOR_APISIX, GENERATOR_CUCUMBER, GENERATOR_DOCKER, GENERATOR_FEIGN_CLIENT, GENERATOR_GATLING, GENERATOR_JASYPT, GENERATOR_LANGUAGES, GENERATOR_SERVER, GENERATOR_SPRING_CACHE, GENERATOR_SPRING_CLOUD_STREAM, GENERATOR_SPRING_DATA_CASSANDRA, GENERATOR_SPRING_DATA_COUCHBASE, GENERATOR_SPRING_DATA_ELASTICSEARCH, GENERATOR_SPRING_DATA_MONGODB, GENERATOR_SPRING_DATA_NEO4J, GENERATOR_SPRING_DATA_RELATIONAL, GENERATOR_SPRING_GRPC, GENERATOR_SPRING_WEBSOCKET, GENERATOR_VAUTHZ, } from '../generator-list.js';
import { ADD_SPRING_MILESTONE_REPOSITORY } from '../generator-constants.js';
import { addSpringFactory, getJavaValueGeneratorForType, getSpecificationBuildForType, insertContentIntoApplicationProperties, javaBeanCase, } from '../server/support/index.js';
import { generateKeyStore } from '../java/support/index.js';
import { createNeedleCallback, mutateData } from '../base/support/index.js';
import { APPLICATION_TYPE_MICROSERVICE, applicationTypes, cacheTypes, databaseTypes, fieldTypes, messageBrokerTypes, searchEngineTypes, testFrameworkTypes, websocketTypes, } from '../../lib/jhipster/index.js';
import { getPomVersionProperties, parseMavenPom } from '../maven/support/index.js';
import { writeFiles as writeEntityFiles } from './entity-files.js';
import Jasypt from 'jasypt';
import cleanupTask from './cleanup.js';
import { serverFiles } from './files.js';
import { askForOptionalItems, askForServerSideOpts, askForServerTestOpts, askForVauthzUserRelationship, askForJasypt } from './prompts.js';
const { CAFFEINE, EHCACHE, HAZELCAST, INFINISPAN, MEMCACHED, REDIS } = cacheTypes;
const { NO: NO_WEBSOCKET, SPRING_WEBSOCKET } = websocketTypes;
const { CASSANDRA, COUCHBASE, MONGODB, NEO4J, SQL } = databaseTypes;
const { MICROSERVICE, GATEWAY } = applicationTypes;
const { KAFKA, PULSAR } = messageBrokerTypes;
const { ELASTICSEARCH } = searchEngineTypes;
const jas = new Jasypt();
const { BYTES: TYPE_BYTES, BYTE_BUFFER: TYPE_BYTE_BUFFER } = fieldTypes.RelationalOnlyDBTypes;
const { CUCUMBER, GATLING } = testFrameworkTypes;
export default class SpringBootGenerator extends BaseApplicationGenerator {
fakeKeytool;
async beforeQueue() {
if (!this.fromBlueprint) {
await this.composeWithBlueprints();
}
if (!this.delegateToBlueprint) {
await this.dependsOnJHipster(GENERATOR_SERVER);
await this.dependsOnJHipster('jhipster:java:domain');
await this.dependsOnJHipster('jhipster:java:build-tool');
await this.dependsOnJHipster('jhipster:java:server');
}
}
get prompting() {
return this.asPromptingTaskGroup({
askForServerTestOpts,
askForServerSideOpts,
askForOptionalItems,
askForVauthzUserRelationship,
askForJasypt
});
}
get [BaseApplicationGenerator.PROMPTING]() {
return this.delegateTasksToBlueprint(() => this.prompting);
}
get configuring() {
return this.asConfiguringTaskGroup({
feignMigration() {
const { reactive, applicationType, feignClient } = this.jhipsterConfigWithDefaults;
if (feignClient) {
if (reactive) {
this.handleCheckFailure('Feign client is not supported by reactive applications.');
}
if (applicationType !== APPLICATION_TYPE_MICROSERVICE) {
this.handleCheckFailure('Feign client is only supported by microservice applications.');
}
}
if (feignClient === undefined &&
this.isJhipsterVersionLessThan('8.0.1') &&
!reactive &&
applicationType === APPLICATION_TYPE_MICROSERVICE) {
this.jhipsterConfig.feignClient = true;
}
},
});
}
get [BaseApplicationGenerator.CONFIGURING]() {
return this.delegateTasksToBlueprint(() => this.configuring);
}
get composing() {
return this.asComposingTaskGroup({
async composing() {
const { applicationType, databaseType, graalvmSupport, messageBroker, searchEngine, websocket, cacheProvider, testFrameworks, feignClient, enableSwaggerCodegen, vauthz, syncRoutesWithApisix, enableGrpc, jasypt } = this.jhipsterConfigWithDefaults;
await this.composeWithJHipster(GENERATOR_DOCKER);
await this.composeWithJHipster('jhipster:java:jib');
await this.composeWithJHipster('jhipster:java:code-quality');
if (graalvmSupport) {
await this.composeWithJHipster('jhipster:java:graalvm');
}
if (enableSwaggerCodegen) {
await this.composeWithJHipster('jhipster:java:openapi-generator');
}
if (applicationType === GATEWAY) {
await this.composeWithJHipster('jhipster:spring-cloud:gateway');
}
if (testFrameworks?.includes(CUCUMBER)) {
await this.composeWithJHipster(GENERATOR_CUCUMBER);
}
if (testFrameworks?.includes(GATLING)) {
await this.composeWithJHipster(GENERATOR_GATLING);
}
if (feignClient) {
await this.composeWithJHipster(GENERATOR_FEIGN_CLIENT);
}
if (databaseType === SQL) {
await this.composeWithJHipster(GENERATOR_SPRING_DATA_RELATIONAL);
}
else if (databaseType === CASSANDRA) {
await this.composeWithJHipster(GENERATOR_SPRING_DATA_CASSANDRA);
}
else if (databaseType === COUCHBASE) {
await this.composeWithJHipster(GENERATOR_SPRING_DATA_COUCHBASE);
}
else if (databaseType === MONGODB) {
await this.composeWithJHipster(GENERATOR_SPRING_DATA_MONGODB);
}
else if (databaseType === NEO4J) {
await this.composeWithJHipster(GENERATOR_SPRING_DATA_NEO4J);
}
if (messageBroker === KAFKA || messageBroker === PULSAR) {
await this.composeWithJHipster(GENERATOR_SPRING_CLOUD_STREAM);
}
if (searchEngine === ELASTICSEARCH) {
await this.composeWithJHipster(GENERATOR_SPRING_DATA_ELASTICSEARCH);
}
if (websocket === SPRING_WEBSOCKET) {
await this.composeWithJHipster(GENERATOR_SPRING_WEBSOCKET);
}
if ([EHCACHE, CAFFEINE, HAZELCAST, INFINISPAN, MEMCACHED, REDIS].includes(cacheProvider)) {
await this.composeWithJHipster(GENERATOR_SPRING_CACHE);
}
if (vauthz) {
await this.composeWithJHipster(GENERATOR_VAUTHZ);
}
if (syncRoutesWithApisix) {
await this.composeWithJHipster(GENERATOR_APISIX);
}
if (enableGrpc) {
await this.composeWithJHipster(GENERATOR_SPRING_GRPC);
}
if (jasypt) {
await this.composeWithJHipster(GENERATOR_JASYPT);
}
},
});
}
get [BaseApplicationGenerator.COMPOSING]() {
return this.delegateTasksToBlueprint(() => this.composing);
}
get composingComponent() {
return this.asComposingComponentTaskGroup({
async composing() {
const { clientFramework, skipClient } = this.jhipsterConfigWithDefaults;
if (!skipClient && clientFramework !== 'no') {
// When using prompts, clientFramework will only be known after composing priority.
await this.composeWithJHipster('jhipster:java:node');
}
},
async composeLanguages() {
if (this.jhipsterConfigWithDefaults.enableTranslation) {
await this.composeWithJHipster(GENERATOR_LANGUAGES);
}
},
});
}
get [BaseApplicationGenerator.COMPOSING_COMPONENT]() {
return this.delegateTasksToBlueprint(() => this.composingComponent);
}
get preparing() {
return this.asPreparingTaskGroup({
checksWebsocket({ application }) {
const { websocket } = application;
if (websocket && websocket !== NO_WEBSOCKET) {
if (application.reactive) {
throw new Error('Spring Websocket is not supported with reactive applications.');
}
if (application.applicationType === MICROSERVICE) {
throw new Error('Spring Websocket is not supported with microservice applications.');
}
}
},
loadSpringBootBom({ application }) {
if (this.useVersionPlaceholders) {
application.springBootDependencies = {
'spring-boot-dependencies': "'SPRING-BOOT-VERSION'",
};
}
else {
const pomFile = this.readTemplate(this.jhipsterTemplatePath('../resources/spring-boot-dependencies.pom')).toString();
const pom = parseMavenPom(pomFile);
application.springBootDependencies = this.prepareDependencies(getPomVersionProperties(pom), 'java');
application.javaDependencies['spring-boot'] = application.springBootDependencies['spring-boot-dependencies'];
Object.assign(application.javaManagedProperties, pom.project.properties);
application.javaDependencies.liquibase = application.javaManagedProperties['liquibase.version'];
}
},
prepareForTemplates({ application }) {
const SPRING_BOOT_VERSION = application.springBootDependencies['spring-boot-dependencies'];
application.addSpringMilestoneRepository =
(application.backendType ?? 'Java') === 'Java' &&
(ADD_SPRING_MILESTONE_REPOSITORY || SPRING_BOOT_VERSION.includes('M') || SPRING_BOOT_VERSION.includes('RC'));
},
prepare({ application, applicationDefaults }) {
const { reactive } = application;
applicationDefaults({
__override__: false,
requiresDeleteAllUsers: data => (data.anyEntityHasRelationshipWithUser && data.authenticationTypeOauth2) ||
data.authenticationTypeOauth2 ||
data.databaseTypeNeo4j ||
(reactive && data.databaseTypeSql) ||
(!reactive && data.databaseTypeMongodb) ||
(!reactive && data.databaseTypeCassandra),
reactorBlock: reactive ? '.block()' : '',
reactorBlockOptional: reactive ? '.blockOptional()' : '',
});
},
registerSpringFactory({ source, application }) {
source.addTestSpringFactory = ({ key, value }) => {
const springFactoriesFile = `${application.srcTestResources}META-INF/spring.factories`;
this.editFile(springFactoriesFile, { create: true }, addSpringFactory({ key, value }));
};
},
addSpringIntegrationTest({ application, source }) {
source.addIntegrationTestAnnotation = annotation => source.editJavaFile(this.destinationPath(`${application.javaPackageTestDir}IntegrationTest.java`), {
annotations: [annotation],
});
},
addLogNeedles({ source }) {
source.addLogbackLogEntry = ({ file, name, level }) => this.editFile(this.destinationPath(file), createNeedleCallback({
needle: 'logback-add-log',
contentToAdd: `<logger name="${name}" level="${level}"/>`,
}));
source.addLogbackMainLog = opts => source.addLogbackLogEntry({ file: 'src/main/resources/logback-spring.xml', ...opts });
source.addLogbackTestLog = opts => source.addLogbackLogEntry({ file: 'src/test/resources/logback.xml', ...opts });
},
addApplicationPropertiesNeedles({ application, source }) {
source.addApplicationPropertiesContent = needles => this.editFile(`${application.javaPackageSrcDir}config/ApplicationProperties.java`, insertContentIntoApplicationProperties(needles));
source.addApplicationPropertiesProperty = ({ propertyName, propertyType }) => source.addApplicationPropertiesContent({
property: `private ${propertyType} ${propertyName};`,
propertyGetter: `
public ${propertyType} get${javaBeanCase(propertyName)}() {
return ${propertyName};
}
public void set${javaBeanCase(propertyName)}(${propertyType} ${propertyName}) {
this.${propertyName} = ${propertyName};
}
`,
});
},
blockhound({ application, source }) {
source.addAllowBlockingCallsInside = ({ classPath, method }) => {
if (!application.reactive)
throw new Error('Blockhound is only supported by reactive applications');
this.editFile(`${application.javaPackageTestDir}config/JHipsterBlockHoundIntegration.java`, createNeedleCallback({
needle: 'blockhound-integration',
contentToAdd: `builder.allowBlockingCallsInside("${classPath}", "${method}");`,
}));
};
},
});
}
get [BaseApplicationGenerator.PREPARING]() {
return this.delegateTasksToBlueprint(() => this.preparing);
}
get preparingEachEntity() {
return this.asPreparingEachEntityTaskGroup({
prepareEntity({ entity }) {
const hasAnyAuthority = authorities => authorities.length > 0 ? `hasAnyAuthority(${authorities.map(auth => `'${auth}'`).join(',')})` : undefined;
mutateData(entity, {
entitySpringPreAuthorize: hasAnyAuthority(entity.entityAuthority?.split(',') ?? []),
entitySpringReadPreAuthorize: hasAnyAuthority([
...(entity.entityAuthority?.split(',') ?? []),
...(entity.entityReadAuthority?.split(',') ?? []),
]),
});
},
});
}
get [BaseApplicationGenerator.PREPARING_EACH_ENTITY]() {
return this.delegateTasksToBlueprint(() => this.preparingEachEntity);
}
get preparingEachEntityField() {
return this.asPreparingEachEntityFieldTaskGroup({
prepareEntity({ field }) {
field.fieldJavaBuildSpecification = getSpecificationBuildForType(field.fieldType);
field.filterableField = ![TYPE_BYTES, TYPE_BYTE_BUFFER].includes(field.fieldType) && !field.transient;
if (field.filterableField) {
const { fieldType, fieldName, fieldInJavaBeanMethod } = field;
mutateData(field, {
propertyJavaFilterName: fieldName,
propertyJavaFilteredType: fieldType,
propertyJavaFilterJavaBeanName: fieldInJavaBeanMethod,
});
if (field.fieldIsEnum) {
const filterType = `${fieldType}Filter`;
field.propertyJavaFilterType = filterType;
field.propertyJavaCustomFilter = { type: filterType, superType: `Filter<${fieldType}>`, fieldType };
}
else if (field.fieldTypeDuration ||
field.fieldTypeTemporal ||
field.fieldTypeCharSequence ||
field.fieldTypeNumeric ||
field.fieldTypeBoolean) {
field.propertyJavaFilterType = `${fieldType}Filter`;
}
else {
field.propertyJavaFilterType = `Filter<${fieldType}>`;
}
}
},
});
}
get [BaseApplicationGenerator.PREPARING_EACH_ENTITY_FIELD]() {
return this.delegateTasksToBlueprint(() => this.preparingEachEntityField);
}
get preparingEachEntityRelationship() {
return this.asPreparingEachEntityRelationshipTaskGroup({
checkUserRelationships({ entity, entityName, relationship }) {
if (!entity.dtoMapstruct && relationship.otherEntity.builtInUser) {
this.log.warn(`Entity ${entityName} doesn't use DTO. You should check for User data leakage through ${relationship.relationshipName} relationship.`);
}
},
checkDtoRelationships({ entity, entityName, relationship }) {
if (entity.dto !== relationship.otherEntity.dto && !relationship.otherEntity.builtIn) {
this.log.warn(`Relationship between entities with different DTO configurations can cause unexpected results. Check ${relationship.relationshipName} in the ${entityName} entity.`);
}
},
prepareEntity({ relationship }) {
const { primaryKey } = relationship.otherEntity;
if (!primaryKey)
return;
const { relationshipName, relationshipNameCapitalized } = relationship;
const otherEntityPkField = primaryKey.fields[0];
mutateData(relationship, {
propertyJavaFilterName: `${relationshipName}Id`,
propertyJavaFilterJavaBeanName: `${relationshipNameCapitalized}Id`,
propertyJavaFilterType: otherEntityPkField.propertyJavaFilterType,
propertyJavaFilteredType: otherEntityPkField.propertyJavaFilteredType,
});
},
});
}
get [BaseApplicationGenerator.PREPARING_EACH_ENTITY_RELATIONSHIP]() {
return this.delegateTasksToBlueprint(() => this.preparingEachEntityRelationship);
}
get postPreparingEachEntity() {
return this.asPostPreparingEachEntityTaskGroup({
prepareEntity({ entity }) {
const { primaryKey } = entity;
if (primaryKey) {
primaryKey.javaBuildSpecification = getSpecificationBuildForType(primaryKey.type);
primaryKey.javaValueGenerator = getJavaValueGeneratorForType(primaryKey.type);
for (const field of primaryKey.fields) {
field.fieldJavaValueGenerator = getJavaValueGeneratorForType(field.fieldType);
}
}
},
prepareFilters({ application, entity }) {
mutateData(entity, {
entityJavaFilterableProperties: [
...entity.fields.filter(field => field.filterableField),
...entity.relationships.filter(rel => !application.reactive || (rel.persistableRelationship && !rel.collection)),
],
entityJavaCustomFilters: sortedUniqBy(entity.fields.map(field => field.propertyJavaCustomFilter).filter(Boolean), 'type'),
});
},
});
}
get [BaseApplicationGenerator.POST_PREPARING_EACH_ENTITY]() {
return this.delegateTasksToBlueprint(() => this.postPreparingEachEntity);
}
get writing() {
return this.asWritingTaskGroup({
cleanupTask,
resetFakeDataSeed() {
this.resetEntitiesFakeData('server');
},
async writeFiles({ application }) {
jas.setPassword(application.jasyptEncryptorPassword);
return this.writeFiles({
sections: serverFiles,
rootTemplatesPath: ['', '../../java/generators/domain/templates/'],
context: { ...application, jasyptEncryptor: jas },
});
},
async generateKeyStore({ application }) {
const keyStoreFile = this.destinationPath(`${application.srcMainResources}config/tls/keystore.p12`);
if (this.fakeKeytool) {
this.writeDestination(keyStoreFile, 'fake key-tool');
}
else {
this.validateResult(await generateKeyStore(keyStoreFile, { packageName: application.packageName }));
}
},
});
}
get [BaseApplicationGenerator.WRITING]() {
return this.delegateTasksToBlueprint(() => this.writing);
}
get writingEntities() {
return this.asWritingEntitiesTaskGroup({
...writeEntityFiles(),
});
}
get [BaseApplicationGenerator.WRITING_ENTITIES]() {
return this.delegateTasksToBlueprint(() => this.writingEntities);
}
get postWriting() {
return this.asPostWritingTaskGroup({
addJHipsterBomDependencies({ application, source }) {
const { applicationTypeGateway, applicationTypeMicroservice, javaDependencies, jhipsterDependenciesVersion, messageBrokerAny, serviceDiscoveryAny, } = application;
source.addJavaDefinitions?.({
dependencies: [{ groupId: 'tech.jhipster', artifactId: 'jhipster-framework', version: "8.8.0" }],
mavenDefinition: {
properties: [
{
property: 'spring-boot.version',
// eslint-disable-next-line no-template-curly-in-string
value: '${project.parent.version}',
},
],
},
}, {
condition: applicationTypeGateway || applicationTypeMicroservice || serviceDiscoveryAny || messageBrokerAny,
dependencies: [
{
groupId: 'org.springframework.cloud',
artifactId: 'spring-cloud-dependencies',
type: 'pom',
scope: 'import',
version: javaDependencies['spring-cloud-dependencies'],
},
],
});
},
addSpringdoc({ application, source }) {
const springdocDependency = `springdoc-openapi-starter-${application.reactive ? 'webflux' : 'webmvc'}-api`;
source.addJavaDependencies?.([
{ groupId: 'org.springdoc', artifactId: springdocDependency, version: application.javaDependencies.springdoc },
]);
},
addFeignReactor({ application, source }) {
const { applicationTypeGateway, applicationTypeMicroservice, javaDependencies, reactive } = application;
if ((applicationTypeMicroservice || applicationTypeGateway) && reactive) {
const groupId = 'com.playtika.reactivefeign';
source.addJavaDependencies?.([
{ groupId, artifactId: 'feign-reactor-bom', type: 'pom', scope: 'import', version: javaDependencies['feign-reactor-bom'] },
{ groupId, artifactId: 'feign-reactor-cloud' },
{ groupId, artifactId: 'feign-reactor-spring-configuration' },
{ groupId, artifactId: 'feign-reactor-webclient' },
]);
}
},
addSpringSnapshotRepository({ application, source }) {
if (application.buildToolMaven) {
if (application.addSpringMilestoneRepository) {
const springRepository = {
id: 'spring-milestone',
name: 'Spring Milestones',
url: 'https://repo.spring.io/milestone',
};
source.addMavenPluginRepository?.(springRepository);
source.addMavenRepository?.(springRepository);
source.addMavenDependency?.({
groupId: 'org.springframework.boot',
artifactId: 'spring-boot-properties-migrator',
scope: 'runtime',
});
}
if (application.jhipsterDependenciesVersion?.endsWith('-SNAPSHOT')) {
source.addMavenRepository?.({
id: 'ossrh-snapshots',
url: 'https://oss.sonatype.org/content/repositories/snapshots/',
releasesEnabled: false,
});
}
}
},
addSpringBootPlugin({ application, source }) {
if (application.buildToolGradle) {
source.addGradleDependencyCatalogPlugins?.([
{
pluginName: 'spring-boot',
id: 'org.springframework.boot',
version: application.javaDependencies['spring-boot'],
addToBuild: true,
},
]);
}
},
addSpringBootCompose({ application, source }) {
source.addLogbackMainLog({ name: 'org.springframework.boot.docker', level: 'WARN' });
const dockerComposeArtifact = { groupId: 'org.springframework.boot', artifactId: 'spring-boot-docker-compose' };
if (application.buildToolGradle) {
source.addGradleDependency({ ...dockerComposeArtifact, scope: 'developmentOnly' });
}
else if (application.buildToolMaven) {
// Add dependency to profile due to jib issue https://github.com/GoogleContainerTools/jib-extensions/issues/158
source.addMavenDefinition({
profiles: [
{
id: 'docker-compose',
content: `
<activation>
<activeByDefault>true</activeByDefault>
</activation>`,
},
],
});
source.addMavenDependency({ inProfile: 'docker-compose', ...dockerComposeArtifact, optional: true });
}
}
});
}
get [BaseApplicationGenerator.POST_WRITING]() {
return this.delegateTasksToBlueprint(() => this.postWriting);
}
get end() {
return this.asEndTaskGroup({
end({ application }) {
this.log.ok('Spring Boot application generated successfully.');
let executable = 'mvnw';
if (application.buildToolGradle) {
executable = 'gradlew';
}
let logMsgComment = '';
if (os.platform() === 'win32') {
logMsgComment = ` (${chalk.yellow.bold(executable)} if using Windows Command Prompt)`;
}
this.log.log(chalk.green(` Run your Spring Boot application:\n ${chalk.yellow.bold(`./${executable}`)}${logMsgComment}`));
},
});
}
get [BaseApplicationGenerator.END]() {
return this.delegateTasksToBlueprint(() => this.end);
}
}