Skip to content
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
30 changes: 30 additions & 0 deletions boat-maven-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ Same with `generate` but with opinionated defaults for Spring
</configOptions>
</configuration>

## boat:generate-spring-boot-embedded-webhooks

Same with `generate-spring-boot-embedded` but with opinionated defaults for Webhooks
It will generate webhook interfaces with prehook and posthook request mapping for each endpoint in the OpenAPI Specs.

<configuration>
<output>${project.build.directory}/generated-sources/openapi</output>
<generateSupportingFiles>true</generateSupportingFiles>
<generatorName>boat-webhooks</generatorName>
<strictSpec>true</strictSpec>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<inputSpec>${project.basedir}/../api/product-service-api/src/main/resources/openapi.yaml</inputSpec>
<configOptions>
<dateLibrary>java8</dateLibrary>
<interfaceOnly>true</interfaceOnly>
<skipDefaultInterface>true</skipDefaultInterface>
<useBeanValidation>true</useBeanValidation>
<useClassLevelBeanValidation>false</useClassLevelBeanValidation>
<useTags>true</useTags>
<java8>true</java8>
<useOptional>false</useOptional>
<apiPackage>com.backbase.product.api.service.v2</apiPackage>
<modelPackage>com.backbase.product.api.service.v2.model</modelPackage>
<useJakartaEe>true</useJakartaEe>
<useSpringBoot3>true</useSpringBoot3>
<containerDefaultToNull>false</containerDefaultToNull>
</configOptions>
</configuration>

## boat:generate-rest-template-embedded

Same with `generate` but with opinionated defaults for Rest Template Client
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.backbase.oss.boat;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;

import java.util.Collection;
import java.util.Set;

/**
* Generating Server Stubs using Spring Boot.
*/
@Mojo(name = "generate-spring-boot-embedded-webhooks", threadSafe = true)
public class GenerateSpringBootEmbeddedWebhookMojo extends AbstractGenerateMojo {

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info("Generating Server Stubs using Spring Boot Webhooks");
execute("boat-webhooks", "boat-webhooks", true, false, false);
}

