diff --git a/boat-maven-plugin/README.md b/boat-maven-plugin/README.md
index 52d2e551e..3197e9d3c 100644
--- a/boat-maven-plugin/README.md
+++ b/boat-maven-plugin/README.md
@@ -60,6 +60,36 @@ Same with `generate` but with opinionated defaults for Spring
+## 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.
+
+
+
+ true
+ boat-webhooks
+ true
+ false
+ false
+ ${project.basedir}/../api/product-service-api/src/main/resources/openapi.yaml
+
+ java8
+ true
+ true
+ true
+ false
+ true
+ true
+ false
+ com.backbase.product.api.service.v2
+ com.backbase.product.api.service.v2.model
+ true
+ true
+ false
+
+
+
## boat:generate-rest-template-embedded
Same with `generate` but with opinionated defaults for Rest Template Client
diff --git a/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateSpringBootEmbeddedWebhookMojo.java b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateSpringBootEmbeddedWebhookMojo.java
new file mode 100644
index 000000000..ccc9c4124
--- /dev/null
+++ b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateSpringBootEmbeddedWebhookMojo.java
@@ -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 getGeneratorSpecificSupportingFiles() {
+ return Set.of("BigDecimalCustomSerializer.java", "WebhookResponse.java", "ServletContent.java", "PosthookRequest.java", "PrehookRequest.java");
+ }
+}
diff --git a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGen.java b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGen.java
new file mode 100644
index 000000000..8ee2fdf65
--- /dev/null
+++ b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGen.java
@@ -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:
+ // ApiUtil.java present or not
+ // true or false
+ final String supFiles = GlobalSettings.getProperty(CodegenConstants.SUPPORTING_FILES);
+ final boolean useApiUtil = supFiles != null && (supFiles.isEmpty()
+ ? needApiUtil() // set to empty by true
+ : supFiles.contains("ApiUtil.java")); // set by
+
+ 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 replaceBeanValidationCollectionType method.
+ */
+ @Override
+ public CodegenParameter fromParameter(Parameter parameter, Set 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 replaceBeanValidationCollectionType 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. Set<@Valid com.backbase.dbs.arrangement.commons.model.TranslationItemDto>
+ *
+ * @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 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);
+ 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");
+ }
+ }
+
+}
diff --git a/boat-scaffold/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/boat-scaffold/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
index 56c1e55dc..548b9ea0f 100644
--- a/boat-scaffold/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
+++ b/boat-scaffold/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
@@ -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
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/BigDecimalCustomSerializer.mustache b/boat-scaffold/src/main/templates/boat-webhooks/BigDecimalCustomSerializer.mustache
new file mode 100644
index 000000000..a6dc40f30
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/BigDecimalCustomSerializer.mustache
@@ -0,0 +1,19 @@
+package {{modelPackage}};
+
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializerBase;
+import java.math.BigDecimal;
+
+public class BigDecimalCustomSerializer extends ToStringSerializerBase {
+
+ public BigDecimalCustomSerializer() {
+ super(BigDecimal.class);
+ }
+
+ @Override
+ public String valueToString(Object value) {
+ if (value instanceof BigDecimal) {
+ return ((BigDecimal) value).toPlainString();
+ }
+ return (value == null) ? null : value.toString();
+ }
+}
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/PosthookRequest.mustache b/boat-scaffold/src/main/templates/boat-webhooks/PosthookRequest.mustache
new file mode 100644
index 000000000..fe600385f
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/PosthookRequest.mustache
@@ -0,0 +1,70 @@
+package {{modelPackage}};
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Predefined request object for posthook endpoints containing API response and the request details.
+ */
+public class PosthookRequest extends ServletContent {
+
+ private PrehookRequest requestDetails;
+
+ public PosthookRequest body(String body) {
+ this.setBody(body);
+ return this;
+ }
+
+ public PosthookRequest headers(Map> headers) {
+ this.setHeaders(headers);
+ return this;
+ }
+
+ public PosthookRequest parameters(Map parameters) {
+ this.setParameters(parameters);
+ return this;
+ }
+
+ public PosthookRequest requestDetails(PrehookRequest requestDetails) {
+ this.requestDetails = requestDetails;
+ return this;
+ }
+
+ public PrehookRequest getRequestDetails() {
+ return requestDetails;
+ }
+
+ public void setRequestDetails(PrehookRequest requestDetails) {
+ this.requestDetails = requestDetails;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ PosthookRequest posthookRequest = (PosthookRequest) o;
+ return super.equals(o) && Objects.equals(this.requestDetails, posthookRequest.requestDetails);
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode() + Objects.hash(requestDetails);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class PosthookRequest {\n");
+ sb.append(" body: ").append(toIndentedString(getBody())).append("\n");
+ sb.append(" headers: ").append(toIndentedString(getHeaders())).append("\n");
+ sb.append(" parameters: ").append(toIndentedString(getParameters())).append("\n");
+ sb.append(" requestDetails: ").append(toIndentedString(requestDetails)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/PrehookRequest.mustache b/boat-scaffold/src/main/templates/boat-webhooks/PrehookRequest.mustache
new file mode 100644
index 000000000..23be71d98
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/PrehookRequest.mustache
@@ -0,0 +1,70 @@
+package {{modelPackage}};
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Predefined request object for prehook endpoints.
+ */
+public class PrehookRequest extends ServletContent {
+
+ private Map pathVariables;
+
+ public PrehookRequest body(String body) {
+ this.setBody(body);
+ return this;
+ }
+
+ public PrehookRequest headers(Map> headers) {
+ this.setHeaders(headers);
+ return this;
+ }
+
+ public PrehookRequest parameters(Map parameters) {
+ this.setParameters(parameters);
+ return this;
+ }
+
+ public PrehookRequest pathVariables(Map pathVariables) {
+ this.pathVariables = pathVariables;
+ return this;
+ }
+
+ public Map getPathVariables() {
+ return pathVariables;
+ }
+
+ public void setPathVariables(Map pathVariables) {
+ this.pathVariables = pathVariables;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ PrehookRequest prehookRequest = (PrehookRequest) o;
+ return super.equals(o) && Objects.equals(this.pathVariables, prehookRequest.pathVariables);
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode() + Objects.hash(pathVariables);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class PrehookRequest {\n");
+ sb.append(" body: ").append(toIndentedString(getBody())).append("\n");
+ sb.append(" headers: ").append(toIndentedString(getHeaders())).append("\n");
+ sb.append(" parameters: ").append(toIndentedString(getParameters())).append("\n");
+ sb.append(" pathVariables: ").append(toIndentedString(pathVariables)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/README.mustache b/boat-scaffold/src/main/templates/boat-webhooks/README.mustache
new file mode 100644
index 000000000..37911ef59
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/README.mustache
@@ -0,0 +1,81 @@
+{{^interfaceOnly}}# OpenAPI generated server
+
+Spring Boot Webhooks Server
+
+## Overview
+This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project.
+By using the [OpenAPI-Spec](https://openapis.org), you can easily generate a server stub.
+This is an example of building a OpenAPI-enabled server in Java using the SpringBoot framework.
+{{#springFoxDocumentationProvider}}
+
+The underlying library integrating OpenAPI to Spring Boot is [springfox](https://github.com/springfox/springfox).
+Springfox will generate an OpenAPI v2 (fka Swagger RESTful API Documentation Specification) specification based on the
+generated Controller and Model classes. The specification is available to download using the following url:
+http://localhost:{{serverPort}}/v2/api-docs/
+
+**HEADS-UP**: Springfox is deprecated for removal in version 6.0.0 of openapi-generator. The project seems to be no longer
+maintained (last commit is of Oct 14, 2020). It works with Spring Boot 2.5.x but not with 2.6. Spring Boot 2.5 is
+supported until 2022-05-19. Users of openapi-generator should migrate to the springdoc documentation provider which is,
+as an added bonus, OpenAPI v3 compatible.
+
+{{/springFoxDocumentationProvider}}
+
+{{#springDocDocumentationProvider}}
+
+The underlying library integrating OpenAPI to Spring Boot is [springdoc](https://springdoc.org).
+Springdoc will generate an OpenAPI v3 specification based on the generated Controller and Model classes.
+The specification is available to download using the following url:
+http://localhost:{{serverPort}}/v3/api-docs/
+{{/springDocDocumentationProvider}}
+{{#sourceDocumentationProvider}}
+
+The OpenAPI specification used to generate this project is available to download using the following url:
+http://localhost:{{serverPort}}/openapi.json
+{{/sourceDocumentationProvider}}
+
+Start your server as a simple java application
+{{#useSwaggerUI}}
+
+You can view the api documentation in swagger-ui by pointing to
+http://localhost:{{serverPort}}/swagger-ui.html
+
+{{/useSwaggerUI}}
+Change default port value in application.properties{{/interfaceOnly}}{{#interfaceOnly}}
+# OpenAPI generated API stub
+
+Spring Framework stub
+
+
+## Overview
+This code was generated by the [OpenAPI Generator](https://openapi-generator.tech) project.
+By using the [OpenAPI-Spec](https://openapis.org), you can easily generate an API stub.
+This is an example of building API stub interfaces in Java using the Spring framework.
+
+The stubs generated can be used in your existing Spring-MVC or Spring-Boot application to create controller endpoints
+by adding ```@Controller``` classes that implement the interface. Eg:
+```java
+@Controller
+public class PetController implements PetApi {
+// implement all PetApi methods
+}
+```
+
+You can also use the interface to create [Spring-Cloud Feign clients](http://projects.spring.io/spring-cloud/spring-cloud.html#spring-cloud-feign-inheritance).Eg:
+```java
+@FeignClient(name="pet", url="http://petstore.swagger.io/v2")
+public interface PetClient extends PetApi {
+
+}
+```
+{{/interfaceOnly}}
+{{#virtualService}}
+
+
+## Virtualan :
+
+You can view Virtualan UI by pointing to
+http://localhost:8080//virtualan-ui.html
+
+How to use guide available in the Virtualan wiki
+https://github.com/virtualansoftware/virtualan/wiki
+{{/virtualService}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/ServletContent.mustache b/boat-scaffold/src/main/templates/boat-webhooks/ServletContent.mustache
new file mode 100644
index 000000000..b5e502c0b
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/ServletContent.mustache
@@ -0,0 +1,104 @@
+package {{modelPackage}};
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public abstract class ServletContent {
+ private String body;
+ private Map> headers;
+ private Map parameters;
+
+ public ServletContent() {
+ }
+
+ /**
+ * Create copy.
+ *
+ * @param content other instance
+ */
+ public ServletContent(ServletContent content) {
+ if (content != null) {
+ this.body = content.getBody();
+ this.headers = content.getHeaders();
+ this.parameters = content.getParameters();
+ }
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public Map> getHeaders() {
+ return headers;
+ }
+
+ public void setHeaders(Map> headers) {
+ this.headers = headers;
+ }
+
+ /**
+ * Add a header overwriting existing.
+ *
+ * @param name header name
+ * @param value header value
+ */
+ public void putHeader(String name, String value) {
+ if (this.headers == null) {
+ this.headers = new HashMap<>();
+ }
+ this.headers.put(name, List.of(value));
+ }
+
+ public Map getParameters() {
+ return parameters;
+ }
+
+ public void setParameters(Map parameters) {
+ this.parameters = parameters;
+ }
+
+ /**
+ * Add a parameter overwriting existing.
+ *
+ * @param name parameter name
+ * @param value parameter value
+ */
+ public void putParameter(String name, String value) {
+ if (this.parameters == null) {
+ this.parameters = new HashMap<>();
+ }
+ this.parameters.put(name, new String[]{value});
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || !(o instanceof ServletContent servletContent)) {
+ return false;
+ }
+ return Objects.equals(this.body, servletContent.body) &&
+ Objects.equals(this.headers, servletContent.headers) &&
+ Objects.equals(this.parameters, servletContent.parameters);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(body, headers, parameters);
+ }
+
+
+ protected String toIndentedString(Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/WebhookResponse.mustache b/boat-scaffold/src/main/templates/boat-webhooks/WebhookResponse.mustache
new file mode 100644
index 000000000..b3930b038
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/WebhookResponse.mustache
@@ -0,0 +1,75 @@
+package {{modelPackage}};
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+
+public class WebhookResponse extends ServletContent {
+
+ private int httpStatusCode;
+
+ public WebhookResponse() {
+ }
+
+ public WebhookResponse(ServletContent content) {
+ super(content);
+ }
+
+ public WebhookResponse httpStatusCode(int httpStatusCode) {
+ this.httpStatusCode = httpStatusCode;
+ return this;
+ }
+
+ public int getHttpStatusCode() {
+ return httpStatusCode;
+ }
+
+ public void setHttpStatusCode(int httpStatusCode) {
+ this.httpStatusCode = httpStatusCode;
+ }
+
+ public WebhookResponse body(String body) {
+ this.setBody(body);
+ return this;
+ }
+
+ public WebhookResponse headers(Map> headers) {
+ this.setHeaders(headers);
+ return this;
+ }
+
+ public WebhookResponse parameters(Map parameters) {
+ this.setParameters(parameters);
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ WebhookResponse webhookResponse = (WebhookResponse) o;
+ return super.equals(o) && Objects.equals(this.httpStatusCode, webhookResponse.httpStatusCode);
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode() + Objects.hash(httpStatusCode);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class WebhookResponse {\n");
+ sb.append(" body: ").append(toIndentedString(getBody())).append("\n");
+ sb.append(" headers: ").append(toIndentedString(getHeaders())).append("\n");
+ sb.append(" parameters: ").append(toIndentedString(getParameters())).append("\n");
+ sb.append(" httpStatusCode: ").append(toIndentedString(this.httpStatusCode)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/additionalEnumTypeAnnotations.mustache b/boat-scaffold/src/main/templates/boat-webhooks/additionalEnumTypeAnnotations.mustache
new file mode 100644
index 000000000..dbb6a373f
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/additionalEnumTypeAnnotations.mustache
@@ -0,0 +1,3 @@
+{{#additionalEnumTypeAnnotations}}
+{{{.}}}
+{{/additionalEnumTypeAnnotations}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/additionalModelTypeAnnotations.mustache b/boat-scaffold/src/main/templates/boat-webhooks/additionalModelTypeAnnotations.mustache
new file mode 100644
index 000000000..f4871c02c
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/additionalModelTypeAnnotations.mustache
@@ -0,0 +1,2 @@
+{{#additionalModelTypeAnnotations}}{{{.}}}
+{{/additionalModelTypeAnnotations}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/additionalOneOfTypeAnnotations.mustache b/boat-scaffold/src/main/templates/boat-webhooks/additionalOneOfTypeAnnotations.mustache
new file mode 100644
index 000000000..283f8f91e
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/additionalOneOfTypeAnnotations.mustache
@@ -0,0 +1,2 @@
+{{#additionalOneOfTypeAnnotations}}{{{.}}}
+{{/additionalOneOfTypeAnnotations}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/allowableValues.mustache b/boat-scaffold/src/main/templates/boat-webhooks/allowableValues.mustache
new file mode 100644
index 000000000..6aa973a6a
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/allowableValues.mustache
@@ -0,0 +1 @@
+{{#allowableValues}}allowableValues ={{#swagger2AnnotationLibrary}} { {{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}} }{{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{#-last}}{{/-last}}{{/values}}"{{/swagger1AnnotationLibrary}}{{/allowableValues}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/api.mustache b/boat-scaffold/src/main/templates/boat-webhooks/api.mustache
new file mode 100644
index 000000000..8c7583158
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/api.mustache
@@ -0,0 +1,399 @@
+/*
+Boat Generator configuration:
+ useBeanValidation: {{useBeanValidation}}
+ useOptional: {{useOptional}}
+ addServletRequest: {{addServletRequest}}
+ addBindingResult: {{addBindingResult}}
+ useLombokAnnotations: {{useLombokAnnotations}}
+ openApiNullable: {{openApiNullable}}
+ useSetForUniqueItems: {{useSetForUniqueItems}}
+ useWithModifiers: {{useWithModifiers}}
+*/
+package {{package}};
+
+{{#imports}}import {{import}};
+{{/imports}}
+{{#swagger2AnnotationLibrary}}
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+{{/swagger2AnnotationLibrary}}
+{{#swagger1AnnotationLibrary}}
+import io.swagger.annotations.*;
+{{/swagger1AnnotationLibrary}}
+{{#jdk8-no-delegate}}
+{{#virtualService}}
+import io.virtualan.annotation.ApiVirtual;
+import io.virtualan.annotation.VirtualService;
+{{/virtualService}}
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+{{/jdk8-no-delegate}}
+import org.springframework.http.ResponseEntity;
+{{#useBeanValidation}}
+import org.springframework.validation.annotation.Validated;
+{{/useBeanValidation}}
+{{#useSpringController}}
+import org.springframework.stereotype.Controller;
+{{/useSpringController}}
+import org.springframework.web.bind.annotation.*;
+{{#jdk8-no-delegate}}
+{{^reactive}}
+import org.springframework.web.context.request.NativeWebRequest;
+{{/reactive}}
+{{/jdk8-no-delegate}}
+import org.springframework.web.multipart.MultipartFile;
+{{#reactive}}
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import org.springframework.http.codec.multipart.Part;
+{{/reactive}}
+
+{{#useBeanValidation}}
+{{#useJakartaEe}}
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+{{/useJakartaEe}}
+{{^useJakartaEe}}
+import javax.validation.Valid;
+import javax.validation.constraints.*;
+{{/useJakartaEe}}
+{{/useBeanValidation}}
+{{#addServletRequest}}
+{{^useJakartaEe}}
+import javax.servlet.http.HttpServletRequest;
+{{/useJakartaEe}}
+{{#useJakartaEe}}
+import jakarta.servlet.http.HttpServletRequest;
+{{/useJakartaEe}}
+{{/addServletRequest}}
+{{#addBindingResult}}
+import org.springframework.validation.BindingResult;
+{{/addBindingResult}}
+import java.util.List;
+import java.util.Map;
+{{#jdk8-no-delegate}}
+import java.util.Optional;
+{{/jdk8-no-delegate}}
+{{^jdk8-no-delegate}}
+{{#useOptional}}
+import java.util.Optional;
+{{/useOptional}}
+{{/jdk8-no-delegate}}
+{{#async}}
+import java.util.concurrent.CompletableFuture;
+{{/async}}
+{{#useJakartaEe}}
+import jakarta.annotation.Generated;
+{{/useJakartaEe}}
+{{^useJakartaEe}}
+import javax.annotation.Generated;
+{{/useJakartaEe}}
+import {{modelPackage}}.WebhookResponse;
+import {{modelPackage}}.PrehookRequest;
+import {{modelPackage}}.PosthookRequest;
+{{>generatedAnnotation}}
+{{#useBeanValidation}}
+{{#useClassLevelBeanValidation}}
+@Validated
+{{/useClassLevelBeanValidation}}
+{{/useBeanValidation}}
+{{#useSpringController}}
+@Controller
+{{/useSpringController}}
+{{#swagger2AnnotationLibrary}}
+@io.swagger.v3.oas.annotations.tags.Tag(name = "{{{baseName}}}", description = {{#tagDescription}}"{{{.}}}"{{/tagDescription}}{{^tagDescription}}"the {{{baseName}}} API"{{/tagDescription}})
+{{/swagger2AnnotationLibrary}}
+{{#swagger1AnnotationLibrary}}
+@Api(value = "{{{baseName}}}", description = {{#tagDescription}}"{{{.}}}"{{/tagDescription}}{{^tagDescription}}"the {{{baseName}}} API"{{/tagDescription}})
+{{/swagger1AnnotationLibrary}}
+{{#operations}}
+{{#virtualService}}
+@VirtualService
+{{/virtualService}}
+{{#useRequestMappingOnInterface}}
+{{=<% %>=}}
+@RequestMapping("${openapi.<%title%>.base-path:<%>defaultBasePath%>}")
+<%={{ }}=%>
+{{/useRequestMappingOnInterface}}
+public interface {{classname}} {
+{{#jdk8-default-interface}}
+ {{^isDelegate}}
+ {{^reactive}}
+
+ default Optional getRequest() {
+ return Optional.empty();
+ }
+ {{/reactive}}
+ {{/isDelegate}}
+ {{#isDelegate}}
+
+ default {{classname}}Delegate getDelegate() {
+ return new {{classname}}Delegate() {};
+ }
+ {{/isDelegate}}
+{{/jdk8-default-interface}}
+{{#operation}}
+
+ /**
+ * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}}
+ {{#notes}}
+ * {{.}}
+ {{/notes}}
+ *
+ {{#allParams}}
+ * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
+ {{/allParams}}
+ * @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
+ * or {{/-last}}{{/responses}}
+ {{#isDeprecated}}
+ * @deprecated
+ {{/isDeprecated}}
+ {{#externalDocs}}
+ * {{description}}
+ * @see {{summary}} Documentation
+ {{/externalDocs}}
+ */
+ {{#isDeprecated}}
+ @Deprecated
+ {{/isDeprecated}}
+ {{#virtualService}}
+ @ApiVirtual
+ {{/virtualService}}
+ {{#swagger2AnnotationLibrary}}
+ @Operation(
+ operationId = "{{{operationId}}}",
+ {{#summary}}
+ summary = "{{{.}}}",
+ {{/summary}}
+ {{#notes}}
+ description = "{{{.}}}",
+ {{/notes}}
+ {{#vendorExtensions.x-tags.size}}
+ tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} },
+ {{/vendorExtensions.x-tags.size}}
+ responses = {
+ {{#responses}}
+ @ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = {
+ {{#produces}}
+ @Content(mediaType = "{{{mediaType}}}"){{^-last}},{{/-last}}
+ {{/produces}}
+ }{{/baseType}}){{^-last}},{{/-last}}
+ {{/responses}}
+ }{{#hasAuthMethods}},
+ security = {
+ {{#authMethods}}
+ @SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes={ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} }{{/isOAuth}}){{^-last}},{{/-last}}
+ {{/authMethods}}
+ }{{/hasAuthMethods}}
+ )
+ {{/swagger2AnnotationLibrary}}
+ {{#swagger1AnnotationLibrary}}
+ @ApiOperation(
+ {{#vendorExtensions.x-tags.size}}
+ tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} },
+ {{/vendorExtensions.x-tags.size}}
+ value = "{{{summary}}}",
+ nickname = "{{{operationId}}}",
+ notes = "{{{notes}}}"{{#returnBaseType}},
+ response = {{{.}}}.class{{/returnBaseType}}{{#returnContainer}},
+ responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}},
+ authorizations = {
+ {{#authMethods}}
+ {{#isOAuth}}
+ @Authorization(value = "{{name}}", scopes = {
+ {{#scopes}}
+ @AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}},{{/-last}}
+ {{/scopes}}
+ }){{^-last}},{{/-last}}
+ {{/isOAuth}}
+ {{^isOAuth}}
+ @Authorization(value = "{{name}}"){{^-last}},{{/-last}}
+ {{/isOAuth}}
+ {{/authMethods}} }{{/hasAuthMethods}}
+ )
+ @ApiResponses({
+ {{#responses}}
+ @ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}.class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}}
+ {{/responses}}
+ })
+ {{/swagger1AnnotationLibrary}}
+ {{#implicitHeadersParams.0}}
+ {{#swagger2AnnotationLibrary}}
+ @Parameters({
+ {{#implicitHeadersParams}}
+ {{>paramDoc}}{{^-last}},{{/-last}}
+ {{/implicitHeadersParams}}
+ })
+ {{/swagger2AnnotationLibrary}}
+ {{#swagger1AnnotationLibrary}}
+ @ApiImplicitParams({
+ {{#implicitHeadersParams}}
+ {{>implicitHeader}}{{^-last}},{{/-last}}
+ {{/implicitHeadersParams}}
+ })
+ {{/swagger1AnnotationLibrary}}
+ {{/implicitHeadersParams.0}}
+ @RequestMapping(
+ method = RequestMethod.POST,
+ value = "/service-api/prehook/{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{{path}}}"{{#singleContentTypes}}{{#hasProduces}},
+ produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}},
+ consumes = "{{{vendorExtensions.x-content-type}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}},
+ produces = { {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }{{/hasProduces}}{{#hasConsumes}},
+ consumes = { {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }{{/hasConsumes}}{{/singleContentTypes}}
+ )
+ {{#jdk8-default-interface}}default {{/jdk8-default-interface}}ResponseEntity prehook{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}(
+ @Valid @RequestBody PrehookRequest prehookRequest{{#allParams.0}}, {{/allParams.0}}{{#allParams}}{{#toOneLine}}{{>queryParams}}{{/toOneLine}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{>cookieParams}}{{>httpServletParam}}{{^-last}},
+ {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}},
+ {{/hasParams}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true){{/swagger2AnnotationLibrary}}{{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
+ {{/hasParams}}{{#springFoxDocumentationProvider}}@ApiIgnore {{/springFoxDocumentationProvider}}{{#springDocDocumentationProvider}}@ParameterObject {{/springDocDocumentationProvider}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}}
+ ){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} {
+ {{#delegate-method}}
+ return {{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, pageable{{/vendorExtensions.x-spring-paginated}});
+ {{/delegate-method}}
+ }
+
+ {{/jdk8-default-interface}}
+
+
+
+
+{{/operation}}
+
+
+{{#operation}}
+
+ /**
+ * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}}
+ {{#notes}}
+ * {{.}}
+ {{/notes}}
+ *
+ {{#allParams}}
+ * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
+ {{/allParams}}
+ * @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
+ * or {{/-last}}{{/responses}}
+ {{#isDeprecated}}
+ * @deprecated
+ {{/isDeprecated}}
+ {{#externalDocs}}
+ * {{description}}
+ * @see {{summary}} Documentation
+ {{/externalDocs}}
+ */
+ {{#isDeprecated}}
+ @Deprecated
+ {{/isDeprecated}}
+ {{#virtualService}}
+ @ApiVirtual
+ {{/virtualService}}
+ {{#swagger2AnnotationLibrary}}
+ @Operation(
+ operationId = "{{{operationId}}}",
+ {{#summary}}
+ summary = "{{{.}}}",
+ {{/summary}}
+ {{#notes}}
+ description = "{{{.}}}",
+ {{/notes}}
+ {{#vendorExtensions.x-tags.size}}
+ tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} },
+ {{/vendorExtensions.x-tags.size}}
+ responses = {
+ {{#responses}}
+ @ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = {
+ {{#produces}}
+ @Content(mediaType = "{{{mediaType}}}"){{^-last}},{{/-last}}
+ {{/produces}}
+ }{{/baseType}}){{^-last}},{{/-last}}
+ {{/responses}}
+ }{{#hasAuthMethods}},
+ security = {
+ {{#authMethods}}
+ @SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes={ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} }{{/isOAuth}}){{^-last}},{{/-last}}
+ {{/authMethods}}
+ }{{/hasAuthMethods}}
+ )
+ {{/swagger2AnnotationLibrary}}
+ {{#swagger1AnnotationLibrary}}
+ @ApiOperation(
+ {{#vendorExtensions.x-tags.size}}
+ tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} },
+ {{/vendorExtensions.x-tags.size}}
+ value = "{{{summary}}}",
+ nickname = "{{{operationId}}}",
+ notes = "{{{notes}}}"{{#returnBaseType}},
+ response = {{{.}}}.class{{/returnBaseType}}{{#returnContainer}},
+ responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}},
+ authorizations = {
+ {{#authMethods}}
+ {{#isOAuth}}
+ @Authorization(value = "{{name}}", scopes = {
+ {{#scopes}}
+ @AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}},{{/-last}}
+ {{/scopes}}
+ }){{^-last}},{{/-last}}
+ {{/isOAuth}}
+ {{^isOAuth}}
+ @Authorization(value = "{{name}}"){{^-last}},{{/-last}}
+ {{/isOAuth}}
+ {{/authMethods}} }{{/hasAuthMethods}}
+ )
+ @ApiResponses({
+ {{#responses}}
+ @ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}.class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}}
+ {{/responses}}
+ })
+ {{/swagger1AnnotationLibrary}}
+ {{#implicitHeadersParams.0}}
+ {{#swagger2AnnotationLibrary}}
+ @Parameters({
+ {{#implicitHeadersParams}}
+ {{>paramDoc}}{{^-last}},{{/-last}}
+ {{/implicitHeadersParams}}
+ })
+ {{/swagger2AnnotationLibrary}}
+ {{#swagger1AnnotationLibrary}}
+ @ApiImplicitParams({
+ {{#implicitHeadersParams}}
+ {{>implicitHeader}}{{^-last}},{{/-last}}
+ {{/implicitHeadersParams}}
+ })
+ {{/swagger1AnnotationLibrary}}
+ {{/implicitHeadersParams.0}}
+
+
+ @RequestMapping(
+ method = RequestMethod.POST,
+ value = "/service-api/posthook/{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{{path}}}"{{#singleContentTypes}}{{#hasProduces}},
+ produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}},
+ consumes = "{{{vendorExtensions.x-content-type}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}},
+ produces = { {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }{{/hasProduces}}{{#hasConsumes}},
+ consumes = { {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }{{/hasConsumes}}{{/singleContentTypes}}
+ )
+ {{#jdk8-default-interface}}default {{/jdk8-default-interface}}ResponseEntity posthook{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}(
+ @RequestBody PosthookRequest posthookRequest{{#allParams.0}}, {{/allParams.0}}{{#allParams}}{{#toOneLine}}{{>queryParams}}{{/toOneLine}}{{>pathParams}}{{>headerParams}}{{>formParams}}{{>cookieParams}}{{>httpServletParam}}{{^-last}},
+ {{/-last}} {{/allParams}}{{#reactive}}{{#hasParams}},
+ {{/hasParams}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true){{/swagger2AnnotationLibrary}}{{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
+ {{/hasParams}}{{#springFoxDocumentationProvider}}@ApiIgnore {{/springFoxDocumentationProvider}}{{#springDocDocumentationProvider}}@ParameterObject {{/springDocDocumentationProvider}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}}
+ ){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} {
+ {{#isDelegate}}
+ return {{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, pageable{{/vendorExtensions.x-spring-paginated}});
+ {{/isDelegate}}
+ }
+
+ {{/jdk8-default-interface}}
+
+
+
+
+{{/operation}}
+
+}
+{{/operations}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidation.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidation.mustache
new file mode 100644
index 000000000..e427a43a0
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidation.mustache
@@ -0,0 +1 @@
+{{#required}}{{^isReadOnly}}@NotNull {{/isReadOnly}}{{/required}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}@Valid {{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{^isContainer}}{{^isPrimitiveType}}@Valid {{/isPrimitiveType}}{{/isContainer}}{{>beanValidationCore}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidationBodyParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationBodyParams.mustache
new file mode 100644
index 000000000..6f4e70f06
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationBodyParams.mustache
@@ -0,0 +1 @@
+{{^useOptional}}{{>beanValidationCore}}{{/useOptional}}{{#useOptional}}{{#required}}{{>beanValidationCore}}{{/required}}{{/useOptional}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidationCore.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationCore.mustache
new file mode 100644
index 000000000..7faf96bc9
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationCore.mustache
@@ -0,0 +1,24 @@
+{{#pattern}}{{^isByteArray}}@Pattern(regexp = "{{{pattern}}}") {{/isByteArray}}{{/pattern}}{{!
+minLength && maxLength set
+}}{{#minLength}}{{#maxLength}}@Size(min = {{minLength}}, max = {{maxLength}}) {{/maxLength}}{{/minLength}}{{!
+minLength set, maxLength not
+}}{{#minLength}}{{^maxLength}}@Size(min = {{minLength}}) {{/maxLength}}{{/minLength}}{{!
+minLength not set, maxLength set
+}}{{^minLength}}{{#maxLength}}@Size(max = {{.}}) {{/maxLength}}{{/minLength}}{{!
+@Size: minItems && maxItems set
+}}{{#minItems}}{{#maxItems}}@Size(min = {{minItems}}, max = {{maxItems}}) {{/maxItems}}{{/minItems}}{{!
+@Size: minItems set, maxItems not
+}}{{#minItems}}{{^maxItems}}@Size(min = {{minItems}}) {{/maxItems}}{{/minItems}}{{!
+@Size: minItems not set && maxItems set
+}}{{^minItems}}{{#maxItems}}@Size(max = {{.}}) {{/maxItems}}{{/minItems}}{{!
+@Email: useBeanValidation set && isEmail && java8 set
+}}{{#useBeanValidation}}{{#isEmail}}{{#java8}}@org.hibernate.validator.constraints.Email{{/java8}}{{/isEmail}}{{/useBeanValidation}}{{!
+@Email: performBeanValidation set && isEmail && not java8 set
+}}{{#performBeanValidation}}{{#isEmail}}{{^java8}}@jakarta.validation.constraints.Email{{/java8}}{{/isEmail}}{{/performBeanValidation}}{{!
+check for integer or long / all others=decimal type with @Decimal*
+isInteger set
+}}{{#isInteger}}{{#minimum}}@Min({{.}}) {{/minimum}}{{#maximum}}@Max({{.}}) {{/maximum}}{{/isInteger}}{{!
+isLong set
+}}{{#isLong}}{{#minimum}}@Min({{.}}L) {{/minimum}}{{#maximum}}@Max({{.}}L) {{/maximum}}{{/isLong}}{{!
+Not Integer, not Long => we have a decimal value!
+}}{{^isInteger}}{{^isLong}}{{#minimum}}@DecimalMin({{#exclusiveMinimum}}value = {{/exclusiveMinimum}}"{{minimum}}"{{#exclusiveMinimum}}, inclusive = false{{/exclusiveMinimum}}) {{/minimum}}{{#maximum}}@DecimalMax({{#exclusiveMaximum}}value = {{/exclusiveMaximum}}"{{maximum}}"{{#exclusiveMaximum}}, inclusive = false{{/exclusiveMaximum}}) {{/maximum}}{{/isLong}}{{/isInteger}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidationPathParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationPathParams.mustache
new file mode 100644
index 000000000..051bd53c0
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationPathParams.mustache
@@ -0,0 +1 @@
+{{! PathParam is always required, no @NotNull necessary }}{{>beanValidationCore}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidationQueryParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationQueryParams.mustache
new file mode 100644
index 000000000..a2f19f774
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationQueryParams.mustache
@@ -0,0 +1 @@
+{{#required}}@NotNull {{/required}}{{^useOptional}}{{>beanValidationCore}}{{/useOptional}}{{#useOptional}}{{#required}}{{>beanValidationCore}}{{/required}}{{/useOptional}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/bodyParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/bodyParams.mustache
new file mode 100644
index 000000000..42ca93d70
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/bodyParams.mustache
@@ -0,0 +1 @@
+{{#isBodyParam}}{{>paramDoc}}{{#useBeanValidation}} @Valid{{>beanValidationBodyParams}}{{/useBeanValidation}} @RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{#useBeanValidation}}{{#addBindingResult}}, BindingResult bindingResult{{/addBindingResult}}{{/useBeanValidation}}{{/isBodyParam}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/collectionDataType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/collectionDataType.mustache
new file mode 100644
index 000000000..b6993f0d9
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/collectionDataType.mustache
@@ -0,0 +1,23 @@
+{{#openApiNullable}}
+ {{#isNullable}}JsonNullable<{{/isNullable}}
+{{/openApiNullable}}
+{{! apply collection type e.g. Set, List }}
+{{{baseType}}}<
+{{#useBeanValidation}}
+ {{#items}}
+ {{#isPrimitiveType}}
+ {{>beanValidationCore}}
+ {{/isPrimitiveType}}
+ {{^isPrimitiveType}}
+ {{packageName}}@Valid
+ {{/isPrimitiveType}}
+ {{/items}}
+{{/useBeanValidation}}
+{{^useBeanValidation}}
+{{items.packageName}}
+{{/useBeanValidation}}
+{{! apply collection item type }}
+{{{items.simpleName}}}>
+{{#openApiNullable}}
+ {{#isNullable}}>{{/isNullable}}
+{{/openApiNullable}}
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/collectionDataTypeParam.mustache b/boat-scaffold/src/main/templates/boat-webhooks/collectionDataTypeParam.mustache
new file mode 100644
index 000000000..623a82ca2
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/collectionDataTypeParam.mustache
@@ -0,0 +1,22 @@
+{{#isArray}}
+ {{#useBeanValidation}}
+ {{#uniqueItems}}java.util.Set{{/uniqueItems}}
+ {{^uniqueItems}}java.util.List{{/uniqueItems}}<
+ {{#items}}
+ {{#isPrimitiveType}}
+ {{>beanValidationCore}} {{{dataType}}}>
+ {{/isPrimitiveType}}
+ {{^isPrimitiveType}}
+ {{packageName}}@Valid {{simpleName}}>
+ {{/isPrimitiveType}}
+ {{/items}}
+ {{/useBeanValidation}}
+ {{^useBeanValidation}}
+ {{#uniqueItems}}java.util.Set{{/uniqueItems}}
+ {{^uniqueItems}}java.util.List{{/uniqueItems}}
+ <{{{items.dataType}}}>
+ {{/useBeanValidation}}
+{{/isArray}}
+{{^isArray}}
+ {{{dataType}}}
+{{/isArray}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/converter.mustache b/boat-scaffold/src/main/templates/boat-webhooks/converter.mustache
new file mode 100644
index 000000000..a331ded63
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/converter.mustache
@@ -0,0 +1,34 @@
+package {{configPackage}};
+
+{{#models}}
+ {{#model}}
+ {{#isEnum}}
+import {{modelPackage}}.{{name}};
+ {{/isEnum}}
+ {{/model}}
+{{/models}}
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.convert.converter.Converter;
+
+@Configuration
+public class EnumConverterConfiguration {
+
+{{#models}}
+{{#model}}
+{{#isEnum}}
+ @Bean
+ Converter<{{{dataType}}}, {{name}}> {{classVarName}}Converter() {
+ return new Converter<{{{dataType}}}, {{name}}>() {
+ @Override
+ public {{name}} convert({{{dataType}}} source) {
+ return {{name}}.fromValue(source);
+ }
+ };
+ }
+{{/isEnum}}
+{{/model}}
+{{/models}}
+
+}
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/cookieParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/cookieParams.mustache
new file mode 100644
index 000000000..74a837988
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/cookieParams.mustache
@@ -0,0 +1 @@
+{{#isCookieParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>optionalDataType}} {{paramName}}{{/isCookieParam}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/dateTimeParam.mustache b/boat-scaffold/src/main/templates/boat-webhooks/dateTimeParam.mustache
new file mode 100644
index 000000000..5f4f3a264
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/dateTimeParam.mustache
@@ -0,0 +1 @@
+{{#isDate}} @DateTimeFormat(iso = DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME){{/isDateTime}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/enumClass.mustache b/boat-scaffold/src/main/templates/boat-webhooks/enumClass.mustache
new file mode 100644
index 000000000..0c8de0641
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/enumClass.mustache
@@ -0,0 +1,50 @@
+ /**
+ * {{^description}}Gets or Sets {{{name}}}{{/description}}{{{description}}}
+ */
+ {{>additionalEnumTypeAnnotations}}public enum {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} {
+ {{#gson}}
+ {{#allowableValues}}
+ {{#enumVars}}
+ @SerializedName({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}})
+ {{{name}}}({{{value}}}){{^-last}},
+ {{/-last}}{{#-last}};{{/-last}}
+ {{/enumVars}}
+ {{/allowableValues}}
+ {{/gson}}
+ {{^gson}}
+ {{#allowableValues}}
+ {{#enumVars}}
+ {{{name}}}({{{value}}}){{^-last}},
+ {{/-last}}{{#-last}};{{/-last}}
+ {{/enumVars}}
+ {{/allowableValues}}
+ {{/gson}}
+
+ private final {{{dataType}}} value;
+
+ {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}({{{dataType}}} value) {
+ this.value = value;
+ }
+
+ {{#jackson}}
+ @JsonValue
+ {{/jackson}}
+ public {{{dataType}}} getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(value);
+ }
+
+ @JsonCreator
+ public static {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) {
+ for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) {
+ if (b.value.equals(value)) {
+ return b;
+ }
+ }
+ {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/isNullable}}
+ }
+ }
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/enumOuterClass.mustache b/boat-scaffold/src/main/templates/boat-webhooks/enumOuterClass.mustache
new file mode 100644
index 000000000..5528f1cef
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/enumOuterClass.mustache
@@ -0,0 +1,51 @@
+{{#jackson}}
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+{{/jackson}}
+
+/**
+ * {{^description}}Gets or Sets {{{name}}}{{/description}}{{{description}}}
+ */
+{{>additionalEnumTypeAnnotations}}
+{{>generatedAnnotation}}
+public enum {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} {
+ {{#gson}}
+ {{#allowableValues}}{{#enumVars}}
+ @SerializedName({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}})
+ {{{name}}}({{{value}}}){{^-last}},
+ {{/-last}}{{#-last}};{{/-last}}{{/enumVars}}{{/allowableValues}}
+ {{/gson}}
+ {{^gson}}
+ {{#allowableValues}}{{#enumVars}}
+ {{{name}}}({{{value}}}){{^-last}},
+ {{/-last}}{{#-last}};{{/-last}}{{/enumVars}}{{/allowableValues}}
+ {{/gson}}
+
+ private final {{{dataType}}} value;
+
+ {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}({{{dataType}}} value) {
+ this.value = value;
+ }
+
+ {{#jackson}}
+ @JsonValue
+ {{/jackson}}
+ public {{{dataType}}} getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(value);
+ }
+
+ @JsonCreator
+ public static {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) {
+ for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) {
+ if (b.value.equals(value)) {
+ return b;
+ }
+ }
+ {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/isNullable}}
+ }
+}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/formParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/formParams.mustache
new file mode 100644
index 000000000..505e5909e
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/formParams.mustache
@@ -0,0 +1 @@
+{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#isMultipart}} @RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{>dateTimeParam}} {{{dataType}}} {{paramName}}{{/isMultipart}}{{^isMultipart}}{{#useBeanValidation}} @Valid{{/useBeanValidation}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{>dateTimeParam}} {{{dataType}}} {{paramName}}{{/isMultipart}}{{/isFile}}{{#isFile}}{{>paramDoc}} @RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#isArray}}List<{{/isArray}}{{#reactive}}Flux{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{#isArray}}>{{/isArray}} {{paramName}}{{/isFile}}{{/isFormParam}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/generatedAnnotation.mustache b/boat-scaffold/src/main/templates/boat-webhooks/generatedAnnotation.mustache
new file mode 100644
index 000000000..2f8ef3059
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/generatedAnnotation.mustache
@@ -0,0 +1 @@
+@Generated(value = "{{generatorClass}}"{{^hideGenerationTimestamp}}, date = "{{generatedDate}}"{{/hideGenerationTimestamp}})
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/headerParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/headerParams.mustache
new file mode 100644
index 000000000..b9589c147
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/headerParams.mustache
@@ -0,0 +1 @@
+{{#isHeaderParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>optionalDataType}} {{paramName}}{{/isHeaderParam}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/httpServletParam.mustache b/boat-scaffold/src/main/templates/boat-webhooks/httpServletParam.mustache
new file mode 100644
index 000000000..4c25519e3
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/httpServletParam.mustache
@@ -0,0 +1 @@
+{{#isHttpServletRequest}}{{^reactive}}{{#addServletRequest}} HttpServletRequest httpServletRequest{{/addServletRequest}}{{/reactive}}{{/isHttpServletRequest}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/implicitHeader.mustache b/boat-scaffold/src/main/templates/boat-webhooks/implicitHeader.mustache
new file mode 100644
index 000000000..0453940ce
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/implicitHeader.mustache
@@ -0,0 +1 @@
+{{#isHeaderParam}}@ApiImplicitParam(name = "{{{baseName}}}", value = "{{{description}}}", {{#required}}required = true,{{/required}} dataType = "{{{dataType}}}", paramType = "header"){{/isHeaderParam}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/mapDataType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/mapDataType.mustache
new file mode 100644
index 000000000..5f7a2196b
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/mapDataType.mustache
@@ -0,0 +1,21 @@
+{{#openApiNullable}}
+ {{#isNullable}}JsonNullable<{{/isNullable}}
+{{/openApiNullable}}
+{{! apply map type with String as key type }}
+{{{baseType}}}beanValidationCore}}
+ {{/isPrimitiveType}}
+ {{^isPrimitiveType}}
+ @Valid
+ {{/isPrimitiveType}}
+ {{/items}}
+{{/useBeanValidation}}
+{{! apply map value type with closing bracket }}
+{{{items.datatypeWithEnum}}}>
+{{#openApiNullable}}
+ {{#isNullable}}>{{/isNullable}}
+{{/openApiNullable}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/methodBody.mustache b/boat-scaffold/src/main/templates/boat-webhooks/methodBody.mustache
new file mode 100644
index 000000000..a7ccd54c4
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/methodBody.mustache
@@ -0,0 +1,47 @@
+{{^reactive}}
+{{#examples}}
+ {{#-first}}
+ {{#async}}
+return CompletableFuture.supplyAsync(()-> {
+ {{/async}}getRequest().ifPresent(request -> {
+{{#async}} {{/async}} for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
+ {{/-first}}
+{{#async}} {{/async}}{{^async}} {{/async}} if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) {
+{{#async}} {{/async}}{{^async}} {{/async}} String exampleString = {{>exampleString}};
+{{#async}} {{/async}}{{^async}} {{/async}} ApiUtil.setExampleResponse(request, "{{{contentType}}}", exampleString);
+{{#async}} {{/async}}{{^async}} {{/async}} break;
+{{#async}} {{/async}}{{^async}} {{/async}} }
+ {{#-last}}
+{{#async}} {{/async}}{{^async}} {{/async}} }
+{{#async}} {{/async}} });
+{{#async}} {{/async}} return new ResponseEntity<>({{#returnSuccessCode}}HttpStatus.valueOf({{{statusCode}}}){{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}});
+ {{#async}}
+ }, Runnable::run);
+ {{/async}}
+ {{/-last}}
+{{/examples}}
+{{^examples}}
+return {{#async}}CompletableFuture.completedFuture({{/async}}new ResponseEntity<>({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}){{#async}}){{/async}};
+{{/examples}}
+{{/reactive}}
+{{#reactive}}
+Mono result = Mono.empty();
+ {{#examples}}
+ {{#-first}}
+ exchange.getResponse().setStatusCode({{#returnSuccessCode}}HttpStatus.valueOf({{{statusCode}}}){{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}});
+ for (MediaType mediaType : exchange.getRequest().getHeaders().getAccept()) {
+ {{/-first}}
+ if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) {
+ String exampleString = {{>exampleString}};
+ result = ApiUtil.getExampleResponse(exchange, mediaType, exampleString);
+ break;
+ }
+ {{#-last}}
+ }
+ {{/-last}}
+ {{/examples}}
+{{^examples}}
+ exchange.getResponse().setStatusCode({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}});
+{{/examples}}
+ return result{{#allParams}}{{#isBodyParam}}{{^isArray}}{{#paramName}}.then({{.}}){{/paramName}}{{/isArray}}{{#isArray}}{{#paramName}}.thenMany({{.}}){{/paramName}}{{/isArray}}{{/isBodyParam}}{{/allParams}}.then(Mono.empty());
+{{/reactive}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/model.mustache b/boat-scaffold/src/main/templates/boat-webhooks/model.mustache
new file mode 100644
index 000000000..896d93104
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/model.mustache
@@ -0,0 +1,83 @@
+/*
+Boat Generator configuration:
+ useBeanValidation: {{useBeanValidation}}
+ useOptional: {{useOptional}}
+ addServletRequest: {{addServletRequest}}
+ useLombokAnnotations: {{useLombokAnnotations}}
+ openApiNullable: {{openApiNullable}}
+ useSetForUniqueItems: {{useSetForUniqueItems}}
+ useWithModifiers: {{useWithModifiers}}
+ useJakartaEe: {{useJakartaEe}}
+ useSpringBoot3: {{useSpringBoot3}}
+*/
+package {{package}};
+
+import java.net.URI;
+import java.util.Objects;
+{{#imports}}import {{import}};
+{{/imports}}
+{{#openApiNullable}}
+import org.openapitools.jackson.nullable.JsonNullable;
+{{/openApiNullable}}
+{{#serializableModel}}
+import java.io.Serializable;
+{{/serializableModel}}
+import java.time.OffsetDateTime;
+{{#useBeanValidation}}
+{{#useJakartaEe}}
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+{{/useJakartaEe}}
+{{^useJakartaEe}}
+import javax.validation.Valid;
+import javax.validation.constraints.*;
+{{/useJakartaEe}}
+{{/useBeanValidation}}
+{{^useBeanValidation}}
+{{#useJakartaEe}}
+import jakarta.validation.constraints.*;
+{{/useJakartaEe}}
+{{^useJakartaEe}}
+import javax.validation.constraints.*;
+{{/useJakartaEe}}
+{{/useBeanValidation}}
+{{#performBeanValidation}}
+import org.hibernate.validator.constraints.*;
+{{/performBeanValidation}}
+{{#jackson}}
+{{#withXml}}
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+{{/withXml}}
+{{/jackson}}
+{{#swagger2AnnotationLibrary}}
+import io.swagger.v3.oas.annotations.media.Schema;
+{{/swagger2AnnotationLibrary}}
+
+{{#withXml}}
+import javax.xml.bind.annotation.*;
+{{/withXml}}
+{{^parent}}
+{{#hateoas}}
+import org.springframework.hateoas.RepresentationModel;
+{{/hateoas}}
+{{/parent}}
+
+import java.util.*;
+{{#useJakartaEe}}
+import jakarta.annotation.Generated;
+{{/useJakartaEe}}
+{{^useJakartaEe}}
+import javax.annotation.Generated;
+{{/useJakartaEe}}
+
+{{#models}}
+{{#model}}
+{{#isEnum}}
+{{>enumOuterClass}}
+{{/isEnum}}
+{{^isEnum}}
+{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>pojo}}{{/vendorExtensions.x-is-one-of-interface}}
+{{/isEnum}}
+{{/model}}
+{{/models}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/nullableDataType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/nullableDataType.mustache
new file mode 100644
index 000000000..ba9bb9463
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/nullableDataType.mustache
@@ -0,0 +1 @@
+{{#openApiNullable}}{{#isNullable}}JsonNullable<{{{datatypeWithEnum}}}>{{/isNullable}}{{^isNullable}}{{{datatypeWithEnum}}}{{/isNullable}}{{/openApiNullable}}{{^openApiNullable}}{{{datatypeWithEnum}}}{{/openApiNullable}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/oneof_interface.mustache b/boat-scaffold/src/main/templates/boat-webhooks/oneof_interface.mustache
new file mode 100644
index 000000000..679fe3d88
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/oneof_interface.mustache
@@ -0,0 +1,13 @@
+{{>additionalOneOfTypeAnnotations}}
+{{#withXml}}
+{{>xmlAnnotation}}
+{{/withXml}}
+{{#discriminator}}
+{{>typeInfoAnnotation}}
+{{/discriminator}}
+{{>generatedAnnotation}}
+public interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
+ {{#discriminator}}
+ public {{propertyType}} {{propertyGetter}}();
+ {{/discriminator}}
+}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/openapiDocumentationConfig.mustache b/boat-scaffold/src/main/templates/boat-webhooks/openapiDocumentationConfig.mustache
new file mode 100644
index 000000000..bae72f1cc
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/openapiDocumentationConfig.mustache
@@ -0,0 +1,89 @@
+package {{configPackage}};
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import org.springframework.web.util.UriComponentsBuilder;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.Contact;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.paths.Paths;
+import springfox.documentation.spring.web.paths.RelativePathProvider;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+{{#useOptional}}
+import java.util.Optional;
+{{/useOptional}}
+{{#useJakartaEe}}
+import jakarta.annotation.Generated;
+import jakarta.servlet.ServletContext;
+{{/useJakartaEe}}
+{{^useJakartaEe}}
+import javax.annotation.Generated;
+import javax.servlet.ServletContext;
+{{/useJakartaEe}}
+
+{{>generatedAnnotation}}
+@Configuration
+@EnableSwagger2
+public class OpenAPIDocumentationConfig {
+
+ ApiInfo apiInfo() {
+ return new ApiInfoBuilder()
+ .title("{{appName}}")
+ .description("{{{appDescription}}}")
+ .license("{{licenseInfo}}")
+ .licenseUrl("{{licenseUrl}}")
+ .termsOfServiceUrl("{{infoUrl}}")
+ .version("{{appVersion}}")
+ .contact(new Contact("","", "{{infoEmail}}"))
+ .build();
+ }
+
+ @Bean
+{{=<% %>=}}
+ public Docket customImplementation(ServletContext servletContext, @Value("${openapi.<%title%>.base-path:<%>defaultBasePath%>}") String basePath) {
+<%={{ }}=%>
+ return new Docket(DocumentationType.SWAGGER_2)
+ .select()
+ .apis(RequestHandlerSelectors.basePackage("{{apiPackage}}"))
+ .build()
+ .pathProvider(new BasePathAwareRelativePathProvider(servletContext, basePath))
+ .directModelSubstitute(java.time.LocalDate.class, java.sql.Date.class)
+ .directModelSubstitute(java.time.OffsetDateTime.class, java.util.Date.class)
+ {{#joda}}
+ .directModelSubstitute(org.joda.time.LocalDate.class, java.sql.Date.class)
+ .directModelSubstitute(org.joda.time.DateTime.class, java.util.Date.class)
+ {{/joda}}
+ {{#useOptional}}
+ .genericModelSubstitutes(Optional.class)
+ {{/useOptional}}
+ .apiInfo(apiInfo());
+ }
+
+ class BasePathAwareRelativePathProvider extends RelativePathProvider {
+ private String basePath;
+
+ public BasePathAwareRelativePathProvider(ServletContext servletContext, String basePath) {
+ super(servletContext);
+ this.basePath = basePath;
+ }
+
+ @Override
+ protected String applicationPath() {
+ return Paths.removeAdjacentForwardSlashes(UriComponentsBuilder.fromPath(super.applicationPath()).path(basePath).build().toString());
+ }
+
+ @Override
+ public String getOperationPath(String operationPath) {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromPath("/");
+ return Paths.removeAdjacentForwardSlashes(
+ uriComponentsBuilder.path(operationPath.replaceFirst("^" + basePath, "")).build().toString());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/optionalDataType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/optionalDataType.mustache
new file mode 100644
index 000000000..ec9df1a76
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/optionalDataType.mustache
@@ -0,0 +1,13 @@
+{{#toOneLine}}
+ {{#useOptional}}
+ {{#required}}
+ {{>collectionDataTypeParam}}
+ {{/required}}
+ {{^required}}
+ Optional<{{>collectionDataTypeParam}}>
+ {{/required}}
+ {{/useOptional}}
+ {{^useOptional}}
+ {{>collectionDataTypeParam}}
+ {{/useOptional}}
+{{/toOneLine}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/paramDoc.mustache b/boat-scaffold/src/main/templates/boat-webhooks/paramDoc.mustache
new file mode 100644
index 000000000..7c0c65eb1
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/paramDoc.mustache
@@ -0,0 +1 @@
+{{#swagger2AnnotationLibrary}}@Parameter(name = "{{{baseName}}}", description = "{{{description}}}"{{#required}}, required = true{{/required}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, {{> allowableValues }}{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/pathParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/pathParams.mustache
new file mode 100644
index 000000000..16888518c
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/pathParams.mustache
@@ -0,0 +1 @@
+{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{>paramDoc}} @PathVariable("{{baseName}}"){{>dateTimeParam}} {{>optionalDataType}} {{paramName}}{{/isPathParam}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/pojo.mustache b/boat-scaffold/src/main/templates/boat-webhooks/pojo.mustache
new file mode 100644
index 000000000..ed55202ae
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/pojo.mustache
@@ -0,0 +1,369 @@
+/**
+* {{description}}{{^description}}{{classname}}{{/description}}
+*/
+{{>additionalModelTypeAnnotations}}
+{{#description}}
+ {{#swagger1AnnotationLibrary}}
+ @ApiModel(description = "{{{description}}}")
+ {{/swagger1AnnotationLibrary}}
+ {{#swagger2AnnotationLibrary}}
+ @Schema({{#name}}name = "{{name}}", {{/name}}description = "{{{description}}}")
+ {{/swagger2AnnotationLibrary}}
+{{/description}}
+{{#discriminator}}
+ {{>typeInfoAnnotation}}
+{{/discriminator}}
+{{#jackson}}
+ {{#isClassnameSanitized}}
+ {{^hasDiscriminatorWithNonEmptyMapping}}
+ @JsonTypeName("{{name}}")
+ {{/hasDiscriminatorWithNonEmptyMapping}}
+ {{/isClassnameSanitized}}
+{{/jackson}}
+{{#withXml}}
+ {{>xmlAnnotation}}
+{{/withXml}}
+{{>generatedAnnotation}}
+{{#useLombokAnnotations}}
+@lombok.EqualsAndHashCode(onlyExplicitlyIncluded = true, doNotUseGetters = true{{#parent}}, callSuper = true{{/parent}})
+@lombok.ToString(onlyExplicitlyIncluded = true, doNotUseGetters = true{{#parent}}, callSuper = true{{/parent}})
+{{/useLombokAnnotations}}
+{{#vendorExtensions.x-class-extra-annotation}}
+ {{{vendorExtensions.x-class-extra-annotation}}}
+{{/vendorExtensions.x-class-extra-annotation}}
+public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}} extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}}{{#vendorExtensions.x-implements}}{{#-first}} implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
+{{#serializableModel}}
+
+ private static final long serialVersionUID = 1L;
+{{/serializableModel}}
+{{#vars}}
+
+ {{#isEnum}}
+ {{^isContainer}}
+ {{>enumClass}}
+ {{/isContainer}}
+ {{#isContainer}}
+ {{#mostInnerItems}}
+ {{>enumClass}}
+ {{/mostInnerItems}}
+ {{/isContainer}}
+ {{/isEnum}}
+ {{#jackson}}
+ {{#withXml}}
+ @JacksonXmlProperty({{#isXmlAttribute}}isAttribute = true, {{/isXmlAttribute}}{{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}localName = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}")
+ {{/withXml}}
+ {{/jackson}}
+ {{#gson}}
+ @SerializedName("{{baseName}}")
+ {{/gson}}
+ {{#useLombokAnnotations}}
+ @lombok.Getter{{#swagger1AnnotationLibrary}}(onMethod_ = @ApiModelProperty({{#example}}example = "{{{example}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}")){{/swagger1AnnotationLibrary}}
+ @lombok.Setter
+ @lombok.EqualsAndHashCode.Include
+ @lombok.ToString.Include
+ {{#vendorExtensions.x-extra-annotation}}
+ {{#indent4}}{{{vendorExtensions.x-extra-annotation}}}{{/indent4}}{{/vendorExtensions.x-extra-annotation}}
+ {{/useLombokAnnotations}}
+ {{#vendorExtensions.x-field-extra-annotation}}
+ {{{vendorExtensions.x-field-extra-annotation}}}
+ {{/vendorExtensions.x-field-extra-annotation}}
+ {{#trimAndIndent4}}
+ {{#isContainer}}
+ {{#useBeanValidation}}
+ @Valid
+ {{#required}}
+ @NotNull
+ {{/required}}
+ {{>beanValidationCore}}
+ {{/useBeanValidation}}
+ {{#isMap}}
+ {{#openApiNullable}}
+ private {{#toOneLine}}{{>mapDataType}}{{/toOneLine}} {{name}}{{#isNullable}} = JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ private {{#toOneLine}}{{>mapDataType}}{{/toOneLine}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
+ {{/openApiNullable}}
+ {{/isMap}}
+ {{^isMap}}
+ {{#isArray}}
+ {{#openApiNullable}}
+ private {{#toOneLine}}{{>collectionDataType}}{{/toOneLine}} {{name}}{{#isNullable}} = JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ private {{#toOneLine}}{{>collectionDataType}}{{/toOneLine}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
+ {{/openApiNullable}}
+ {{/isArray}}
+ {{^isArray}}
+ {{#openApiNullable}}
+ private {{>nullableDataType}} {{name}}{{#isNullable}} = JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ private {{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
+ {{/openApiNullable}}
+ {{/isArray}}
+ {{/isMap}}
+ {{/isContainer}}
+ {{^isContainer}}
+ {{#isDate}}
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
+ {{/isDate}}
+ {{#isDateTime}}
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
+ {{/isDateTime}}
+ {{#openApiNullable}}
+ {{#required}}
+ @NotNull
+ {{/required}}
+ {{#useBeanValidation}}
+ @Valid
+ {{#required}}
+ @NotNull
+ {{/required}}
+ {{>beanValidationCore}}
+ {{/useBeanValidation}}
+ private {{>nullableDataType}} {{name}}{{#isNullable}} = JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ {{#useBeanValidation}}
+ @Valid
+ {{#required}}
+ @NotNull
+ {{/required}}
+ {{>beanValidationCore}}
+ {{/useBeanValidation}}
+ private {{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{/isContainer}}
+ {{/trimAndIndent4}}
+{{/vars}}
+{{#generatedConstructorWithRequiredArgs}}
+ {{#hasRequired}}
+
+ public {{classname}}() {
+ super();
+ }
+
+ /**
+ * Constructor with only required parameters
+ */
+ public {{classname}}({{#requiredVars}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/requiredVars}}) {
+ {{#parent}}
+ super({{#parentRequiredVars}}{{name}}{{^-last}}, {{/-last}}{{/parentRequiredVars}});
+ {{/parent}}
+ {{#vars}}
+ {{#required}}
+ {{#openApiNullable}}
+ this.{{name}} = {{#isNullable}}JsonNullable.of({{name}}){{/isNullable}}{{^isNullable}}{{name}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ this.{{name}} = {{name}};
+ {{/openApiNullable}}
+ {{/required}}
+ {{/vars}}
+ }
+ {{/hasRequired}}
+{{/generatedConstructorWithRequiredArgs}}
+{{#vars}}
+
+ {{! begin feature: fluent setter methods }}
+ public {{classname}} {{#useWithModifiers}}with{{nameInCamelCase}}{{/useWithModifiers}}{{^useWithModifiers}}{{name}}{{/useWithModifiers}}({{{datatypeWithEnum}}} {{name}}) {
+ {{#openApiNullable}}
+ this.{{name}} = {{#isNullable}}JsonNullable.of({{name}}){{/isNullable}}{{^isNullable}}{{name}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ this.{{name}} = {{name}};
+ {{/openApiNullable}}
+ return this;
+ }
+ {{#isArray}}
+
+ public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) {
+ {{#openApiNullable}}
+ if (this.{{name}} == null{{#isNullable}} || !this.{{name}}.isPresent(){{/isNullable}}) {
+ this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}{{#isNullable}}){{/isNullable}};
+ }
+ this.{{name}}{{#isNullable}}.get(){{/isNullable}}.add({{name}}Item);
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ if (this.{{name}} == null) {
+ this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}};
+ }
+ this.{{name}}.add({{name}}Item);
+ {{/openApiNullable}}
+ return this;
+ }
+ {{/isArray}}
+ {{#isMap}}
+
+ {{#openApiNullable}}
+ {{#isNullable}}
+ public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {
+ {{^required}}
+ if (this.{{name}} == null) {
+ this.{{name}} = JsonNullable.of({{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}});
+ }
+ {{/required}}
+ this.{{name}}.get().put(key, {{name}}Item);
+ return this;
+ }
+ {{/isNullable}}
+ {{^isNullable}}
+ public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {
+ {{^required}}
+
+ if (this.{{name}} == null) {
+ this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}};
+ }
+ {{/required}}
+ this.{{name}}.put(key, {{name}}Item);
+ return this;
+ }
+ {{/isNullable}}
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {
+ {{^required}}
+
+ if (this.{{name}} == null) {
+ this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}};
+ }
+ {{/required}}
+ this.{{name}}.put(key, {{name}}Item);
+ return this;
+ }
+ {{/openApiNullable}}
+ {{/isMap}}
+ {{! end feature: fluent setter methods }}
+ {{! begin feature: getter and setter }}
+
+{{^useLombokAnnotations}}
+ /**
+ {{#description}}
+ * {{{.}}}
+ {{/description}}
+ {{^description}}
+ * Get {{name}}
+ {{/description}}
+ {{#minimum}}
+ * minimum: {{.}}
+ {{/minimum}}
+ {{#maximum}}
+ * maximum: {{.}}
+ {{/maximum}}
+ * @return {{name}}
+ */
+ {{#vendorExtensions.x-extra-annotation}}
+ {{{vendorExtensions.x-extra-annotation}}}
+ {{/vendorExtensions.x-extra-annotation}}
+ {{#useBeanValidation}}
+ {{>beanValidation}}
+ {{/useBeanValidation}}
+ {{^useBeanValidation}}
+ {{#required}}@NotNull{{/required}}
+ {{/useBeanValidation}}
+ {{#swagger2AnnotationLibrary}}
+ @Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}})
+ {{/swagger2AnnotationLibrary}}
+ {{#swagger1AnnotationLibrary}}
+ @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}")
+ {{/swagger1AnnotationLibrary}}
+ {{#jackson}}
+ @JsonProperty("{{baseName}}")
+ {{/jackson}}
+ public {{>nullableDataType}} {{getter}}() {
+ return {{name}};
+ }
+
+ {{#vendorExtensions.x-setter-extra-annotation}}
+ {{{vendorExtensions.x-setter-extra-annotation}}}
+ {{/vendorExtensions.x-setter-extra-annotation}}
+ public void {{setter}}({{>nullableDataType}} {{name}}) {
+ this.{{name}} = {{name}};
+ }
+ {{/useLombokAnnotations}}
+ {{! end feature: getter and setter }}
+ {{/vars}}
+ {{#parentVars}}
+
+{{! begin feature: fluent setter methods for inherited properties }}
+ public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) {
+ super.{{setter}}({{name}});
+ return this;
+ }
+ {{#isArray}}
+
+ public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) {
+ super.add{{nameInCamelCase}}Item({{name}}Item);
+ return this;
+ }
+ {{/isArray}}
+ {{#isMap}}
+
+ public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {
+ super.put{{nameInCamelCase}}Item(key, {{name}}Item);
+ return this;
+ }
+ {{/isMap}}
+{{! end feature: fluent setter methods for inherited properties }}
+{{/parentVars}}
+
+{{^useLombokAnnotations}}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }{{#hasVars}}
+ {{classname}} {{classVarName}} = ({{classname}}) o;
+ return {{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}equalsNullable(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}} &&
+ {{/-last}}{{/vars}}{{#parent}} &&
+ super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}}
+ return true;{{/hasVars}}
+ }
+ {{#vendorExtensions.x-jackson-optional-nullable-helpers}}
+
+ private static boolean equalsNullable(JsonNullable a, JsonNullable b) {
+ return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get()));
+ }
+ {{/vendorExtensions.x-jackson-optional-nullable-helpers}}
+
+ @Override
+ public int hashCode() {
+ return Objects.hash({{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}hashCodeNullable({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}});
+ }
+ {{#vendorExtensions.x-jackson-optional-nullable-helpers}}
+
+ private static int hashCodeNullable(JsonNullable a) {
+ if (a == null) {
+ return 1;
+ }
+ return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31;
+ }
+ {{/vendorExtensions.x-jackson-optional-nullable-helpers}}
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class {{classname}} {\n");
+ {{#parent}}
+ sb.append(" ").append(toIndentedString(super.toString())).append("\n");
+ {{/parent}}
+ {{#vars}}sb.append(" {{name}}: ").append(toIndentedString({{name}})).append("\n");
+ {{/vars}}sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+ {{/useLombokAnnotations}}
+}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/pom.mustache b/boat-scaffold/src/main/templates/boat-webhooks/pom.mustache
new file mode 100644
index 000000000..9e792f14e
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/pom.mustache
@@ -0,0 +1,281 @@
+
+ 4.0.0
+ {{groupId}}
+ {{artifactId}}
+ jar
+ {{artifactId}}
+ {{artifactVersion}}
+
+ 1.8
+ ${java.version}
+ ${java.version}
+ UTF-8
+ {{#springFoxDocumentationProvider}}
+ 2.9.2
+ {{/springFoxDocumentationProvider}}
+ {{#springDocDocumentationProvider}}
+ 1.6.14
+ {{/springDocDocumentationProvider}}
+ {{^springFoxDocumentationProvider}}
+ {{^springDocDocumentationProvider}}
+ {{#swagger1AnnotationLibrary}}
+ 1.6.6
+ {{/swagger1AnnotationLibrary}}
+ {{#swagger2AnnotationLibrary}}
+ }2.2.7
+ {{/swagger2AnnotationLibrary}}
+ {{/springDocDocumentationProvider}}
+ {{/springFoxDocumentationProvider}}
+ {{#virtualService}}
+ 2.5.2
+ {{/virtualService}}
+ {{#useSwaggerUI}}
+ 4.15.5
+ {{/useSwaggerUI}}
+
+{{#parentOverridden}}
+
+ {{{parentGroupId}}}
+ {{{parentArtifactId}}}
+ {{{parentVersion}}}
+
+{{/parentOverridden}}
+{{^parentOverridden}}
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ {{#springFoxDocumentationProvider}}2.5.14{{/springFoxDocumentationProvider}}{{^springFoxDocumentationProvider}}2.7.6{{/springFoxDocumentationProvider}}
+
+
+{{/parentOverridden}}
+
+ src/main/java
+ {{^interfaceOnly}}
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ {{#classifier}}
+
+ {{{classifier}}}
+
+ {{/classifier}}
+
+ {{#apiFirst}}
+
+ org.openapitools
+ openapi-generator-maven-plugin
+ {{{generatorVersion}}}
+
+
+
+ generate
+
+
+ src/main/resources/openapi.yaml
+ spring
+ {{{apiPackage}}}
+ {{{modelPackage}}}
+ false
+ {{#modelNamePrefix}}
+ {{{.}}}
+ {{/modelNamePrefix}}
+ {{#modelNameSuffix}}
+ {{{.}}}
+ {{/modelNameSuffix}}
+
+ {{#configOptions}}
+ <{{left}}>{{right}}{{left}}>
+ {{/configOptions}}
+
+
+
+
+
+ {{/apiFirst}}
+
+ {{/interfaceOnly}}
+
+
+ {{#useJakartaEe}}
+
+ jakarta.persistence
+ jakarta.persistence-api
+ 3.1.0
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ 6.0.0
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ 2.1.1
+
+
+ jakarta.validation
+ jakarta.validation-api
+ 3.0.2
+
+ {{/useJakartaEe}}
+ {{^useJakartaEe}}
+
+ javax.persistence
+ javax.persistence-api
+ 2.2
+
+
+ javax.servlet
+ javax.servlet-api
+ 4.0.1
+
+ {{/useJakartaEe}}
+ {{#useLombokAnnotations}}
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+ {{/useLombokAnnotations}}
+
+ org.springframework.boot
+ spring-boot-starter-web{{#reactive}}flux{{/reactive}}
+
+
+ org.springframework.data
+ spring-data-commons
+
+ {{#springDocDocumentationProvider}}
+
+ {{#useSwaggerUI}}
+
+ org.springdoc
+ springdoc-openapi-{{#reactive}}webflux-{{/reactive}}ui
+ ${springdoc.version}
+
+ {{/useSwaggerUI}}
+ {{^useSwaggerUI}}
+
+ org.springdoc
+ springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core
+ ${springdoc.version}
+
+ {{/useSwaggerUI}}
+ {{/springDocDocumentationProvider}}
+ {{#springFoxDocumentationProvider}}
+
+
+ io.springfox
+ springfox-swagger2
+ ${springfox.version}
+
+ {{/springFoxDocumentationProvider}}
+ {{#useSwaggerUI}}
+ {{^springDocDocumentationProvider}}
+
+ org.webjars
+ swagger-ui
+ ${swagger-ui.version}
+
+
+ org.webjars
+ webjars-locator-core
+
+ {{/springDocDocumentationProvider}}
+ {{/useSwaggerUI}}
+ {{^springFoxDocumentationProvider}}
+ {{^springDocDocumentationProvider}}
+ {{#swagger1AnnotationLibrary}}
+
+ io.swagger
+ swagger-annotations
+ ${swagger-annotations.version}
+
+ {{/swagger1AnnotationLibrary}}
+ {{#swagger2AnnotationLibrary}}
+
+ io.swagger.core.v3
+ swagger-annotations
+ ${swagger-annotations.version}
+
+ {{/swagger2AnnotationLibrary}}
+ {{/springDocDocumentationProvider}}
+ {{/springFoxDocumentationProvider}}
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+
+ {{#withXml}}
+
+
+ jakarta.xml.bind
+ jakarta.xml.bind-api
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+
+ {{/withXml}}
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+ {{#joda}}
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-joda
+
+ {{/joda}}
+ {{#openApiNullable}}
+
+ org.openapitools
+ jackson-databind-nullable
+ 0.2.2
+
+ {{/openApiNullable}}
+{{#useBeanValidation}}
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+{{/useBeanValidation}}
+{{#virtualService}}
+
+
+ io.virtualan
+ virtualan-plugin
+ ${virtualan.version}
+
+
+
+ org.hsqldb
+ hsqldb
+
+
+{{/virtualService}}
+{{#hateoas}}
+
+
+ org.springframework.boot
+ spring-boot-starter-hateoas
+
+{{/hateoas}}
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/queryParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/queryParams.mustache
new file mode 100644
index 000000000..6b78fb343
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/queryParams.mustache
@@ -0,0 +1 @@
+{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}}{{#useBeanValidation}} @Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = {{#isMap}}""{{/isMap}}{{^isMap}}"{{baseName}}"{{/isMap}}{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/isModel}}{{>dateTimeParam}} {{>optionalDataType}} {{paramName}}{{/isQueryParam}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/responseType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/responseType.mustache
new file mode 100644
index 000000000..305f28e6d
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/responseType.mustache
@@ -0,0 +1 @@
+{{^vendorExtensions.x-sse}}{{#reactive}}{{#useResponseEntity}}MonoreturnTypes}}>>{{/useResponseEntity}}{{^useResponseEntity}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/useResponseEntity}}{{/reactive}}{{^reactive}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}}{{#responseWrapper}}>{{/responseWrapper}}{{/reactive}}{{/vendorExtensions.x-sse}}{{#vendorExtensions.x-sse}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-sse}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/returnTypes.mustache b/boat-scaffold/src/main/templates/boat-webhooks/returnTypes.mustache
new file mode 100644
index 000000000..0d2b380d7
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/returnTypes.mustache
@@ -0,0 +1 @@
+{{#isMap}}Map{{/isMap}}{{#isArray}}{{#reactive}}Flux{{/reactive}}{{^reactive}}{{{returnContainer}}}{{/reactive}}<{{{returnType}}}>{{/isArray}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/springdocDocumentationConfig.mustache b/boat-scaffold/src/main/templates/boat-webhooks/springdocDocumentationConfig.mustache
new file mode 100644
index 000000000..467d92155
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/springdocDocumentationConfig.mustache
@@ -0,0 +1,54 @@
+package {{configPackage}};
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+
+@Configuration
+public class SpringDocConfiguration {
+
+ @Bean
+ OpenAPI apiInfo() {
+ return new OpenAPI()
+ .info(
+ new Info(){{#appName}}
+ .title("{{appName}}"){{/appName}}
+ .description("{{{appDescription}}}"){{#termsOfService}}
+ .termsOfService("{{termsOfService}}"){{/termsOfService}}{{#openAPI}}{{#info}}{{#contact}}
+ .contact(
+ new Contact(){{#infoName}}
+ .name("{{infoName}}"){{/infoName}}{{#infoUrl}}
+ .url("{{infoUrl}}"){{/infoUrl}}{{#infoEmail}}
+ .email("{{infoEmail}}"){{/infoEmail}}
+ ){{/contact}}{{#license}}
+ .license(
+ new License()
+ {{#licenseInfo}}.name("{{licenseInfo}}")
+ {{/licenseInfo}}{{#licenseUrl}}.url("{{licenseUrl}}")
+ {{/licenseUrl}}
+ ){{/license}}{{/info}}{{/openAPI}}
+ .version("{{appVersion}}")
+ ){{#hasAuthMethods}}
+ .components(
+ new Components(){{#authMethods}}
+ .addSecuritySchemes("{{name}}", new SecurityScheme(){{#isBasic}}
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("{{scheme}}"){{#bearerFormat}}
+ .bearerFormat("{{bearerFormat}}"){{/bearerFormat}}{{/isBasic}}{{#isApiKey}}
+ .type(SecurityScheme.Type.APIKEY){{#isKeyInHeader}}
+ .in(SecurityScheme.In.HEADER){{/isKeyInHeader}}{{#isKeyInQuery}}
+ .in(SecurityScheme.In.QUERY){{/isKeyInQuery}}{{#isKeyInCookie}}
+ .in(SecurityScheme.In.COOKIE){{/isKeyInCookie}}
+ .name("{{keyParamName}}"){{/isApiKey}}{{#isOAuth}}
+ .type(SecurityScheme.Type.OAUTH2){{/isOAuth}}
+ ){{/authMethods}}
+ ){{/hasAuthMethods}}
+ ;
+ }
+}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/typeInfoAnnotation.mustache b/boat-scaffold/src/main/templates/boat-webhooks/typeInfoAnnotation.mustache
new file mode 100644
index 000000000..4f49c4a0f
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/typeInfoAnnotation.mustache
@@ -0,0 +1,22 @@
+{{#jackson}}
+{{#discriminator.mappedModels}}
+{{#-first}}
+@JsonIgnoreProperties(
+ value = "{{{discriminator.propertyBaseName}}}", // ignore manually set {{{discriminator.propertyBaseName}}}, it will be automatically generated by Jackson during serialization
+ allowGetters = true, // allows the {{{discriminator.propertyBaseName}}} to be set during serialization
+ allowSetters = true // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization
+)
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true)
+@JsonSubTypes({
+{{/-first}}
+ {{^vendorExtensions.x-discriminator-value}}
+ @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{{mappingName}}}"){{^-last}},{{/-last}}
+ {{/vendorExtensions.x-discriminator-value}}
+ {{#vendorExtensions.x-discriminator-value}}
+ @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{{vendorExtensions.x-discriminator-value}}}"){{^-last}},{{/-last}}
+ {{/vendorExtensions.x-discriminator-value}}
+{{#-last}}
+})
+{{/-last}}
+{{/discriminator.mappedModels}}
+{{/jackson}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/validatedDataType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/validatedDataType.mustache
new file mode 100644
index 000000000..44add65e8
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/validatedDataType.mustache
@@ -0,0 +1,16 @@
+{{#useBeanValidation}}{{!
+ }}{{#isContainer}}{{!
+ }}{{#isMapContainer}}{{!
+ }}{{{baseType}}}beanValidationCore}}{{{dataType}}}{{/items}}>{{!
+ }}{{/isMapContainer}}{{!
+ }}{{^isMapContainer}}{{!
+ }}{{#reactive}}{{{dataType}}}{{/reactive}}{{^reactive}}{{{baseType}}}<{{#items}}{{>beanValidationCore}}{{{dataType}}}{{/items}}>{{/reactive}}{{!
+ }}{{/isMapContainer}}{{!
+ }}{{/isContainer}}{{!
+ }}{{^isContainer}}{{!
+ }}{{>beanValidationCore}} {{{dataType}}}{{!
+ }}{{/isContainer}}{{!
+}}{{/useBeanValidation}}{{!
+}}{{^useBeanValidation}}{{!
+ }}{{{dataType}}}{{!
+}}{{/useBeanValidation}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/validatedDataTypeWithEnum.mustache b/boat-scaffold/src/main/templates/boat-webhooks/validatedDataTypeWithEnum.mustache
new file mode 100644
index 000000000..a2165f996
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/validatedDataTypeWithEnum.mustache
@@ -0,0 +1,16 @@
+{{#useBeanValidation}}{{!
+ }}{{#isContainer}}{{!
+ }}{{#isMapContainer}}{{!
+ }}{{{baseType}}}beanValidationCore}}{{{datatypeWithEnum}}}{{/items}}>{{!
+ }}{{/isMapContainer}}{{!
+ }}{{^isMapContainer}}{{!
+ }}{{#reactive}}{{{dataType}}}{{/reactive}}{{^reactive}}{{{baseType}}}<{{#items}}{{>beanValidationCore}}{{{datatypeWithEnum}}}{{/items}}>{{/reactive}}{{!
+ }}{{/isMapContainer}}{{!
+ }}{{/isContainer}}{{!
+ }}{{^isContainer}}{{!
+ }}{{>beanValidationCore}} {{{datatypeWithEnum}}}{{!
+ }}{{/isContainer}}{{!
+}}{{/useBeanValidation}}{{!
+}}{{^useBeanValidation}}{{!
+ }}{{{datatypeWithEnum}}}{{!
+}}{{/useBeanValidation}}
\ No newline at end of file
diff --git a/boat-scaffold/src/main/templates/boat-webhooks/xmlAnnotation.mustache b/boat-scaffold/src/main/templates/boat-webhooks/xmlAnnotation.mustache
new file mode 100644
index 000000000..a9e6fb0fa
--- /dev/null
+++ b/boat-scaffold/src/main/templates/boat-webhooks/xmlAnnotation.mustache
@@ -0,0 +1,7 @@
+{{#withXml}}
+{{#jackson}}
+@JacksonXmlRootElement({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}localName = "{{xmlName}}{{^xmlName}}{{classname}}{{/xmlName}}")
+{{/jackson}}
+@XmlRootElement({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{classname}}{{/xmlName}}")
+@XmlAccessorType(XmlAccessType.FIELD)
+{{/withXml}}
\ No newline at end of file
diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGenTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGenTests.java
new file mode 100644
index 000000000..204ea2e3e
--- /dev/null
+++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGenTests.java
@@ -0,0 +1,239 @@
+package com.backbase.oss.codegen.java;
+
+import com.github.javaparser.StaticJavaParser;
+import com.github.javaparser.ast.body.MethodDeclaration;
+import com.github.javaparser.ast.body.Parameter;
+import com.samskivert.mustache.Template;
+import io.swagger.parser.OpenAPIParser;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.parser.core.models.ParseOptions;
+import org.apache.commons.io.FileUtils;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.openapitools.codegen.CodegenModel;
+import org.openapitools.codegen.CodegenOperation;
+import org.openapitools.codegen.CodegenProperty;
+import org.openapitools.codegen.ClientOptInput;
+import org.openapitools.codegen.DefaultGenerator;
+import org.openapitools.codegen.SupportingFile;
+import org.openapitools.codegen.CodegenParameter;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.*;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class BoatWebhooksCodeGenTests {
+
+ static final String PROP_BASE = BoatWebhooksCodeGenTests.class.getSimpleName() + ".";
+ static final String TEST_OUTPUT = System.getProperty(PROP_BASE + "output", "target/boat-webhooks-codegen-tests");
+
+ @BeforeAll
+ static void before() throws IOException {
+ Files.createDirectories(Paths.get(TEST_OUTPUT));
+ FileUtils.deleteDirectory(new File(TEST_OUTPUT));
+ }
+
+ @Test
+ void newLineIndent() throws IOException {
+ final BoatSpringCodeGen.NewLineIndent indent = new BoatSpringCodeGen.NewLineIndent(2, "_");
+ final StringWriter output = new StringWriter();
+ final Template.Fragment frag = mock(Template.Fragment.class);
+
+ when(frag.execute()).thenReturn("\n Good \r\n morning, \r\n Dave ");
+
+ indent.execute(frag, output);
+
+ assertThat(output.toString(), equalTo(String.format("__%n__Good%n__ morning,%n__ Dave%n")));
+ }
+
+ @Test
+ void addServletRequestTestFromOperation() {
+ final BoatWebhooksCodeGen gen = new BoatWebhooksCodeGen();
+ gen.addServletRequest = true;
+ CodegenOperation co = gen.fromOperation("/test", "POST", new Operation(), null);
+ assertEquals(1, co.allParams.size());
+ assertEquals("httpServletRequest", co.allParams.get(0).paramName);
+ }
+
+ @Test
+ void webhookWithCardsApi() throws IOException {
+ var codegen = new BoatWebhooksCodeGen();
+ var input = new File("src/test/resources/boat-spring/cardsapi.yaml");
+ codegen.setLibrary("spring-boot");
+ codegen.setInterfaceOnly(true);
+ codegen.setOutputDir(TEST_OUTPUT + "/cards");
+ codegen.setInputSpec(input.getAbsolutePath());
+
+ var openApiInput = new OpenAPIParser().readLocation(input.getAbsolutePath(), null, new ParseOptions()).getOpenAPI();
+ var clientOptInput = new ClientOptInput();
+ clientOptInput.config(codegen);
+ clientOptInput.openAPI(openApiInput);
+
+ List files = new DefaultGenerator().opts(clientOptInput).generate();
+
+ File testApi = files.stream().filter(file -> file.getName().equals("WebhookClientApi.java"))
+ .findFirst()
+ .get();
+ MethodDeclaration testPostMethod = StaticJavaParser.parse(testApi)
+ .findAll(MethodDeclaration.class)
+ .get(1);
+
+ Parameter contentParam = testPostMethod.getParameterByName("prehookRequest").get();
+ assertThat(contentParam.getTypeAsString(), equalTo("PrehookRequest"));
+ }
+
+ @Test
+ void testReplaceBeanValidationCollectionType() {
+ var codegen = new BoatWebhooksCodeGen();
+ codegen.setUseBeanValidation(true);
+ var codegenProperty = new CodegenProperty();
+ codegenProperty.isModel = true;
+ codegenProperty.baseName = "request"; // not a response
+
+ String result = codegen.replaceBeanValidationCollectionType(
+ codegenProperty, "Set");
+ assertEquals("Set<@Valid com.backbase.dbs.arrangement.commons.model.TranslationItemDto>", result);
+ }
+
+ @Test
+ void replaceBeanValidationCollectionType_shouldCoverAllBranches() {
+ BoatWebhooksCodeGen codegen = new BoatWebhooksCodeGen();
+ CodegenProperty property = new CodegenProperty();
+ property.isModel = true;
+ property.baseName = "request";
+ String baseType = "Set";
+
+ codegen.setUseBeanValidation(false);
+ assertEquals(baseType, codegen.replaceBeanValidationCollectionType(property, baseType));
+
+ codegen.setUseBeanValidation(true);
+ assertEquals("", codegen.replaceBeanValidationCollectionType(property, ""));
+
+ property.isModel = false;
+ assertEquals(baseType, codegen.replaceBeanValidationCollectionType(property, baseType));
+ property.isModel = true;
+
+ property.baseName = "response";
+ assertEquals(baseType, codegen.replaceBeanValidationCollectionType(property, baseType));
+ property.baseName = "request";
+
+ String noValidType = "Set";
+ String expectedValidType = "Set<@Valid com.example.Model>";
+ assertEquals(expectedValidType, codegen.replaceBeanValidationCollectionType(property, noValidType));
+
+ String regexType = "Set<@Valid com.example.Model>";
+ String expectedRegexResult = "Set";
+ assertEquals(expectedRegexResult, codegen.replaceBeanValidationCollectionType(property, regexType));
+
+ String unmatchedType = "List<@Valid Model>";
+ assertEquals(unmatchedType, codegen.replaceBeanValidationCollectionType(property, unmatchedType));
+ }
+
+ @Test
+ void testFromParameter() {
+ BoatWebhooksCodeGen codeGen = new BoatWebhooksCodeGen();
+ io.swagger.v3.oas.models.parameters.Parameter swaggerParam = new io.swagger.v3.oas.models.parameters.Parameter();
+ swaggerParam.setName("testParam");
+ Set imports = new HashSet<>();
+
+ // Call the method under test
+ var result = codeGen.fromParameter(swaggerParam, imports);
+
+ // Assert result is not null and has expected properties
+ assertThat(result, notNullValue());
+ assertEquals("testParam", result.baseName);
+ }
+
+ @Test
+ void testPostProcessModelProperty_addsBigDecimalSerializerAnnotation() {
+ BoatWebhooksCodeGen codeGen = new BoatWebhooksCodeGen();
+ // Simulate the condition for shouldSerializeBigDecimalAsString to return true
+ CodegenProperty property = new CodegenProperty();
+ property.baseType = "BigDecimal";
+ property.openApiType = "string";
+ property.dataFormat = "number";
+ CodegenModel model = new CodegenModel();
+ model.imports = new HashSet<>();
+ property.vendorExtensions = new HashMap<>();
+
+ codeGen.setSerializeBigDecimalAsString(true);
+ codeGen.postProcessModelProperty(model, property);
+
+ assertThat(property.vendorExtensions, hasEntry("x-extra-annotation", "@JsonSerialize(using = BigDecimalCustomSerializer.class)"));
+ assertThat(model.imports, hasItems("BigDecimalCustomSerializer", "JsonSerialize"));
+ }
+
+ @Test
+ void postProcessModelProperty_shouldNotAddAnnotationForNonBigDecimal() {
+ BoatWebhooksCodeGen codegen = new BoatWebhooksCodeGen();
+ codegen.setSerializeBigDecimalAsString(true);
+
+ CodegenModel model = new CodegenModel();
+ CodegenProperty property = new CodegenProperty();
+ property.baseType = "String";
+ property.openApiType = "string";
+ property.dataFormat = "number";
+
+ codegen.postProcessModelProperty(model, property);
+
+ // Assert that no annotation is added
+ assertThat(property.vendorExtensions.containsKey("x-extra-annotation"), is(false));
+ }
+
+ @Test
+ void toApiName_shouldReturnDefaultNameForEmptyString() {
+ BoatWebhooksCodeGen codegen = new BoatWebhooksCodeGen();
+ String result = codegen.toApiName("");
+ assertEquals("WebhookDefaultApi", result);
+ }
+
+ @Test
+ void processOpts_shouldRemoveApiUtilSupportingFileWhenNotUsed() {
+ BoatWebhooksCodeGen codegen = new BoatWebhooksCodeGen();
+ var outputDir = TEST_OUTPUT + "/supportingFilesTest";
+ codegen.supportingFiles().add(new SupportingFile("apiUtil.mustache", outputDir, "ApiUtil.java"));
+ codegen.additionalProperties().put("generateSupportingFiles", false);
+ String originalSupportingFiles = org.openapitools.codegen.config.GlobalSettings.getProperty("supportingFiles");
+ try {
+ org.openapitools.codegen.config.GlobalSettings.setProperty("supportingFiles", "");
+ codegen.processOpts();
+ boolean apiUtilPresent = codegen.supportingFiles().stream()
+ .anyMatch(sf -> "apiUtil.mustache".equals(sf.getTemplateFile()));
+ assertEquals(false, apiUtilPresent);
+ } finally {
+ if (originalSupportingFiles != null) {
+ org.openapitools.codegen.config.GlobalSettings.setProperty("supportingFiles", originalSupportingFiles);
+ } else {
+ org.openapitools.codegen.config.GlobalSettings.clearProperty("supportingFiles");
+ }
+ }
+ }
+
+ @Test
+ void postProcessParameter_setsBaseTypeForContainer() {
+ BoatWebhooksCodeGen codeGen = new BoatWebhooksCodeGen();
+ codeGen.setReactive(false);
+ CodegenParameter param = new CodegenParameter();
+ param.isContainer = true;
+ param.isMap = false;
+ param.dataType = "List";
+ param.baseType = null;
+
+ codeGen.postProcessParameter(param);
+
+ assertEquals("List", param.baseType, "Base type should be extracted from dataType for containers");
+ }
+}
diff --git a/boat-scaffold/src/test/resources/boat-spring/cardsapi.yaml b/boat-scaffold/src/test/resources/boat-spring/cardsapi.yaml
new file mode 100644
index 000000000..0036ebcec
--- /dev/null
+++ b/boat-scaffold/src/test/resources/boat-spring/cardsapi.yaml
@@ -0,0 +1,89 @@
+openapi: 3.0.2
+info:
+ version: 1.0.0
+ title: test
+
+paths:
+ /client/v3/cards:
+ post:
+ tags:
+ - card
+ operationId: postCards
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardItem'
+ responses:
+ "201":
+ $ref: '#/components/responses/CardIdResponse'
+ get:
+ tags:
+ - card
+ operationId: getCards
+ responses:
+ "200":
+ $ref: '#/components/responses/CardsResponse'
+
+ /client/v3/cards/{cardId}:
+ parameters:
+ - $ref: '#/components/parameters/cardIdPathParam'
+ get:
+ tags:
+ - card
+ operationId: getCardById
+ responses:
+ "200":
+ $ref: '#/components/responses/CardItemResponse'
+
+components:
+
+ parameters:
+ cardIdPathParam:
+ name: cardId
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The ID of the card
+
+ schemas:
+ CardItem:
+ type: object
+ required:
+ - cardId
+ properties:
+ cardId:
+ type: number
+ name:
+ type: string
+
+ CardId:
+ type: object
+ properties:
+ cardId:
+ type: number
+
+ responses:
+ CardIdResponse:
+ description: Created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardId'
+
+ CardsResponse:
+ description: Fetch cards
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardItem'
+
+ CardItemResponse:
+ description: Card by id
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardItem'
+
+