diff --git a/archetypes/helidon/filters.properties b/archetypes/helidon/filters.properties index 5339ac28ed8..9f9b791688a 100644 --- a/archetypes/helidon/filters.properties +++ b/archetypes/helidon/filters.properties @@ -41,7 +41,7 @@ tracing=!${tracing} || (${tracing} && ${metrics.provider} == 'microprofile') # group extra options extra=${extra} == [] || ${extra} == ['cors', 'fault-tolerance'] || \ - ((${extra} && ${flavor} == 'nima') && ${extra} == ['fault-tolerance']) + (${flavor} == 'se' && ${extra} == ['webclient', 'cors', 'fault-tolerance']) # group docker, k8s and v8o packaging=!(${docker} || ${k8s} || ${v8o}) || (${docker} && ${k8s} && ${v8o}) diff --git a/archetypes/helidon/src/main/archetype/common/common.xml b/archetypes/helidon/src/main/archetype/common/common.xml index 5b833f4bf5f..57dd1567815 100644 --- a/archetypes/helidon/src/main/archetype/common/common.xml +++ b/archetypes/helidon/src/main/archetype/common/common.xml @@ -28,6 +28,9 @@ + + + diff --git a/archetypes/helidon/src/main/archetype/common/extra.xml b/archetypes/helidon/src/main/archetype/common/extra.xml index 28ff8a4bead..b631998f42b 100644 --- a/archetypes/helidon/src/main/archetype/common/extra.xml +++ b/archetypes/helidon/src/main/archetype/common/extra.xml @@ -23,14 +23,72 @@ + - io.helidon.microprofile.faulttolerance + io.helidon.microprofile.faulttolerance - + io.helidon.http.Http - + java.util.concurrent.TimeoutException - + org.apache.maven.plugins @@ -69,7 +127,7 @@ - io.helidon.microprofile.cors.CrossOrigin @@ -92,13 +158,16 @@ jakarta.ws.rs.HttpMethod - io.helidon.microprofile.cors + io.helidon.microprofile.cors + io.helidon.webserver.cors + io.helidon.cors + java.logging - @AddExtension(CorsCdiExtension.class) + @AddExtension(CorsCdiExtension.class) - io.helidon.microprofile.cors.CorsCdiExtension + io.helidon.microprofile.cors.CorsCdiExtension - + + + + io.helidon.cors.CrossOriginConfig + io.helidon.webserver.cors.CorsSupport + + + java.util.logging.Logger + + + + + + + + + { + Logger.getLogger(Main.class.getName()).info("Using the override configuration"); + corsBuilder.mappedConfig(c); + }); + corsBuilder + .config(restrictiveConfig) // restricted sharing for PUT, DELETE + .addCrossOrigin(CrossOriginConfig.create()) // open sharing for other methods + .build(); + + return corsBuilder.build(); + } +]]> + + + java.util.Optional + + + io.helidon.http.Headers + io.helidon.cors.CrossOriginConfig + + + io.helidon.http.Http.HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN + io.helidon.http.Http.HeaderNames.HOST + io.helidon.http.Http.HeaderNames.ORIGIN + + + org.hamcrest.CoreMatchers.containsString + + + it + .set(ORIGIN, "http://foo.com") + .set(HOST, "here.com")) + .request()) { + + assertThat(response.status().code(), is(200)); + String payload = response.entity().as(String.class); + assertThat(payload, containsString("Hello World")); + Headers responseHeaders = response.headers(); + Optional allowOrigin = responseHeaders.value(ACCESS_CONTROL_ALLOW_ORIGIN); + assertThat("Expected CORS header " + CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN + " is absent", + allowOrigin.isPresent(), is(true)); + assertThat(allowOrigin.get(), is("*")); + } + } +]]> + + + diff --git a/archetypes/helidon/src/main/archetype/common/files/Dockerfile.mustache b/archetypes/helidon/src/main/archetype/common/files/Dockerfile.mustache index b576203be88..73d1f165531 100644 --- a/archetypes/helidon/src/main/archetype/common/files/Dockerfile.mustache +++ b/archetypes/helidon/src/main/archetype/common/files/Dockerfile.mustache @@ -1,6 +1,15 @@ # 1st stage, build the app -FROM maven:3.8.4-openjdk-17-slim as build +FROM container-registry.oracle.com/java/openjdk:21 as build + +# Install maven +WORKDIR /usr/share +RUN set -x && \ + curl -O https://archive.apache.org/dist/maven/maven-3/3.8.4/binaries/apache-maven-3.8.4-bin.tar.gz && \ + tar -xvf apache-maven-*-bin.tar.gz && \ + rm apache-maven-*-bin.tar.gz && \ + mv apache-maven-* maven && \ + ln -s /usr/share/maven/bin/mvn /bin/ WORKDIR /helidon @@ -10,7 +19,7 @@ WORKDIR /helidon {{#poms}} ADD {{.}} {{.}} {{/poms}} -RUN mvn package -Dmaven.test.skip {{#docker-phase1-options}}{{.}}{{^last}} {{/last}}{{/docker-phase1-options}} +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip {{#docker-phase1-options}}{{.}}{{^last}} {{/last}}{{/docker-phase1-options}} # Do the Maven build! # Incremental docker builds will resume here when you change sources @@ -22,7 +31,7 @@ RUN mvn package -DskipTests RUN echo "done!" # 2nd stage, build the runtime image -FROM openjdk:17-jdk-slim +FROM container-registry.oracle.com/java/openjdk:21 WORKDIR /helidon # Copy the binary built in the 1st stage diff --git a/archetypes/helidon/src/main/archetype/common/files/README.md.mustache b/archetypes/helidon/src/main/archetype/common/files/README.md.mustache index f26910edc3b..fa778d0ee47 100644 --- a/archetypes/helidon/src/main/archetype/common/files/README.md.mustache +++ b/archetypes/helidon/src/main/archetype/common/files/README.md.mustache @@ -12,7 +12,7 @@ {{.}} {{/readme-run-commands}} {{^readme-run-commands}} -With JDK20 +With JDK21 ```bash mvn package java -jar target/{{artifactId}}.jar diff --git a/archetypes/helidon/src/main/archetype/common/files/pom.xml.mustache b/archetypes/helidon/src/main/archetype/common/files/pom.xml.mustache index e368d94749c..cba530bef03 100644 --- a/archetypes/helidon/src/main/archetype/common/files/pom.xml.mustache +++ b/archetypes/helidon/src/main/archetype/common/files/pom.xml.mustache @@ -35,6 +35,9 @@ {{#type}} {{.}} {{/type}} +{{#optional}} + {{.}} +{{/optional}} {{/dependencies}} diff --git a/archetypes/helidon/src/main/archetype/common/media-sources.xml b/archetypes/helidon/src/main/archetype/common/media-sources.xml index 2cc5a3c2e77..3dcff7c45b7 100644 --- a/archetypes/helidon/src/main/archetype/common/media-sources.xml +++ b/archetypes/helidon/src/main/archetype/common/media-sources.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + ../database/files + + src/main/** + + + + true + ${server} + + + io.helidon.dbclient + helidon-dbclient-jdbc + + + io.helidon.dbclient + helidon-dbclient-metrics-jdbc + + + io.helidon.dbclient + helidon-dbclient-jsonp + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.dbclient + helidon-dbclient-tracing + + + io.helidon.dbclient + helidon-dbclient-metrics + + + io.helidon.dbclient + helidon-dbclient-health + + + io.helidon.tracing + helidon-tracing + + + io.helidon.tracing.providers + helidon-tracing-providers-zipkin + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.http.media + helidon-http-media-jsonb + + + jakarta.json + jakarta.json-api + + + org.slf4j + slf4j-jdk14 + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.webserver + helidon-webserver-tracing + + + io.helidon.config + helidon-config-yaml + + + io.helidon.metrics + helidon-metrics + + + com.h2database + h2 + test + + + io.helidon.dbclient + helidon-dbclient-jdbc + test + + + + io.helidon.dbclient.DbClient + io.helidon.webserver.observe.ObserveFeature + io.helidon.webserver.tracing.TracingFeature + io.helidon.tracing.TracerBuilder + + + + + + + + + + + + io.helidon.dbclient + io.helidon.tracing + io.helidon.webserver.tracing + io.helidon.dbclient.metrics + io.helidon.dbclient.tracing + jakarta.json + + + + + + + + + + diff --git a/archetypes/helidon/src/main/archetype/nima/custom/extra.xml b/archetypes/helidon/src/main/archetype/se/custom/extra.xml similarity index 89% rename from archetypes/helidon/src/main/archetype/nima/custom/extra.xml rename to archetypes/helidon/src/main/archetype/se/custom/extra.xml index ab4e1c2a347..0af20037c2f 100644 --- a/archetypes/helidon/src/main/archetype/nima/custom/extra.xml +++ b/archetypes/helidon/src/main/archetype/se/custom/extra.xml @@ -52,7 +52,8 @@ void testAsync() { String response = client.get() .path("/async") - .request(String.class); + .request() + .as(String.class); assertThat(response, is("blocked for 100 millis")); } @@ -83,7 +84,8 @@ void testCircuitBreaker() { String response = client.get() .path("/circuitBreaker/true") - .request(String.class); + .request() + .as(String.class); assertThat(response, is("blocked for 100 millis")); @@ -96,7 +98,8 @@ // should work after first response = client.get() .path("/circuitBreaker/true") - .request(String.class); + .request() + .as(String.class); assertThat(response, is("blocked for 100 millis")); @@ -119,13 +122,15 @@ void testFallback() { String response = client.get() .path("/fallback/true") - .request(String.class); + .request() + .as(String.class); assertThat(response, is("blocked for 100 millis")); response = client.get() .path("/fallback/false") - .request(String.class); + .request() + .as(String.class); assertThat(response, is("Failed back because of reactive failure")); } @@ -134,19 +139,22 @@ void testRetry() { String response = client.get() .path("/retry/1") - .request(String.class); + .request() + .as(String.class); assertThat(response, is("calls/failures: 1/0")); response = client.get() .path("/retry/2") - .request(String.class); + .request() + .as(String.class); assertThat(response, is("calls/failures: 2/1")); response = client.get() .path("/retry/3") - .request(String.class); + .request() + .as(String.class); assertThat(response, is("calls/failures: 3/2")); @@ -164,7 +172,8 @@ void testTimeout() { String response = client.get() .path("/timeout/10") - .request(String.class); + .request() + .as(String.class); assertThat(response, is("Slept for 10 ms")); @@ -178,6 +187,9 @@ } ]]> + + io.helidon.faulttolerance + diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/README.md.native.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/README.md.native.mustache new file mode 100644 index 00000000000..608377e3e72 --- /dev/null +++ b/archetypes/helidon/src/main/archetype/se/custom/files/README.md.native.mustache @@ -0,0 +1,20 @@ +Make sure you have GraalVM locally installed: + +``` +$GRAALVM_HOME/bin/native-image --version +``` + +Build the native image using the native image profile: + +``` +mvn package -Pnative-image +``` + +This uses the helidon-maven-plugin to perform the native compilation using your installed copy of GraalVM. It might take a while to complete. +Once it completes start the application using the native executable (no JVM!): + +``` +./target/{{artifactId}} +``` + +Yep, it starts fast. You can exercise the application’s endpoints as before. \ No newline at end of file diff --git a/archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/FileService.java.multipart.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/FileService.java.multipart.mustache similarity index 100% rename from archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/FileService.java.multipart.mustache rename to archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/FileService.java.multipart.mustache diff --git a/archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/FtService.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/FtService.java.mustache similarity index 100% rename from archetypes/helidon/src/main/archetype/nima/custom/files/src/main/java/__pkg__/FtService.java.mustache rename to archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/FtService.java.mustache diff --git a/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GoogleMain.java.mustache b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GoogleMain.java.mustache new file mode 100644 index 00000000000..f26f67cf695 --- /dev/null +++ b/archetypes/helidon/src/main/archetype/se/custom/files/src/main/java/__pkg__/GoogleMain.java.mustache @@ -0,0 +1,74 @@ + +package {{package}}; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import io.helidon.http.HttpMediaTypes; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.context.ContextFeature; +import io.helidon.webserver.staticcontent.StaticContentService; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; +import io.helidon.webserver.security.SecurityFeature; +import io.helidon.security.providers.google.login.GoogleTokenProvider; + +/** + * Google login button example main class using builders. + */ +@SuppressWarnings({"SpellCheckingInspection", "DuplicatedCode"}) +public final class GoogleMain { + + private GoogleMain() { + } + + /** + * Start the example. + * + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + WebServerConfig.Builder builder = WebServerConfig.builder(); + setup(builder); + WebServer server = builder.build(); + + long t = System.nanoTime(); + server.start(); + long time = System.nanoTime() - t; + + System.out.printf(""" + Server started in %d ms + Started server on localhost: %2$d + You can access this example at http://localhost:%2$d/index.html + + Check application.yaml in case you are behind a proxy to configure it + """, + TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS), + server.port()); + } + + static void setup(WebServerConfig.Builder server) { + Security security = Security.builder() + .addProvider(GoogleTokenProvider.builder() + .clientId("your-client-id.apps.googleusercontent.com")) + .build(); + server.routing(routing -> routing + .addFeature(ContextFeature.create()) + .addFeature(SecurityFeature.create(security)) + .get("/rest/profile", SecurityFeature.authenticate(), + (req, res) -> { + Optional 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 @@ + diff --git a/examples/cors/src/main/resources/application.yaml b/examples/cors/src/main/resources/application.yaml index eea832f731a..45b405e2bd4 100644 --- a/examples/cors/src/main/resources/application.yaml +++ b/examples/cors/src/main/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2020 Oracle and/or its affiliates. +# Copyright (c) 2020, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ restrictive-cors: allow-origins: ["http://foo.com", "http://there.com"] allow-methods: ["PUT", "DELETE"] -# The the example app uses the following for overriding other settings. +# The example app uses the following for overriding other settings. #cors: # paths: # - path-pattern: /greeting diff --git a/examples/health/basics/README.md b/examples/health/basics/README.md index bca6a94a541..6ae10e5d654 100644 --- a/examples/health/basics/README.md +++ b/examples/health/basics/README.md @@ -20,4 +20,4 @@ Probe the health endpoints: ```bash curl -X GET http://localhost:PORT/health/ curl -X GET http://localhost:PORT/health/ready - +``` diff --git a/examples/quickstarts/helidon-quickstart-se/Dockerfile b/examples/quickstarts/helidon-quickstart-se/Dockerfile index 899a8046a49..70910635cbd 100644 --- a/examples/quickstarts/helidon-quickstart-se/Dockerfile +++ b/examples/quickstarts/helidon-quickstart-se/Dockerfile @@ -15,7 +15,7 @@ # # 1st stage, build the app -FROM container-registry.oracle.com/java/openjdk:21 as maven +FROM container-registry.oracle.com/java/openjdk:21 as build # Install maven WORKDIR /usr/share