securityContext = req.context().get(SecurityContext.class);
+ res.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8);
+ res.send("Response from builder based service, you are: \n" + securityContext
+ .flatMap(SecurityContext::user)
+ .map(Subject::toString)
+ .orElse("Security context is null"));
+ res.next();
+ })
+ .register(StaticContentService.create("/WEB")));
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/GreetService.java.json.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GreetService.java.json.mustache
similarity index 90%
rename from archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/GreetService.java.json.mustache
rename to archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GreetService.java.json.mustache
index 66a201e8612..9bb2c7be6e4 100644
--- a/archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/GreetService.java.json.mustache
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GreetService.java.json.mustache
@@ -2,6 +2,9 @@ package {{package}};
import java.util.concurrent.atomic.AtomicReference;
+{{#GreetService-imports}}
+{{.}}
+{{/GreetService-imports}}
import io.helidon.http.Http;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.HttpService;
@@ -10,6 +13,10 @@ import io.helidon.webserver.http.ServerResponse;
public class GreetService implements HttpService {
+{{#GreetService-fields}}
+{{.}}
+{{/GreetService-fields}}
+
/**
* The config value for the key {@code greeting}.
*/
@@ -19,6 +26,10 @@ public class GreetService implements HttpService {
greeting.set("Hello");
}
+{{#GreetService-constructor}}
+{{.}}
+{{/GreetService-constructor}}
+
/**
* A service registers itself by updating the routing rules.
*
@@ -27,6 +38,9 @@ public class GreetService implements HttpService {
@Override
public void routing(HttpRules rules) {
rules
+ {{#GreetService-routing}}
+ {{.}}
+ {{/GreetService-routing}}
.get("/", this::getDefaultMessageHandler)
.get("/{name}", this::getMessageHandler)
.put("/greeting", this::updateGreetingHandler);
diff --git a/archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/GreetService.java.jsonp.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GreetService.java.jsonp.mustache
similarity index 92%
rename from archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/GreetService.java.jsonp.mustache
rename to archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GreetService.java.jsonp.mustache
index 7566e40717e..8fda67a3d2f 100644
--- a/archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/GreetService.java.jsonp.mustache
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GreetService.java.jsonp.mustache
@@ -3,6 +3,9 @@ package {{package}};
import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;
+{{#GreetService-imports}}
+{{.}}
+{{/GreetService-imports}}
import io.helidon.http.Http;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.HttpService;
@@ -29,6 +32,10 @@ import jakarta.json.JsonObject;
*/
class GreetService implements HttpService {
+{{#GreetService-fields}}
+{{.}}
+{{/GreetService-fields}}
+
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
/**
@@ -40,6 +47,10 @@ class GreetService implements HttpService {
greeting.set("Hello");
}
+{{#GreetService-constructor}}
+{{.}}
+{{/GreetService-constructor}}
+
/**
* A service registers itself by updating the routing rules.
*
@@ -48,6 +59,9 @@ class GreetService implements HttpService {
@Override
public void routing(HttpRules rules) {
rules
+ {{#GreetService-routing}}
+ {{.}}
+ {{/GreetService-routing}}
.get("/", this::getDefaultMessageHandler)
.get("/{name}", this::getMessageHandler)
.put("/greeting", this::updateGreetingHandler);
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GreetService.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GreetService.java.mustache
new file mode 100644
index 00000000000..4c967e117e4
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GreetService.java.mustache
@@ -0,0 +1,108 @@
+package {{package}};
+
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicReference;
+
+{{#GreetService-imports}}
+{{.}}
+{{/GreetService-imports}}
+import io.helidon.http.Http;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.http.HttpService;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+
+/**
+ * A simple service to greet you. Examples:
+ *
+ * Get default greeting message:
+ * {@code curl -X GET http://localhost:8080/greet}
+ *
+ * Get greeting message for Joe:
+ * {@code curl -X GET http://localhost:8080/greet/Joe}
+ *
+ * Change greeting
+ * {@code curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting}
+ *
+ * The message is returned as a String
+ */
+class GreetService implements HttpService {
+
+{{#GreetService-fields}}
+{{.}}
+{{/GreetService-fields}}
+
+ /**
+ * The config value for the key {@code greeting}.
+ */
+ private final AtomicReference greeting = new AtomicReference<>();
+
+ GreetService() {
+ greeting.set("Hello");
+ }
+
+{{#GreetService-constructor}}
+{{.}}
+{{/GreetService-constructor}}
+
+ /**
+ * A service registers itself by updating the routing rules.
+ *
+ * @param rules the routing rules.
+ */
+ @Override
+ public void routing(HttpRules rules) {
+ rules
+ {{#GreetService-routing}}
+ {{.}}
+ {{/GreetService-routing}}
+ .get("/", this::getDefaultMessageHandler)
+ .get("/{name}", this::getMessageHandler)
+ .put("/greeting", this::updateGreetingHandler);
+ }
+
+ /**
+ * Return a worldly greeting message.
+ *
+ * @param request the server request
+ * @param response the server response
+ */
+ private void getDefaultMessageHandler(ServerRequest request,
+ ServerResponse response) {
+ sendResponse(response, "World");
+ }
+
+ /**
+ * Return a greeting message using the name that was provided.
+ *
+ * @param request the server request
+ * @param response the server response
+ */
+ private void getMessageHandler(ServerRequest request,
+ ServerResponse response) {
+ String name = request.path().pathParameters().value("name");
+ sendResponse(response, name);
+ }
+
+ private void sendResponse(ServerResponse response, String name) {
+ String msg = String.format("%s %s!", greeting.get(), name);
+ response.send(msg);
+ }
+
+ private void updateGreeting(String update, ServerResponse response) {
+ greeting.set(update);
+ response.status(Http.Status.NO_CONTENT_204).send();
+ }
+
+ /**
+ * Set the greeting to use in future messages.
+ *
+ * @param request the server request
+ * @param response the server response
+ */
+ private void updateGreetingHandler(ServerRequest request,
+ ServerResponse response) {
+ updateGreeting(request.content().as(String.class), response);
+ }
+
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/HttpStatusMetricService.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/HttpStatusMetricService.java.mustache
new file mode 100644
index 00000000000..df722fbf32b
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/HttpStatusMetricService.java.mustache
@@ -0,0 +1,78 @@
+package {{package}};
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.helidon.metrics.api.Registry;
+import io.helidon.metrics.api.RegistryFactory;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.http.HttpService;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+
+import org.eclipse.microprofile.metrics.Counter;
+import org.eclipse.microprofile.metrics.Metadata;
+import org.eclipse.microprofile.metrics.MetricRegistry;
+import org.eclipse.microprofile.metrics.MetricUnits;
+import org.eclipse.microprofile.metrics.Tag;
+
+/**
+ * Helidon SE service to update a family of counters based on the HTTP status of each response. Add an instance of this service
+ * to the application's routing.
+ *
+ * The service uses one {@link org.eclipse.microprofile.metrics.Counter} for each HTTP status family (1xx, 2xx, etc.).
+ * All counters share the same name--{@value STATUS_COUNTER_NAME}--and each has the tag {@value STATUS_TAG_NAME} with
+ * value {@code 1xx}, {@code 2xx}, etc.
+ *
+ */
+public class HttpStatusMetricService implements HttpService {
+
+ static final String STATUS_COUNTER_NAME = "httpStatus";
+
+ static final String STATUS_TAG_NAME = "range";
+
+ private static final AtomicInteger IN_PROGRESS = new AtomicInteger();
+
+ private final Counter[] responseCounters = new Counter[6];
+
+ static HttpStatusMetricService create() {
+ return new HttpStatusMetricService();
+ }
+
+ private HttpStatusMetricService() {
+ MetricRegistry appRegistry = RegistryFactory.getInstance().getRegistry(Registry.APPLICATION_SCOPE);
+ Metadata metadata = Metadata.builder()
+ .withName(STATUS_COUNTER_NAME)
+ .withDescription("Counts the number of HTTP responses in each status category (1xx, 2xx, etc.)")
+ .withUnit(MetricUnits.NONE)
+ .build();
+ // Declare the counters and keep references to them.
+ for (int i = 1; i < responseCounters.length; i++) {
+ responseCounters[i] = appRegistry.counter(metadata, new Tag(STATUS_TAG_NAME, i + "xx"));
+ }
+ }
+
+ @Override
+ public void routing(HttpRules rules) {
+ rules.any(this::updateRange);
+ }
+
+ // for testing
+ static boolean isInProgress() {
+ return IN_PROGRESS.get() != 0;
+ }
+
+ // Edited to adopt Ciaran's fix later in the thread.
+ private void updateRange(ServerRequest request, ServerResponse response) {
+ IN_PROGRESS.incrementAndGet();
+ response.next();
+ logMetric(response);
+ }
+
+ private void logMetric(ServerResponse response) {
+ int range = response.status().code() / 100;
+ if (range > 0 && range < responseCounters.length) {
+ responseCounters[range].inc();
+ }
+ IN_PROGRESS.decrementAndGet();
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/JwtOverrideService.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/JwtOverrideService.java.mustache
new file mode 100644
index 00000000000..cccdea39d02
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/JwtOverrideService.java.mustache
@@ -0,0 +1,55 @@
+package {{package}};
+
+import io.helidon.webclient.http1.Http1Client;
+import io.helidon.webclient.security.WebClientSecurity;
+import io.helidon.webserver.WebServer;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.http.HttpService;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+import io.helidon.security.SecurityContext;
+import io.helidon.security.providers.jwt.JwtProvider;
+
+final class JwtOverrideService implements HttpService {
+
+ private final Http1Client client = Http1Client.builder()
+ .addService(WebClientSecurity.create())
+ .build();
+
+ @Override
+ public void routing(HttpRules rules) {
+ rules.get("/override", this::override)
+ .get("/propagate", this::propagate);
+ }
+
+ private void override(ServerRequest req, ServerResponse res) {
+ SecurityContext context = req.context()
+ .get(SecurityContext.class)
+ .orElseThrow(() -> new RuntimeException("Security not configured"));
+
+ WebServer server = req.context()
+ .get(WebServer.class)
+ .orElseThrow(() -> new RuntimeException("WebServer not found in context"));
+
+ String result = client.get("http://localhost:" + server.port("backend") + "/hello")
+ .property(JwtProvider.EP_PROPERTY_OUTBOUND_USER, "jill")
+ .requestEntity(String.class);
+
+ res.send("You are: " + context.userName() + ", backend service returned: " + result);
+ }
+
+ private void propagate(ServerRequest req, ServerResponse res) {
+ SecurityContext context = req.context()
+ .get(SecurityContext.class)
+ .orElseThrow(() -> new RuntimeException("Security not configured"));
+
+ WebServer server = req.context()
+ .get(WebServer.class)
+ .orElseThrow(() -> new RuntimeException("WebServer not found in context"));
+
+ String result = client.get("http://localhost:" + server.port("backend") + "/hello")
+ .requestEntity(String.class);
+
+ res.send("You are: " + context.userName() + ", backend service returned: " + result);
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/Message.java.json.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/Message.java.json.mustache
similarity index 100%
rename from archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/Message.java.json.mustache
rename to archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/Message.java.json.mustache
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/MetricsService.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/MetricsService.java.mustache
new file mode 100644
index 00000000000..3f97b9cd691
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/MetricsService.java.mustache
@@ -0,0 +1,88 @@
+package {{package}};
+
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicReference;
+
+{{#MetricsService-imports}}
+import {{.}};
+{{/MetricsService-imports}}
+import io.helidon.http.Http;
+import io.helidon.config.Config;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.http.HttpService;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+
+/**
+ * A simple service to greet you. Examples:
+ *
+ * Get default greeting message:
+ * {@code curl -X GET http://localhost:8080/metrics-greet}
+ *
+ * Get greeting message for Joe:
+ * {@code curl -X GET http://localhost:8080/metrics-greet/Joe}
+ *
+ *
+ * The message is returned as a String
+ */
+class MetricsService implements HttpService {
+
+{{#MetricsService-fields}}
+{{.}}
+{{/MetricsService-fields}}
+
+
+ MetricsService(Config config) {
+ {{#MetricsService-constructor}}
+ {{.}}
+ {{/MetricsService-constructor}}
+ }
+
+ /**
+ * A service registers itself by updating the routing rules.
+ *
+ * @param rules the routing rules.
+ */
+ @Override
+ public void routing(HttpRules rules) {
+ rules
+ {{#MetricsService-routing}}
+ {{.}}
+ {{/MetricsService-routing}}
+ .get("/", this::getDefaultMessageHandler)
+ .get("/{name}", this::getMessageHandler);
+ }
+
+ /**
+ * Return a worldly greeting message.
+ *
+ * @param request the server request
+ * @param response the server response
+ */
+ private void getDefaultMessageHandler(ServerRequest request,
+ ServerResponse response) {
+ sendResponse(response, "Hello World!");
+ }
+
+ /**
+ * Return a greeting message using the name that was provided.
+ *
+ * @param request the server request
+ * @param response the server response
+ */
+ private void getMessageHandler(ServerRequest request,
+ ServerResponse response) {
+ String name = request.path().pathParameters().value("name");
+ sendResponse(response, name);
+ }
+
+ private void sendResponse(ServerResponse response, String name) {
+ String msg = String.format("Hello %s!", name);
+ response.send(msg);
+ }
+
+{{#MetricsService-methods}}
+{{.}}
+{{/MetricsService-methods}}
+
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/OutboundOverrideJwt.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/OutboundOverrideJwt.java.mustache
new file mode 100644
index 00000000000..bf0a18cba36
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/OutboundOverrideJwt.java.mustache
@@ -0,0 +1,100 @@
+package {{package}};
+
+import java.util.concurrent.TimeUnit;
+
+import io.helidon.http.Http;
+import io.helidon.config.Config;
+import io.helidon.config.ConfigSources;
+import io.helidon.webserver.WebServer;
+import io.helidon.webserver.WebServerConfig;
+import io.helidon.webserver.context.ContextFeature;
+import io.helidon.security.Principal;
+import io.helidon.security.SecurityContext;
+import io.helidon.security.Subject;
+import io.helidon.webserver.security.SecurityFeature;
+
+/**
+ * Creates two services.
+ * First service invokes the second with outbound security.
+ * There are two endpoints:
+ *
+ * One that does simple identity propagation and one that uses an explicit username
+ * One that uses basic authentication to authenticate users and JWT to propagate identity
+ * - one that does simple identity propagation and one that uses an explicit username.
+ *
+ * The difference between this example and basic authentication example:
+ *
+ * Configuration files (this example uses ones with -jwt.yaml suffix)
+ * Client property used to override username
+ *
+ */
+public final class OutboundOverrideJwt {
+
+ private OutboundOverrideJwt() {
+ }
+
+ /**
+ * Example that propagates identity and on one endpoint explicitly sets the username and password.
+ *
+ * @param args ignored
+ */
+ public static void main(String[] args) {
+ WebServerConfig.Builder builder = WebServer.builder();
+ setup(builder);
+ WebServer server = builder.build();
+
+ long t = System.nanoTime();
+ server.start();
+ long time = System.nanoTime() - t;
+
+ server.context().register(server);
+
+ System.out.printf("""
+ Server started in %3$d ms
+
+ ***********************
+ ** Endpoints: **
+ ***********************
+
+ http://localhost:%1$d/propagate
+ http://localhost:%1$d/override
+
+ Backend service started on: http://localhost:%2$d/hello
+
+ """,
+ server.port(),
+ server.port("backend"),
+ TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS));
+ }
+
+ static void setup(WebServerConfig.Builder server) {
+ Config clientConfig = Config.create(ConfigSources.classpath("client-service-jwt.yaml"));
+ Config backendConfig = Config.create(ConfigSources.classpath("backend-service-jwt.yaml"));
+
+ server.routing(routing -> routing
+ .addFeature(ContextFeature.create())
+ .addFeature(SecurityFeature.create(clientConfig.get("security")))
+ .register(new JwtOverrideService()))
+
+ // backend that prints the current user
+ .putSocket("backend", socket -> socket
+ .routing(routing -> routing
+ .addFeature(ContextFeature.create())
+ .addFeature(SecurityFeature.create(backendConfig.get("security")))
+ .get("/hello", (req, res) -> {
+
+ // This is the token. It should be bearer
+ req.headers().first(Http.Header.AUTHORIZATION)
+ .ifPresent(System.out::println);
+
+ String username = req.context()
+ .get(SecurityContext.class)
+ .flatMap(SecurityContext::user)
+ .map(Subject::principal)
+ .map(Principal::getName)
+ .orElse("Anonymous");
+
+ res.send(username);
+ })));
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/Service1.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/Service1.java.mustache
new file mode 100644
index 00000000000..71b1b6dfd69
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/Service1.java.mustache
@@ -0,0 +1,53 @@
+package {{package}};
+
+import io.helidon.common.LazyValue;
+import io.helidon.common.context.Contexts;
+import io.helidon.http.Http;
+import io.helidon.http.HttpMediaTypes;
+import io.helidon.webclient.http1.Http1Client;
+import io.helidon.webclient.http1.Http1ClientResponse;
+import io.helidon.webserver.WebServer;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.http.HttpService;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+import io.helidon.security.SecurityContext;
+
+class Service1 implements HttpService {
+
+ private final LazyValue client = LazyValue.create(() -> Contexts.context()
+ .flatMap(c -> c.get(WebServer.class))
+ .map(server -> Http1Client.builder()
+ .baseUri("http://localhost:" + server.port("service2"))
+ .build())
+ .orElseThrow(() -> new IllegalStateException("Unable to get server instance from current context")));
+
+ @Override
+ public void routing(HttpRules rules) {
+ rules.get("/service1", this::service1)
+ .get("/service1-rsa", this::service1Rsa);
+ }
+
+ private void service1(ServerRequest req, ServerResponse res) {
+ handle(req, res, "/service2");
+ }
+
+ private void service1Rsa(ServerRequest req, ServerResponse res) {
+ handle(req, res, "/service2-rsa");
+ }
+
+ private void handle(ServerRequest req, ServerResponse res, String path) {
+ res.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8);
+ req.context()
+ .get(SecurityContext.class)
+ .ifPresentOrElse(context -> {
+ try (Http1ClientResponse clientRes = client.get().get(path).request()) {
+ if (clientRes.status() == Http.Status.OK_200) {
+ res.send(clientRes.entity().as(String.class));
+ } else {
+ res.send("Request failed, status: " + clientRes.status());
+ }
+ }
+ }, () -> res.send("Security context is null"));
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/Service2.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/Service2.java.mustache
new file mode 100644
index 00000000000..3f4c14a49d4
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/Service2.java.mustache
@@ -0,0 +1,30 @@
+package {{package}};
+
+import java.util.Optional;
+
+import io.helidon.http.HttpMediaTypes;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.http.HttpService;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+import io.helidon.security.SecurityContext;
+import io.helidon.security.Subject;
+
+class Service2 implements HttpService {
+
+ @Override
+ public void routing(HttpRules rules) {
+ rules.get("/{*}", this::handle);
+ }
+
+ private void handle(ServerRequest req, ServerResponse res) {
+ Optional securityContext = req.context().get(SecurityContext.class);
+ res.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8);
+ res.send("Response from service2, you are: \n" + securityContext
+ .flatMap(SecurityContext::user)
+ .map(Subject::toString)
+ .orElse("Security context is null") + ", service: " + securityContext
+ .flatMap(SecurityContext::service)
+ .map(Subject::toString));
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/SignatureMain.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/SignatureMain.java.mustache
new file mode 100644
index 00000000000..16b44db1750
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/SignatureMain.java.mustache
@@ -0,0 +1,222 @@
+
+package {{package}};
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import io.helidon.common.configurable.Resource;
+import io.helidon.common.pki.Keys;
+import io.helidon.webserver.WebServer;
+import io.helidon.webserver.WebServerConfig;
+import io.helidon.webserver.context.ContextFeature;
+import io.helidon.webserver.http.HttpRouting;
+import io.helidon.security.CompositeProviderFlag;
+import io.helidon.security.CompositeProviderSelectionPolicy;
+import io.helidon.security.Security;
+import io.helidon.webserver.security.SecurityFeature;
+import io.helidon.security.providers.common.OutboundConfig;
+import io.helidon.security.providers.common.OutboundTarget;
+import io.helidon.security.providers.httpauth.HttpBasicAuthProvider;
+import io.helidon.security.providers.httpauth.SecureUserStore;
+import io.helidon.security.providers.httpsign.HttpSignProvider;
+import io.helidon.security.providers.httpsign.InboundClientDefinition;
+import io.helidon.security.providers.httpsign.OutboundTargetDefinition;
+
+/**
+ * Example of authentication of service with http signatures, using configuration file as much as possible.
+ */
+@SuppressWarnings("DuplicatedCode")
+public class SignatureMain {
+
+ private static final Map USERS = new HashMap<>();
+
+ static {
+ addUser("jack", "password", List.of("user", "admin"));
+ addUser("jill", "password", List.of("user"));
+ addUser("john", "password", List.of());
+ }
+
+ private SignatureMain() {
+ }
+
+ private static void addUser(String user, String password, List roles) {
+ USERS.put(user, new SecureUserStore.User() {
+ @Override
+ public String login() {
+ return user;
+ }
+
+ char[] password() {
+ return password.toCharArray();
+ }
+
+ @Override
+ public boolean isPasswordValid(char[] password) {
+ return Arrays.equals(password(), password);
+ }
+
+ @Override
+ public Collection roles() {
+ return roles;
+ }
+ });
+ }
+
+ /**
+ * Starts this example.
+ *
+ * @param args ignored
+ */
+ public static void main(String[] args) {
+ WebServerConfig.Builder builder = WebServer.builder();
+ setup(builder);
+ WebServer server = builder.build();
+ server.context().register(server);
+
+ long t = System.nanoTime();
+ server.start();
+ long time = System.nanoTime() - t;
+
+ System.out.printf("""
+ Server started in %1d ms
+
+ Signature example: from builder
+
+ Users:
+ jack/password in roles: user, admin
+ jill/password in roles: user
+ john/password in no roles
+
+ ***********************
+ ** Endpoints: **
+ ***********************
+
+ Basic authentication, user role required, will use symmetric signatures for outbound:
+ http://localhost:%2$d/service1
+ Basic authentication, user role required, will use asymmetric signatures for outbound:
+ http://localhost:%3$d/service1-rsa
+
+ """, TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS), server.port(), server.port("service2"));
+ }
+
+ static void setup(WebServerConfig.Builder server) {
+ server.routing(SignatureMain::routing1)
+ .putSocket("service2", socket -> socket
+ .routing(SignatureMain::routing2));
+ }
+
+ private static void routing2(HttpRouting.Builder routing) {
+ SecurityFeature security = SecurityFeature.create(security2())
+ .securityDefaults(SecurityFeature.authenticate());
+
+ routing.addFeature(ContextFeature.create())
+ .addFeature(security)
+ .get("/service2*", SecurityFeature.rolesAllowed("user"))
+ .register(new Service2());
+ }
+
+ private static void routing1(HttpRouting.Builder routing) {
+ SecurityFeature security = SecurityFeature.create(security1())
+ .securityDefaults(SecurityFeature.authenticate());
+ routing.addFeature(ContextFeature.create())
+ .addFeature(security)
+ .get("/service1*", SecurityFeature.rolesAllowed("user"))
+ .register(new Service1());
+ }
+
+ private static Security security2() {
+ return Security.builder()
+ .providerSelectionPolicy(CompositeProviderSelectionPolicy
+ .builder()
+ .addAuthenticationProvider("http-signatures", CompositeProviderFlag.OPTIONAL)
+ .addAuthenticationProvider("basic-auth")
+ .build())
+ .addProvider(HttpBasicAuthProvider
+ .builder()
+ .realm("mic")
+ .userStore(users()),
+ "basic-auth")
+ .addProvider(HttpSignProvider.builder()
+ .addInbound(InboundClientDefinition
+ .builder("service1-hmac")
+ .principalName("Service1 - HMAC signature")
+ .hmacSecret("somePasswordForHmacShouldBeEncrypted")
+ .build())
+ .addInbound(InboundClientDefinition
+ .builder("service1-rsa")
+ .principalName("Service1 - RSA signature")
+ .publicKeyConfig(Keys.builder()
+ .keystore(k -> k
+ .keystore(Resource.create("keystore.p12"))
+ .passphrase("password")
+ .certAlias("service_cert")
+ .build())
+ .build())
+ .build()),
+ "http-signatures")
+ .build();
+ }
+
+ private static Security security1() {
+ return Security.builder()
+ .providerSelectionPolicy(CompositeProviderSelectionPolicy
+ .builder()
+ .addOutboundProvider("basic-auth")
+ .addOutboundProvider("http-signatures")
+ .build())
+ .addProvider(HttpBasicAuthProvider
+ .builder()
+ .realm("mic")
+ .userStore(users())
+ .addOutboundTarget(OutboundTarget.builder("propagate-all").build()),
+ "basic-auth")
+ .addProvider(HttpSignProvider
+ .builder()
+ .outbound(OutboundConfig
+ .builder()
+ .addTarget(hmacTarget())
+ .addTarget(rsaTarget())
+ .build()),
+ "http-signatures")
+ .build();
+ }
+
+ private static OutboundTarget rsaTarget() {
+ return OutboundTarget.builder("service2-rsa")
+ .addHost("localhost")
+ .addPath("/service2-rsa.*")
+ .customObject(OutboundTargetDefinition.class,
+ OutboundTargetDefinition.builder("service1-rsa")
+ .privateKeyConfig(Keys.builder()
+ .keystore(k -> k
+ .keystore(Resource.create("keystore.p12"))
+ .passphrase("password")
+ .keyAlias("myPrivateKey")
+ .build())
+ .build())
+ .build())
+ .build();
+ }
+
+ private static OutboundTarget hmacTarget() {
+ return OutboundTarget.builder("service2")
+ .addHost("localhost")
+ .addPath("/service2")
+ .customObject(
+ OutboundTargetDefinition.class,
+ OutboundTargetDefinition
+ .builder("service1-hmac")
+ .hmacSecret("somePasswordForHmacShouldBeEncrypted")
+ .build())
+ .build();
+ }
+
+ private static SecureUserStore users() {
+ return login -> Optional.ofNullable(USERS.get(login));
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/WebClientMain.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/WebClientMain.java.mustache
new file mode 100644
index 00000000000..b1fd109c96c
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/WebClientMain.java.mustache
@@ -0,0 +1,65 @@
+package {{package}};
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+import io.helidon.http.Http;
+import io.helidon.config.Config;
+import io.helidon.config.ConfigValue;
+import io.helidon.webclient.http1.Http1Client;
+import io.helidon.webclient.http1.Http1ClientResponse;
+
+
+/**
+ * A simple WebClient usage class.
+ *
+ * Each of the methods demonstrates different usage of the WebClient.
+ */
+public class WebClientMain {
+
+ private WebClientMain() {
+ }
+
+ /**
+ * Executes WebClient examples.
+ *
+ * If no argument provided it will take server port from configuration server.port.
+ *
+ * User can override port from configuration by main method parameter with the specific port.
+ *
+ * @param args main method
+ */
+ public static void main(String[] args) {
+ Config config = Config.create();
+ String url;
+ if (args.length == 0) {
+ ConfigValue port = config.get("server.port").asInt();
+ if (!port.isPresent() || port.get() == -1) {
+ throw new IllegalStateException("Unknown port! Please specify port as a main method parameter "
+ + "or directly to config server.port");
+ }
+ url = "http://localhost:" + port.get();
+ } else {
+ url = "http://localhost:" + Integer.parseInt(args[0]);
+ }
+
+ Http1Client client = Http1Client.builder()
+ .baseUri(url)
+ .build();
+
+ performGetMethod(client);
+ }
+
+ static String performGetMethod(Http1Client client) {
+ System.out.println("Get request execution.");
+ String result = client.get().path("/simple-greet").request().as(String.class);
+ System.out.println("GET request successfully executed.");
+ System.out.println(result);
+ return result;
+ }
+
+}
diff --git a/archetypes/helidon/src/main/archetype/nima/custom/files/src/main/resources/WEB/index.html b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/WEB/index.html
similarity index 100%
rename from archetypes/helidon/src/main/archetype/nima/custom/files/src/main/resources/WEB/index.html
rename to archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/WEB/index.html
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/WEB/static.js/google-app.js b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/WEB/static.js/google-app.js
new file mode 100644
index 00000000000..576190f6fef
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/WEB/static.js/google-app.js
@@ -0,0 +1,32 @@
+
+(function () {
+ // new module with no dependencies
+ var app = angular.module('g', []);
+
+ app.controller('GoogleController', ['$http', function ($http) {
+ this.callBackend = function () {
+ var accessToken = document.getElementById('gat_input').value;
+
+ if (accessToken === "") {
+ console.log("No access token, not calling backend");
+ alert("Please login before calling backend service");
+ return;
+ }
+ console.log("Submit attempt to server: " + accessToken);
+
+ $http({
+ method: 'GET',
+ url: '/rest/profile',
+ headers: {
+ 'Authorization': "Bearer " + accessToken
+ }
+ }).success(function (data, status, headers, config) {
+ console.log('Successfully sent data to backend, received' + data);
+ alert(data);
+ }).error(function (data, status, headers, config) {
+ console.log('Failed to send data to backend. Status: ' + status);
+ alert(status + ", auth header: " + headers('WWW-Authenticate') + ", error: " + data);
+ });
+ }
+ }]);
+})();
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/backend-service-jwt.yaml b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/backend-service-jwt.yaml
new file mode 100644
index 00000000000..5cb09739e98
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/backend-service-jwt.yaml
@@ -0,0 +1,13 @@
+
+security:
+ providers:
+ - jwt:
+ atn-token:
+ jwk.resource.resource-path: "verifying-jwk.json"
+ jwt-audience: "http://example.helidon.io"
+ web-server:
+ defaults:
+ authenticate: true
+ paths:
+ - path: "/hello"
+ methods: ["get"]
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/client-service-jwt.yaml b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/client-service-jwt.yaml
new file mode 100644
index 00000000000..1d892f0c02f
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/client-service-jwt.yaml
@@ -0,0 +1,48 @@
+
+security:
+ provider-policy:
+ type: "COMPOSITE"
+ authentication:
+ - name: "http-basic-auth"
+ outbound:
+ - name: "jwt"
+ providers:
+ - http-basic-auth:
+ users:
+ - login: "john"
+ password: "johnnyPassword"
+ roles: ["admin"]
+ - login: "jack"
+ password: "password"
+ roles: ["user", "admin"]
+ - login: "jill"
+ password: "anotherPassword"
+ roles: ["user"]
+ - jwt:
+ allow-impersonation: true
+ atn-token:
+ # we are not interested in inbound tokens
+ verify-signature: false
+ sign-token:
+ jwk.resource.resource-path: "signing-jwk.json"
+ jwt-issuer: "example.helidon.io"
+ outbound:
+ - name: "propagate-identity"
+ jwk-kid: "example"
+ jwt-kid: "helidon"
+ jwt-audience: "http://example.helidon.io"
+ outbound-token:
+ header: "Authorization"
+ format: "bearer %1$s"
+ outbound:
+ - name: "propagate-all"
+ web-server:
+ defaults:
+ authenticate: true
+ paths:
+ - path: "/propagate"
+ methods: ["get"]
+ roles-allowed: "user"
+ - path: "/override"
+ methods: ["get"]
+ roles-allowed: "user"
\ No newline at end of file
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/signing-jwk.json b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/signing-jwk.json
new file mode 100644
index 00000000000..09df45ed5b0
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/signing-jwk.json
@@ -0,0 +1,14 @@
+{
+ "keys": [
+ {
+ "kty": "oct",
+ "kid": "example",
+ "alg": "HS256",
+ "key_ops": [
+ "sign",
+ "verify"
+ ],
+ "k": "FdFYFzERwC2uCBB46pZQi4GG85LujR8obt-KWRBICVQ"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/verifying-jwk.json b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/verifying-jwk.json
new file mode 100644
index 00000000000..09df45ed5b0
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/resources/verifying-jwk.json
@@ -0,0 +1,14 @@
+{
+ "keys": [
+ {
+ "kty": "oct",
+ "kid": "example",
+ "alg": "HS256",
+ "key_ops": [
+ "sign",
+ "verify"
+ ],
+ "k": "FdFYFzERwC2uCBB46pZQi4GG85LujR8obt-KWRBICVQ"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/GoogleMainTest.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/GoogleMainTest.java.mustache
new file mode 100644
index 00000000000..607beadc1d7
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/GoogleMainTest.java.mustache
@@ -0,0 +1,43 @@
+
+package {{package}};
+
+import io.helidon.http.Http;
+import io.helidon.webserver.testing.junit5.ServerTest;
+import io.helidon.webserver.testing.junit5.SetUpServer;
+import io.helidon.webclient.http1.Http1Client;
+import io.helidon.webclient.http1.Http1ClientResponse;
+import io.helidon.webserver.WebServerConfig;
+
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Unit test for {@link GoogleBuilderMain}.
+ */
+@ServerTest
+public class GoogleMainTest {
+
+ private final Http1Client client;
+
+ GoogleMainTest(Http1Client client) {
+ this.client = client;
+ }
+
+ @SetUpServer
+ public static void setup(WebServerConfig.Builder server) {
+ GoogleMain.setup(server);
+ }
+
+ @Test
+ public void testEndpoint() {
+ try (Http1ClientResponse response = client.get("/rest/profile").request()) {
+
+ assertThat(response.status(), is(Http.Status.UNAUTHORIZED_401));
+ assertThat(response.headers().first(Http.HeaderNames.WWW_AUTHENTICATE),
+ optionalValue(is("Bearer realm=\"helidon\",scope=\"openid profile email\"")));
+ }
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/OutboundOverrideJwtTest.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/OutboundOverrideJwtTest.java.mustache
new file mode 100644
index 00000000000..72b95d1a19b
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/OutboundOverrideJwtTest.java.mustache
@@ -0,0 +1,75 @@
+package {{package}};
+
+import java.net.URI;
+
+import io.helidon.webserver.testing.junit5.ServerTest;
+import io.helidon.webserver.testing.junit5.SetUpServer;
+import io.helidon.webclient.http1.Http1Client;
+import io.helidon.webclient.http1.Http1ClientRequest;
+import io.helidon.webclient.http1.Http1ClientResponse;
+import io.helidon.webclient.security.WebClientSecurity;
+import io.helidon.webserver.WebServer;
+import io.helidon.webserver.WebServerConfig;
+import io.helidon.security.Security;
+import io.helidon.security.providers.httpauth.HttpBasicAuthProvider;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Test of security override example.
+ */
+@ServerTest
+public class OutboundOverrideJwtTest {
+
+ private final Http1Client client;
+
+ OutboundOverrideJwtTest(WebServer server, URI uri) {
+ server.context().register(server);
+ Security security = Security.builder()
+ .addProvider(HttpBasicAuthProvider.builder().build())
+ .build();
+ client = Http1Client.builder()
+ .baseUri(uri)
+ .addService(WebClientSecurity.create(security))
+ .build();
+ }
+
+ @SetUpServer
+ public static void setup(WebServerConfig.Builder server) {
+ OutboundOverrideJwt.setup(server);
+ }
+
+ @Test
+ public void testOverrideExample() {
+ try (Http1ClientResponse response = client.get()
+ .path("/override")
+ .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_USER, "jack")
+ .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_PASSWORD, "password")
+ .request()) {
+
+ assertThat(response.status().code(), is(200));
+
+ String entity = response.entity().as(String.class);
+ assertThat(entity, is("You are: jack, backend service returned: jill"));
+ }
+ }
+
+ @Test
+ public void testPropagateExample() {
+ try (Http1ClientResponse response = client.get()
+ .path("/propagate")
+ .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_USER, "jack")
+ .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_PASSWORD, "password")
+ .request()) {
+
+ assertThat(response.status().code(), is(200));
+
+ String entity = response.entity().as(String.class);
+ assertThat(entity, is("You are: jack, backend service returned: jack"));
+ }
+
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/SignatureMainTest.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/SignatureMainTest.java.mustache
new file mode 100644
index 00000000000..2f03ecf9751
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/SignatureMainTest.java.mustache
@@ -0,0 +1,70 @@
+package {{package}};
+
+import java.net.URI;
+import java.util.Set;
+
+import io.helidon.webserver.testing.junit5.ServerTest;
+import io.helidon.webclient.http1.Http1Client;
+import io.helidon.webclient.http1.Http1ClientResponse;
+import io.helidon.webclient.security.WebClientSecurity;
+import io.helidon.webserver.WebServer;
+import io.helidon.security.Security;
+import io.helidon.security.providers.httpauth.HttpBasicAuthProvider;
+
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.security.providers.httpauth.HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_PASSWORD;
+import static io.helidon.security.providers.httpauth.HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_USER;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@ServerTest
+public abstract class SignatureMainTest {
+
+ private final Http1Client client;
+
+ protected SignatureMainTest(WebServer server, URI uri) {
+ server.context().register(server);
+
+ Security security = Security.builder()
+ .addProvider(HttpBasicAuthProvider.builder().build())
+ .build();
+
+ client = Http1Client.builder()
+ .addService(WebClientSecurity.create(security))
+ .baseUri(uri)
+ .build();
+ }
+
+ @Test
+ public void testService1Hmac() {
+ test("/service1", Set.of("user", "admin"), Set.of(), "Service1 - HMAC signature");
+ }
+
+ @Test
+ public void testService1Rsa() {
+ test("/service1-rsa", Set.of("user", "admin"), Set.of(), "Service1 - RSA signature");
+ }
+
+ private void test(String uri, Set expectedRoles, Set invalidRoles, String service) {
+ try (Http1ClientResponse response = client.get(uri)
+ .property(EP_PROPERTY_OUTBOUND_USER, "jack")
+ .property(EP_PROPERTY_OUTBOUND_PASSWORD, "password")
+ .request()) {
+
+ assertThat(response.status().code(), is(200));
+
+ String payload = response.as(String.class);
+
+ // check login
+ assertThat(payload, containsString("id='" + "jack" + "'"));
+
+ // check roles
+ expectedRoles.forEach(role -> assertThat(payload, containsString(":" + role)));
+ invalidRoles.forEach(role -> assertThat(payload, not(containsString(":" + role))));
+ assertThat(payload, containsString("id='" + service + "'"));
+ }
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/StatusService.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/StatusService.java.mustache
new file mode 100644
index 00000000000..527ff772a27
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/StatusService.java.mustache
@@ -0,0 +1,33 @@
+package {{package}};
+
+import io.helidon.http.Http;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.http.HttpService;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+
+/**
+ * Test-only service that allows the client to specify what HTTP status the service should return in its response.
+ * This allows the client to know which status family counter should be updated.
+ */
+public class StatusService implements HttpService {
+
+ @Override
+ public void routing(HttpRules rules) {
+ rules.get("/{status}", this::respondWithRequestedStatus);
+ }
+
+ private void respondWithRequestedStatus(ServerRequest request, ServerResponse response) {
+ String statusText = request.path().pathParameters().value("status");
+ int status;
+ String msg;
+ try {
+ status = Integer.parseInt(statusText);
+ msg = "Successful conversion";
+ } catch (NumberFormatException ex) {
+ status = Http.Status.INTERNAL_SERVER_ERROR_500.code();
+ msg = "Unsuccessful conversion";
+ }
+ response.status(status).send(msg);
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/StatusTest.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/StatusTest.java.mustache
new file mode 100644
index 00000000000..8e2b1dab123
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/test/java/__pkg__/StatusTest.java.mustache
@@ -0,0 +1,113 @@
+package {{package}};
+
+import io.helidon.http.Http.Status;
+import io.helidon.common.media.type.MediaTypes;
+import io.helidon.config.Config;
+import io.helidon.metrics.api.RegistryFactory;
+import io.helidon.webserver.testing.junit5.ServerTest;
+import io.helidon.webserver.testing.junit5.SetUpServer;
+import io.helidon.webclient.http1.Http1Client;
+import io.helidon.webclient.http1.Http1ClientResponse;
+import io.helidon.webserver.WebServerConfig;
+
+import org.eclipse.microprofile.metrics.Counter;
+import org.eclipse.microprofile.metrics.MetricID;
+import org.eclipse.microprofile.metrics.MetricRegistry;
+import org.eclipse.microprofile.metrics.Tag;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.isEmptyString;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@ServerTest
+@Disabled
+public class StatusTest {
+
+ private final Counter[] STATUS_COUNTERS = new Counter[6];
+ private final Http1Client client;
+
+ public StatusTest(Http1Client client) {
+ this.client = client;
+ }
+
+ @SetUpServer
+ public static void setup(WebServerConfig.Builder server) {
+ server.routing(r -> {
+ Main.routing(r, Config.create());
+ r.register("/status", new StatusService());
+ });
+ }
+
+ @BeforeEach
+ void findStatusMetrics() {
+ MetricRegistry metricRegistry = RegistryFactory.getInstance().getRegistry(MetricRegistry.APPLICATION_SCOPE);
+ for (int i = 1; i < STATUS_COUNTERS.length; i++) {
+ STATUS_COUNTERS[i] = metricRegistry.counter(new MetricID(HttpStatusMetricService.STATUS_COUNTER_NAME,
+ new Tag(HttpStatusMetricService.STATUS_TAG_NAME, i + "xx")));
+ }
+ }
+
+ @Test
+ void checkStatusMetrics() throws InterruptedException {
+ checkAfterStatus(Status.create(171));
+ checkAfterStatus(Status.OK_200);
+ checkAfterStatus(Status.CREATED_201);
+ checkAfterStatus(Status.NO_CONTENT_204);
+ checkAfterStatus(Status.MOVED_PERMANENTLY_301);
+ checkAfterStatus(Status.UNAUTHORIZED_401);
+ checkAfterStatus(Status.NOT_FOUND_404);
+ }
+
+ @Test
+ void checkStatusAfterGreet() throws InterruptedException {
+ long[] before = new long[6];
+ for (int i = 1; i < 6; i++) {
+ before[i] = STATUS_COUNTERS[i].getCount();
+ }
+ try (Http1ClientResponse response = client.get("/greet")
+ .accept(MediaTypes.APPLICATION_JSON)
+ .request()) {
+ assertThat("Status of /greet", response.status(), is(Status.OK_200));
+ String entity = response.as(String.class);
+ assertThat(entity, not(isEmptyString()));
+ checkCounters(response.status(), before);
+ }
+ }
+
+ void checkAfterStatus(Status status) throws InterruptedException {
+ long[] before = new long[6];
+ for (int i = 1; i < 6; i++) {
+ before[i] = STATUS_COUNTERS[i].getCount();
+ }
+ try (Http1ClientResponse response = client.get("/status/" + status.code())
+ .accept(MediaTypes.APPLICATION_JSON)
+ .request()) {
+ assertThat("Response status", response.status(), is(status));
+ checkCounters(status, before);
+ }
+ }
+
+ @SuppressWarnings("BusyWait")
+ private void checkCounters(Status status, long[] before) throws InterruptedException {
+ // first make sure we do not have a request in progress
+ long now = System.currentTimeMillis();
+
+ while (HttpStatusMetricService.isInProgress()) {
+ Thread.sleep(50);
+ if (System.currentTimeMillis() - now > 5000) {
+ fail("Timed out while waiting for monitoring to finish");
+ }
+ }
+
+ int family = status.code() / 100;
+ for (int i = 1; i < 6; i++) {
+ long expectedDiff = i == family ? 1 : 0;
+ assertThat("Diff in counter " + family + "xx", STATUS_COUNTERS[i].getCount() - before[i], is(expectedDiff));
+ }
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/nima/custom/observability.xml b/archetypes/helidon/src/main/archetype/se/custom/observability.xml
similarity index 88%
rename from archetypes/helidon/src/main/archetype/nima/custom/observability.xml
rename to archetypes/helidon/src/main/archetype/se/custom/observability.xml
index a888bd7f5cd..5c6479012ba 100644
--- a/archetypes/helidon/src/main/archetype/nima/custom/observability.xml
+++ b/archetypes/helidon/src/main/archetype/se/custom/observability.xml
@@ -20,6 +20,12 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://helidon.io/archetype/2.0 https://helidon.io/xsd/archetype-2.0.xsd">
+
+ files
+
+ **/MetricsService.java.mustache
+
+
io.helidon.webserver.observe.ObserveFeature
@@ -89,13 +95,6 @@
static io.helidon.http.Http.Method.GET
-
-
-
-
io.helidon.webserver.observe
- io.helidon.webserver.observe.health
- io.helidon.health.checks
io.helidon.webserver.observe.metrics
io.helidon.metrics.api
io.helidon.webserver.http2
diff --git a/archetypes/helidon/src/main/archetype/se/custom/security.xml b/archetypes/helidon/src/main/archetype/se/custom/security.xml
new file mode 100644
index 00000000000..ebaf32b3601
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/custom/security.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ files
+
+ src/*/java/**/OutboundOverrideJwtExample.java.mustache
+ src/*/java/**/OutboundOverrideJwtExampleTest.java.mustache
+ src/*/java/**/JwtOverrideService.java.mustache
+ src/*/java/**/GoogleMain.java.mustache
+ src/*/java/**/GoogleMainTest.java.mustache
+ src/*/java/**/Service1.java.mustache
+ src/*/java/**/Service2.java.mustache
+ src/*/java/**/SignatureMain.java.mustache
+ src/*/java/**/SignatureMainTest.java.mustache
+ /**/trick-to-avoid-empty-tag
+
+
+
+ files
+
+ src/*/resources/**/backend-service-jwt.yaml
+ src/*/resources/**/client-service-jwt.yaml
+ src/*/resources/**/signing-jwk.json
+ src/*/resources/**/verifying-jwk.json
+ src/*/resources/**/google.js/google-app.js
+ /**/trick-to-avoid-empty-tag
+
+
+
+
diff --git a/archetypes/helidon/src/main/archetype/se/database/database-se.xml b/archetypes/helidon/src/main/archetype/se/database/database-se.xml
new file mode 100644
index 00000000000..bea6df3f10c
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/database/database-se.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ true
+
+
+
+
+ Helidon SE Database
+
+
+
diff --git a/archetypes/helidon/src/main/archetype/se/database/files/application-jdbc.yaml.mustache b/archetypes/helidon/src/main/archetype/se/database/files/application-jdbc.yaml.mustache
new file mode 100644
index 00000000000..340fa2c63d6
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/database/files/application-jdbc.yaml.mustache
@@ -0,0 +1,31 @@
+db:
+ source: jdbc
+ connection:
+{{#db-connection}}
+{{.}}
+{{/db-connection}}
+ initializationFailTimeout: -1
+ connectionTimeout: 2000
+ helidon:
+ pool-metrics:
+ enabled: true
+ # name prefix defaults to "db.pool." - if you have more than one client within a JVM, you may want to distinguish between them
+ name-prefix: "hikari."
+ health-check:
+ type: "query"
+ statementName: "health-check"
+ statements:
+ # Health check query statement for MySQL and H2 databases
+# health-check: "SELECT 0"
+ # Health check query statement for Oracle database
+ health-check: "SELECT 1 FROM DUAL"
+ # Insert new pokemon
+ create-table: "CREATE TABLE pokemons (name VARCHAR(64) NOT NULL PRIMARY KEY, type VARCHAR(32))"
+ insert1: "INSERT INTO pokemons VALUES(?, ?)"
+ insert2: "INSERT INTO pokemons VALUES(:name, :type)"
+ select-by-type: "SELECT * FROM pokemons WHERE type = ?"
+ select-one: "SELECT * FROM pokemons WHERE name = ?"
+ select-all: "SELECT * FROM pokemons"
+ select-for-update: "SELECT * FROM pokemons WHERE name = :name for UPDATE"
+ update: "UPDATE pokemons SET type = :type WHERE name = :name"
+ delete: "DELETE FROM pokemons WHERE name = ?"
\ No newline at end of file
diff --git a/archetypes/helidon/src/main/archetype/se/database/files/application-test.yaml b/archetypes/helidon/src/main/archetype/se/database/files/application-test.yaml
new file mode 100644
index 00000000000..5069ff17543
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/database/files/application-test.yaml
@@ -0,0 +1,33 @@
+db:
+ source: jdbc
+ connection:
+ url: jdbc:h2:~/test
+ # Server mode, run: docker run --rm --name h2 -p 9092:9082 -p 8082:8082 nemerosa/h2
+ username: sa
+ password:
+ poolName: h2
+ initializationFailTimeout: -1
+ connectionTimeout: 2000
+ helidon:
+ pool-metrics:
+ enabled: true
+ # name prefix defaults to "db.pool." - if you have more than one client within a JVM, you may want to distinguish between them
+ name-prefix: "hikari."
+ health-check:
+ type: "query"
+ statementName: "health-check"
+ statements:
+ # Health check query statement for MySQL and H2 databases
+ # health-check: "SELECT 0"
+ # Health check query statement for Oracle database
+ health-check: "SELECT 1 FROM DUAL"
+ # Insert new pokemon
+ create-table: "CREATE TABLE pokemons (name VARCHAR(64) NOT NULL PRIMARY KEY, type VARCHAR(32))"
+ insert1: "INSERT INTO pokemons VALUES(?, ?)"
+ insert2: "INSERT INTO pokemons VALUES(:name, :type)"
+ select-by-type: "SELECT * FROM pokemons WHERE type = ?"
+ select-one: "SELECT * FROM pokemons WHERE name = ?"
+ select-all: "SELECT * FROM pokemons"
+ select-for-update: "SELECT * FROM pokemons WHERE name = :name for UPDATE"
+ update: "UPDATE pokemons SET type = :type WHERE name = :name"
+ delete: "DELETE FROM pokemons WHERE name = ?"
\ No newline at end of file
diff --git a/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/Pokemon.java.mustache b/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/Pokemon.java.mustache
new file mode 100644
index 00000000000..8466dfbe190
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/Pokemon.java.mustache
@@ -0,0 +1,46 @@
+package {{package}};
+
+import io.helidon.common.Reflected;
+
+/**
+ * POJO representing a very simplified Pokémon.
+ */
+@Reflected
+public class Pokemon {
+ private String name;
+ private String type;
+
+ /**
+ * Default constructor.
+ */
+ public Pokemon() {
+ // JSON-B
+ }
+
+ /**
+ * Create Pokémon with name and type.
+ *
+ * @param name name of the beast
+ * @param type type of the beast
+ */
+ public Pokemon(String name, String type) {
+ this.name = name;
+ this.type = type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/PokemonMapper.java.mustache b/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/PokemonMapper.java.mustache
new file mode 100644
index 00000000000..665d60cf04e
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/PokemonMapper.java.mustache
@@ -0,0 +1,44 @@
+package {{package}};
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.helidon.dbclient.DbColumn;
+import io.helidon.dbclient.DbMapper;
+import io.helidon.dbclient.DbRow;
+
+/**
+ * Maps database statements to {@link io.helidon.examples.dbclient.common.Pokemon} class.
+ */
+public class PokemonMapper implements DbMapper {
+
+ @Override
+ public Pokemon read(DbRow row) {
+ DbColumn name = row.column("name");
+ // we know that in mongo this is not true
+ if (null == name) {
+ name = row.column("_id");
+ }
+
+ DbColumn type = row.column("type");
+ return new Pokemon(name.as(String.class), type.as(String.class));
+ }
+
+ @Override
+ public Map toNamedParameters(Pokemon value) {
+ Map map = new HashMap<>(1);
+ map.put("name", value.getName());
+ map.put("type", value.getType());
+ return map;
+ }
+
+ @Override
+ public List toIndexedParameters(Pokemon value) {
+ List list = new ArrayList<>(2);
+ list.add(value.getName());
+ list.add(value.getType());
+ return list;
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/PokemonMapperProvider.java.mustache b/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/PokemonMapperProvider.java.mustache
new file mode 100644
index 00000000000..66acefe9818
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/PokemonMapperProvider.java.mustache
@@ -0,0 +1,24 @@
+package {{package}};
+
+import java.util.Optional;
+
+import io.helidon.common.Weight;
+import io.helidon.dbclient.DbMapper;
+import io.helidon.dbclient.spi.DbMapperProvider;
+
+/**
+ * Provides pokemon mappers.
+ */
+@Weight(100)
+public class PokemonMapperProvider implements DbMapperProvider {
+ private static final PokemonMapper MAPPER = new PokemonMapper();
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Optional> mapper(Class type) {
+ if (type.equals(Pokemon.class)) {
+ return Optional.of((DbMapper) MAPPER);
+ }
+ return Optional.empty();
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/PokemonService.java.mustache b/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/PokemonService.java.mustache
new file mode 100644
index 00000000000..6b5240c772b
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/database/files/src/main/java/__pkg__/PokemonService.java.mustache
@@ -0,0 +1,184 @@
+package {{package}};
+
+import io.helidon.http.NotFoundException;
+import io.helidon.common.media.type.MediaTypes;
+import io.helidon.common.parameters.Parameters;
+import io.helidon.dbclient.DbClient;
+import io.helidon.dbclient.DbTransaction;
+import io.helidon.webserver.http.Handler;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.http.HttpService;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+
+import jakarta.json.JsonObject;
+
+/**
+ * Common methods that do not differ between JDBC and MongoDB.
+ */
+public class PokemonService implements HttpService {
+
+ private final DbClient dbClient;
+
+ /**
+ * Create a new Pokémon service with a DB client.
+ *
+ * @param dbClient DB client to use for database operations
+ */
+ PokemonService(DbClient dbClient) {
+ this.dbClient = dbClient;
+ }
+
+
+ @Override
+ public void routing(HttpRules rules) {
+ rules
+ .get("/", this::listUsage)
+ // create new
+ .put("/", Handler.create(Pokemon.class, this::insertPokemon))
+ // update existing
+ .post("/{name}/type/{type}", this::insertPokemonSimple)
+ // delete all
+ .delete("/", this::deleteAllPokemons)
+ // get one
+ .get("/{name}", this::getPokemon)
+ // delete one
+ .delete("/{name}", this::deletePokemon)
+ // example of transactional API (local transaction only!)
+ .put("/transactional", this::transactional);
+ }
+
+ /**
+ * The DB client associated with this service.
+ *
+ * @return DB client instance
+ */
+ protected DbClient dbClient() {
+ return dbClient;
+ }
+
+ /**
+ * Delete all pokemons.
+ *
+ * @param req Server request
+ * @param res Server response
+ */
+ private void deleteAllPokemons(ServerRequest req, ServerResponse res) {
+ {{#pokemon-service-delete-all-pokemon}}
+ {{.}}
+ {{/pokemon-service-delete-all-pokemon}}
+ }
+
+ /**
+ * Insert new Pokémon with specified name.
+ *
+ * @param pokemon pokemon request entity
+ * @param res the server response
+ */
+ private void insertPokemon(Pokemon pokemon, ServerResponse res) {
+ long count = dbClient.execute().createNamedInsert("insert2")
+ .namedParam(pokemon)
+ .execute();
+ res.send("Inserted: " + count + " values");
+ }
+
+ /**
+ * Insert new Pokémon with specified name.
+ *
+ * @param req the server request
+ * @param res the server response
+ */
+ private void insertPokemonSimple(ServerRequest req, ServerResponse res) {
+ Parameters params = req.path().pathParameters();
+ // Test Pokémon POJO mapper
+ Pokemon pokemon = new Pokemon(params.value("name"), params.value("type"));
+
+ long count = dbClient.execute().createNamedInsert("insert2")
+ .namedParam(pokemon)
+ .execute();
+ res.send("Inserted: " + count + " values");
+ }
+
+ /**
+ * Get a single Pokémon by name.
+ *
+ * @param req server request
+ * @param res server response
+ */
+ private void getPokemon(ServerRequest req, ServerResponse res) {
+ String pokemonName = req.path().pathParameters().value("name");
+ res.send(dbClient.execute()
+ .namedGet("select-one", pokemonName)
+ .orElseThrow(() -> new NotFoundException("Pokemon " + pokemonName + " not found"))
+ .as(JsonObject.class));
+ }
+
+ /**
+ * Display endpoint usage.
+ *
+ * @param req the server request
+ * @param res the server response
+ */
+ private void listUsage(ServerRequest req, ServerResponse res) {
+ res.headers().contentType(MediaTypes.TEXT_PLAIN);
+ res.send("""
+ Pokemon Database Example:
+ PUT / - Create pokemon
+ DELETE / - Delete all pokemon
+ POST /{name}/type/{type} - Update existing pokemon
+ GET /{name} - Get pokemon by name
+ DELETE /{name} - Delete pokemon by name
+ PUT /transactional - example of transactional API (local transaction only!)
+ """);
+ }
+
+ /**
+ * Update a Pokémon.
+ * Uses a transaction.
+ *
+ * @param req the server request
+ * @param res the server response
+ */
+ private void updatePokemonType(ServerRequest req, ServerResponse res) {
+ Parameters params = req.path().pathParameters();
+ String name = params.value("name");
+ String type = params.value("type");
+ long count = dbClient.execute()
+ .createNamedUpdate("update")
+ .addParam("name", name)
+ .addParam("type", type)
+ .execute();
+ res.send("Updated: " + count + " values");
+ }
+
+ private void transactional(ServerRequest req, ServerResponse res) {
+ Pokemon pokemon = req.content().as(Pokemon.class);
+ DbTransaction tx = dbClient.transaction();
+ try {
+ long count = tx.createNamedGet("select-for-update")
+ .namedParam(pokemon)
+ .execute()
+ .map(dbRow -> tx.createNamedUpdate("update")
+ .namedParam(pokemon)
+ .execute())
+ .orElse(0L);
+ tx.commit();
+ res.send("Updated " + count + " records");
+ } catch (Throwable t) {
+ tx.rollback();
+ throw t;
+ }
+ }
+
+ /**
+ * Delete a Pokémon with specified name (key).
+ *
+ * @param req the server request
+ * @param res the server response
+ */
+ private void deletePokemon(ServerRequest req, ServerResponse res) {
+ String name = req.path().pathParameters().value("name");
+ long count = dbClient.execute().namedDelete("delete", name);
+ res.send("Deleted: " + count + " values");
+ }
+}
diff --git a/archetypes/helidon/src/main/archetype/se/database/files/src/main/resources/META-INF/services/package.PokemonMapperProvider.mustache b/archetypes/helidon/src/main/archetype/se/database/files/src/main/resources/META-INF/services/package.PokemonMapperProvider.mustache
new file mode 100644
index 00000000000..45dff47b348
--- /dev/null
+++ b/archetypes/helidon/src/main/archetype/se/database/files/src/main/resources/META-INF/services/package.PokemonMapperProvider.mustache
@@ -0,0 +1 @@
+{{package}}.PokemonMapperProvider
\ No newline at end of file
diff --git a/archetypes/helidon/src/main/archetype/nima/quickstart/files/src/main/java/__pkg__/GreetClientHttp.java.mustache b/archetypes/helidon/src/main/archetype/se/quickstart/files/src/main/java/__pkg__/GreetClientHttp.java.mustache
similarity index 100%
rename from archetypes/helidon/src/main/archetype/nima/quickstart/files/src/main/java/__pkg__/GreetClientHttp.java.mustache
rename to archetypes/helidon/src/main/archetype/se/quickstart/files/src/main/java/__pkg__/GreetClientHttp.java.mustache
diff --git a/archetypes/helidon/src/main/archetype/nima/quickstart/quickstart-nima.xml b/archetypes/helidon/src/main/archetype/se/quickstart/quickstart-se.xml
similarity index 95%
rename from archetypes/helidon/src/main/archetype/nima/quickstart/quickstart-nima.xml
rename to archetypes/helidon/src/main/archetype/se/quickstart/quickstart-se.xml
index d651502c632..6d4783ee72a 100644
--- a/archetypes/helidon/src/main/archetype/nima/quickstart/quickstart-nima.xml
+++ b/archetypes/helidon/src/main/archetype/se/quickstart/quickstart-se.xml
@@ -23,8 +23,9 @@
json
+ false
jsonp
- false
+ true
microprofile
true
true
@@ -38,7 +39,7 @@
false
false
-
+
diff --git a/archetypes/helidon/src/main/archetype/nima/nima.xml b/archetypes/helidon/src/main/archetype/se/se.xml
similarity index 79%
rename from archetypes/helidon/src/main/archetype/nima/nima.xml
rename to archetypes/helidon/src/main/archetype/se/se.xml
index 594f0f43517..2a67344554e 100644
--- a/archetypes/helidon/src/main/archetype/nima/nima.xml
+++ b/archetypes/helidon/src/main/archetype/se/se.xml
@@ -30,12 +30,17 @@
-
+
-
+ description="Custom Helidon SE project">
+
+
+
+