@Override
protected Collection<String> getGeneratorSpecificSupportingFiles() {
return Set.of("BigDecimalCustomSerializer.java", "WebhookResponse.java", "ServletContent.java", "PosthookRequest.java", "PrehookRequest.java");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
package com.backbase.oss.codegen.java;

import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.servers.Server;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.languages.SpringCodegen;
import org.openapitools.codegen.templating.mustache.IndentedLambda;
import org.openapitools.codegen.utils.ModelUtils;

import java.io.File;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static com.backbase.oss.codegen.java.BoatCodeGenUtils.getCollectionCodegenValue;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.openapitools.codegen.utils.StringUtils.camelize;

@Slf4j
public class BoatWebhooksCodeGen extends SpringCodegen {
public static final String NAME = "boat-webhooks";

public static final String USE_CLASS_LEVEL_BEAN_VALIDATION = "useClassLevelBeanValidation";
public static final String ADD_SERVLET_REQUEST = "addServletRequest";
public static final String ADD_BINDING_RESULT = "addBindingResult";
public static final String USE_LOMBOK_ANNOTATIONS = "useLombokAnnotations";
public static final String USE_WITH_MODIFIERS = "useWithModifiers";
public static final String USE_PROTECTED_FIELDS = "useProtectedFields";
public static final String MUSTACHE_EXTENSION =".mustache";
public static final String JAVA_EXTENSION =".java";


/**
* Add @Validated to class-level Api interfaces. Defaults to false
*/
@Setter
@Getter
protected boolean useClassLevelBeanValidation;

/**
* Adds a HttpServletRequest object to the API definition method.
*/
@Setter
@Getter
protected boolean addServletRequest;

/**
* Adds BindingResult to API interface method if @validate is used
*/
@Setter
@Getter
protected boolean addBindingResult;

/**
* Add Lombok to class-level Api models. Defaults to false
*/
@Setter
@Getter
protected boolean useLombokAnnotations;


/**
* Whether to use {@code with} prefix for pojos modifiers.
*/
@Setter
@Getter
protected boolean useWithModifiers;

@Setter
@Getter
protected boolean useProtectedFields;

public BoatWebhooksCodeGen() {
super();
log.info("BoatWebhooksCodeGen constructor called. NAME: {}", NAME);
this.embeddedTemplateDir = this.templateDir = NAME;
this.openapiNormalizer.put("REF_AS_PARENT_IN_ALLOF", "true");

this.cliOptions.add(CliOption.newBoolean(USE_CLASS_LEVEL_BEAN_VALIDATION,
"Add @Validated to class-level Api interfaces.", this.useClassLevelBeanValidation));
this.cliOptions.add(CliOption.newBoolean(ADD_SERVLET_REQUEST,
"Adds a HttpServletRequest object to the API definition method.", this.addServletRequest));
this.cliOptions.add(CliOption.newBoolean(ADD_BINDING_RESULT,
"Adds a Binding result as method perimeter. Only implemented if @validate is being used.",
this.addBindingResult));
this.cliOptions.add(CliOption.newBoolean(USE_LOMBOK_ANNOTATIONS,
"Add Lombok to class-level Api models. Defaults to false.", this.useLombokAnnotations));
this.cliOptions.add(CliOption.newBoolean(USE_WITH_MODIFIERS,
"Whether to use \"with\" prefix for POJO modifiers.", this.useWithModifiers));
this.cliOptions.add(CliOption.newString(USE_PROTECTED_FIELDS,
"Whether to use protected visibility for model fields"));
supportedLibraries.put(NAME, "Boat Webhooks codegen");
this.apiNameSuffix = "Api";
this.apiNamePrefix = "Webhook";
}

@Override
public String getName() {
return NAME;
}

@Override
public String toApiName(String name) {
if (name.isEmpty()) {
name = "default";
}

name = sanitizeName(name);

return camelize(this.apiNamePrefix + "_" + name + "_" + this.apiNameSuffix);
}

@Override
public void processOpts() {
super.processOpts();
log.info("BoatWebhooksCodeGen processOpts called. Adding supporting files and properties.");

// Whether it's using ApiUtil or not.
// cases:
// <supportingFilesToGenerate>ApiUtil.java present or not</supportingFilesToGenerate>
// <generateSupportingFiles>true or false</generateSupportingFiles>
final String supFiles = GlobalSettings.getProperty(CodegenConstants.SUPPORTING_FILES);
final boolean useApiUtil = supFiles != null && (supFiles.isEmpty()
? needApiUtil() // set to empty by <generateSuportingFiles>true</generateSuportingFiles>
: supFiles.contains("ApiUtil.java")); // set by <supportingFilesToGenerate/>

if (!useApiUtil) {
this.supportingFiles
.removeIf(sf -> "apiUtil.mustache".equals(sf.getTemplateFile()));
}
writePropertyBack("useApiUtil", useApiUtil);
final var serializerTemplate = "BigDecimalCustomSerializer";
this.supportingFiles.add(new SupportingFile(
serializerTemplate + MUSTACHE_EXTENSION,
(sourceFolder + File.separator + modelPackage).replace(".", File.separator),
serializerTemplate + JAVA_EXTENSION
));
final var webhookResponseTemplate = "WebhookResponse";
this.supportingFiles.add(new SupportingFile(webhookResponseTemplate + MUSTACHE_EXTENSION,
(sourceFolder + File.separator + modelPackage).replace(".", File.separator),
webhookResponseTemplate + JAVA_EXTENSION));
final var servletContentTemplate = "ServletContent";
this.supportingFiles.add(new SupportingFile(servletContentTemplate + MUSTACHE_EXTENSION,
(sourceFolder + File.separator + modelPackage).replace(".", File.separator),
servletContentTemplate + JAVA_EXTENSION));
final var posthookRequestTemplate = "PosthookRequest";
this.supportingFiles.add(new SupportingFile(posthookRequestTemplate + MUSTACHE_EXTENSION,
(sourceFolder + File.separator + modelPackage).replace(".", File.separator),
posthookRequestTemplate + JAVA_EXTENSION));
final var prehookRequestTemplate = "PrehookRequest";
this.supportingFiles.add(new SupportingFile(prehookRequestTemplate + MUSTACHE_EXTENSION,
(sourceFolder + File.separator + modelPackage).replace(".", File.separator),
prehookRequestTemplate + JAVA_EXTENSION));
this.additionalProperties.put("indent4", new IndentedLambda(4, " ", true, true));
this.additionalProperties.put("newLine4", new BoatSpringCodeGen.NewLineIndent(4, " "));
this.additionalProperties.put("indent8", new IndentedLambda(8, " ", true, true));
this.additionalProperties.put("newLine8", new BoatSpringCodeGen.NewLineIndent(8, " "));
this.additionalProperties.put("toOneLine", new BoatSpringCodeGen.FormatToOneLine());
this.additionalProperties.put("trimAndIndent4", new BoatSpringCodeGen.TrimAndIndent(4, " "));
}

private boolean needApiUtil() {
return this.apiTemplateFiles.containsKey("api.mustache")
&& this.apiTemplateFiles.containsKey("apiDelegate.mustache");
}

/*
* Overridden to be able to override the private <code>replaceBeanValidationCollectionType</code> method.
*/
@Override
public CodegenParameter fromParameter(Parameter parameter, Set<String> imports) {
CodegenParameter codegenParameter = super.fromParameter(parameter, imports);
if (!isListOrSet(codegenParameter)) {
return new BoatSpringCodegenParameter(codegenParameter);
} else {
codegenParameter.datatypeWithEnum = replaceBeanValidationCollectionType(codegenParameter.items, codegenParameter.datatypeWithEnum);
codegenParameter.dataType = replaceBeanValidationCollectionType(codegenParameter.items, codegenParameter.dataType);
return new BoatSpringCodegenParameter(codegenParameter);
}
}

/*
* Overridden to be able to override the private <code>replaceBeanValidationCollectionType</code> method.
*/
@Override
public CodegenProperty fromProperty(String name, Schema p, boolean required, boolean schemaIsFromAdditionalProperties) {
CodegenProperty codegenProperty = super.fromProperty(name, p, required, schemaIsFromAdditionalProperties);
if (!isListOrSet(codegenProperty)) {
return new BoatSpringCodegenProperty(codegenProperty);
} else {
codegenProperty.datatypeWithEnum = replaceBeanValidationCollectionType(codegenProperty.items, codegenProperty.datatypeWithEnum);
codegenProperty.dataType = replaceBeanValidationCollectionType(codegenProperty.items, codegenProperty.dataType);
return new BoatSpringCodegenProperty(codegenProperty);
}
}

/**
* "overridden" to fix invalid code when the data type is a collection of a fully qualified classname.
* eg. <code>Set<@Valid com.backbase.dbs.arrangement.commons.model.TranslationItemDto></code>
*
* @param codegenProperty
* @param dataType
* @return
*/
String replaceBeanValidationCollectionType(CodegenProperty codegenProperty, String dataType) {
if (!useBeanValidation || isEmpty(dataType) || !codegenProperty.isModel || isResponseType(codegenProperty)) {
return dataType;
}
String result = dataType;
if (!dataType.contains("@Valid")) {
result = dataType.replace("<", "<@Valid ");
}
// Use a safer regex to avoid catastrophic backtracking
Matcher m = Pattern.compile("^([^<]+<)(@Valid) ([a-z\\.]+)([A-Z].*)(>)$").matcher(dataType);
if (m.matches()) {
// Set<@Valid com.backbase.dbs.arrangement.commons.model.TranslationItemDto>
result = m.group(1) + m.group(3) + m.group(2) + " " + m.group(4) + m.group(5);
}
return result;
}

// Copied, but not modified
private static boolean isListOrSet(CodegenProperty codegenProperty) {
return codegenProperty.isContainer && !codegenProperty.isMap;
}

// Copied, but not modified
private static boolean isListOrSet(CodegenParameter codegenParameter) {
return codegenParameter.isContainer && !codegenParameter.isMap;
}

// Copied, but not modified
private static boolean isResponseType(CodegenProperty codegenProperty) {
return codegenProperty.baseName.toLowerCase(Locale.ROOT).contains("response");
}

/**
This method has been overridden in order to add a parameter to codegen operation for adding HttpServletRequest to
the service interface. There is a relevant httpServletParam.mustache file.
*/
@Override
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<Server> servers) {
if (operation.getExtensions() == null) {
operation.setExtensions(new LinkedHashMap<>());
}
final CodegenOperation codegenOperation = super.fromOperation(path, httpMethod, operation, servers);
// Remove the standard body parameter (if it exists) ---
// This prevents the generator's default logic from inserting its own request body.
codegenOperation.allParams.removeIf(p -> p.isBodyParam);
if (this.addServletRequest) {
final CodegenParameter codegenParameter = new CodegenParameter();
codegenParameter.paramName = "httpServletRequest";
codegenOperation.allParams.add(codegenParameter);
}
if (codegenOperation.returnType != null) {
codegenOperation.returnType = codegenOperation.returnType.replace("@Valid", "");
}
return codegenOperation;
}

@Override
public String toDefaultValue(CodegenProperty cp, Schema schema) {
final Schema referencedSchema = ModelUtils.getReferencedSchema(this.openAPI, schema);

Check warning on line 279 in boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGen.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=com.backbase.oss%3Abackbase-openapi-tools&issues=AZr-FobAgpbx1_4ArRU2&open=AZr-FobAgpbx1_4ArRU2&pullRequest=1188
return getCollectionCodegenValue(cp, referencedSchema, containerDefaultToNull, instantiationTypes())
.map(BoatCodeGenUtils.CodegenValueType::getValue)
.orElseGet(() -> super.toDefaultValue(cp, referencedSchema));
}

@Override
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {

super.postProcessModelProperty(model, property);

if (shouldSerializeBigDecimalAsString(property)) {
property.vendorExtensions.put("x-extra-annotation", "@JsonSerialize(using = BigDecimalCustomSerializer.class)");
model.imports.add("BigDecimalCustomSerializer");
model.imports.add("JsonSerialize");
}
}

private boolean shouldSerializeBigDecimalAsString(CodegenProperty property) {
return (serializeBigDecimalAsString && ("decimal".equalsIgnoreCase(property.baseType) || "bigdecimal".equalsIgnoreCase(property.baseType)))
|| (isApiStringFormattedAsNumber(property) && !isDataTypeString(property));
}

private boolean isApiStringFormattedAsNumber(CodegenProperty property) {
return "string".equalsIgnoreCase(property.openApiType) && "number".equalsIgnoreCase(property.dataFormat);
}

private boolean isDataTypeString(CodegenProperty property) {
return Stream.of(property.baseType, property.dataType, property.datatypeWithEnum)
.anyMatch("string"::equalsIgnoreCase);
}

@Override
public void postProcessParameter(CodegenParameter p) {
super.postProcessParameter(p);
if (p.isContainer && !this.reactive) {
p.baseType = p.dataType.replaceAll("^([^<]+)<.+>$", "$1");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ com.backbase.oss.codegen.angular.BoatAngularGenerator
com.backbase.oss.codegen.marina.BoatMarinaGenerator
org.openapitools.codegen.languages.BoatSwift5Codegen
org.openapitools.codegen.languages.BoatAndroidClientCodegen
com.backbase.oss.codegen.java.BoatWebhooksCodeGen
Loading