diff --git a/etc/copyright-exclude.txt b/etc/copyright-exclude.txt index fbd24d71e..4fd2b2298 100644 --- a/etc/copyright-exclude.txt +++ b/etc/copyright-exclude.txt @@ -46,6 +46,10 @@ node_modules archetype-resources/pom.xml # excluded as it contains both Oracle and original copyright notice src/main/java/org/jboss/weld/bean/proxy/ProxyFactory.java +src/main/java/io/helidon/examples/integrations/neo4j/domain/Actor.java +src/main/java/io/helidon/examples/integrations/neo4j/domain/Movie.java +src/main/java/io/helidon/examples/integrations/neo4j/domain/Person.java +src/main/java/io/helidon/examples/integrations/neo4j/domain/MovieRepository.java src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Actor.java src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Movie.java src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Person.java diff --git a/examples/config/README.md b/examples/config/README.md new file mode 100644 index 000000000..01555f332 --- /dev/null +++ b/examples/config/README.md @@ -0,0 +1,5 @@ + +# Helidon SE Config Examples + +Each subdirectory contains example code that highlights specific aspects of +Helidon configuration. \ No newline at end of file diff --git a/examples/config/basics/README.md b/examples/config/basics/README.md new file mode 100644 index 000000000..f946aa591 --- /dev/null +++ b/examples/config/basics/README.md @@ -0,0 +1,16 @@ +# Helidon Config Basic Example + +This example shows the basics of using Helidon SE Config. The +[Main.java](src/main/java/io/helidon/examples/config/basics/Main.java) class shows: + +* loading configuration from a resource +[`application.conf`](./src/main/resources/application.conf) on the classpath +containing config in HOCON (Human-Optimized Config Object Notation) format +* getting configuration values of various types + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-config-basics.jar +``` diff --git a/examples/config/basics/pom.xml b/examples/config/basics/pom.xml new file mode 100644 index 000000000..d61872f60 --- /dev/null +++ b/examples/config/basics/pom.xml @@ -0,0 +1,65 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.config + helidon-examples-config-basics + 1.0.0-SNAPSHOT + Helidon Examples Config Basics + + + The simplest example shows how to use Configuration API. + + + + io.helidon.examples.config.basics.Main + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-hocon + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/config/basics/src/main/java/io/helidon/examples/config/basics/Main.java b/examples/config/basics/src/main/java/io/helidon/examples/config/basics/Main.java new file mode 100644 index 000000000..1f6ba0b12 --- /dev/null +++ b/examples/config/basics/src/main/java/io/helidon/examples/config/basics/Main.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.basics; + +import java.nio.file.Path; +import java.util.List; + +import io.helidon.config.Config; + +import static io.helidon.config.ConfigSources.classpath; + +/** + * Basics example. + */ +public class Main { + + private Main() { + } + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + Config config = Config.create(classpath("application.conf")); + + int pageSize = config.get("app.page-size").asInt().get(); + + boolean storageEnabled = config.get("app.storageEnabled").asBoolean().orElse(false); + + List basicRange = config.get("app.basic-range").asList(Integer.class).get(); + + Path loggingOutputPath = config.get("logging.outputs.file.name").as(Path.class).get(); + + System.out.println(pageSize); + System.out.println(storageEnabled); + System.out.println(basicRange); + System.out.println(loggingOutputPath); + } + +} diff --git a/examples/config/basics/src/main/java/io/helidon/examples/config/basics/package-info.java b/examples/config/basics/src/main/java/io/helidon/examples/config/basics/package-info.java new file mode 100644 index 000000000..f950c0a23 --- /dev/null +++ b/examples/config/basics/src/main/java/io/helidon/examples/config/basics/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The simplest example shows how to use Configuration API. + */ +package io.helidon.examples.config.basics; diff --git a/examples/config/basics/src/main/resources/application.conf b/examples/config/basics/src/main/resources/application.conf new file mode 100644 index 000000000..8f283613f --- /dev/null +++ b/examples/config/basics/src/main/resources/application.conf @@ -0,0 +1,69 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app { + greeting = "Hello" + name = "Demo" + page-size = 20 + basic-range = [ -20, 20 ] + storagePassphrase = "${AES=thisIsEncriptedPassphrase}" +} + +logging { + outputs { + console { + pattern = simple.colored + level = INFO + } + file { + pattern = verbose.colored + level = DEBUG + name = target/root.log + } + } + level = INFO + app.level = DEBUG + com.oracle.prime.level = WARN +} + +# (this is snippet of complex configuration of security component) +security { + providers: [ # First provider in the list is the default one + { + name = "BMCS" + class = "com.oracle.prime.security.bmcs.BmcsProvider" + BmcsProvider { + # Configuration of OPC (Bare metal) security provider + # (configuration cleaned to be short ...) + + # targets for outbound configuration + targets: [ + { + name = "s2s" + transports = ["http"] + hosts = ["127.0.0.1"] + s2sType = "S2S" + } + #, other targets ... + ] + } + }, + { + name = "ForEndUsers" + class = "com.oracle.prime.examples.security.primeruntime.bmcs.ExampleSecurityProvider" + } + ] +} diff --git a/examples/config/basics/src/main/resources/logging.properties b/examples/config/basics/src/main/resources/logging.properties new file mode 100644 index 000000000..721cc5fc1 --- /dev/null +++ b/examples/config/basics/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n + +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/config/changes/README.md b/examples/config/changes/README.md new file mode 100644 index 000000000..2f290e98b --- /dev/null +++ b/examples/config/changes/README.md @@ -0,0 +1,29 @@ +# Helidon Config Changes Example + +This example shows how an application can deal with changes to +configuration. + +## Change notification + +[`OnChangeExample.java`](src/main/java/io/helidon/examples/config/changes/OnChangeExample.java): +uses `Config.onChange`, passing either a method reference (a lambda expression +would also work) which the config system invokes when the config source changes +) + +## Latest-value supplier + +Recall that once your application obtains a `Config` instance, its config values +do not change. The +[`AsSupplierExample.java`](src/main/java/io/helidon/examples/config/changes/AsSupplierExample.java) +example shows how your application can get a config _supplier_ that always reports +the latest config value for a key, including any changes made after your +application obtained the `Config` object. Although this approach does not notify +your application _when_ changes occur, it _does_ permit your code to always use +the most up-to-date value. Sometimes that is all you need. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-config-changes.jar +``` diff --git a/examples/config/changes/conf/config.yaml b/examples/config/changes/conf/config.yaml new file mode 100644 index 000000000..69d8030ed --- /dev/null +++ b/examples/config/changes/conf/config.yaml @@ -0,0 +1,21 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# In config.yaml we are going to override default configuration. +# It is supposed to be deployment dependent configuration. + +app: + greeting: Hello diff --git a/examples/config/changes/conf/dev.yaml b/examples/config/changes/conf/dev.yaml new file mode 100644 index 000000000..f2166b731 --- /dev/null +++ b/examples/config/changes/conf/dev.yaml @@ -0,0 +1,25 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# In dev.yaml we are going to override all lower layer configuration files. +# It is supposed to be development specific configuration. + +# IT SHOULD NOT BE PLACED IN VCS. EACH DEVELOPER CAN CUSTOMIZE THE FILE AS NEEDED. +# I.e. for example with GIT place following line into .gitignore file: +#*/conf/dev.yaml + +app: + name: Developer diff --git a/examples/config/changes/conf/secrets/password b/examples/config/changes/conf/secrets/password new file mode 100644 index 000000000..5bbaf8758 --- /dev/null +++ b/examples/config/changes/conf/secrets/password @@ -0,0 +1 @@ +changeit \ No newline at end of file diff --git a/examples/config/changes/conf/secrets/username b/examples/config/changes/conf/secrets/username new file mode 100644 index 000000000..1ce97e36c --- /dev/null +++ b/examples/config/changes/conf/secrets/username @@ -0,0 +1 @@ +libor \ No newline at end of file diff --git a/examples/config/changes/pom.xml b/examples/config/changes/pom.xml new file mode 100644 index 000000000..8a9843449 --- /dev/null +++ b/examples/config/changes/pom.xml @@ -0,0 +1,69 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.config + helidon-examples-config-changes + 1.0.0-SNAPSHOT + Helidon Examples Config Changes + + + The example shows how to use Configuration Changes API. + + + + io.helidon.examples.config.changes.Main + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.common + helidon-common-reactive + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/config/changes/src/main/java/io/helidon/examples/config/changes/AsSupplierExample.java b/examples/config/changes/src/main/java/io/helidon/examples/config/changes/AsSupplierExample.java new file mode 100644 index 000000000..08fe0ecc1 --- /dev/null +++ b/examples/config/changes/src/main/java/io/helidon/examples/config/changes/AsSupplierExample.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.changes; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.config.FileSystemWatcher; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; +import static io.helidon.config.PollingStrategies.regular; + +/** + * Example shows how to use Config accessor methods that return {@link Supplier}. + * {@link Supplier} returns always the last loaded config value. + *

+ * The feature is based on using {@link io.helidon.config.spi.PollingStrategy} with + * selected config source(s) to check for changes. + */ +public class AsSupplierExample { + + private static final Logger LOGGER = Logger.getLogger(AsSupplierExample.class.getName()); + + private final AtomicReference lastPrinted = new AtomicReference<>(); + private final ScheduledExecutorService executor = initExecutor(); + + /** + * Executes the example. + */ + public void run() { + Config config = Config + .create(file("conf/dev.yaml") + .optional() + // change watcher is a standalone component that watches for + // changes and notifies the config system when a change occurs + .changeWatcher(FileSystemWatcher.create()), + file("conf/config.yaml") + .optional() + // polling strategy triggers regular checks on the source to check + // for changes, utilizing a concept of "stamp" of the data that is provided + // and validated by the source + .pollingStrategy(regular(Duration.ofSeconds(2))), + classpath("default.yaml")); + + // greeting.get() always return up-to-date value + final Supplier greeting = config.get("app.greeting").asString().supplier(); + // name.get() always return up-to-date value + final Supplier name = config.get("app.name").asString().supplier(); + + // first greeting + printIfChanged(greeting.get() + " " + name.get() + "."); + + // use same Supplier instances to get up-to-date value + executor.scheduleWithFixedDelay( + () -> printIfChanged(greeting.get() + " " + name.get() + "."), + // check every 1 second for changes + 0, 1, TimeUnit.SECONDS); + } + + /** + * Utility to print same message just once. + */ + private void printIfChanged(String message) { + lastPrinted.accumulateAndGet(message, (origValue, newValue) -> { + //print MESSAGE only if changed since the last print + if (!Objects.equals(origValue, newValue)) { + LOGGER.info("[AsSupplier] " + newValue); + } + return newValue; + }); + } + + private static ScheduledExecutorService initExecutor() { + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + Runtime.getRuntime().addShutdownHook(new Thread(executor::shutdown)); + return executor; + } + + /** + * Shutdowns executor. + */ + public void shutdown() { + executor.shutdown(); + } + +} diff --git a/examples/config/changes/src/main/java/io/helidon/examples/config/changes/Main.java b/examples/config/changes/src/main/java/io/helidon/examples/config/changes/Main.java new file mode 100644 index 000000000..92127f132 --- /dev/null +++ b/examples/config/changes/src/main/java/io/helidon/examples/config/changes/Main.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.changes; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * Config changes examples. + */ +public class Main { + + private static final Logger LOGGER = Logger.getLogger(Main.class.getName()); + + private Main() { + } + + /** + * Executes the example. + * + * @param args arguments + * @throws InterruptedException in case you cannot sleep + * @throws IOException in case of IO error + */ + public static void main(String... args) throws IOException, InterruptedException { + // subscribe using simple onChange function + new OnChangeExample().run(); + // use same Supplier instances to get up-to-date value + AsSupplierExample asSupplier = new AsSupplierExample(); + asSupplier.run(); + + // waiting for user made changes in config files + long sleep = 60; + LOGGER.info("Application is waiting " + sleep + " seconds for change..."); + TimeUnit.SECONDS.sleep(sleep); + + asSupplier.shutdown(); + LOGGER.info("Goodbye."); + } + +} diff --git a/examples/config/changes/src/main/java/io/helidon/examples/config/changes/OnChangeExample.java b/examples/config/changes/src/main/java/io/helidon/examples/config/changes/OnChangeExample.java new file mode 100644 index 000000000..635bb0528 --- /dev/null +++ b/examples/config/changes/src/main/java/io/helidon/examples/config/changes/OnChangeExample.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.changes; + +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.config.PollingStrategies; + +import static java.time.Duration.ofSeconds; + +/** + * Example shows how to listen on Config node changes using simplified API, {@link Config#onChange(java.util.function.Consumer)}. + * The Function is invoked with new instance of Config. + *

+ * The feature is based on using {@link io.helidon.config.spi.PollingStrategy} with + * selected config source(s) to check for changes. + */ +public class OnChangeExample { + + private static final Logger LOGGER = Logger.getLogger(OnChangeExample.class.getName()); + + /** + * Executes the example. + */ + public void run() { + Config secrets = Config + .builder(ConfigSources.directory("conf/secrets") + .pollingStrategy(PollingStrategies.regular(ofSeconds(5)))) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + + logSecrets(secrets); + + // subscribe using simple onChange consumer -- could be a lambda as well + secrets.onChange(OnChangeExample::logSecrets); + } + + private static void logSecrets(Config secrets) { + LOGGER.info("Loaded secrets are u: " + secrets.get("username").asString().get() + + ", p: " + secrets.get("password").asString().get()); + } + +} diff --git a/examples/config/changes/src/main/java/io/helidon/examples/config/changes/package-info.java b/examples/config/changes/src/main/java/io/helidon/examples/config/changes/package-info.java new file mode 100644 index 000000000..3374e05ef --- /dev/null +++ b/examples/config/changes/src/main/java/io/helidon/examples/config/changes/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The example shows how to use Configuration Changes API. + */ +package io.helidon.examples.config.changes; diff --git a/examples/config/changes/src/main/resources/default.yaml b/examples/config/changes/src/main/resources/default.yaml new file mode 100644 index 000000000..32f1207f2 --- /dev/null +++ b/examples/config/changes/src/main/resources/default.yaml @@ -0,0 +1,24 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# In default.yaml we are going to configure application default values. +# It is expected it is overridden in: +# - conf/config.yaml in production environment +# - conf/dev.yaml by developer on local machine + +app: + greeting: Hi + name: Example diff --git a/examples/config/changes/src/main/resources/logging.properties b/examples/config/changes/src/main/resources/logging.properties new file mode 100644 index 000000000..1e6cab22c --- /dev/null +++ b/examples/config/changes/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %5$s %6$s%n + +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/config/git/README.md b/examples/config/git/README.md new file mode 100644 index 000000000..efa442e8f --- /dev/null +++ b/examples/config/git/README.md @@ -0,0 +1,28 @@ +# Helidon Config Git Example + +This example shows how to load configuration from a Git repository +and switch which branch to load from at runtime. + +## Prerequisites + +The example assumes that the GitHub repository +has a branch named `test` that contains `application.conf` which sets the key +`greeting` to value `hello`. (The Helidon team has created and populated this +repository.) + +The code in [`Main.java`](src/main/java/io/helidon/examples/config/git/Main.java) +uses the environment variable `ENVIRONMENT_NAME` to fetch the branch name +in the GitHub repository to use; it uses `master` by default (which does _not_ +contain the expected value). + +The example application constructs a `Config` instance from that file in the +GitHub repository and branch, prints out the value for key `greeting`, and +checks to make sure the value is the expected `hello`. + +## Build and run + +```shell +mvn package +export ENVIRONMENT_NAME=test +java -jar target/helidon-examples-config-git.jar +``` diff --git a/examples/config/git/pom.xml b/examples/config/git/pom.xml new file mode 100644 index 000000000..780ed697f --- /dev/null +++ b/examples/config/git/pom.xml @@ -0,0 +1,82 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.config + helidon-examples-config-git + 1.0.0-SNAPSHOT + Helidon Examples Config Git + + + The example shows how to use GitConfigSource. + + + + io.helidon.examples.config.git.Main + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-hocon + + + io.helidon.config + helidon-config-git + + + org.slf4j + slf4j-jdk14 + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + test + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/config/git/src/main/java/io/helidon/examples/config/git/Main.java b/examples/config/git/src/main/java/io/helidon/examples/config/git/Main.java new file mode 100644 index 000000000..2e1126d57 --- /dev/null +++ b/examples/config/git/src/main/java/io/helidon/examples/config/git/Main.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.config.git; + +import java.io.IOException; +import java.net.URI; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.config.git.GitConfigSource; + +/** + * Git source example. + *

+ * This example expects: + *

    + *
  1. a Git repository {@code helidonrobot/test-config} which contains: + *
      + *
    1. the branch {@code test} containing {@code application.conf} which sets + * {@code greeting} to {@code hello}, + *
    2. the branch {@code main} containing the file {@code application.conf} + * which sets the property {@code greeting} to any value other than + * {@code hello}, + *
    3. optionally, any other branch in which {@code application.conf} sets + * {@code greeting} to {@code hello}. + *
    + *
  2. the environment variable {@code ENVIRONMENT_NAME} set to: + *
      + *
    1. {@code test}, or + *
    2. the name of the optional additional branch described above. + *
    + *
+ */ +public class Main { + + private static final String ENVIRONMENT_NAME_PROPERTY = "ENVIRONMENT_NAME"; + + private Main() { + } + + /** + * Executes the example. + * + * @param args arguments + * @throws IOException when some git repo operation failed + */ + public static void main(String... args) throws IOException { + + // we expect a name of the current environment in envvar ENVIRONMENT_NAME + // in this example we just set envvar in maven plugin 'exec', but can be set in k8s pod via ConfigMap + Config env = Config.create(ConfigSources.environmentVariables()); + + String branch = env.get(ENVIRONMENT_NAME_PROPERTY).asString().orElse("master"); + + System.out.println("Loading from branch " + branch); + + Config config = Config.create( + GitConfigSource.builder() + .path("application.conf") + .uri(URI.create("https://github.com/helidonrobot/test-config.git")) + .branch(branch) + .build()); + + System.out.println("Greeting is " + config.get("greeting").asString().get()); + assert config.get("greeting").asString().get().equals("hello"); + } + +} diff --git a/examples/config/git/src/main/java/io/helidon/examples/config/git/package-info.java b/examples/config/git/src/main/java/io/helidon/examples/config/git/package-info.java new file mode 100644 index 000000000..b98b46200 --- /dev/null +++ b/examples/config/git/src/main/java/io/helidon/examples/config/git/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The example shows how to use GitConfigSource. + */ +package io.helidon.examples.config.git; diff --git a/examples/config/git/src/main/resources/logging.properties b/examples/config/git/src/main/resources/logging.properties new file mode 100644 index 000000000..721cc5fc1 --- /dev/null +++ b/examples/config/git/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n + +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/config/mapping/README.md b/examples/config/mapping/README.md new file mode 100644 index 000000000..125233320 --- /dev/null +++ b/examples/config/mapping/README.md @@ -0,0 +1,23 @@ +# Helidon Config Mapping Example + +This example shows how to implement mappers that convert configuration +to POJOs. + +1. [`BuilderExample.java`](src/main/java/io/helidon/examples/config/mapping/BuilderExample.java) +shows how you can add a `builder()` method to a POJO. That method returns a `Builder` +object which the config system uses to update with various key settings and then, +finally, invoke `build()` so the builder can instantiate the POJO with the +assigned values. +2. [`DeserializationExample.java`](src/main/java/io/helidon/examples/config/mapping/DeserializationExample.java) +uses the config system's support for automatic mapping to POJOs that have bean-style +setter methods. +3. [`FactoryMethodExample.java`](src/main/java/io/helidon/examples/config/mapping/FactoryMethodExample.java) +illustrates how you can add a static factory method `create` to a POJO to tell the config +system how to construct a POJO instance. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-config-mapping.jar +``` diff --git a/examples/config/mapping/pom.xml b/examples/config/mapping/pom.xml new file mode 100644 index 000000000..110ac84a1 --- /dev/null +++ b/examples/config/mapping/pom.xml @@ -0,0 +1,69 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.config + helidon-examples-config-mapping + 1.0.0-SNAPSHOT + Helidon Examples Config Mapping + + + The example shows how to use Config Mapping functionality. + + + + io.helidon.examples.config.mapping.Main + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-hocon + + + io.helidon.config + helidon-config-object-mapping + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/BuilderExample.java b/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/BuilderExample.java new file mode 100644 index 000000000..0bfd9e4bf --- /dev/null +++ b/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/BuilderExample.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.mapping; + +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.config.objectmapping.Value; + +/** + * This example shows how to automatically deserialize configuration instance into POJO beans + * using Builder pattern. + */ +public class BuilderExample { + + private BuilderExample() { + } + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + Config config = Config.create(ConfigSources.classpath("application.conf")); + + AppConfig appConfig = config + // get "app" sub-node + .get("app") + // let config automatically deserialize the node to new AppConfig instance + // note that this requires additional dependency - config-beans + .as(AppConfig.class) + .get(); + + System.out.println(appConfig); + + // assert values loaded from application.conf + + assert appConfig.getGreeting().equals("Hello"); + + assert appConfig.getPageSize() == 20; + + assert appConfig.getBasicRange().size() == 2; + assert appConfig.getBasicRange().get(0) == -20; + assert appConfig.getBasicRange().get(1) == 20; + } + + /** + * POJO representing an application configuration. + * Class is initialized from {@link Config} instance. + * During deserialization {@link #builder()} builder method} is invoked + * and {@link Builder} is used to initialize properties from configuration. + */ +public static class AppConfig { + private final String greeting; + private final int pageSize; + private final List basicRange; + + private AppConfig(String greeting, int pageSize, List basicRange) { + this.greeting = greeting; + this.pageSize = pageSize; + this.basicRange = basicRange; + } + + public String getGreeting() { + return greeting; + } + + public int getPageSize() { + return pageSize; + } + + public List getBasicRange() { + return basicRange; + } + + @Override + public String toString() { + return "AppConfig:\n" + + " greeting = " + greeting + "\n" + + " pageSize = " + pageSize + "\n" + + " basicRange= " + basicRange; + } + + /** + * Creates new Builder instance used to be initialized from configuration. + * + * @return new Builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * {@link AppConfig} Builder used to be initialized from configuration. + */ + public static class Builder { + private String greeting; + private int pageSize; + private List basicRange; + + private Builder() { + } + + /** + * Set greeting property. + *

+ * POJO property and config key are same, no need to customize it. + * {@link Value} is used just to specify default value + * in case configuration does not contain appropriate value. + * + * @param greeting greeting value + */ + @Value(withDefault = "Hi") + public void setGreeting(String greeting) { + this.greeting = greeting; + } + + /** + * Set a page size. + *

+ * {@link Value} is used to specify correct config key and default value + * in case configuration does not contain appropriate value. + * Original string value is mapped to target int using appropriate + * {@link io.helidon.config.ConfigMappers ConfigMapper}. + * + * @param pageSize page size + */ + @Value(key = "page-size", withDefault = "10") + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + /** + * Set a basic range. + *

+ * {@link Value} is used to specify correct config key and default value supplier + * in case configuration does not contain appropriate value. + * Supplier already returns default value in target type of a property. + * + * @param basicRange basic range + */ + @Value(key = "basic-range", withDefaultSupplier = DefaultBasicRangeSupplier.class) + public void setBasicRange(List basicRange) { + this.basicRange = basicRange; + } + + /** + * Creates new instance of {@link AppConfig} using values provided by configuration. + * + * @return new instance of {@link AppConfig}. + */ + public AppConfig build() { + return new AppConfig(greeting, pageSize, basicRange); + } + } + + /** + * Supplier of default value for {@link Builder#setBasicRange(List) basic-range} property. + */ + public static class DefaultBasicRangeSupplier implements Supplier> { + @Override + public List get() { + return List.of(-10, 10); + } + } + } +} diff --git a/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/DeserializationExample.java b/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/DeserializationExample.java new file mode 100644 index 000000000..4d1e79ca3 --- /dev/null +++ b/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/DeserializationExample.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.mapping; + +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.config.objectmapping.Value; + +/** + * This example shows how to automatically deserialize configuration instance into POJO beans + * using setters. + */ +public class DeserializationExample { + + private DeserializationExample() { + } + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + Config config = Config.create(ConfigSources.classpath("application.conf")); + + AppConfig appConfig = config + // get "app" sub-node + .get("app") + // let config automatically deserialize the node to new AppConfig instance + .as(AppConfig.class) + .get(); + + System.out.println(appConfig); + + // assert values loaded from application.conf + + assert appConfig.getGreeting().equals("Hello"); + + assert appConfig.getPageSize() == 20; + + assert appConfig.getBasicRange().size() == 2; + assert appConfig.getBasicRange().get(0) == -20; + assert appConfig.getBasicRange().get(1) == 20; + } + + /** + * POJO representing an application configuration. + * Class is initialized from {@link Config} instance. + * During deserialization setter methods are invoked. + */ + public static class AppConfig { + private String greeting; + private int pageSize; + private List basicRange; + + public String getGreeting() { + return greeting; + } + + /** + * Set greeting property. + *

+ * POJO property and config key are same, no need to customize it. + * {@link Value} is used just to specify default value + * in case configuration does not contain appropriate value. + * + * @param greeting greeting value + */ + @Value(withDefault = "Hi") + public void setGreeting(String greeting) { + this.greeting = greeting; + } + + public int getPageSize() { + return pageSize; + } + + /** + * Set a page size. + *

+ * {@link Value} is used to specify correct config key and default value + * in case configuration does not contain appropriate value. + * Original string value is mapped to target int using appropriate + * {@link io.helidon.config.ConfigMappers ConfigMapper}. + * + * @param pageSize page size + */ + @Value(key = "page-size", withDefault = "10") + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public List getBasicRange() { + return basicRange; + } + + /** + * Set a basic range. + *

+ * {@link Value} is used to specify correct config key and default value supplier + * in case configuration does not contain appropriate value. + * Supplier already returns default value in target type of a property. + * + * @param basicRange basic range + */ + @Value(key = "basic-range", withDefaultSupplier = DefaultBasicRangeSupplier.class) + public void setBasicRange(List basicRange) { + this.basicRange = basicRange; + } + + @Override + public String toString() { + return "AppConfig:\n" + + " greeting = " + greeting + "\n" + + " pageSize = " + pageSize + "\n" + + " basicRange= " + basicRange; + } + + /** + * Supplier of default value for {@link #setBasicRange(List) basic-range} property. + */ + public static class DefaultBasicRangeSupplier implements Supplier> { + @Override + public List get() { + return List.of(-10, 10); + } + } + } +} diff --git a/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/FactoryMethodExample.java b/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/FactoryMethodExample.java new file mode 100644 index 000000000..a34faee50 --- /dev/null +++ b/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/FactoryMethodExample.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.mapping; + +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.config.objectmapping.Value; + +/** + * This example shows how to automatically deserialize configuration instance into POJO beans + * using factory method. + */ +public class FactoryMethodExample { + + private FactoryMethodExample() { + } + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + Config config = Config.create(ConfigSources.classpath("application.conf")); + + AppConfig appConfig = config + // get "app" sub-node + .get("app") + // let config automatically deserialize the node to new AppConfig instance + .as(AppConfig.class) + .get(); + + System.out.println(appConfig); + + // assert values loaded from application.conf + + assert appConfig.getGreeting().equals("Hello"); + + assert appConfig.getPageSize() == 20; + + assert appConfig.getBasicRange().size() == 2; + assert appConfig.getBasicRange().get(0) == -20; + assert appConfig.getBasicRange().get(1) == 20; + } + + /** + * POJO representing an application configuration. + * Class is initialized from {@link Config} instance. + * During deserialization {@link #create(String, int, List) factory method} is invoked. + */ + public static class AppConfig { + private final String greeting; + private final int pageSize; + private final List basicRange; + + private AppConfig(String greeting, int pageSize, List basicRange) { + this.greeting = greeting; + this.pageSize = pageSize; + this.basicRange = basicRange; + } + + public String getGreeting() { + return greeting; + } + + public int getPageSize() { + return pageSize; + } + + public List getBasicRange() { + return basicRange; + } + + @Override + public String toString() { + return "AppConfig:\n" + + " greeting = " + greeting + "\n" + + " pageSize = " + pageSize + "\n" + + " basicRange= " + basicRange; + } + + /** + * Creates new {@link AppConfig} instances. + *

+ * {@link Value} is used to specify config keys + * and default values in case configuration does not contain appropriate value. + * + * @param greeting greeting + * @param pageSize page size + * @param basicRange basic range + * @return new instance of {@link AppConfig}. + */ + public static AppConfig create(@Value(key = "greeting", withDefault = "Hi") + String greeting, + @Value(key = "page-size", withDefault = "10") + int pageSize, + @Value(key = "basic-range", withDefaultSupplier = DefaultBasicRangeSupplier.class) + List basicRange) { + return new AppConfig(greeting, pageSize, basicRange); + } + + /** + * Supplier of default value for {@code basic-range} property, see {@link #create(String, int, List)}. + */ + public static class DefaultBasicRangeSupplier implements Supplier> { + @Override + public List get() { + return List.of(-10, 10); + } + } + } +} diff --git a/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/Main.java b/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/Main.java new file mode 100644 index 000000000..730200638 --- /dev/null +++ b/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/Main.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.mapping; + +/** + * Runs every example main class in this module/package. + */ +public class Main { + + private Main() { + } + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String[] args) { + DeserializationExample.main(args); + FactoryMethodExample.main(args); + BuilderExample.main(args); + } + +} diff --git a/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/package-info.java b/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/package-info.java new file mode 100644 index 000000000..9c785ace4 --- /dev/null +++ b/examples/config/mapping/src/main/java/io/helidon/examples/config/mapping/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The example shows how to use Config Mapping functionality. + */ +package io.helidon.examples.config.mapping; diff --git a/examples/config/mapping/src/main/resources/application.conf b/examples/config/mapping/src/main/resources/application.conf new file mode 100644 index 000000000..3699dcd83 --- /dev/null +++ b/examples/config/mapping/src/main/resources/application.conf @@ -0,0 +1,21 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app { + greeting = "Hello" + page-size = 20 + basic-range = [ -20, 20 ] +} diff --git a/examples/config/mapping/src/main/resources/logging.properties b/examples/config/mapping/src/main/resources/logging.properties new file mode 100644 index 000000000..721cc5fc1 --- /dev/null +++ b/examples/config/mapping/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n + +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/config/metadata/README.md b/examples/config/metadata/README.md new file mode 100644 index 000000000..7057c7213 --- /dev/null +++ b/examples/config/metadata/README.md @@ -0,0 +1,24 @@ +# Configuration metadata usage example + +This example reads configuration metadata from the classpath and creates a full +`application.yaml` content with all possible configuration. + +## Setup + +This application does not have any root configuration on classpath, so it would generate an empty file. +Add at least one module to `pom.xml` that can be configured to see the output. + +Example: +```xml + + io.helidon.security.providers + helidon-security-providers-oidc + +``` + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-config-metadata.jar > application.yaml +``` diff --git a/examples/config/metadata/pom.xml b/examples/config/metadata/pom.xml new file mode 100644 index 000000000..a90c058ca --- /dev/null +++ b/examples/config/metadata/pom.xml @@ -0,0 +1,66 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.config + helidon-examples-config-metadata + 1.0.0-SNAPSHOT + Helidon Examples Config Metadata + + + This example shows possibilities with configuration metadata. To test this, add a configurable library on the classpath + and run the example. + + + + io.helidon.examples.config.metadata.ConfigMetadataMain + + + + + org.eclipse.parsson + parsson + + + io.helidon.config + helidon-config-metadata + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/config/metadata/src/main/java/io/helidon/examples/config/metadata/ConfigMetadataMain.java b/examples/config/metadata/src/main/java/io/helidon/examples/config/metadata/ConfigMetadataMain.java new file mode 100644 index 000000000..8fd062242 --- /dev/null +++ b/examples/config/metadata/src/main/java/io/helidon/examples/config/metadata/ConfigMetadataMain.java @@ -0,0 +1,461 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.metadata; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.config.metadata.ConfiguredOption; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import jakarta.json.JsonReaderFactory; +import jakarta.json.JsonValue; + +/** + * Reads configuration metadata and prints a full configuration example. + */ +public final class ConfigMetadataMain { + private static final Map TYPED_VALUES = Map.of("java.lang.Integer", + new TypedValue("1"), + "java.lang.Boolean", + new TypedValue("true"), + "java.lang.Long", + new TypedValue("1000"), + "java.lang.Short", + new TypedValue("0"), + "java.lang.String", + new TypedValue("value", true), + "java.net.URI", + new TypedValue("https://www.example.net", true), + "java.lang.Class", + new TypedValue("net.example.SomeClass", true), + "java.time.ZoneId", + new TypedValue("UTC", true)); + + private ConfigMetadataMain() { + } + + /** + * Start the example. + * @param args ignored + */ + public static void main(String[] args) throws IOException { + JsonReaderFactory readerFactory = Json.createReaderFactory(Map.of()); + + Enumeration files = ConfigMetadataMain.class.getClassLoader().getResources("META-INF/helidon/config-metadata.json"); + List configuredTypes = new LinkedList<>(); + Map typesMap = new HashMap<>(); + + while (files.hasMoreElements()) { + URL url = files.nextElement(); + try (InputStream is = url.openStream()) { + JsonReader reader = readerFactory.createReader(is, StandardCharsets.UTF_8); + processMetadataJson(configuredTypes, typesMap, reader.readArray()); + } + } + for (ConfiguredType configuredType : configuredTypes) { + if (configuredType.standalone()) { + printType(configuredType, typesMap); + } + } + } + + private static void printType(ConfiguredType configuredType, Map typesMap) { + String prefix = configuredType.prefix(); + System.out.println("# " + configuredType.description()); + System.out.println("# " + configuredType.targetClass()); + System.out.println(prefix + ":"); + printType(configuredType, typesMap, 1, false); + } + + private static void printType(ConfiguredType configuredType, + Map typesMap, + int nesting, + boolean listStart) { + String spaces = " ".repeat(nesting * 2); + Set properties = configuredType.properties(); + boolean isListStart = listStart; + + for (ConfiguredType.ConfiguredProperty property : properties) { + if (property.key() != null && property.key().contains(".*.")) { + // this is a nested key, must be resolved by the parent list node + continue; + } + + printProperty(property, properties, typesMap, nesting, isListStart); + isListStart = false; + } + + List inherited = configuredType.inherited(); + for (String inheritedTypeName : inherited) { + ConfiguredType inheritedType = typesMap.get(inheritedTypeName); + if (inheritedType == null) { + System.out.println(spaces + "# Missing inherited type: " + inheritedTypeName); + } else { + printType(inheritedType, typesMap, nesting, false); + } + } + } + + private static void printProperty(ConfiguredType.ConfiguredProperty property, + Set properties, + Map typesMap, + int nesting, + boolean listStart) { + String spaces = " ".repeat(nesting * 2); + + printDocs(property, spaces, listStart); + + if (property.kind() == ConfiguredOption.Kind.LIST) { + printListProperty(property, properties, typesMap, nesting, spaces); + return; + } + + if (property.kind() == ConfiguredOption.Kind.MAP) { + printMapProperty(property, typesMap, nesting, spaces); + return; + } + + TypedValue typed = TYPED_VALUES.get(property.type()); + if (typed == null) { + // this is a nested type, or a missing type + ConfiguredType nestedType = typesMap.get(property.type()); + if (nestedType == null) { + // either we have a list of allowed values, default value, or this is really a missing type + printAllowedValuesOrMissing(property, typesMap, nesting, spaces); + } else { + // proper nested type + if (property.merge()) { + printType(nestedType, typesMap, nesting, false); + } else { + System.out.println(spaces + property.outputKey() + ":"); + printType(nestedType, typesMap, nesting + 1, false); + } + } + } else { + // this is a "leaf" node + if (property.defaultValue() == null) { + System.out.println(spaces + "# Generated value (property does not have a configured default)"); + } + System.out.println(spaces + property.outputKey() + ": " + toTypedValue(property, typed)); + } + } + + private static void printMapProperty(ConfiguredType.ConfiguredProperty property, + Map typesMap, + int nesting, + String spaces) { + System.out.print(spaces); + System.out.println(property.outputKey() + ":"); + TypedValue typedValue = TYPED_VALUES.get(property.type()); + + String mySpaces = " ".repeat((nesting + 1) * 2); + if (typedValue == null) { + System.out.println(mySpaces + "key: \"Unsupported map value type: " + property.type() + "\""); + } else { + System.out.println(mySpaces + "key-1: " + output(typedValue, typedValue.defaultsDefault())); + System.out.println(mySpaces + "key-2: " + output(typedValue, typedValue.defaultsDefault())); + } + } + + private static void printListProperty(ConfiguredType.ConfiguredProperty property, + Set properties, + Map typesMap, + int nesting, + String spaces) { + System.out.print(spaces); + System.out.print(property.outputKey() + ":"); + ConfiguredType listType = typesMap.get(property.type()); + + if (listType == null) { + if (property.provider()) { + listFromProvider(property, typesMap, nesting, spaces); + } else { + listFromTypes(property, properties, typesMap, nesting, spaces); + } + } else { + System.out.println(); + System.out.print(spaces + "- "); + printType(listType, typesMap, nesting + 1, true); + } + } + + private static void listFromProvider(ConfiguredType.ConfiguredProperty property, + Map typesMap, + int nesting, + String spaces) { + // let's find all supported providers + List providers = new LinkedList<>(); + for (ConfiguredType value : typesMap.values()) { + if (value.provides().contains(property.type())) { + providers.add(value); + } + } + + System.out.println(); + if (providers.isEmpty()) { + System.out.print(spaces + "- # There are no modules on classpath providing " + property.type()); + return; + } + for (ConfiguredType provider : providers) { + System.out.print(spaces + "- "); + if (provider.prefix() != null) { + System.out.println("# " + provider.description()); + System.out.println(spaces + " " + provider.prefix() + ":"); + printType(provider, typesMap, nesting + 2, false); + } else { + printType(provider, typesMap, nesting + 1, true); + } + } + } + + private static void fromProvider(ConfiguredType.ConfiguredProperty property, + Map typesMap, + int nesting) { + String spaces = " ".repeat(nesting + 1); + // let's find all supported providers + List providers = new LinkedList<>(); + for (ConfiguredType value : typesMap.values()) { + if (value.provides().contains(property.type())) { + providers.add(value); + } + } + + if (providers.isEmpty()) { + System.out.println(spaces + " # There are no modules on classpath providing " + property.type()); + return; + } + + for (ConfiguredType provider : providers) { + System.out.println(spaces + " # ****** Provider Configuration ******"); + System.out.println(spaces + " # " + provider.description()); + System.out.println(spaces + " # " + provider.targetClass()); + System.out.println(spaces + " # ************************************"); + if (provider.prefix() != null) { + System.out.println(spaces + " " + provider.prefix() + ":"); + printType(provider, typesMap, nesting + 2, false); + } else { + printType(provider, typesMap, nesting + 1, false); + } + } + } + + private static void listFromTypes(ConfiguredType.ConfiguredProperty property, + Set properties, + Map typesMap, + int nesting, + String spaces) { + // this may be a list defined in configuration itself (*) + String prefix = property.outputKey() + ".*."; + Map children = new HashMap<>(); + for (ConfiguredType.ConfiguredProperty configuredProperty : properties) { + if (configuredProperty.outputKey().startsWith(prefix)) { + children.put(configuredProperty.outputKey().substring(prefix.length()), configuredProperty); + } + } + if (children.isEmpty()) { + // this may be an array of primitive types / String + TypedValue typedValue = TYPED_VALUES.get(property.type()); + if (typedValue == null) { + List allowedValues = property.allowedValues(); + if (allowedValues.isEmpty()) { + System.out.println(); + System.out.println(spaces + "# Missing type: " + property.type()); + } else { + System.out.println(); + typedValue = new TypedValue("", true); + for (ConfiguredType.AllowedValue allowedValue : allowedValues) { + // # Description + // # This is the default value + // actual value + System.out.print(spaces + " - "); + String nextLinePrefix = spaces + " "; + boolean firstLine = true; + + if (allowedValue.description() != null && !allowedValue.description().isBlank()) { + firstLine = false; + System.out.println("#" + allowedValue.description()); + } + if (allowedValue.value().equals(property.defaultValue())) { + if (firstLine) { + firstLine = false; + } else { + System.out.print(nextLinePrefix); + } + System.out.println("# This is the default value"); + } + if (!firstLine) { + System.out.print(nextLinePrefix); + } + System.out.println(output(typedValue, allowedValue.value())); + } + } + } else { + printArray(typedValue); + } + } else { + System.out.println(); + System.out.print(spaces + "- "); + boolean listStart = true; + for (var entry : children.entrySet()) { + ConfiguredType.ConfiguredProperty element = entry.getValue(); + // we must modify the key + element.key(entry.getKey()); + printProperty(element, properties, typesMap, nesting + 1, listStart); + listStart = false; + } + } + } + + private static void printDocs(ConfiguredType.ConfiguredProperty property, String spaces, boolean firstLineNoSpaces) { + String description = property.description(); + description = (description == null || description.isBlank()) ? null : description; + + // type + System.out.print((firstLineNoSpaces ? "" : spaces)); + System.out.print("# "); + System.out.println(property.type()); + + // description + if (description != null) { + description = description.replace('\n', ' '); + System.out.print(spaces); + System.out.print("# "); + System.out.println(description); + } + + // required + if (!property.optional()) { + System.out.print(spaces); + System.out.println("# *********** REQUIRED ***********"); + } + } + + private static void printArray(TypedValue typedValue) { + String element = output(typedValue, typedValue.defaultsDefault()); + String toPrint = " [" + element + "," + element + "]"; + System.out.println(toPrint); + } + + private static String output(TypedValue typed, String value) { + if (typed.escaped()) { + return "\"" + value + "\""; + } + return value; + } + + private static void printAllowedValuesOrMissing(ConfiguredType.ConfiguredProperty property, + Map typesMap, + int nesting, String spaces) { + if (property.provider()) { + System.out.println(spaces + property.outputKey() + ":"); + fromProvider(property, typesMap, nesting); + return; + } + + List allowedValues = property.allowedValues(); + if (allowedValues.isEmpty()) { + if (property.defaultValue() == null) { + System.out.println(spaces + property.outputKey() + ": \"Missing nested type: " + property.type() + "\""); + } else { + System.out.println(spaces + property.outputKey() + ": " + toTypedValue(property, + new TypedValue(property.defaultValue(), + true))); + } + } else { + List values = allowedValues.stream() + .map(ConfiguredType.AllowedValue::value) + .collect(Collectors.toList()); + for (ConfiguredType.AllowedValue allowedValue : allowedValues) { + System.out.println(spaces + "# " + allowedValue.value() + ": " + allowedValue.description() + .replace("\n", " ")); + } + if (property.defaultValue() == null) { + System.out.println(spaces + property.outputKey() + ": \"One of: " + values + "\""); + } else { + System.out.println(spaces + property.outputKey() + ": \"" + property.defaultValue() + "\""); + } + } + } + + private static String toTypedValue(ConfiguredType.ConfiguredProperty property, + TypedValue typed) { + String value = property.defaultValue(); + + if (value == null) { + value = typed.defaultsDefault; + } + + return output(typed, value); + } + + private static void processMetadataJson(List configuredTypes, + Map typesMap, + JsonArray jsonArray) { + for (JsonValue jsonValue : jsonArray) { + processTypeArray(configuredTypes, typesMap, jsonValue.asJsonObject().getJsonArray("types")); + } + } + + private static void processTypeArray(List configuredTypes, + Map typesMap, + JsonArray jsonArray) { + if (jsonArray == null) { + return; + } + for (JsonValue jsonValue : jsonArray) { + JsonObject type = jsonValue.asJsonObject(); + ConfiguredType configuredType = ConfiguredType.create(type); + configuredTypes.add(configuredType); + typesMap.put(configuredType.targetClass(), configuredType); + } + } + + private static final class TypedValue { + private final String defaultsDefault; + private final boolean escaped; + + private TypedValue(String defaultsDefault) { + this(defaultsDefault, false); + } + + private TypedValue(String defaultsDefault, boolean escaped) { + this.defaultsDefault = defaultsDefault; + this.escaped = escaped; + } + + String defaultsDefault() { + return defaultsDefault; + } + + boolean escaped() { + return escaped; + } + } +} diff --git a/examples/config/metadata/src/main/java/io/helidon/examples/config/metadata/ConfiguredType.java b/examples/config/metadata/src/main/java/io/helidon/examples/config/metadata/ConfiguredType.java new file mode 100644 index 000000000..a5b5730df --- /dev/null +++ b/examples/config/metadata/src/main/java/io/helidon/examples/config/metadata/ConfiguredType.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.metadata; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import io.helidon.config.metadata.ConfiguredOption; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; + +final class ConfiguredType { + private final Set allProperties = new HashSet<>(); + private final List producerMethods = new LinkedList<>(); + /** + * The type that is built by a builder, or created using create method. + */ + private final String targetClass; + private final boolean standalone; + private final String prefix; + private final String description; + private final List provides; + private final List inherited = new LinkedList<>(); + + ConfiguredType(String targetClass, boolean standalone, String prefix, String description, List provides) { + this.targetClass = targetClass; + this.standalone = standalone; + this.prefix = prefix; + this.description = description; + this.provides = provides; + } + + private static String paramsToString(String[] params) { + String result = Arrays.toString(params); + if (result.startsWith("[") && result.endsWith("]")) { + return result.substring(1, result.length() - 1); + } + return result; + } + + String description() { + return description; + } + + static ConfiguredType create(JsonObject type) { + ConfiguredType ct = new ConfiguredType( + type.getString("type"), + type.getBoolean("standalone", false), + type.getString("prefix", null), + type.getString("description", null), + toList(type.getJsonArray("provides")) + ); + + List producers = toList(type.getJsonArray("producers")); + for (String producer : producers) { + ct.addProducer(ProducerMethod.parse(producer)); + } + List inherits = toList(type.getJsonArray("inherits")); + for (String inherit : inherits) { + ct.addInherited(inherit); + } + + JsonArray options = type.getJsonArray("options"); + for (JsonValue option : options) { + ct.addProperty(ConfiguredProperty.create(option.asJsonObject())); + } + + return ct; + } + + private static List toList(JsonArray array) { + if (array == null) { + return List.of(); + } + List result = new ArrayList<>(array.size()); + + for (JsonValue jsonValue : array) { + result.add(((JsonString) jsonValue).getString()); + } + + return result; + } + + ConfiguredType addProducer(ProducerMethod producer) { + producerMethods.add(producer); + return this; + } + + ConfiguredType addProperty(ConfiguredProperty property) { + allProperties.add(property); + return this; + } + + List producers() { + return producerMethods; + } + + Set properties() { + return allProperties; + } + + String targetClass() { + return targetClass; + } + + boolean standalone() { + return standalone; + } + + String prefix() { + return prefix; + } + + List provides() { + return provides; + } + + @Override + public String toString() { + return targetClass; + } + + void addInherited(String classOrIface) { + inherited.add(classOrIface); + } + + List inherited() { + return inherited; + } + + static final class ProducerMethod { + private final boolean isStatic; + private final String owningClass; + private final String methodName; + private final String[] methodParams; + + ProducerMethod(boolean isStatic, String owningClass, String methodName, String[] methodParams) { + this.isStatic = isStatic; + this.owningClass = owningClass; + this.methodName = methodName; + this.methodParams = methodParams; + } + + public static ProducerMethod parse(String producer) { + int methodSeparator = producer.indexOf('#'); + String owningClass = producer.substring(0, methodSeparator); + int paramBraceStart = producer.indexOf('(', methodSeparator); + String methodName = producer.substring(methodSeparator + 1, paramBraceStart); + int paramBraceEnd = producer.indexOf(')', paramBraceStart); + String parameters = producer.substring(paramBraceStart + 1, paramBraceEnd); + String[] methodParams = parameters.split(","); + + return new ProducerMethod(false, + owningClass, + methodName, + methodParams); + } + + @Override + public String toString() { + return owningClass + + "#" + + methodName + "(" + + paramsToString(methodParams) + ")"; + } + } + + static final class ConfiguredProperty { + private final String builderMethod; + private final String key; + private final String description; + private final String defaultValue; + private final String type; + private final boolean experimental; + private final boolean optional; + private final ConfiguredOption.Kind kind; + private final List allowedValues; + private final boolean provider; + private final boolean merge; + + // if this is a nested type + private ConfiguredType configuredType; + private String outputKey; + + ConfiguredProperty(String builderMethod, + String key, + String description, + String defaultValue, + String type, + boolean experimental, + boolean optional, + ConfiguredOption.Kind kind, + boolean provider, + boolean merge, + List allowedValues) { + this.builderMethod = builderMethod; + this.key = key; + this.description = description; + this.defaultValue = defaultValue; + this.type = type; + this.experimental = experimental; + this.optional = optional; + this.kind = kind; + this.allowedValues = allowedValues; + this.outputKey = key; + this.provider = provider; + this.merge = merge; + } + + public static ConfiguredProperty create(JsonObject json) { + return new ConfiguredProperty( + json.getString("method", null), + json.getString("key", null), + json.getString("description"), + json.getString("defaultValue", null), + json.getString("type", "java.lang.String"), + json.getBoolean("experimental", false), + !json.getBoolean("required", false), + toKind(json.getString("kind", null)), + json.getBoolean("provider", false), + json.getBoolean("merge", false), + toAllowedValues(json.getJsonArray("allowedValues")) + ); + } + + private static ConfiguredOption.Kind toKind(String kind) { + if (kind == null) { + return ConfiguredOption.Kind.VALUE; + } + return ConfiguredOption.Kind.valueOf(kind); + } + + List allowedValues() { + return allowedValues; + } + + private static List toAllowedValues(JsonArray allowedValues) { + if (allowedValues == null) { + return List.of(); + } + List result = new ArrayList<>(allowedValues.size()); + + for (JsonValue allowedValue : allowedValues) { + JsonObject json = allowedValue.asJsonObject(); + result.add(new AllowedValue(json.getString("value"), json.getString("description", null))); + } + + return result; + } + + String builderMethod() { + return builderMethod; + } + + String outputKey() { + return outputKey; + } + + String key() { + return key; + } + + void key(String key) { + this.outputKey = key; + } + + String description() { + return description; + } + + String defaultValue() { + return defaultValue; + } + + String type() { + return type; + } + + boolean experimental() { + return experimental; + } + + boolean optional() { + return optional; + } + + ConfiguredOption.Kind kind() { + return kind; + } + + boolean merge() { + return merge; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfiguredProperty that = (ConfiguredProperty) o; + return key.equals(that.key); + } + + @Override + public int hashCode() { + return Objects.hash(key); + } + + @Override + public String toString() { + return key; + } + + boolean provider() { + return provider; + } + } + + static final class AllowedValue { + private final String value; + private final String description; + + private AllowedValue(String value, String description) { + this.value = value; + this.description = description; + } + + String value() { + return value; + } + + String description() { + return description; + } + } +} diff --git a/examples/config/metadata/src/main/java/io/helidon/examples/config/metadata/package-info.java b/examples/config/metadata/src/main/java/io/helidon/examples/config/metadata/package-info.java new file mode 100644 index 000000000..959e669f3 --- /dev/null +++ b/examples/config/metadata/src/main/java/io/helidon/examples/config/metadata/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of processing configuration metadata. + */ +package io.helidon.examples.config.metadata; diff --git a/examples/config/overrides/README.md b/examples/config/overrides/README.md new file mode 100644 index 000000000..966a6435d --- /dev/null +++ b/examples/config/overrides/README.md @@ -0,0 +1,27 @@ +# Helidon Config Overrides Example + +This example shows how to load configuration from multiple +configuration sources, specifically where one of the sources _overrides_ other +sources. + +The application treats +[`resources/application.yaml`](./src/main/resources/application.yaml) and +[`conf/priority-config.yaml`](./conf/priority-config.yaml) +as the config sources for the its configuration. + +The `application.yaml` file is packaged along with the application code, while +the files in `conf` would be provided during deployment to tailor the behavior +of the application to that specific environment. + +The application also loads +[`conf/overrides.properties`](./conf/overrides.properties) but as an +_override_ config +source. This file contains key _expressions_ (including wildcards) and values which +take precedence over the settings in the original config sources. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-config-overrides.jar +``` diff --git a/examples/config/overrides/conf/overrides.properties b/examples/config/overrides/conf/overrides.properties new file mode 100644 index 000000000..3ff3dbc0a --- /dev/null +++ b/examples/config/overrides/conf/overrides.properties @@ -0,0 +1,29 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Overrides provides central place for +# managing app configuration across envs. + +# key format: $env.$pod... + +# override selected pod (abcdef) logging level +prod.abcdef.logging.level = FINEST + +# "production" environment, any pod +prod.*.logging.level = WARNING + +# "test" environment, any pod +test.*.logging.level = FINE diff --git a/examples/config/overrides/conf/priority-config.yaml b/examples/config/overrides/conf/priority-config.yaml new file mode 100644 index 000000000..70b33f389 --- /dev/null +++ b/examples/config/overrides/conf/priority-config.yaml @@ -0,0 +1,29 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# set context + +env: prod +pod: abcdef + +$env: + $pod: + logging: + level: ERROR + + app: + greeting: Ahoy + page-size: 42 diff --git a/examples/config/overrides/pom.xml b/examples/config/overrides/pom.xml new file mode 100644 index 000000000..1e8dd408b --- /dev/null +++ b/examples/config/overrides/pom.xml @@ -0,0 +1,61 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.config + helidon-examples-config-overrides + 1.0.0-SNAPSHOT + Helidon Examples Config Overrides + + + The example shows how to use Overrides in Configuration API. + + + + io.helidon.examples.config.overrides.Main + + + + + io.helidon.bundles + helidon-bundles-config + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/config/overrides/src/main/java/io/helidon/examples/config/overrides/Main.java b/examples/config/overrides/src/main/java/io/helidon/examples/config/overrides/Main.java new file mode 100644 index 000000000..30a0b5e9c --- /dev/null +++ b/examples/config/overrides/src/main/java/io/helidon/examples/config/overrides/Main.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.overrides; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import io.helidon.config.Config; +import io.helidon.config.OverrideSources; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; +import static io.helidon.config.PollingStrategies.regular; + +/** + * Overrides example. + *

+ * Shows the Overrides feature where values from config sources might be overridden by override source. + *

+ * In this example, {@code application.yaml} is meant to be default application configuration distributed with an app, containing + * a wildcard configuration nodes representing the defaults for every environment and pod as well as a default definition of + * these values. The source {@code conf/priority-config.yaml} is a higher priority configuration source which can be in a real + * app dynamically changed (i.e. {@code Kubernetes ConfigMap} mapped as the file) and contains the current {@code env} and {@code + * pod} values ({@code prod} and {@code abcdef} in this example) and higher priority default configuration. So far the current + * configuration looks like this: + *

+ * prod:
+ *   abcdef:
+ *     logging:
+ *     level: ERROR
+ *     app:
+ *       greeting:  Ahoy
+ *       page-size: 42
+ *       basic-range:
+ *         - -20
+ *         -  20
+ * 
+ * But the override source just overrides values for environment: {@code prod} and pod: {@code abcdef} (it is the first + * overriding rule found) and value for key {@code prod.abcdef.logging.level = FINEST}. For completeness, we would say that the + * other pods in {@code prod} environment has overridden config value {@code prod.*.logging.level} to {@code WARNING} and all + * pods + * {@code test.*.logging.level} to {@code FINE}. + */ +public final class Main { + + private Main() { + } + + /** + * Executes the example. + * + * @param args arguments + * @throws InterruptedException when a sleeper awakes + */ + public static void main(String... args) throws InterruptedException { + Config config = Config + .builder() + // specify config sources + .sources(file("conf/priority-config.yaml").pollingStrategy(regular(Duration.ofSeconds(1))), + classpath("application.yaml")) + // specify overrides source + .overrides(OverrideSources.file("conf/overrides.properties") + .pollingStrategy(regular(Duration.ofSeconds(1)))) + .build(); + + // Resolve current runtime context + String env = config.get("env").asString().get(); + String pod = config.get("pod").asString().get(); + + // get logging config for the current runtime + Config loggingConfig = config + .get(env) + .get(pod) + .get("logging"); + + // initialize logging from config + initLogging(loggingConfig); + + // react on changes of logging configuration + loggingConfig.onChange(Main::initLogging); + + TimeUnit.MINUTES.sleep(1); + } + + /** + * Initialize logging from config. + */ + private static void initLogging(Config loggingConfig) { + String level = loggingConfig.get("level").asString().orElse("WARNING"); + //e.g. initialize logging using configured level... + + System.out.println("Set logging level to " + level + "."); + } + +} diff --git a/examples/config/overrides/src/main/java/io/helidon/examples/config/overrides/package-info.java b/examples/config/overrides/src/main/java/io/helidon/examples/config/overrides/package-info.java new file mode 100644 index 000000000..9a3f5877f --- /dev/null +++ b/examples/config/overrides/src/main/java/io/helidon/examples/config/overrides/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The example shows how to use Overrides in Configuration API. + */ +package io.helidon.examples.config.overrides; diff --git a/examples/config/overrides/src/main/resources/application.yaml b/examples/config/overrides/src/main/resources/application.yaml new file mode 100644 index 000000000..b13e7ad01 --- /dev/null +++ b/examples/config/overrides/src/main/resources/application.yaml @@ -0,0 +1,35 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# default context + +env: dev +pod: local + +# app config +# key format: $env.$pod... + +$env: + $pod: + logging: + level: CONFIG + + app: + greeting: Hello + page-size: 20 + basic-range: + - -20 + - 20 diff --git a/examples/config/overrides/src/main/resources/logging.properties b/examples/config/overrides/src/main/resources/logging.properties new file mode 100644 index 000000000..721cc5fc1 --- /dev/null +++ b/examples/config/overrides/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n + +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/config/pom.xml b/examples/config/pom.xml new file mode 100644 index 000000000..6f416e447 --- /dev/null +++ b/examples/config/pom.xml @@ -0,0 +1,45 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + io.helidon.examples.config + helidon-examples-config-project + 1.0.0-SNAPSHOT + pom + Helidon Examples Config + + + basics + changes + git + mapping + overrides + sources + profiles + metadata + + + diff --git a/examples/config/profiles/README.md b/examples/config/profiles/README.md new file mode 100644 index 000000000..8c98e3308 --- /dev/null +++ b/examples/config/profiles/README.md @@ -0,0 +1,31 @@ +# Helidon Config Profiles Example + +This example shows how to load configuration from multiple +configuration sources using profiles. + +This example contains the following profiles: + +1. no profile - if you start the application with no profile, the usual `src/main/resources/application.yaml` will be used +2. `local` - `src/main/resources/application-local.yaml` will be used +3. `dev` - has an explicit profile file `config-profile-dev.yaml` on classpath that defines an inlined configuration +4. `stage` - has an explicit profile file `config-profile-stage.yaml` on classpath that defines a classpath config source +4. `prod` - has an explicit profile file `config-profile-prod.yaml` on file system that defines a path config source + +To switch profiles +- either use a system property `config.profile` +- or use an environment variable `HELIDON_CONFIG_PROFILE` + + +## How to run this example: + +Build the application +```shell +mvn clean package +``` + +Run it with a profile +```shell +java -Dconfig.profile=prod -jar target/helidon-examples-config-profiles.jar +``` + +Changing the profile name should use different configuration. \ No newline at end of file diff --git a/examples/config/profiles/config-profile-prod.yaml b/examples/config/profiles/config-profile-prod.yaml new file mode 100644 index 000000000..fdf11d8d9 --- /dev/null +++ b/examples/config/profiles/config-profile-prod.yaml @@ -0,0 +1,22 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +sources: + - type: "environment-variables" + - type: "system-properties" + - type: "file" + properties: + path: "config/config-prod.yaml" diff --git a/examples/config/profiles/config/config-prod.yaml b/examples/config/profiles/config/config-prod.yaml new file mode 100644 index 000000000..f75b5e67c --- /dev/null +++ b/examples/config/profiles/config/config-prod.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +message: "config/config-prod.yaml" diff --git a/examples/config/profiles/pom.xml b/examples/config/profiles/pom.xml new file mode 100644 index 000000000..318453675 --- /dev/null +++ b/examples/config/profiles/pom.xml @@ -0,0 +1,65 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.config + helidon-examples-config-profiles + 1.0.0-SNAPSHOT + Helidon Examples Config Profiles + + + The example shows how to use Config Profiles (meta configuration). + + + + io.helidon.examples.config.profiles.Main + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/config/profiles/src/main/java/io/helidon/examples/config/profiles/Main.java b/examples/config/profiles/src/main/java/io/helidon/examples/config/profiles/Main.java new file mode 100644 index 000000000..f9531db1b --- /dev/null +++ b/examples/config/profiles/src/main/java/io/helidon/examples/config/profiles/Main.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.profiles; + +import io.helidon.config.Config; + +/** + * Example main class. + */ +public final class Main { + private Main() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + Config config = Config.create(); + + System.out.println("Configured message: " + config.get("message").asString().orElse("MISSING")); + } +} diff --git a/examples/config/profiles/src/main/java/io/helidon/examples/config/profiles/package-info.java b/examples/config/profiles/src/main/java/io/helidon/examples/config/profiles/package-info.java new file mode 100644 index 000000000..e00866372 --- /dev/null +++ b/examples/config/profiles/src/main/java/io/helidon/examples/config/profiles/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example showing usage of configuration profiles in Helidon SE. + */ +package io.helidon.examples.config.profiles; diff --git a/examples/config/profiles/src/main/resources/application-local.yaml b/examples/config/profiles/src/main/resources/application-local.yaml new file mode 100644 index 000000000..7bf6500e9 --- /dev/null +++ b/examples/config/profiles/src/main/resources/application-local.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +message: "src/main/resources/application-local.yaml" diff --git a/examples/config/profiles/src/main/resources/application-stage.yaml b/examples/config/profiles/src/main/resources/application-stage.yaml new file mode 100644 index 000000000..ab0057ee3 --- /dev/null +++ b/examples/config/profiles/src/main/resources/application-stage.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +message: "src/main/resources/application-stage.yaml" diff --git a/examples/config/profiles/src/main/resources/application.yaml b/examples/config/profiles/src/main/resources/application.yaml new file mode 100644 index 000000000..b1a9a1238 --- /dev/null +++ b/examples/config/profiles/src/main/resources/application.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +message: "src/main/resources/application.yaml" diff --git a/examples/config/profiles/src/main/resources/config-profile-dev.yaml b/examples/config/profiles/src/main/resources/config-profile-dev.yaml new file mode 100644 index 000000000..74e7e903b --- /dev/null +++ b/examples/config/profiles/src/main/resources/config-profile-dev.yaml @@ -0,0 +1,20 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +sources: + - type: "inlined" + properties: + message: "inlined in dev profile" diff --git a/examples/config/profiles/src/main/resources/config-profile-stage.yaml b/examples/config/profiles/src/main/resources/config-profile-stage.yaml new file mode 100644 index 000000000..bbc285948 --- /dev/null +++ b/examples/config/profiles/src/main/resources/config-profile-stage.yaml @@ -0,0 +1,20 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +sources: + - type: "classpath" + properties: + resource: "application-stage.yaml" diff --git a/examples/config/sources/README.md b/examples/config/sources/README.md new file mode 100644 index 000000000..cfb5c3210 --- /dev/null +++ b/examples/config/sources/README.md @@ -0,0 +1,23 @@ +# Helidon Config Sources Example + +This example shows how to load configuration from multiple +configuration sources. + +1. [`DirectorySourceExample.java`](src/main/java/io/helidon/examples/config/sources/DirectorySourceExample.java) +reads configuration from multiple files in a directory by specifying only the directory. +2. [`LoadSourcesExample.java`](src/main/java/io/helidon/examples/config/sources/LoadSourcesExample.java) +uses _meta-configuration_ files [`conf/meta-config.yaml`](./conf/meta-config.yaml) +and [`src/main/resources/meta-config.yaml`](./src/main/resources/meta-config.yaml) +which contain not the configuration itself but +_instructions for loading_ the configuration: what type, from where, etc. It also +applies a filter to modify config values whose keys match a certain pattern. +3. [`WithSourcesExample.java`](src/main/java/io/helidon/examples/config/sources/WithSourcesExample.java) +combines multiple config sources into a single configuration instance (and adds a +filter. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-config-sources.jar +``` diff --git a/examples/config/sources/conf/config.yaml b/examples/config/sources/conf/config.yaml new file mode 100644 index 000000000..4b08deacc --- /dev/null +++ b/examples/config/sources/conf/config.yaml @@ -0,0 +1,25 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# In config.yaml we are going to override default configuration. +# It is supposed to be deployment dependent configuration. + +meta: + env: PROD + +app: + page-size: 10 + diff --git a/examples/config/sources/conf/dev.yaml b/examples/config/sources/conf/dev.yaml new file mode 100644 index 000000000..312f8d84c --- /dev/null +++ b/examples/config/sources/conf/dev.yaml @@ -0,0 +1,30 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# In dev.yaml we are going to override all lower layer configuration files. +# It is supposed to be development specific configuration. + +# IT SHOULD NOT BE PLACED IN VCS. EACH DEVELOPER CAN CUSTOMIZE THE FILE AS NEEDED. +# I.e. for example with GIT place following line into .gitignore file: +#*/conf/dev.yaml + +meta: + env: DEV + +component: + audit: + logging: + level: fine diff --git a/examples/config/sources/conf/meta-config.yaml b/examples/config/sources/conf/meta-config.yaml new file mode 100644 index 000000000..31e31fe2d --- /dev/null +++ b/examples/config/sources/conf/meta-config.yaml @@ -0,0 +1,30 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +sources: + - type: "environment-variables" + - type: "system-properties" + - type: "file" + properties: + path: "conf/dev.yaml" + optional: true + - type: "file" + properties: + path: "conf/config.yaml" + optional: true + - type: "classpath" + properties: + resource: "default.yaml" diff --git a/examples/config/sources/conf/secrets/password b/examples/config/sources/conf/secrets/password new file mode 100644 index 000000000..5bbaf8758 --- /dev/null +++ b/examples/config/sources/conf/secrets/password @@ -0,0 +1 @@ +changeit \ No newline at end of file diff --git a/examples/config/sources/conf/secrets/username b/examples/config/sources/conf/secrets/username new file mode 100644 index 000000000..1ce97e36c --- /dev/null +++ b/examples/config/sources/conf/secrets/username @@ -0,0 +1 @@ +libor \ No newline at end of file diff --git a/examples/config/sources/pom.xml b/examples/config/sources/pom.xml new file mode 100644 index 000000000..147edb88c --- /dev/null +++ b/examples/config/sources/pom.xml @@ -0,0 +1,65 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.config + helidon-examples-config-sources + 1.0.0-SNAPSHOT + Helidon Examples Config Sources + + + This example shows how to merge the configuration from different sources. + + + + io.helidon.examples.config.sources.Main + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/config/sources/src/main/java/io/helidon/examples/config/sources/DirectorySourceExample.java b/examples/config/sources/src/main/java/io/helidon/examples/config/sources/DirectorySourceExample.java new file mode 100644 index 000000000..226c9e349 --- /dev/null +++ b/examples/config/sources/src/main/java/io/helidon/examples/config/sources/DirectorySourceExample.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.sources; + +import io.helidon.config.Config; + +import static io.helidon.config.ConfigSources.directory; + +/** + * This example shows how to read configuration from several files placed in selected directory. + */ +public class DirectorySourceExample { + + private DirectorySourceExample() { + } + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + /* + Creates a config from files from specified directory. + E.g. Kubernetes Secrets: + */ + + Config secrets = Config.builder(directory("conf/secrets")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + + String username = secrets.get("username").asString().get(); + System.out.println("Username: " + username); + assert username.equals("libor"); + + String password = secrets.get("password").asString().get(); + System.out.println("Password: " + password); + assert password.equals("changeit"); + } + +} diff --git a/examples/config/sources/src/main/java/io/helidon/examples/config/sources/LoadSourcesExample.java b/examples/config/sources/src/main/java/io/helidon/examples/config/sources/LoadSourcesExample.java new file mode 100644 index 000000000..472cb7810 --- /dev/null +++ b/examples/config/sources/src/main/java/io/helidon/examples/config/sources/LoadSourcesExample.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.sources; + +import io.helidon.config.Config; +import io.helidon.config.ConfigValue; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * This example shows how to merge the configuration from different sources + * loaded from meta configuration. + * + * @see WithSourcesExample + */ +public class LoadSourcesExample { + + private LoadSourcesExample() { + } + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + /* + Creates a configuration from list of config sources loaded from meta sources: + - conf/meta-config.yaml - - deployment dependent meta-config file, loaded from file on filesystem; + - meta-config.yaml - application default meta-config file, loaded form classpath; + with a filter which convert values with keys ending with "level" to upper case + */ + + Config metaConfig = Config.create(file("conf/meta-config.yaml").optional(), + classpath("meta-config.yaml")); + + Config config = Config.builder() + .config(metaConfig) + .addFilter((key, stringValue) -> key.name().equals("level") ? stringValue.toUpperCase() : stringValue) + .build(); + + // Optional environment type, from dev.yaml: + ConfigValue env = config.get("meta.env").asString(); + env.ifPresent(e -> System.out.println("Environment: " + e)); + assert env.get().equals("DEV"); + + // Default value (default.yaml): Config Sources Example + String appName = config.get("app.name").asString().get(); + System.out.println("Name: " + appName); + assert appName.equals("Config Sources Example"); + + // Page size, from config.yaml: 10 + int pageSize = config.get("app.page-size").asInt().get(); + System.out.println("Page size: " + pageSize); + assert pageSize == 10; + + // Applied filter (uppercase logging level), from dev.yaml: finest -> FINEST + String level = config.get("component.audit.logging.level").asString().get(); + System.out.println("Level: " + level); + assert level.equals("FINE"); + } + +} diff --git a/examples/config/sources/src/main/java/io/helidon/examples/config/sources/Main.java b/examples/config/sources/src/main/java/io/helidon/examples/config/sources/Main.java new file mode 100644 index 000000000..a0c1be3e0 --- /dev/null +++ b/examples/config/sources/src/main/java/io/helidon/examples/config/sources/Main.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.sources; + +/** + * Runs every example main class in this module/package. + */ +public class Main { + + private Main() { + } + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String[] args) { + WithSourcesExample.main(args); + LoadSourcesExample.main(args); + DirectorySourceExample.main(args); + } + +} diff --git a/examples/config/sources/src/main/java/io/helidon/examples/config/sources/WithSourcesExample.java b/examples/config/sources/src/main/java/io/helidon/examples/config/sources/WithSourcesExample.java new file mode 100644 index 000000000..96c0adce8 --- /dev/null +++ b/examples/config/sources/src/main/java/io/helidon/examples/config/sources/WithSourcesExample.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.config.sources; + +import io.helidon.config.Config; +import io.helidon.config.ConfigValue; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * This example shows how to merge the configuration from different sources. + * + * @see LoadSourcesExample + */ +public class WithSourcesExample { + + private WithSourcesExample() { + } + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + /* + Creates a config source composed of following sources: + - conf/dev.yaml - developer specific configuration, should not be placed in VCS; + - conf/config.yaml - deployment dependent configuration, for example prod, stage, etc; + - default.yaml - application default values, loaded form classpath; + with a filter which convert values with keys ending with "level" to upper case + */ + + Config config = Config + .builder(file("conf/dev.yaml").optional(), + file("conf/config.yaml").optional(), + classpath("default.yaml")) + .addFilter((key, stringValue) -> key.name().equals("level") ? stringValue.toUpperCase() : stringValue) + .build(); + + // Environment type, from dev.yaml: + ConfigValue env = config.get("meta.env").asString(); + env.ifPresent(e -> System.out.println("Environment: " + e)); + assert env.get().equals("DEV"); + + // Default value (default.yaml): Config Sources Example + String appName = config.get("app.name").asString().get(); + System.out.println("Name: " + appName); + assert appName.equals("Config Sources Example"); + + // Page size, from config.yaml: 10 + int pageSize = config.get("app.page-size").asInt().get(); + System.out.println("Page size: " + pageSize); + assert pageSize == 10; + + // Applied filter (uppercase logging level), from dev.yaml: finest -> FINEST + String level = config.get("component.audit.logging.level").asString().get(); + System.out.println("Level: " + level); + assert level.equals("FINE"); + } + +} diff --git a/examples/config/sources/src/main/java/io/helidon/examples/config/sources/package-info.java b/examples/config/sources/src/main/java/io/helidon/examples/config/sources/package-info.java new file mode 100644 index 000000000..a9d4530f2 --- /dev/null +++ b/examples/config/sources/src/main/java/io/helidon/examples/config/sources/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This example shows how to merge the configuration from different sources. + */ +package io.helidon.examples.config.sources; diff --git a/examples/config/sources/src/main/resources/default.yaml b/examples/config/sources/src/main/resources/default.yaml new file mode 100644 index 000000000..b1dfac982 --- /dev/null +++ b/examples/config/sources/src/main/resources/default.yaml @@ -0,0 +1,32 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# In default.yaml we are going to configure application default values. +# It is expected it is overridden in: +# - conf/config.yaml in production environment +# - conf/dev.yaml by developer on local machine + +app: + name: Config Sources Example + page-size: 20 + +component: + audit: + logging: + level: info + monitoring: + logging: + level: warning diff --git a/examples/config/sources/src/main/resources/logging.properties b/examples/config/sources/src/main/resources/logging.properties new file mode 100644 index 000000000..721cc5fc1 --- /dev/null +++ b/examples/config/sources/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n + +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/config/sources/src/main/resources/meta-config.yaml b/examples/config/sources/src/main/resources/meta-config.yaml new file mode 100644 index 000000000..c0b1365f0 --- /dev/null +++ b/examples/config/sources/src/main/resources/meta-config.yaml @@ -0,0 +1,21 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# ordered list of sources +sources: + - type: "classpath" + properties: + resource: "default.yaml" diff --git a/examples/config/sources/src/main/resources/overrides.properties b/examples/config/sources/src/main/resources/overrides.properties new file mode 100644 index 000000000..a2046d7a1 --- /dev/null +++ b/examples/config/sources/src/main/resources/overrides.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +component.*.logging.level = finest diff --git a/examples/cors/README.md b/examples/cors/README.md new file mode 100644 index 000000000..54517d949 --- /dev/null +++ b/examples/cors/README.md @@ -0,0 +1,197 @@ + +# Helidon SE CORS Example + +This example shows a simple greeting application, similar to the one from the +Helidon SE QuickStart, enhanced with CORS support. + +Look at the `resources/application.yaml` file and notice the `restrictive-cors` +section. (We'll come back to the `cors` section later.) The `Main#corsSupportForGreeting` method loads this +configuration and uses it to set up CORS for the application's endpoints. + +Near the end of the `resources/logging.properties` file, a commented line would turn on `FINE +` logging that would reveal how the Helidon CORS support makes it decisions. To see that logging, +uncomment that line and then package and run the application. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-cors.jar +``` + +## Using the app endpoints as with the "classic" greeting app + +These normal greeting app endpoints work just as in the original greeting app: + +```shell +curl -X GET http://localhost:8080/greet +#{"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#{"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#{"message":"Hola Jose!"} +``` + +## Using CORS + +### Sending "simple" CORS requests + +The following requests illustrate the CORS protocol with the example app. + +By setting `Origin` and `Host` headers that do not indicate the same system we trigger CORS processing in the + server: + +```shell +# Follow the CORS protocol for GET +curl -i -X GET -H "Origin: http://foo.com" -H "Host: here.com" http://localhost:8080/greet + +``` +```text +HTTP/1.1 200 OK +Access-Control-Allow-Origin: * +Content-Type: application/json +Date: Thu, 30 Apr 2020 17:25:51 -0500 +Vary: Origin +connection: keep-alive +content-length: 27 + +{"greeting":"Hola World!"} +``` +Note the new headers `Access-Control-Allow-Origin` and `Vary` in the response. + +The same happens for a `GET` requesting a personalized greeting (by passing the name of the + person to be greeted): +```shell +curl -i -X GET -H "Origin: http://foo.com" -H "Host: here.com" http://localhost:8080/greet/Joe + +``` +```text +HTTP/1.1 200 OK +Date: Wed, 31 Jan 2024 11:58:06 +0100 +Access-Control-Allow-Origin: * +Connection: keep-alive +Content-Length: 24 +Content-Type: application/json +Vary: ORIGIN + +{"greeting":"Hola Joe!"} +``` +These two `GET` requests work because the `Main#corsSupportForGreeting` method adds a default `CrossOriginConfig` to the +`CorsSupport.Builder` it sets up. This is in addition to adding a `CrossOriginConfig` based on the `restrictive-cors` +configuration in `application.yaml` we looked at earlier. + +These are what CORS calls "simple" requests; the CORS protocol for these adds headers to the request and response which +the client and server exchange anyway. + +### "Non-simple" CORS requests + +The CORS protocol requires the client to send a _pre-flight_ request before sending a request + that changes state on the server, such as `PUT` or `DELETE`, and to check the returned status + and headers to make sure the server is willing to accept the actual request. CORS refers to such `PUT` and `DELETE` + requests as "non-simple" ones. + +This command sends a pre-flight `OPTIONS` request to see if the server will accept a subsequent `PUT` request from the +specified origin to change the greeting: +```shell +curl -i -X OPTIONS \ + -H "Access-Control-Request-Method: PUT" \ + -H "Origin: http://foo.com" \ + -H "Host: here.com" \ + http://localhost:8080/greet/greeting +``` +```text +HTTP/1.1 200 OK +Date: Wed, 31 Jan 2024 12:00:15 +0100 +Access-Control-Allow-Methods: PUT +Access-Control-Allow-Origin: http://foo.com +Access-Control-Max-Age: 3600 +Connection: keep-alive +Content-Length: 0 +``` +The successful status and the returned `Access-Control-Allow-xxx` headers indicate that the + server accepted the pre-flight request. That means it is OK for us to send `PUT` request to perform the actual change + of greeting. (See below for how the server rejects a pre-flight request.) +```shell +curl -i -X PUT \ + -H "Origin: http://foo.com" \ + -H "Host: here.com" \ + -H "Access-Control-Allow-Methods: PUT" \ + -H "Access-Control-Allow-Origin: http://foo.com" \ + -H "Content-Type: application/json" \ + -d "{ \"greeting\" : \"Cheers\" }" \ + http://localhost:8080/greet/greeting +``` +```text +HTTP/1.1 204 No Content +Date: Wed, 31 Jan 2024 12:01:45 +0100 +Access-Control-Allow-Origin: http://foo.com +Connection: keep-alive +Content-Length: 0 +Vary: ORIGIN +``` +And we run one more `GET` to observe the change in the greeting: +```shell +curl -i -X GET -H "Origin: http://foo.com" -H "Host: here.com" http://localhost:8080/greet/Joe +``` +```text +HTTP/1.1 200 OK +Date: Wed, 31 Jan 2024 12:02:13 +0100 +Access-Control-Allow-Origin: * +Connection: keep-alive +Content-Length: 26 +Content-Type: application/json +Vary: ORIGIN + +{"greeting":"Cheers Joe!"} +``` +Note that the tests in the example `MainTest` class follow these same steps. + +## Using overrides + +The `Main#corsSupportForGreeting` method loads override settings for any other CORS set-up if the config contains a +"cors" section. (That section is initially commented out in the example `application.yaml` file.) Not all applications +need this feature, but the example shows how easy it is to add. + +With the same server running, repeat the `OPTIONS` request from above, but change the `Origin` header to refer to +`other.com`: +```shell +curl -i -X OPTIONS \ + -H "Access-Control-Request-Method: PUT" \ + -H "Origin: http://other.com" \ + -H "Host: here.com" \ + http://localhost:8080/greet/greeting +``` +```text +HTTP/1.1 403 CORS origin is not in allowed list +Date: Wed, 31 Jan 2024 12:02:51 +0100 +Connection: keep-alive +Content-Length: 0 +``` +This fails because the app set up CORS using the "restrictive-cors" configuration in `application.yaml` which allows +sharing only with `foo.com` and `there.com`, not with `other.com`. + +Stop the running app, uncomment the commented section at the end of `application.yaml`, and build and run the app again. +```shell +mvn package +java -jar target/helidon-examples-cors.jar +``` +Send the previous `OPTIONS` request again and note the successful result: +```text +HTTP/1.1 200 OK +Date: Wed, 31 Jan 2024 12:05:36 +0100 +Access-Control-Allow-Methods: PUT +Access-Control-Allow-Origin: http://other.com +Access-Control-Max-Age: 3600 +Connection: keep-alive +Content-Length: 0 +``` +The application uses the now-uncommented portion of the config file to override the rest of the CORS set-up. You can +choose whatever key name you want for the override. Just make sure you tell your end users whatever the key is your app +uses for overrides. + +A real application might read the normal configuration (`restrictive-cors`) from one config source and any overrides +from another. This example combines them in one config source just for simplicity. diff --git a/examples/cors/pom.xml b/examples/cors/pom.xml new file mode 100644 index 000000000..70fbd8350 --- /dev/null +++ b/examples/cors/pom.xml @@ -0,0 +1,115 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples + helidon-examples-cors + 1.0.0-SNAPSHOT + Helidon Examples CORS SE + + + Basic illustration of CORS support in Helidon SE + + + + io.helidon.examples.cors.Main + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.config + helidon-config-yaml + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.health + helidon-health-checks + + + io.helidon.logging + helidon-logging-jul + runtime + + + jakarta.json + jakarta.json-api + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/examples/cors/src/main/java/io/helidon/examples/cors/GreetService.java b/examples/cors/src/main/java/io/helidon/examples/cors/GreetService.java new file mode 100644 index 000000000..bc4c37865 --- /dev/null +++ b/examples/cors/src/main/java/io/helidon/examples/cors/GreetService.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.cors; + +import java.util.Collections; + +import io.helidon.config.Config; +import io.helidon.http.Status; +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.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple service to greet you. Examples: + *

+ * Get default greeting message: + * curl -X GET http://localhost:8080/greet + *

+ * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + *

+ * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + *

+ * The message is returned as a JSON object + */ + +public class GreetService implements HttpService { + + /** + * The config value for the key {@code greeting}. + */ + private String greeting; + + private static final JsonBuilderFactory JSON_BF = Json.createBuilderFactory(Collections.emptyMap()); + + GreetService() { + Config config = Config.global(); + this.greeting = config.get("app.greeting").asString().orElse("Ciao"); + } + + /** + * A service registers itself by updating the routine rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules .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().get("name"); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + GreetingMessage msg = new GreetingMessage(String.format("%s %s!", greeting, name)); + response.send(msg.forRest()); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + + if (!jo.containsKey(GreetingMessage.JSON_LABEL)) { + JsonObject jsonErrorObject = JSON_BF.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting = GreetingMessage.fromRest(jo).getMessage(); + response.status(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) { + JsonObject jo = request.content().as(JsonObject.class); + updateGreetingFromJson(jo, response); + } +} diff --git a/examples/cors/src/main/java/io/helidon/examples/cors/GreetingMessage.java b/examples/cors/src/main/java/io/helidon/examples/cors/GreetingMessage.java new file mode 100644 index 000000000..42dd76d6e --- /dev/null +++ b/examples/cors/src/main/java/io/helidon/examples/cors/GreetingMessage.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.cors; + +import java.util.Collections; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; + +/** + * POJO for the greeting message exchanged between the server and the client. + */ +public class GreetingMessage { + + /** + * Label for tagging a {@code GreetingMessage} instance in JSON. + */ + public static final String JSON_LABEL = "greeting"; + + private static final JsonBuilderFactory JSON_BF = Json.createBuilderFactory(Collections.emptyMap()); + + private String message; + + /** + * Create a new greeting with the specified message content. + * + * @param message the message to store in the greeting + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Returns the message value. + * + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message value to be set + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Converts a JSON object (typically read from the request payload) + * into a {@code GreetingMessage}. + * + * @param jsonObject the {@link JsonObject} to convert. + * @return {@code GreetingMessage} set according to the provided object + */ + public static GreetingMessage fromRest(JsonObject jsonObject) { + return new GreetingMessage(jsonObject.getString(JSON_LABEL)); + } + + /** + * Prepares a {@link JsonObject} corresponding to this instance. + * + * @return {@code JsonObject} representing this {@code GreetingMessage} instance + */ + public JsonObject forRest() { + JsonObjectBuilder builder = JSON_BF.createObjectBuilder(); + return builder.add(JSON_LABEL, message) + .build(); + } +} diff --git a/examples/cors/src/main/java/io/helidon/examples/cors/Main.java b/examples/cors/src/main/java/io/helidon/examples/cors/Main.java new file mode 100644 index 000000000..b9167fbd8 --- /dev/null +++ b/examples/cors/src/main/java/io/helidon/examples/cors/Main.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.cors; + +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.cors.CrossOriginConfig; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.cors.CorsSupport; +import io.helidon.webserver.http.HttpRouting; + +/** + * Simple Hello World rest application. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(final String[] args) { + // load logging configuration + LogConfig.configureRuntime(); + + // initialize global config from default configuration + Config config = Config.create(); + Config.global(config); + + // Get webserver config from the "server" section of application.yaml + WebServerConfig.Builder builder = WebServer.builder(); + WebServer server = builder + .config(config.get("server")) + .routing(Main::routing) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + /** + * Setup routing. + * + * @param routing routing builder + */ + static void routing(HttpRouting.Builder routing) { + + // Note: Add the CORS routing *before* registering the GreetService routing. + routing.register("/greet", corsSupportForGreeting(), new GreetService()); + } + + private static CorsSupport corsSupportForGreeting() { + Config config = Config.global(); + + // The default CorsSupport object (obtained using CorsSupport.create()) allows sharing for any HTTP method and with any + // origin. Using CorsSupport.create(Config) with a missing config node yields a default CorsSupport, which might not be + // what you want. This example warns if either expected config node is missing and then continues with the default. + + Config restrictiveConfig = config.get("restrictive-cors"); + if (!restrictiveConfig.exists()) { + Logger.getLogger(Main.class.getName()) + .warning("Missing restrictive config; continuing with default CORS support"); + } + + CorsSupport.Builder corsBuilder = CorsSupport.builder(); + + // Use possible overrides first. + config.get("cors") + .ifExists(c -> { + 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(); + } +} diff --git a/examples/cors/src/main/java/io/helidon/examples/cors/package-info.java b/examples/cors/src/main/java/io/helidon/examples/cors/package-info.java new file mode 100644 index 000000000..46b0ef2cd --- /dev/null +++ b/examples/cors/src/main/java/io/helidon/examples/cors/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example application showing CORS support in Helidon SE + *

+ * Start with {@link io.helidon.examples.cors.Main} class. + * + * @see io.helidon.examples.cors.Main + */ +package io.helidon.examples.cors; diff --git a/examples/cors/src/main/resources/META-INF/openapi.yml b/examples/cors/src/main/resources/META-INF/openapi.yml new file mode 100644 index 000000000..8886b61bf --- /dev/null +++ b/examples/cors/src/main/resources/META-INF/openapi.yml @@ -0,0 +1,79 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +--- +openapi: 3.0.0 +info: + title: Helidon SE Quickstart Example + description: A very simple application to reply with friendly greetings + version: 1.0.0 + +servers: + - url: http://localhost:8000 + description: Local test server + +paths: + /greet: + get: + summary: Returns a generic greeting + description: Greets the user generically + responses: + default: + description: Simple JSON containing the greeting + content: + application/json: + schema: + $ref: '#/components/schemas/GreetingMessage' + /greet/greeting: + put: + summary: Set the greeting prefix + description: Permits the client to set the prefix part of the greeting ("Hello") + requestBody: + description: Conveys the new greeting prefix to use in building greetings + content: + application/json: + schema: + $ref: '#/components/schemas/GreetingMessage' + examples: + greeting: + summary: Example greeting message to update + value: New greeting message + responses: + "200": + description: OK + content: + application/json: {} + /greet/{name}: + get: + summary: Returns a personalized greeting + parameters: + - name: name + in: path + required: true + schema: + type: string + responses: + default: + description: Simple JSON containing the greeting + content: + application/json: + schema: + $ref: '#/components/schemas/GreetingMessage' +components: + schemas: + GreetingMessage: + properties: + message: + type: string diff --git a/examples/cors/src/main/resources/application.yaml b/examples/cors/src/main/resources/application.yaml new file mode 100644 index 000000000..adef82fb9 --- /dev/null +++ b/examples/cors/src/main/resources/application.yaml @@ -0,0 +1,34 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + greeting: "Hello" + +server: + port: 8080 + host: 0.0.0.0 + +restrictive-cors: + allow-origins: ["http://foo.com", "http://there.com"] + allow-methods: ["PUT", "DELETE"] + +# The example app uses the following for overriding other settings. +#cors: +# paths: +# - path-pattern: /greeting +# allow-origins: ["http://foo.com", "http://there.com", "http://other.com"] +# allow-methods: ["PUT", "DELETE"] + diff --git a/examples/cors/src/main/resources/logging.properties b/examples/cors/src/main/resources/logging.properties new file mode 100644 index 000000000..261494867 --- /dev/null +++ b/examples/cors/src/main/resources/logging.properties @@ -0,0 +1,30 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Uncomment the following to see CORS-related decision-making +# io.helidon.webserver.cors.level=FINE diff --git a/examples/cors/src/test/java/io/helidon/examples/cors/MainTest.java b/examples/cors/src/test/java/io/helidon/examples/cors/MainTest.java new file mode 100644 index 000000000..5f293f303 --- /dev/null +++ b/examples/cors/src/test/java/io/helidon/examples/cors/MainTest.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.cors; + +import java.util.List; +import java.util.Optional; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.config.Config; +import io.helidon.cors.CrossOriginConfig; +import io.helidon.http.Headers; +import io.helidon.http.WritableHeaders; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientRequest; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +import jakarta.json.JsonObject; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static io.helidon.http.HeaderNames.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.http.HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN; +import static io.helidon.http.HeaderNames.ACCESS_CONTROL_REQUEST_METHOD; +import static io.helidon.http.HeaderNames.HOST; +import static io.helidon.http.HeaderNames.ORIGIN; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; + +@SuppressWarnings("HttpUrlsUsage") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@ServerTest +public class MainTest { + + private final Http1Client client; + + MainTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + server.routing(Main::routing); + } + + @Order(1) // Make sure this runs before the greeting message changes so responses are deterministic. + @Test + public void testHelloWorld() { + try (Http1ClientResponse response = client.get("/greet") + .accept(MediaTypes.APPLICATION_JSON) + .request()) { + + assertThat(response.status().code(), is(200)); + + String payload = GreetingMessage.fromRest(response.entity().as(JsonObject.class)).getMessage(); + assertThat(payload, is("Hello World!")); + } + + try (Http1ClientResponse response = client.get("/greet/Joe") + .accept(MediaTypes.APPLICATION_JSON) + .request()) { + + assertThat(response.status().code(), is(200)); + + String payload = GreetingMessage.fromRest(response.entity().as(JsonObject.class)).getMessage(); + assertThat(payload, is("Hello Joe!")); + } + + try (Http1ClientResponse response = client.put("/greet/greeting") + .accept(MediaTypes.APPLICATION_JSON) + .submit(new GreetingMessage("Hola").forRest())) { + + assertThat(response.status().code(), is(204)); + } + + try (Http1ClientResponse response = client.get("/greet/Jose") + .accept(MediaTypes.APPLICATION_JSON) + .request()) { + + assertThat(response.status().code(), is(200)); + + String payload = GreetingMessage.fromRest(response.entity().as(JsonObject.class)).getMessage(); + assertThat(payload, is("Hola Jose!")); + } + + try (Http1ClientResponse response = client.get("/observe/health").request()) { + assertThat(response.status().code(), is(204)); + } + + try (Http1ClientResponse response = client.get("/observe/metrics").request()) { + assertThat(response.status().code(), is(200)); + } + } + + @Order(10) + // Run after the non-CORS tests (so the greeting is Hola) but before the CORS test that changes the greeting again. + @Test + void testAnonymousGreetWithCors() { + try (Http1ClientResponse response = client.get() + .path("/greet") + .accept(MediaTypes.APPLICATION_JSON) + .headers(it -> it + .set(ORIGIN, "http://foo.com") + .set(HOST, "here.com")) + .request()) { + + assertThat(response.status().code(), is(200)); + String payload = GreetingMessage.fromRest(response.entity().as(JsonObject.class)).getMessage(); + assertThat(payload, containsString("Hola 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("*")); + } + } + + @Order(11) // Run after the non-CORS tests but before other CORS tests. + @Test + void testGreetingChangeWithCors() { + // Send the pre-flight request and check the response. + WritableHeaders preFlightHeaders = WritableHeaders.create(); + try (Http1ClientResponse response = client.options() + .path("/greet/greeting") + .headers(it -> it + .set(ORIGIN, "http://foo.com") + .set(HOST, "here.com") + .set(ACCESS_CONTROL_REQUEST_METHOD, "PUT")) + .request()) { + response.headers().forEach(preFlightHeaders::add); + List allowMethods = preFlightHeaders.values(ACCESS_CONTROL_ALLOW_METHODS); + assertThat("pre-flight response does not include " + CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS, + allowMethods, not(empty())); + assertThat(allowMethods, hasItem("PUT")); + List allowOrigins = preFlightHeaders.values(ACCESS_CONTROL_ALLOW_ORIGIN); + assertThat("pre-flight response does not include " + CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN, + allowOrigins, not(empty())); + assertThat("Header " + CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN + + " should contain '*' but does not; " + allowOrigins, + allowOrigins, hasItem("http://foo.com")); + } + + // Send the follow-up request. + GreetingMessage payload = new GreetingMessage("Cheers"); + try (Http1ClientResponse response = client.put("/greet/greeting") + .accept(MediaTypes.APPLICATION_JSON) + .headers(headers -> { + headers.set(ORIGIN, "http://foo.com"); + headers.set(HOST, "here.com"); + preFlightHeaders.forEach(headers::add); + }).submit(payload.forRest())) { + + assertThat(response.status().code(), is(204)); + List allowOrigins = preFlightHeaders.values(ACCESS_CONTROL_ALLOW_ORIGIN); + assertThat("Expected CORS header " + CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN + " has no value(s)", + allowOrigins, not(empty())); + assertThat("Header " + CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN + + " should contain '*' but does not; " + allowOrigins, + allowOrigins, hasItem("http://foo.com")); + } + } + + @Order(12) // Run after CORS test changes greeting to Cheers. + @Test + void testNamedGreetWithCors() { + try (Http1ClientResponse response = client.get() + .path("/greet/Maria") + .headers(headers -> headers + .set(ORIGIN, "http://foo.com") + .set(HOST, "here.com")) + .request()) { + assertThat("HTTP response", response.status().code(), is(200)); + String payload = GreetingMessage.fromRest(response.entity().as(JsonObject.class)).getMessage(); + assertThat(payload, containsString("Cheers Maria")); + 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("*")); + } + } + + @Order(100) // After all other tests, so we can rely on deterministic greetings. + @Test + void testGreetingChangeWithCorsAndOtherOrigin() { + Http1ClientRequest request = client.put() + .path("/greet/greeting"); + request.headers(headers -> { + headers.set(ORIGIN, "http://other.com"); + headers.set(HOST, "here.com"); + }); + + GreetingMessage payload = new GreetingMessage("Ahoy"); + try (Http1ClientResponse response = request.submit(payload.forRest())) { + // Result depends on whether we are using overrides or not. + boolean isOverriding = Config.create().get("cors").exists(); + assertThat("HTTP response3", response.status().code(), is(isOverriding ? 204 : 403)); + } + } +} diff --git a/examples/dbclient/README.md b/examples/dbclient/README.md new file mode 100644 index 000000000..5345384f7 --- /dev/null +++ b/examples/dbclient/README.md @@ -0,0 +1,14 @@ +# Helidon DB Client Examples + +Each subdirectory contains example code that highlights specific aspects of +Helidon DB Client. + +build examples in all folders (including the common folder) by +```shell +mvn package +``` +in the current folder. + +--- + +Pokémon, and Pokémon character names are trademarks of Nintendo. diff --git a/examples/dbclient/common/README.md b/examples/dbclient/common/README.md new file mode 100644 index 000000000..875f95c92 --- /dev/null +++ b/examples/dbclient/common/README.md @@ -0,0 +1,22 @@ +# Common library for the DB Client examples + +shall be built using + +```shell + +mvn install + +``` + +to be accessible for the DB Client examples + +or make the whole pack of DB Client examples at the level above by + +```shell + +cd ../ +mvn package + +``` + +and get all DB Client examples compiled at once. \ No newline at end of file diff --git a/examples/dbclient/common/pom.xml b/examples/dbclient/common/pom.xml new file mode 100644 index 000000000..51a19915c --- /dev/null +++ b/examples/dbclient/common/pom.xml @@ -0,0 +1,54 @@ + + + + + 4.0.0 + + io.helidon.examples.dbclient + helidon-examples-dbclient-project + 1.0.0-SNAPSHOT + + + helidon-examples-dbclient-common + 1.0.0-SNAPSHOT + Helidon Examples DB Client Common + + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.http.media + helidon-http-media-jsonb + + + io.helidon.webserver + helidon-webserver + + + jakarta.json + jakarta.json-api + + + diff --git a/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/AbstractPokemonService.java b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/AbstractPokemonService.java new file mode 100644 index 000000000..ce10b3e90 --- /dev/null +++ b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/AbstractPokemonService.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.common; + +import io.helidon.common.parameters.Parameters; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbTransaction; +import io.helidon.http.NotFoundException; +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 abstract class AbstractPokemonService 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 + */ + protected AbstractPokemonService(DbClient dbClient) { + this.dbClient = dbClient; + } + + + @Override + public void routing(HttpRules rules) { + rules + .get("/", this::listPokemons) + // 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) + // update one (TODO this is intentionally wrong - should use JSON request, just to make it simple we use path) + .put("/{name}/type/{type}", this::updatePokemonType); + } + + /** + * The DB client associated with this service. + * + * @return DB client instance + */ + protected DbClient dbClient() { + return dbClient; + } + + /** + * This method is left unimplemented to show differences between native statements that can be used. + * + * @param req Server request + * @param res Server response + */ + protected abstract void deleteAllPokemons(ServerRequest req, ServerResponse res); + + /** + * 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.get("name"), params.get("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().get("name"); + res.send(dbClient.execute() + .namedGet("select-one", pokemonName) + .orElseThrow(() -> new NotFoundException("Pokemon " + pokemonName + " not found")) + .as(JsonObject.class)); + } + + /** + * Return JsonArray with all stored Pokémon. + * + * @param req the server request + * @param res the server response + */ + private void listPokemons(ServerRequest req, ServerResponse res) { + res.send(dbClient.execute() + .namedQuery("select-all") + .map(it -> it.as(JsonObject.class)) + .toList()); + } + + /** + * 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.get("name"); + String type = params.get("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().get("name"); + long count = dbClient.execute().namedDelete("delete", name); + res.send("Deleted: " + count + " values"); + } +} diff --git a/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/Pokemon.java b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/Pokemon.java new file mode 100644 index 000000000..fe2078826 --- /dev/null +++ b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/Pokemon.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.common; + +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/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapper.java b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapper.java new file mode 100644 index 000000000..a0a0e560d --- /dev/null +++ b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapper.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.common; + +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.get(String.class), type.get(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/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapperProvider.java b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapperProvider.java new file mode 100644 index 000000000..5f8784bb8 --- /dev/null +++ b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapperProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.common; + +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/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/package-info.java b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/package-info.java new file mode 100644 index 000000000..78d7f6680 --- /dev/null +++ b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Common classes shared by JDBC and MongoDB examples. + */ +package io.helidon.examples.dbclient.common; diff --git a/examples/dbclient/common/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider b/examples/dbclient/common/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider new file mode 100644 index 000000000..886f95d56 --- /dev/null +++ b/examples/dbclient/common/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +io.helidon.examples.dbclient.common.PokemonMapperProvider diff --git a/examples/dbclient/jdbc/README.md b/examples/dbclient/jdbc/README.md new file mode 100644 index 000000000..ddc40f52d --- /dev/null +++ b/examples/dbclient/jdbc/README.md @@ -0,0 +1,69 @@ +# Helidon DB Client JDBC Example + +This example shows how to run Helidon DB Client over JDBC. + +Examples are given for H2, Oracle, or MySQL databases (note that MySQL is currently not supported for GraalVM native image) + +Uncomment the appropriate dependencies in the pom.xml for the desired database (H2, Oracle, or MySQL) and insure others are commented. + +Uncomment the appropriate configuration in the application.xml for the desired database (H2, Oracle, or MySQL) and insure others are commented. + +## Build + +```shell +mvn package +``` + +This example may also be run as a GraalVM native image in which case can be built using the following: + +```shell +mvn package -Pnative-image +``` + + +## Run + +This example requires a database. + +Instructions for H2 can be found here: http://www.h2database.com/html/cheatSheet.html + +Instructions for Oracle can be found here: https://github.com/oracle/docker-images/tree/master/OracleDatabase/SingleInstance + +MySQL can be run as a docker container with the following command: +```shell +docker run --rm --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=pokemon -e MYSQL_USER=user -e MYSQL_PASSWORD=changeit mysql:5.7 +``` + + +Then run the application: + +```shell +java -jar target/helidon-examples-dbclient-jdbc.jar +``` +or in the case of native image +```shell +./target/helidon-examples-dbclient-jdbc +``` + +## Exercise + +The application has the following endpoints: + +- http://localhost:8079/db - the main business endpoint (see `curl` commands below) +- http://localhost:8079/metrics - the metrics endpoint (query adds application metrics) +- http://localhost:8079/health - has a custom database health check + +Application also connects to zipkin on default address. +The query operation adds database trace. + +`curl` commands: + +- `curl http://localhost:8079/db` - list all Pokemon in the database +- `curl -i -X PUT -d '{"name":"Squirtle","type":"water"}' http://localhost:8079/db` - add a new pokemon +- `curl http://localhost:8079/db/Squirtle` - get a single pokemon + +The application also supports update and delete - see `PokemonService.java` for bound endpoints. + +--- + +Pokémon, and Pokémon character names are trademarks of Nintendo. diff --git a/examples/dbclient/jdbc/pom.xml b/examples/dbclient/jdbc/pom.xml new file mode 100644 index 000000000..d59a3119b --- /dev/null +++ b/examples/dbclient/jdbc/pom.xml @@ -0,0 +1,211 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + helidon-examples-dbclient-jdbc + 1.0.0-SNAPSHOT + Helidon Examples DB Client JDBC + + + io.helidon.examples.dbclient.jdbc.JdbcExampleMain + + + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.tracing + helidon-tracing + + + io.helidon.tracing.providers + helidon-tracing-providers-zipkin + + + io.helidon.dbclient + helidon-dbclient-jdbc + + + io.helidon.dbclient + helidon-dbclient-tracing + + + io.helidon.dbclient + helidon-dbclient-metrics + + + io.helidon.dbclient + helidon-dbclient-metrics-hikari + + + io.helidon.dbclient + helidon-dbclient-health + + + io.helidon.dbclient + helidon-dbclient-jsonp + + + + + + io.helidon.integrations.db + ojdbc + + + + org.slf4j + slf4j-jdk14 + + + io.helidon.http.media + helidon-http-media-jsonb + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver.observe + helidon-webserver-observe-tracing + + + io.helidon.config + helidon-config-yaml + + + io.helidon.examples.dbclient + helidon-examples-dbclient-common + ${project.version} + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.health + helidon-health-checks + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mysql + test + + + org.testcontainers + oracle-xe + + + com.mysql + mysql-connector-j + test + + + io.helidon.integrations.db + h2 + test + + + io.helidon.webclient + helidon-webclient + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + false + + + + + + + + diff --git a/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/JdbcExampleMain.java b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/JdbcExampleMain.java new file mode 100644 index 000000000..1f9caabb0 --- /dev/null +++ b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/JdbcExampleMain.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.dbclient.jdbc; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.health.DbClientHealthCheck; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.health.HealthObserver; + +/** + * Simple Hello World rest application. + */ +public final class JdbcExampleMain { + + /** + * Cannot be instantiated. + */ + private JdbcExampleMain() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + // load logging configuration + LogConfig.configureRuntime(); + + // Prepare routing for the server + WebServer server = setupServer(WebServer.builder()); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/"); + } + + static WebServer setupServer(WebServerConfig.Builder builder) { + // By default, this will pick up application.yaml from the classpath + Config config = Config.global(); + + Config dbConfig = config.get("db"); + DbClient dbClient = DbClient.create(dbConfig); + + ObserveFeature observe = ObserveFeature.builder() + .config(config.get("server.features.observe")) + .addObserver(HealthObserver.builder() + .addCheck(DbClientHealthCheck.create(dbClient, dbConfig.get("health-check"))) + .build()) + .build(); + + return builder + .config(config.get("server")) + .addFeature(observe) + .routing(routing -> routing.register("/db", new PokemonService(dbClient))) + .build() + .start(); + } +} diff --git a/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/PokemonService.java b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/PokemonService.java new file mode 100644 index 000000000..fc75bf6d4 --- /dev/null +++ b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/PokemonService.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.dbclient.jdbc; + +import io.helidon.dbclient.DbClient; +import io.helidon.examples.dbclient.common.AbstractPokemonService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * Example service using a database. + */ +public class PokemonService extends AbstractPokemonService { + + PokemonService(DbClient dbClient) { + super(dbClient); + + // dirty hack to prepare database for our POC + // MySQL init + long count = dbClient().execute().namedDml("create-table"); + System.out.println(count); + } + + @Override + protected void deleteAllPokemons(ServerRequest req, ServerResponse res) { + // this is to show how ad-hoc statements can be executed (and their naming in Tracing and Metrics) + long count = dbClient().execute().createDelete("DELETE FROM pokemons").execute(); + res.send("Deleted: " + count + " values"); + } +} diff --git a/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/package-info.java b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/package-info.java new file mode 100644 index 000000000..f54ce8e11 --- /dev/null +++ b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quick start demo application. + */ +package io.helidon.examples.dbclient.jdbc; diff --git a/examples/dbclient/jdbc/src/main/resources/application.yaml b/examples/dbclient/jdbc/src/main/resources/application.yaml new file mode 100644 index 000000000..cb175d49d --- /dev/null +++ b/examples/dbclient/jdbc/src/main/resources/application.yaml @@ -0,0 +1,99 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8079 + host: 0.0.0.0 + +tracing: + service: jdbc-db + +db: + source: jdbc + connection: + # + # H2 configuration + # + # Embedded mode (does not work with native image) +# url: jdbc:h2:~/test + # Server mode, run: docker run --rm --name h2 -p 9092:9082 -p 8082:8082 nemerosa/h2 +# url: "jdbc:h2:tcp://localhost:9092/~test" +# username: sa +# password: +# poolName: h2 + # + # MySQL configuration + # + # docker run --rm --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root \ + # -e MYSQL_DATABASE=pokemon -e MYSQL_USER=user -e MYSQL_PASSWORD=changeit mysql:5.7 +# url: jdbc:mysql://127.0.0.1:3306/pokemon?useSSL=false +# username: user +# password: changeit +# poolName: mysql + # + # Oracle configuration + # + # docker run --rm --name xe -p 1521:1521 -p 8888:8080 -e ORACLE_PWD=oracle wnameless/oracle-xe-11g-r2 + url: jdbc:oracle:thin:@localhost:1521/XE + username: system + password: oracle + poolName: oracle + 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" + services: + tracing: + # would trace all statement names that start with select- + - statement-names: ["select-.*"] + # would trace all delete statements + - statement-types: ["DELETE"] + metrics: + - type: TIMER + errors: false + statement-names: ["select-.*"] + description: "Timer for successful selects" + - type: COUNTER + errors: false + statement-types: ["DELETE", "UPDATE", "INSERT", "DML"] + name-format: "db.counter.%s.success" + description: "Counter of successful DML statements" + - type: COUNTER + statement-types: ["DELETE", "UPDATE", "INSERT", "DML"] + success: false + name-format: "db.counter.%s.error" + description: "Counter of failed DML statements" + 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 = ?" diff --git a/examples/dbclient/jdbc/src/main/resources/logging.properties b/examples/dbclient/jdbc/src/main/resources/logging.properties new file mode 100644 index 000000000..550b8d9a4 --- /dev/null +++ b/examples/dbclient/jdbc/src/main/resources/logging.properties @@ -0,0 +1,35 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# Global default logging level. Can be overridden by specific handlers and loggers +.level=INFO + +# Helidon Web Server has a custom log formatter that extends SimpleFormatter. +# It replaces "!thread!" with the current thread name +io.helidon.logging.jul.HelidonConsoleHandler.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/AbstractPokemonServiceTest.java b/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/AbstractPokemonServiceTest.java new file mode 100644 index 000000000..384e7daa1 --- /dev/null +++ b/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/AbstractPokemonServiceTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.dbclient.jdbc; + +import java.util.List; +import java.util.Map; + +import io.helidon.http.Status; +import io.helidon.http.media.jsonp.JsonpSupport; +import io.helidon.webclient.api.ClientResponseTyped; +import io.helidon.webclient.api.WebClient; +import io.helidon.webserver.WebServer; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +abstract class AbstractPokemonServiceTest { + private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of()); + + private static WebServer server; + private static WebClient client; + + static void beforeAll() { + server = JdbcExampleMain.setupServer(WebServer.builder()); + client = WebClient.create(config -> config.baseUri("http://localhost:" + server.port()) + .addMediaSupport(JsonpSupport.create())); + } + + static void afterAll() { + if (server != null && server.isRunning()) { + server.stop(); + } + } + + @Test + void testListAndDeleteAllPokemons() { + List names = listAllPokemons(); + assertThat(names.isEmpty(), is(true)); + + String endpoint = String.format("/db/%s/type/%s", "Raticate", 1); + ClientResponseTyped response = client.post(endpoint).request(String.class); + assertThat(response.status(), is(Status.OK_200)); + + names = listAllPokemons(); + assertThat(names.size(), is(1)); + assertThat(names.getFirst(), is("Raticate")); + + response = client.delete("/db").request(String.class); + assertThat(response.status(), is(Status.OK_200)); + + names = listAllPokemons(); + assertThat(names.isEmpty(), is(true)); + } + + @Test + void testAddUpdateDeletePokemon() { + ClientResponseTyped response; + ClientResponseTyped jsonResponse; + JsonObject pokemon = JSON_FACTORY.createObjectBuilder() + .add("type", 1) + .add("name", "Raticate") + .build(); + + // Add new pokemon + response = client.put("/db").submit(pokemon, String.class); + assertThat(response.entity(), is("Inserted: 1 values")); + + // Get the new pokemon added + jsonResponse = client.get("/db/Raticate").request(JsonObject.class); + assertThat(jsonResponse.status(), is(Status.OK_200)); + assertThat(getName(jsonResponse.entity()), is("Raticate")); + assertThat(getType(jsonResponse.entity()), is("1")); + + // Update pokemon + response = client.put("/db/Raticate/type/2").request(String.class); + assertThat(response.status(), is(Status.OK_200)); + + // Verify updated pokemon + jsonResponse = client.get("/db/Raticate").request(JsonObject.class); + assertThat(jsonResponse.status(), is(Status.OK_200)); + assertThat(getName(jsonResponse.entity()), is("Raticate")); + assertThat(getType(jsonResponse.entity()), is("2")); + + // Delete Pokemon + response = client.delete("/db/Raticate").request(String.class); + assertThat(response.status(), is(Status.OK_200)); + + // Verify pokemon is correctly deleted + response = client.get("/db/Raticate").request(String.class); + assertThat(response.status(), is(Status.NOT_FOUND_404)); + } + + private List listAllPokemons() { + ClientResponseTyped response = client.get("/db").request(JsonArray.class); + assertThat(response.status(), is(Status.OK_200)); + return response.entity().stream().map(e -> getName(e.asJsonObject())).toList(); + } + + private String getName(JsonObject json) { + return json.containsKey("name") + ? json.getString("name") + : json.getString("NAME"); + } + + private String getType(JsonObject json) { + return json.containsKey("type") + ? json.getString("type") + : json.getString("TYPE"); + } +} diff --git a/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/PokemonServiceH2IT.java b/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/PokemonServiceH2IT.java new file mode 100644 index 000000000..7f5c8dee1 --- /dev/null +++ b/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/PokemonServiceH2IT.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.dbclient.jdbc; + +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static io.helidon.config.ConfigSources.classpath; + +@Testcontainers(disabledWithoutDocker = true) +class PokemonServiceH2IT extends AbstractPokemonServiceTest { + private static final DockerImageName H2_IMAGE = DockerImageName.parse("nemerosa/h2"); + + @Container + static GenericContainer container = new GenericContainer<>(H2_IMAGE) + .withExposedPorts(9082) + .waitingFor(Wait.forLogMessage("(.*)Web Console server running at(.*)", 1)); + + @BeforeAll + static void start() { + String url = String.format("jdbc:h2:tcp://localhost:%s/~./test", container.getMappedPort(9082)); + Config.global(Config.builder() + .addSource(ConfigSources.create(Map.of("db.connection.url", url))) + .addSource(classpath("application-h2-test.yaml")) + .build()); + beforeAll(); + } + + @AfterAll + static void stop() { + afterAll(); + } +} diff --git a/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/PokemonServiceMySQLIT.java b/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/PokemonServiceMySQLIT.java new file mode 100644 index 000000000..eee51c64b --- /dev/null +++ b/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/PokemonServiceMySQLIT.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.dbclient.jdbc; + +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static io.helidon.config.ConfigSources.classpath; + +@Testcontainers(disabledWithoutDocker = true) +class PokemonServiceMySQLIT extends AbstractPokemonServiceTest { + + @Container + static MySQLContainer container = new MySQLContainer<>("mysql:8.0.36") + .withUsername("user") + .withPassword("changeit") + .withNetworkAliases("mysql") + .withDatabaseName("pokemon"); + + @BeforeAll + static void start() { + Config.global(Config.builder() + .addSource(ConfigSources.create(Map.of("db.connection.url", container.getJdbcUrl()))) + .addSource(classpath("application-mysql-test.yaml")) + .build()); + beforeAll(); + } + + @AfterAll + static void stop() { + afterAll(); + } + +} diff --git a/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/PokemonServiceOracleIT.java b/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/PokemonServiceOracleIT.java new file mode 100644 index 000000000..a37525049 --- /dev/null +++ b/examples/dbclient/jdbc/src/test/java/io/helidon/examples/dbclient/jdbc/PokemonServiceOracleIT.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.jdbc; + +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.OracleContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static io.helidon.config.ConfigSources.classpath; + +@Testcontainers(disabledWithoutDocker = true) +public class PokemonServiceOracleIT extends AbstractPokemonServiceTest { + + private static final DockerImageName image = DockerImageName.parse("wnameless/oracle-xe-11g-r2") + .asCompatibleSubstituteFor("gvenzl/oracle-xe"); + + @Container + static OracleContainer container = new OracleContainer(image) + .withExposedPorts(1521, 8080) + .withDatabaseName("XE") + .usingSid() + .waitingFor(Wait.forListeningPorts(1521, 8080)); + + @BeforeAll + static void start() { + Config.global(Config.builder() + .addSource(ConfigSources.create(Map.of("db.connection.url", container.getJdbcUrl()))) + .addSource(classpath("application-oracle-test.yaml")) + .build()); + beforeAll(); + } + + @AfterAll + static void stop() { + afterAll(); + } +} diff --git a/examples/dbclient/jdbc/src/test/resources/application-h2-test.yaml b/examples/dbclient/jdbc/src/test/resources/application-h2-test.yaml new file mode 100644 index 000000000..f75ba554c --- /dev/null +++ b/examples/dbclient/jdbc/src/test/resources/application-h2-test.yaml @@ -0,0 +1,72 @@ +# +# Copyright (c) 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8079 + host: 0.0.0.0 + +tracing: + service: jdbc-db + +db: + source: jdbc + connection: + 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" + services: + tracing: + # would trace all statement names that start with select- + - statement-names: ["select-.*"] + # would trace all delete statements + - statement-types: ["DELETE"] + metrics: + - type: TIMER + errors: false + statement-names: ["select-.*"] + description: "Timer for successful selects" + - type: COUNTER + errors: false + statement-types: ["DELETE", "UPDATE", "INSERT", "DML"] + name-format: "db.counter.%s.success" + description: "Counter of successful DML statements" + - type: COUNTER + statement-types: ["DELETE", "UPDATE", "INSERT", "DML"] + success: false + name-format: "db.counter.%s.error" + description: "Counter of failed DML statements" + statements: + health-check: "SELECT 0" + # 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 = ?" diff --git a/examples/dbclient/jdbc/src/test/resources/application-mysql-test.yaml b/examples/dbclient/jdbc/src/test/resources/application-mysql-test.yaml new file mode 100644 index 000000000..8bf828879 --- /dev/null +++ b/examples/dbclient/jdbc/src/test/resources/application-mysql-test.yaml @@ -0,0 +1,72 @@ +# +# Copyright (c) 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8079 + host: 0.0.0.0 + +tracing: + service: jdbc-db + +db: + source: jdbc + connection: + username: user + password: changeit + poolName: mysql + 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" + services: + tracing: + # would trace all statement names that start with select- + - statement-names: ["select-.*"] + # would trace all delete statements + - statement-types: ["DELETE"] + metrics: + - type: TIMER + errors: false + statement-names: ["select-.*"] + description: "Timer for successful selects" + - type: COUNTER + errors: false + statement-types: ["DELETE", "UPDATE", "INSERT", "DML"] + name-format: "db.counter.%s.success" + description: "Counter of successful DML statements" + - type: COUNTER + statement-types: ["DELETE", "UPDATE", "INSERT", "DML"] + success: false + name-format: "db.counter.%s.error" + description: "Counter of failed DML statements" + statements: + health-check: "SELECT 0" + # 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 = ?" diff --git a/examples/dbclient/jdbc/src/test/resources/application-oracle-test.yaml b/examples/dbclient/jdbc/src/test/resources/application-oracle-test.yaml new file mode 100644 index 000000000..dd713c055 --- /dev/null +++ b/examples/dbclient/jdbc/src/test/resources/application-oracle-test.yaml @@ -0,0 +1,74 @@ +# +# Copyright (c) 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8079 + host: 0.0.0.0 + +tracing: + service: jdbc-db + +db: + source: jdbc + connection: + username: "system" + password: "oracle" + 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" + services: + tracing: + # would trace all statement names that start with select- + - statement-names: ["select-.*"] + # would trace all delete statements + - statement-types: ["DELETE"] + metrics: + - type: TIMER + errors: false + statement-names: ["select-.*"] + description: "Timer for successful selects" + - type: COUNTER + errors: false + statement-types: ["DELETE", "UPDATE", "INSERT", "DML"] + name-format: "db.counter.%s.success" + description: "Counter of successful DML statements" + - type: COUNTER + statement-types: ["DELETE", "UPDATE", "INSERT", "DML"] + success: false + name-format: "db.counter.%s.error" + description: "Counter of failed DML statements" + 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 = ?" diff --git a/examples/dbclient/mongodb/README.md b/examples/dbclient/mongodb/README.md new file mode 100644 index 000000000..45bebc5ec --- /dev/null +++ b/examples/dbclient/mongodb/README.md @@ -0,0 +1,50 @@ +# Helidon DB Client mongoDB Example + +This example shows how to run Helidon DB over mongoDB. + + +## Build + +```shell +mvn package +``` + +## Run + +This example requires a mongoDB database, start it using docker: + +```shell +docker run --rm --name mongo -p 27017:27017 mongo +``` + +Then run the application: + +```shell +java -jar target/helidon-examples-dbclient-mongodb.jar +``` + + +## Exercise + +The application has the following endpoints: + +- http://localhost:8079/db - the main business endpoint (see `curl` commands below) +- http://localhost:8079/metrics - the metrics endpoint (query adds application metrics) +- http://localhost:8079/health - has a custom database health check + +Application also connects to zipkin on default address. +The query operation adds database trace. + +`curl` commands: + +- `curl http://localhost:8079/db` - list all Pokemon in the database +- `curl -i -X PUT -H 'Content-type: application/json' -d '{"name":"Squirtle","type":"water"}' http://localhost:8079/db` - add a new pokemon +- `curl http://localhost:8079/db/Squirtle` - get a single pokemon +- `curl -i -X DELETE http://localhost:8079/db/Squirtle` - delete a single pokemon +- `curl -i -X DELETE http://localhost:8079/db` - delete all pokemon + +The application also supports update and delete - see `PokemonService.java` for bound endpoints. + +--- + +Pokémon, and Pokémon character names are trademarks of Nintendo. diff --git a/examples/dbclient/mongodb/pom.xml b/examples/dbclient/mongodb/pom.xml new file mode 100644 index 000000000..0701e6270 --- /dev/null +++ b/examples/dbclient/mongodb/pom.xml @@ -0,0 +1,155 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + helidon-examples-dbclient-mongodb + 1.0.0-SNAPSHOT + Helidon Examples DB Client MongoDB + + + io.helidon.examples.dbclient.mongo.MongoDbExampleMain + + + + + io.helidon.common + helidon-common + + + io.helidon.common + helidon-common-mapper + + + io.helidon.dbclient + helidon-dbclient-mongodb + + + io.helidon.dbclient + helidon-dbclient-tracing + + + io.helidon.dbclient + helidon-dbclient-metrics + + + io.helidon.dbclient + helidon-dbclient-health + + + io.helidon.dbclient + helidon-dbclient-jsonp + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver.observe + helidon-webserver-observe-tracing + + + io.helidon.metrics + helidon-metrics-api + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.tracing + helidon-tracing + + + io.helidon.tracing.providers + helidon-tracing-providers-zipkin + + + io.helidon.config + helidon-config-yaml + + + io.helidon.examples.dbclient + helidon-examples-dbclient-common + ${project.version} + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mongodb + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + diff --git a/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/MongoDbExampleMain.java b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/MongoDbExampleMain.java new file mode 100644 index 000000000..de853eb9c --- /dev/null +++ b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/MongoDbExampleMain.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.dbclient.mongo; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.metrics.DbClientMetrics; +import io.helidon.dbclient.tracing.DbClientTracing; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * Simple Hello World rest application. + */ +public final class MongoDbExampleMain { + + /** + * Cannot be instantiated. + */ + private MongoDbExampleMain() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + startServer(); + } + + /** + * Start the server. + * + * @return the created {@link WebServer} instance + */ + static WebServer startServer() { + + // load logging configuration + LogConfig.configureRuntime(); + + WebServer server = setupServer(WebServer.builder()); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/"); + return server; + } + + static WebServer setupServer(WebServerConfig.Builder builder) { + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + + return builder.routing(routing -> routing(routing, config)) + .config(config.get("server")) + .build() + .start(); + } + + /** + * Setup routing. + * + * @param config configuration of this server + */ + private static void routing(HttpRouting.Builder routing, Config config) { + Config dbConfig = config.get("db"); + + DbClient dbClient = DbClient.builder(dbConfig) + // add an interceptor to named statement(s) + .addService(DbClientMetrics.counter().statementNames("select-all", "select-one")) + // add an interceptor to statement type(s) + .addService(DbClientMetrics.timer() + .statementTypes(DbStatementType.DELETE, DbStatementType.UPDATE, DbStatementType.INSERT)) + // add an interceptor to all statements + .addService(DbClientTracing.create()) + .build(); + + routing.register("/db", new PokemonService(dbClient)); + } +} diff --git a/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/PokemonService.java b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/PokemonService.java new file mode 100644 index 000000000..be1c7b251 --- /dev/null +++ b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/PokemonService.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.dbclient.mongo; + +import io.helidon.dbclient.DbClient; +import io.helidon.examples.dbclient.common.AbstractPokemonService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * A simple service to greet you. Examples: + *

+ * Get default greeting message: + * curl -X GET {@code http://localhost:8080/greet} + *

+ * Get greeting message for Joe: + * curl -X GET {@code http://localhost:8080/greet/Joe} + *

+ * Change greeting + * curl -X PUT {@code http://localhost:8080/greet/greeting/Hola} + *

+ * The message is returned as a JSON object + */ + +public class PokemonService extends AbstractPokemonService { + + PokemonService(DbClient dbClient) { + super(dbClient); + } + + @Override + protected void deleteAllPokemons(ServerRequest req, ServerResponse res) { + long count = dbClient().execute().createNamedDelete("delete-all") + .execute(); + res.send("Deleted: " + count + " values"); + } +} diff --git a/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/package-info.java b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/package-info.java new file mode 100644 index 000000000..699f9ba3e --- /dev/null +++ b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quick start demo application. + */ +package io.helidon.examples.dbclient.mongo; diff --git a/examples/dbclient/mongodb/src/main/resources/application.yaml b/examples/dbclient/mongodb/src/main/resources/application.yaml new file mode 100644 index 000000000..d0cd67c62 --- /dev/null +++ b/examples/dbclient/mongodb/src/main/resources/application.yaml @@ -0,0 +1,76 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8079 + host: 0.0.0.0 + +tracing: + service: "mongo-db" + +# docker run --rm --name mongo -p 27017:27017 mongo +db: + source: "mongoDb" + connection: + url: "mongodb://127.0.0.1:27017/pokemon" + health-check: + type: "query" + statementName: "health-check" + statements: + # Health check statement. HealthCheck statement type must be query. + health-check: '{ + "operation": "command", + "query": { ping: 1 } + }' + # Insert operation contains collection name, operation type and data to be inserted. + # Name variable is stored as MongoDB primary key attribute _id + insert2: '{ + "collection": "pokemons", + "value": { + "_id": $name, + "type": $type + } + }' + select-all: '{ + "collection": "pokemons", + "query": {} + }' + select-one: '{ + "collection": "pokemons", + "query": { + "_id": ? + } + }' + delete-all: '{ + "collection": "pokemons", + "operation": "delete" + }' + update: '{ + "collection": "pokemons", + "query": { + "_id": $name + }, + "value": { + $set: { "type": $type } + } + }' + delete: '{ + "collection": "pokemons", + "query": { + "_id": ? + } + }' + diff --git a/examples/dbclient/mongodb/src/main/resources/logging.properties b/examples/dbclient/mongodb/src/main/resources/logging.properties new file mode 100644 index 000000000..550b8d9a4 --- /dev/null +++ b/examples/dbclient/mongodb/src/main/resources/logging.properties @@ -0,0 +1,35 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# Global default logging level. Can be overridden by specific handlers and loggers +.level=INFO + +# Helidon Web Server has a custom log formatter that extends SimpleFormatter. +# It replaces "!thread!" with the current thread name +io.helidon.logging.jul.HelidonConsoleHandler.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/dbclient/mongodb/src/test/java/io/helidon/examples/dbclient/mongo/MainIT.java b/examples/dbclient/mongodb/src/test/java/io/helidon/examples/dbclient/mongo/MainIT.java new file mode 100644 index 000000000..18b89b947 --- /dev/null +++ b/examples/dbclient/mongodb/src/test/java/io/helidon/examples/dbclient/mongo/MainIT.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.dbclient.mongo; + +import java.util.List; +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.http.media.jsonb.JsonbSupport; +import io.helidon.http.media.jsonp.JsonpSupport; +import io.helidon.webclient.api.ClientResponseTyped; +import io.helidon.webclient.api.WebClient; +import io.helidon.webserver.WebServer; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@Testcontainers(disabledWithoutDocker = true) +public class MainIT { + + @Container + static final MongoDBContainer container = new MongoDBContainer("mongo") + .withExposedPorts(27017); + + private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of()); + private static final String CONNECTION_URL_KEY = "db.connection.url"; + + private static WebServer server; + private static WebClient client; + + @BeforeAll + static void beforeAll() { + String url = String.format("mongodb://127.0.0.1:%s/pokemon", container.getMappedPort(27017)); + System.setProperty(CONNECTION_URL_KEY, url); + server = MongoDbExampleMain.setupServer(WebServer.builder()); + client = WebClient.create(config -> config.baseUri("http://localhost:" + server.port()) + .addMediaSupport(JsonbSupport.create(Config.create())) + .addMediaSupport(JsonpSupport.create())); + } + + @AfterAll + static void afterAll() { + if (server != null && server.isRunning()) { + server.stop(); + } + System.clearProperty(CONNECTION_URL_KEY); + } + + @Test + void testListAndDeleteAllPokemons() { + List names = listAllPokemons(); + assertThat(names.isEmpty(), is(true)); + + String endpoint = String.format("/db/%s/type/%s", "Raticate", 1); + ClientResponseTyped response = client.post(endpoint).request(String.class); + assertThat(response.status(), is(Status.OK_200)); + + names = listAllPokemons(); + assertThat(names.size(), is(1)); + assertThat(names.getFirst(), is("Raticate")); + + response = client.delete("/db").request(String.class); + assertThat(response.status(), is(Status.OK_200)); + + names = listAllPokemons(); + assertThat(names.isEmpty(), is(true)); + } + + @Test + void testAddUpdateDeletePokemon() { + ClientResponseTyped response; + ClientResponseTyped jsonResponse; + JsonObject pokemon = JSON_FACTORY.createObjectBuilder() + .add("type", 1) + .add("name", "Raticate") + .build(); + + // Add new pokemon + response = client.put("/db").submit(pokemon, String.class); + assertThat(response.entity(), is("Inserted: 1 values")); + + // Get the new pokemon added + jsonResponse = client.get("/db/Raticate").request(JsonObject.class); + assertThat(jsonResponse.status(), is(Status.OK_200)); + assertThat(jsonResponse.entity().getString("_id"), is("Raticate")); + assertThat(jsonResponse.entity().getString("type"), is("1")); + + // Update pokemon + response = client.put("/db/Raticate/type/2").request(String.class); + assertThat(response.status(), is(Status.OK_200)); + + // Verify updated pokemon + jsonResponse = client.get("/db/Raticate").request(JsonObject.class); + assertThat(jsonResponse.status(), is(Status.OK_200)); + assertThat(jsonResponse.entity().getString("_id"), is("Raticate")); + assertThat(jsonResponse.entity().getString("type"), is("2")); + + // Delete Pokemon + response = client.delete("/db/Raticate").request(String.class); + assertThat(response.status(), is(Status.OK_200)); + + // Verify pokemon is correctly deleted + response = client.get("/db/Raticate").request(String.class); + assertThat(response.status(), is(Status.NOT_FOUND_404)); + } + + private List listAllPokemons() { + ClientResponseTyped response = client.get("/db").request(JsonArray.class); + assertThat(response.status(), is(Status.OK_200)); + return response.entity().stream().map(e -> e.asJsonObject().getString("_id")).toList(); + } +} diff --git a/examples/dbclient/pokemons/README.md b/examples/dbclient/pokemons/README.md new file mode 100644 index 000000000..8d76e9913 --- /dev/null +++ b/examples/dbclient/pokemons/README.md @@ -0,0 +1,141 @@ +# Helidon DB Client Pokémon Example with JDBC + +This example shows how to run Helidon DB Client over JDBC. + +Application provides REST service endpoint with CRUD operations on Pokémons +database. + +## Database + +Database model contains two tables: + +**Types** + +| Column | Type | Integrity | +|--------|---------|-------------| +| id | integer | Primary key | +| name | varchar |   | + +**Pokemons** + +| Column | Type | Integrity | +|---------|---------|-------------| +| id | integer | Primary key | +| name | varchar |   | +| id_type | integer | Type(id) | + +with 1:N relationship between *Types* and *Pokémons* + +Examples are given for H2, Oracle, or MySQL databases (note that MySQL is currently not supported for GraalVM native image) + +To switch between JDBC drivers: + +- Uncomment the appropriate dependency in `pom.xml` +- Uncomment the configuration section in `application.yaml` and comment out the current one + +## Build + +To build a jar file +```shell +mvn package +``` + +To build a native image (supported only with Oracle, MongoDB, or H2 databases) +```shell +mvn package -Pnative-image +``` + +## Database +This example can run with any JDBC supported database. +In the `pom.xml` and `application.yaml` we provide configuration needed for Oracle database, MySQL and H2 database. +Start your database before running this example. + +Example docker commands to start databases in temporary containers: + +Oracle: +```shell +docker run --rm --name xe -p 1521:1521 -p 8888:8080 wnameless/oracle-xe-11g-r2 +``` +For details on an Oracle Docker image, see https://github.com/oracle/docker-images/tree/master/OracleDatabase/SingleInstance + +H2: +```shell +docker run --rm --name h2 -p 9092:9082 -p 8082:8082 nemerosa/h2 +``` +For details, see http://www.h2database.com/html/cheatSheet.html + +MySQL: +```shell +docker run --rm --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root \ + -e MYSQL_DATABASE=pokemon -e MYSQL_USER=user -e MYSQL_PASSWORD=changeit mysql:5.7 +``` + + +## Run + +Then run the `io.helidon.examples.dbclient.pokemons.Main` class: +```shell +java -jar target/helidon-examples-dbclient-pokemons.jar +``` + +Or run the native image: +```shell +./target/helidon-examples-dbclient-pokemons +``` + +### Run with MongoDB + +It's possible to run example with MongoDB database. Start it using docker: +```shell +docker run --rm --name mongo -p 27017:27017 mongo +``` + +Then run the `io.helidon.examples.dbclient.pokemons.Main` class with `mongo` argument: +```shell +java -jar target/helidon-examples-dbclient-pokemons.jar mongo +``` + +## Test Example + +The application has the following endpoints: + +- http://localhost:8080/db - the main business endpoint (see `curl` commands below) +- http://localhost:8080/metrics - the metrics endpoint (query adds application metrics) +- http://localhost:8080/health - has a custom database health check + +Application also connects to zipkin on default address. +The query operation adds database trace. + +```shell +# List all Pokémon +curl http://localhost:8080/db/pokemon + +# List all Pokémon types +curl http://localhost:8080/db/type + +# Get a single Pokémon by id +curl http://localhost:8080/db/pokemon/2 + +# Get a single Pokémon by name +curl http://localhost:8080/db/pokemon/name/Squirtle + +# Add a new Pokémon Rattata +curl -i -X POST -H 'Content-type: application/json' -d '{"id":7,"name":"Rattata","idType":1}' http://localhost:8080/db/pokemon + +# Rename Pokémon with id 7 to Raticate +curl -i -X PUT -H 'Content-type: application/json' -d '{"id":7,"name":"Raticate","idType":2}' http://localhost:8080/db/pokemon + +# Delete Pokémon with id 7 +curl -i -X DELETE http://localhost:8080/db/pokemon/7 +``` + +### Proxy + +Make sure that `localhost` is not being accessed trough proxy when proxy is configured on your system: +```shell +export NO_PROXY='localhost' +``` + +--- + +Pokémon, and Pokémon character names are trademarks of Nintendo. diff --git a/examples/dbclient/pokemons/pom.xml b/examples/dbclient/pokemons/pom.xml new file mode 100644 index 000000000..0de324ad1 --- /dev/null +++ b/examples/dbclient/pokemons/pom.xml @@ -0,0 +1,209 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + helidon-examples-dbclient-pokemons + 1.0.0-SNAPSHOT + Helidon Examples DB Client: Pokemons Database + + + io.helidon.examples.dbclient.pokemons.Main + + + + + io.helidon.tracing + helidon-tracing + + + io.helidon.tracing.providers + helidon-tracing-providers-zipkin + + + io.helidon.dbclient + helidon-dbclient-jdbc + + + io.helidon.dbclient + helidon-dbclient-mongodb + + + io.helidon.dbclient + helidon-dbclient-tracing + + + io.helidon.dbclient + helidon-dbclient-metrics + + + io.helidon.dbclient + helidon-dbclient-metrics-hikari + + + io.helidon.dbclient + helidon-dbclient-health + + + io.helidon.dbclient + helidon-dbclient-jsonp + + + io.helidon.integrations.db + ojdbc + + + io.helidon.integrations.db + h2 + + + org.slf4j + slf4j-jdk14 + + + io.helidon.config + helidon-config-yaml + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.health + helidon-health-checks + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.http.media + helidon-http-media-jsonb + + + org.mongodb + mongodb-driver-sync + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mongodb + test + + + org.testcontainers + mysql + test + + + org.testcontainers + oracle-xe + test + + + io.helidon.webclient + helidon-webclient + test + + + com.mysql + mysql-connector-j + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + false + + + + + + + + diff --git a/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/Main.java b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/Main.java new file mode 100644 index 000000000..d3e83ca89 --- /dev/null +++ b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/Main.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.dbclient.pokemons; + +import io.helidon.common.context.Contexts; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.health.DbClientHealthCheck; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.health.HealthObserver; + +/** + * Simple Hello World rest application. + */ +public final class Main { + + /** + * MongoDB configuration. Default configuration file {@code application.yaml} contains JDBC configuration. + */ + private static final String MONGO_CFG = "mongo.yaml"; + + /** + * Whether MongoDB support is selected. + */ + private static boolean mongo; + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args Command line arguments. Run with MongoDB support when 1st argument is mongo, run with JDBC support otherwise. + */ + public static void main(final String[] args) { + if (args != null && args.length > 0 && args[0] != null && "mongo".equalsIgnoreCase(args[0])) { + System.out.println("MongoDB database selected"); + mongo = true; + } else { + System.out.println("JDBC database selected"); + mongo = false; + } + startServer(); + } + + private static void startServer() { + + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + Config config = mongo ? Config.create(ConfigSources.classpath(MONGO_CFG)) : Config.create(); + Config.global(config); + + WebServer server = setupServer(WebServer.builder()); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/"); + } + + static WebServer setupServer(WebServerConfig.Builder builder) { + + Config config = Config.global(); + // Client services are added through a service loader - see mongoDB example for explicit services + DbClient dbClient = DbClient.create(config.get("db")); + Contexts.globalContext().register(dbClient); + + ObserveFeature observe = ObserveFeature.builder() + .config(config.get("server.features.observe")) + .addObserver(HealthObserver.builder() + .addCheck(DbClientHealthCheck.create(dbClient, config.get("db.health-check"))) + .build()) + .build(); + return builder.config(config.get("server")) + .addFeature(observe) + .routing(Main::routing) + .build() + .start(); + } + + /** + * Updates HTTP Routing. + */ + static void routing(HttpRouting.Builder routing) { + routing.register("/db", new PokemonService()); + } +} diff --git a/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/Pokemon.java b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/Pokemon.java new file mode 100644 index 000000000..691dda574 --- /dev/null +++ b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/Pokemon.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.pokemons; + +/** + * POJO representing Pokémon. + * + * @param id id of the beast + * @param name name of the beast + * @param idType id of the beast type + */ +public record Pokemon(int id, String name, int idType) { +} diff --git a/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonMapper.java b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonMapper.java new file mode 100644 index 000000000..1fc9fa8c4 --- /dev/null +++ b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonMapper.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.pokemons; + +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.pokemons.Pokemon} class. + */ +public class PokemonMapper implements DbMapper { + + @Override + public Pokemon read(DbRow row) { + DbColumn id = row.column("id"); + DbColumn name = row.column("name"); + DbColumn type = row.column("idType"); + return new Pokemon(id.get(Integer.class), name.get(String.class), type.get(Integer.class)); + } + + @Override + public Map toNamedParameters(Pokemon value) { + Map map = new HashMap<>(3); + map.put("id", value.id()); + map.put("name", value.name()); + map.put("idType", value.idType()); + return map; + } + + @Override + public List toIndexedParameters(Pokemon value) { + List list = new ArrayList<>(3); + list.add(value.id()); + list.add(value.name()); + list.add(value.idType()); + return list; + } +} diff --git a/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonMapperProvider.java b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonMapperProvider.java new file mode 100644 index 000000000..12d2a837f --- /dev/null +++ b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonMapperProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.pokemons; + +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/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonService.java b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonService.java new file mode 100644 index 000000000..7454b1eaf --- /dev/null +++ b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/PokemonService.java @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.dbclient.pokemons; + +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.util.Map; + +import io.helidon.common.context.Contexts; +import io.helidon.common.media.type.MediaTypes; +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbExecute; +import io.helidon.dbclient.DbTransaction; +import io.helidon.http.BadRequestException; +import io.helidon.http.NotFoundException; +import io.helidon.http.Status; +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.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import jakarta.json.JsonValue; + +/** + * An {@link HttpService} that uses {@link DbClient}. + */ +public class PokemonService implements HttpService { + + private static final Logger LOGGER = System.getLogger(PokemonService.class.getName()); + private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of()); + + private final DbClient dbClient; + private final boolean initSchema; + private final boolean initData; + + /** + * Create a new Pokémon service with a DB client. + */ + PokemonService() { + Config config = Config.global().get("db"); + this.dbClient = Contexts.globalContext() + .get(DbClient.class) + .orElseGet(() -> DbClient.create(config)); + + initSchema = config.get("init-schema").asBoolean().orElse(true); + initData = config.get("init-data").asBoolean().orElse(true); + init(); + } + + private void init() { + if (initSchema) { + initSchema(); + } + if (initData) { + initData(); + } + } + + private void initSchema() { + DbExecute exec = dbClient.execute(); + try { + exec.namedDml("create-types"); + exec.namedDml("create-pokemons"); + } catch (Exception ex1) { + LOGGER.log(Level.WARNING, "Could not create tables", ex1); + try { + deleteData(); + } catch (Exception ex2) { + LOGGER.log(Level.WARNING, "Could not delete tables", ex2); + } + } + } + + private void initData() { + DbTransaction tx = dbClient.transaction(); + try { + initTypes(tx); + initPokemons(tx); + tx.commit(); + } catch (Throwable t) { + tx.rollback(); + throw t; + } + } + + private static void initTypes(DbExecute exec) { + try (JsonReader reader = Json.createReader(PokemonService.class.getResourceAsStream("/pokemon-types.json"))) { + JsonArray types = reader.readArray(); + for (JsonValue typeValue : types) { + JsonObject type = typeValue.asJsonObject(); + exec.namedInsert("insert-type", + type.getInt("id"), + type.getString("name")); + } + } + } + + private static void initPokemons(DbExecute exec) { + try (JsonReader reader = Json.createReader(PokemonService.class.getResourceAsStream("/pokemons.json"))) { + JsonArray pokemons = reader.readArray(); + for (JsonValue pokemonValue : pokemons) { + JsonObject pokemon = pokemonValue.asJsonObject(); + exec.namedInsert("insert-pokemon", + pokemon.getInt("id"), + pokemon.getString("name"), + pokemon.getInt("idType")); + } + } + } + + private void deleteData() { + DbTransaction tx = dbClient.transaction(); + try { + tx.namedDelete("delete-all-pokemons"); + tx.namedDelete("delete-all-types"); + tx.commit(); + } catch (Throwable t) { + tx.rollback(); + throw t; + } + } + + @Override + public void routing(HttpRules rules) { + rules.get("/", this::index) + // List all types + .get("/type", this::listTypes) + // List all Pokémon + .get("/pokemon", this::listPokemons) + // Get Pokémon by name + .get("/pokemon/name/{name}", this::getPokemonByName) + // Get Pokémon by ID + .get("/pokemon/{id}", this::getPokemonById) + // Create new Pokémon + .post("/pokemon", Handler.create(Pokemon.class, this::insertPokemon)) + // Update name of existing Pokémon + .put("/pokemon", Handler.create(Pokemon.class, this::updatePokemon)) + // Delete Pokémon by ID including type relation + .delete("/pokemon/{id}", this::deletePokemonById); + } + + /** + * Return index page. + * + * @param request the server request + * @param response the server response + */ + private void index(ServerRequest request, ServerResponse response) { + response.headers().contentType(MediaTypes.TEXT_PLAIN); + response.send(""" + Pokemon JDBC Example: + GET /type - List all pokemon types + GET /pokemon - List all pokemons + GET /pokemon/{id} - Get pokemon by id + GET /pokemon/name/{name} - Get pokemon by name + POST /pokemon - Insert new pokemon: + {"id":,"name":,"type":} + PUT /pokemon - Update pokemon + {"id":,"name":,"type":} + DELETE /pokemon/{id} - Delete pokemon with specified id + """); + } + + /** + * Return JsonArray with all stored Pokémon. + * Pokémon object contains list of all type names. + * This method is abstract because implementation is DB dependent. + * + * @param request the server request + * @param response the server response + */ + private void listTypes(ServerRequest request, ServerResponse response) { + JsonArray jsonArray = dbClient.execute() + .namedQuery("select-all-types") + .map(row -> row.as(JsonObject.class)) + .collect(JSON_FACTORY::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::addAll) + .build(); + response.send(jsonArray); + } + + /** + * Return JsonArray with all stored Pokémon. + * Pokémon object contains list of all type names. + * This method is abstract because implementation is DB dependent. + * + * @param request the server request + * @param response the server response + */ + private void listPokemons(ServerRequest request, ServerResponse response) { + JsonArray jsonArray = dbClient.execute().namedQuery("select-all-pokemons") + .map(row -> row.as(JsonObject.class)) + .collect(JSON_FACTORY::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::addAll) + .build(); + response.send(jsonArray); + } + + /** + * Get a single Pokémon by id. + * + * @param request server request + * @param response server response + */ + private void getPokemonById(ServerRequest request, ServerResponse response) { + int pokemonId = Integer.parseInt(request.path() + .pathParameters() + .get("id")); + + response.send(dbClient.execute().createNamedGet("select-pokemon-by-id") + .addParam("id", pokemonId) + .execute() + .orElseThrow(() -> new NotFoundException("Pokemon " + pokemonId + " not found")) + .as(JsonObject.class)); + } + + /** + * Get a single Pokémon by name. + * + * @param request server request + * @param response server response + */ + private void getPokemonByName(ServerRequest request, ServerResponse response) { + String pokemonName = request.path().pathParameters().get("name"); + response.send(dbClient.execute().namedGet("select-pokemon-by-name", pokemonName) + .orElseThrow(() -> new NotFoundException("Pokemon " + pokemonName + " not found")) + .as(JsonObject.class)); + } + + /** + * Insert new Pokémon with specified name. + * + * @param pokemon request entity + * @param response the server response + */ + private void insertPokemon(Pokemon pokemon, ServerResponse response) { + long count = dbClient.execute().createNamedInsert("insert-pokemon") + .indexedParam(pokemon) + .execute(); + response.status(Status.CREATED_201) + .send("Inserted: " + count + " values\n"); + } + + /** + * Update a Pokémon. + * Uses a transaction. + * + * @param pokemon request entity + * @param response the server response + */ + private void updatePokemon(Pokemon pokemon, ServerResponse response) { + long count = dbClient.execute().createNamedUpdate("update-pokemon-by-id") + .namedParam(pokemon) + .execute(); + response.send("Updated: " + count + " values\n"); + } + + /** + * Delete Pokémon with specified id (key). + * + * @param request the server request + * @param response the server response + */ + private void deletePokemonById(ServerRequest request, ServerResponse response) { + int id = request.path() + .pathParameters() + .first("id").map(Integer::parseInt) + .orElseThrow(() -> new BadRequestException("No pokemon id")); + dbClient.execute().createNamedDelete("delete-pokemon-by-id") + .addParam("id", id) + .execute(); + response.status(Status.NO_CONTENT_204) + .send(); + } +} diff --git a/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/package-info.java b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/package-info.java new file mode 100644 index 000000000..df1b4e759 --- /dev/null +++ b/examples/dbclient/pokemons/src/main/java/io/helidon/examples/dbclient/pokemons/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quick start demo application. + */ +package io.helidon.examples.dbclient.pokemons; diff --git a/examples/dbclient/pokemons/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider b/examples/dbclient/pokemons/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider new file mode 100644 index 000000000..7f56b6a6d --- /dev/null +++ b/examples/dbclient/pokemons/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +io.helidon.examples.dbclient.pokemons.PokemonMapperProvider diff --git a/examples/dbclient/pokemons/src/main/resources/application.yaml b/examples/dbclient/pokemons/src/main/resources/application.yaml new file mode 100644 index 000000000..a1dbca78b --- /dev/null +++ b/examples/dbclient/pokemons/src/main/resources/application.yaml @@ -0,0 +1,79 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + +tracing: + service: jdbc-db + +# see README.md for details how to run databases in docker +db: + source: jdbc + connection: + # + # Oracle configuration + # + url: "jdbc:oracle:thin:@localhost:1521/XE" + username: "system" + password: "oracle" + # + # MySQL configuration + # +# url: jdbc:mysql://127.0.0.1:3306/pokemon?useSSL=false +# username: user +# password: password +# poolName: "mysql" + # + # H2 configuration + # +# url: "jdbc:h2:tcp://localhost:9092/~test" +# username: h2 +# password: "${EMPTY}" +# 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." + services: + tracing: + - enabled: true + metrics: + - type: TIMER + 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" + create-types: "CREATE TABLE PokeTypes (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL)" + create-pokemons: "CREATE TABLE Pokemons (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL, id_type INTEGER NOT NULL REFERENCES PokeTypes(id))" + select-all-types: "SELECT id, name FROM PokeTypes" + select-all-pokemons: "SELECT id, name, id_type FROM Pokemons" + select-pokemon-by-id: "SELECT id, name, id_type FROM Pokemons WHERE id = :id" + select-pokemon-by-name: "SELECT id, name, id_type FROM Pokemons WHERE name = ?" + insert-type: "INSERT INTO PokeTypes(id, name) VALUES(?, ?)" + insert-pokemon: "INSERT INTO Pokemons(id, name, id_type) VALUES(?, ?, ?)" + update-pokemon-by-id: "UPDATE Pokemons SET name = :name, id_type = :idType WHERE id = :id" + delete-pokemon-by-id: "DELETE FROM Pokemons WHERE id = :id" + delete-all-types: "DELETE FROM PokeTypes" + delete-all-pokemons: "DELETE FROM Pokemons" diff --git a/examples/dbclient/pokemons/src/main/resources/logging.properties b/examples/dbclient/pokemons/src/main/resources/logging.properties new file mode 100644 index 000000000..550b8d9a4 --- /dev/null +++ b/examples/dbclient/pokemons/src/main/resources/logging.properties @@ -0,0 +1,35 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# Global default logging level. Can be overridden by specific handlers and loggers +.level=INFO + +# Helidon Web Server has a custom log formatter that extends SimpleFormatter. +# It replaces "!thread!" with the current thread name +io.helidon.logging.jul.HelidonConsoleHandler.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/dbclient/pokemons/src/main/resources/mongo.yaml b/examples/dbclient/pokemons/src/main/resources/mongo.yaml new file mode 100644 index 000000000..104c637e2 --- /dev/null +++ b/examples/dbclient/pokemons/src/main/resources/mongo.yaml @@ -0,0 +1,115 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + +tracing: + service: mongo-db + +db: + source: "mongoDb" + connection: + url: "mongodb://127.0.0.1:27017/pokemon" + init-schema: false + # Transactions are not supported + init-data: false + services: + tracing: + - enabled: true + health-check: + type: "query" + statementName: "health-check" + statements: + # Health check statement. HealthCheck statement type must be a query. + health-check: '{ + "operation": "command", + "query": { ping: 1 } + }' + ## Create database schema + # Select all types + select-all-types: '{ + "collection": "types", + "operation": "query", + "projection": { id: 1, name: 1, _id: 0 }, + "query": {} + }' + # Select all pokemons without type information + select-all-pokemons: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, id_type: 1, _id: 0 }, + "query": {} + }' + # Select pokemon by id + select-pokemon-by-id: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, id_type: 1, _id: 0 }, + "query": { id: $id } + }' + # Select pokemon by name + select-pokemon-by-name: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, id_type: 1, _id: 0 }, + "query": { name: ? } + }' + # Insert records into database + insert-type: '{ + "collection": "types", + "operation": "insert", + "value": { + "id": ?, + "name": ? + } + }' + insert-pokemon: '{ + "collection": "pokemons", + "operation": "insert", + "value": { + "id": ?, + "name": ?, + "id_type": ? + } + }' + # Update name of pokemon specified by id + update-pokemon-by-id: '{ + "collection": "pokemons", + "operation": "update", + "value":{ $set: { "name": $name, "id_type": $idType } }, + "query": { id: $id } + }' + # Delete pokemon by id + delete-pokemon-by-id: '{ + "collection": "pokemons", + "operation": "delete", + "query": { id: $id } + }' + # Delete all types + delete-all-types: '{ + "collection": "types", + "operation": "delete", + "query": { } + }' + # Delete all pokemons + delete-all-pokemons: '{ + "collection": "pokemons", + "operation": "delete", + "query": { } + }' + diff --git a/examples/dbclient/pokemons/src/main/resources/pokemon-types.json b/examples/dbclient/pokemons/src/main/resources/pokemon-types.json new file mode 100644 index 000000000..646ec725b --- /dev/null +++ b/examples/dbclient/pokemons/src/main/resources/pokemon-types.json @@ -0,0 +1,20 @@ +[ + {"id": 1, "name": "Normal"}, + {"id": 2, "name": "Fighting"}, + {"id": 3, "name": "Flying"}, + {"id": 4, "name": "Poison"}, + {"id": 5, "name": "Ground"}, + {"id": 6, "name": "Rock"}, + {"id": 7, "name": "Bug"}, + {"id": 8, "name": "Ghost"}, + {"id": 9, "name": "Steel"}, + {"id": 10, "name": "Fire"}, + {"id": 11, "name": "Water"}, + {"id": 12, "name": "Grass"}, + {"id": 13, "name": "Electric"}, + {"id": 14, "name": "Psychic"}, + {"id": 15, "name": "Ice"}, + {"id": 16, "name": "Dragon"}, + {"id": 17, "name": "Dark"}, + {"id": 18, "name": "Fairy"} +] diff --git a/examples/dbclient/pokemons/src/main/resources/pokemons.json b/examples/dbclient/pokemons/src/main/resources/pokemons.json new file mode 100644 index 000000000..c4e78bed7 --- /dev/null +++ b/examples/dbclient/pokemons/src/main/resources/pokemons.json @@ -0,0 +1,8 @@ +[ + {"id": 1, "name": "Bulbasaur", "idType": 12}, + {"id": 2, "name": "Charmander", "idType": 10}, + {"id": 3, "name": "Squirtle", "idType": 11}, + {"id": 4, "name": "Caterpie", "idType": 7}, + {"id": 5, "name": "Weedle", "idType": 7}, + {"id": 6, "name": "Pidgey", "idType": 3} +] diff --git a/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/AbstractPokemonServiceTest.java b/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/AbstractPokemonServiceTest.java new file mode 100644 index 000000000..6f51f44de --- /dev/null +++ b/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/AbstractPokemonServiceTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.pokemons; + +import java.util.List; +import java.util.Map; + +import io.helidon.http.Status; +import io.helidon.http.media.jsonp.JsonpSupport; +import io.helidon.webclient.api.ClientResponseTyped; +import io.helidon.webclient.api.WebClient; +import io.helidon.webserver.WebServer; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import jakarta.json.JsonValue; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +abstract class AbstractPokemonServiceTest { + private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of()); + + private static WebServer server; + private static WebClient client; + + static void beforeAll() { + server = Main.setupServer(WebServer.builder()); + client = WebClient.create(config -> config.baseUri("http://localhost:" + server.port()) + .addMediaSupport(JsonpSupport.create())); + } + + static void afterAll() { + if (server != null && server.isRunning()) { + server.stop(); + } + } + + @Test + void testListAllPokemons() { + ClientResponseTyped response = client.get("/db/pokemon").request(JsonArray.class); + assertThat(response.status(), is(Status.OK_200)); + List names = response.entity().stream().map(AbstractPokemonServiceTest::mapName).toList(); + assertThat(names, is(pokemonNames())); + } + + @Test + void testListAllPokemonTypes() { + ClientResponseTyped response = client.get("/db/type").request(JsonArray.class); + assertThat(response.status(), is(Status.OK_200)); + List names = response.entity().stream().map(AbstractPokemonServiceTest::mapName).toList(); + assertThat(names, is(pokemonTypes())); + } + + @Test + void testGetPokemonById() { + ClientResponseTyped response = client.get("/db/pokemon/2").request(JsonObject.class); + assertThat(response.status(), is(Status.OK_200)); + assertThat(name(response.entity()), is("Charmander")); + } + + @Test + void testGetPokemonByName() { + ClientResponseTyped response = client.get("/db/pokemon/name/Squirtle").request(JsonObject.class); + assertThat(response.status(), is(Status.OK_200)); + assertThat(id(response.entity()), is(3)); + } + + @Test + void testAddUpdateDeletePokemon() { + JsonObject pokemon; + ClientResponseTyped response; + + // add a new Pokémon Rattata + pokemon = JSON_FACTORY.createObjectBuilder() + .add("id", 7) + .add("name", "Rattata") + .add("idType", 1) + .build(); + response = client.post("/db/pokemon").submit(pokemon, String.class); + assertThat(response.status(), is(Status.CREATED_201)); + + // rename Pokémon with id 7 to Raticate + pokemon = JSON_FACTORY.createObjectBuilder() + .add("id", 7) + .add("name", "Raticate") + .add("idType", 2) + .build(); + + response = client.put("/db/pokemon").submit(pokemon, String.class); + assertThat(response.status(), is(Status.OK_200)); + + // delete Pokémon with id 7 + response = client.delete("/db/pokemon/7").request(String.class); + assertThat(response.status(), is(Status.NO_CONTENT_204)); + + response = client.get("/db/pokemon/7").request(String.class); + assertThat(response.status(), is(Status.NOT_FOUND_404)); + } + + private static List pokemonNames() { + try (JsonReader reader = Json.createReader(PokemonService.class.getResourceAsStream("/pokemons.json"))) { + return reader.readArray().stream().map(AbstractPokemonServiceTest::mapName).toList(); + } + } + + private static List pokemonTypes() { + try (JsonReader reader = Json.createReader(PokemonService.class.getResourceAsStream("/pokemon-types.json"))) { + return reader.readArray().stream().map(AbstractPokemonServiceTest::mapName).toList(); + } + } + + private static String mapName(JsonValue value) { + return name(value.asJsonObject()); + } + + private static String name(JsonObject json) { + return json.containsKey("name") + ? json.getString("name") + : json.getString("NAME"); + } + + private static int id(JsonObject json) { + return json.containsKey("id") + ? json.getInt("id") + : json.getInt("ID"); + } + +} diff --git a/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceH2IT.java b/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceH2IT.java new file mode 100644 index 000000000..25774f174 --- /dev/null +++ b/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceH2IT.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.pokemons; + +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static io.helidon.config.ConfigSources.classpath; + +/** + * Tests {@link io.helidon.examples.dbclient.pokemons.PokemonService}. + */ +@Testcontainers(disabledWithoutDocker = true) +class PokemonServiceH2IT extends AbstractPokemonServiceTest { + private static final DockerImageName H2_IMAGE = DockerImageName.parse("nemerosa/h2"); + + @Container + static GenericContainer container = new GenericContainer<>(H2_IMAGE) + .withExposedPorts(9082) + .waitingFor(Wait.forLogMessage("(.*)Web Console server running at(.*)", 1)); + + @BeforeAll + static void start() { + String url = String.format("jdbc:h2:tcp://localhost:%s/~./test", container.getMappedPort(9082)); + Config.global(Config.builder() + .addSource(ConfigSources.create(Map.of("db.connection.url", url))) + .addSource(classpath("application-h2-test.yaml")) + .build()); + beforeAll(); + } + + @AfterAll + static void stop() { + afterAll(); + } +} diff --git a/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceMongoIT.java b/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceMongoIT.java new file mode 100644 index 000000000..d0013d565 --- /dev/null +++ b/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceMongoIT.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.pokemons; + +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static io.helidon.config.ConfigSources.classpath; + +@Testcontainers(disabledWithoutDocker = true) +public class PokemonServiceMongoIT extends AbstractPokemonServiceTest { + + @Container + static final MongoDBContainer container = new MongoDBContainer("mongo") + .withExposedPorts(27017); + + @BeforeAll + static void start() { + String url = String.format("mongodb://127.0.0.1:%s/pokemon", container.getMappedPort(27017)); + Config.global(Config.builder() + .addSource(ConfigSources.create(Map.of("db.connection.url", url))) + .addSource(classpath("application-mongo-test.yaml")) + .build()); + beforeAll(); + } + + @AfterAll + static void stop() { + afterAll(); + } + + void testListAllPokemons() { + //Skip this test - Transactions are not supported + } + + void testListAllPokemonTypes() { + //Skip this test - Transactions are not supported + } + + void testGetPokemonById() { + //Skip this test - Transactions are not supported + } + + void testGetPokemonByName() { + //Skip this test - Transactions are not supported + } + +} diff --git a/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceMySQLIT.java b/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceMySQLIT.java new file mode 100644 index 000000000..36d03ddc0 --- /dev/null +++ b/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceMySQLIT.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.pokemons; + +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static io.helidon.config.ConfigSources.classpath; + +@Testcontainers(disabledWithoutDocker = true) +public class PokemonServiceMySQLIT extends AbstractPokemonServiceTest { + + @Container + static MySQLContainer container = new MySQLContainer<>("mysql:8.0.36") + .withUsername("user") + .withPassword("changeit") + .withNetworkAliases("mysql") + .withDatabaseName("pokemon"); + + @BeforeAll + static void start() { + Config.global(Config.builder() + .addSource(ConfigSources.create(Map.of("db.connection.url", container.getJdbcUrl()))) + .addSource(classpath("application-mysql-test.yaml")) + .build()); + beforeAll(); + } + + @AfterAll + static void stop() { + afterAll(); + } + +} diff --git a/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceOracleIT.java b/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceOracleIT.java new file mode 100644 index 000000000..cf2d4f6d9 --- /dev/null +++ b/examples/dbclient/pokemons/src/test/java/io/helidon/examples/dbclient/pokemons/PokemonServiceOracleIT.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.dbclient.pokemons; + +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.OracleContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static io.helidon.config.ConfigSources.classpath; + +@Testcontainers(disabledWithoutDocker = true) +public class PokemonServiceOracleIT extends AbstractPokemonServiceTest { + + private static final DockerImageName image = DockerImageName.parse("wnameless/oracle-xe-11g-r2") + .asCompatibleSubstituteFor("gvenzl/oracle-xe"); + + @Container + static OracleContainer container = new OracleContainer(image) + .withExposedPorts(1521, 8080) + .withDatabaseName("XE") + .usingSid() + .waitingFor(Wait.forListeningPorts(1521, 8080)); + + @BeforeAll + static void setup() { + Config.global(Config.builder() + .addSource(ConfigSources.create(Map.of("db.connection.url", container.getJdbcUrl()))) + .addSource(classpath("application-oracle-test.yaml")) + .build()); + beforeAll(); + } + + @AfterAll + static void stop() { + afterAll(); + } +} diff --git a/examples/dbclient/pokemons/src/test/resources/application-h2-test.yaml b/examples/dbclient/pokemons/src/test/resources/application-h2-test.yaml new file mode 100644 index 000000000..daccc3736 --- /dev/null +++ b/examples/dbclient/pokemons/src/test/resources/application-h2-test.yaml @@ -0,0 +1,59 @@ +# +# Copyright (c) 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + +tracing: + service: jdbc-db + +# see README.md for details how to run databases in docker +db: + source: jdbc + connection: + username: sa + password: "${EMPTY}" + 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." + services: + tracing: + - enabled: true + metrics: + - type: TIMER + health-check: + type: "query" + statementName: "health-check" + statements: + health-check: "SELECT 0" + create-types: "CREATE TABLE PokeTypes (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL)" + create-pokemons: "CREATE TABLE Pokemons (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL, id_type INTEGER NOT NULL REFERENCES PokeTypes(id))" + select-all-types: "SELECT id, name FROM PokeTypes" + select-all-pokemons: "SELECT id, name, id_type FROM Pokemons" + select-pokemon-by-id: "SELECT id, name, id_type FROM Pokemons WHERE id = :id" + select-pokemon-by-name: "SELECT id, name, id_type FROM Pokemons WHERE name = ?" + insert-type: "INSERT INTO PokeTypes(id, name) VALUES(?, ?)" + insert-pokemon: "INSERT INTO Pokemons(id, name, id_type) VALUES(?, ?, ?)" + update-pokemon-by-id: "UPDATE Pokemons SET name = :name, id_type = :idType WHERE id = :id" + delete-pokemon-by-id: "DELETE FROM Pokemons WHERE id = :id" + delete-all-types: "DELETE FROM PokeTypes" + delete-all-pokemons: "DELETE FROM Pokemons" diff --git a/examples/dbclient/pokemons/src/test/resources/application-mongo-test.yaml b/examples/dbclient/pokemons/src/test/resources/application-mongo-test.yaml new file mode 100644 index 000000000..aee39d5e4 --- /dev/null +++ b/examples/dbclient/pokemons/src/test/resources/application-mongo-test.yaml @@ -0,0 +1,113 @@ +# +# Copyright (c) 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + +tracing: + service: mongo-db + +db: + source: "mongoDb" + init-schema: false + # Transactions are not supported + init-data: false + services: + tracing: + - enabled: true + health-check: + type: "query" + statementName: "health-check" + statements: + # Health check statement. HealthCheck statement type must be a query. + health-check: '{ + "operation": "command", + "query": { ping: 1 } + }' + ## Create database schema + # Select all types + select-all-types: '{ + "collection": "types", + "operation": "query", + "projection": { id: 1, name: 1, _id: 0 }, + "query": {} + }' + # Select all pokemons without type information + select-all-pokemons: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, id_type: 1, _id: 0 }, + "query": {} + }' + # Select pokemon by id + select-pokemon-by-id: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, id_type: 1, _id: 0 }, + "query": { id: $id } + }' + # Select pokemon by name + select-pokemon-by-name: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, id_type: 1, _id: 0 }, + "query": { name: ? } + }' + # Insert records into database + insert-type: '{ + "collection": "types", + "operation": "insert", + "value": { + "id": ?, + "name": ? + } + }' + insert-pokemon: '{ + "collection": "pokemons", + "operation": "insert", + "value": { + "id": ?, + "name": ?, + "id_type": ? + } + }' + # Update name of pokemon specified by id + update-pokemon-by-id: '{ + "collection": "pokemons", + "operation": "update", + "value":{ $set: { "name": $name, "id_type": $idType } }, + "query": { id: $id } + }' + # Delete pokemon by id + delete-pokemon-by-id: '{ + "collection": "pokemons", + "operation": "delete", + "query": { id: $id } + }' + # Delete all types + delete-all-types: '{ + "collection": "types", + "operation": "delete", + "query": { } + }' + # Delete all pokemons + delete-all-pokemons: '{ + "collection": "pokemons", + "operation": "delete", + "query": { } + }' + diff --git a/examples/dbclient/pokemons/src/test/resources/application-mysql-test.yaml b/examples/dbclient/pokemons/src/test/resources/application-mysql-test.yaml new file mode 100644 index 000000000..58e56aee3 --- /dev/null +++ b/examples/dbclient/pokemons/src/test/resources/application-mysql-test.yaml @@ -0,0 +1,59 @@ +# +# Copyright (c) 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + +tracing: + service: jdbc-db + +# see README.md for details how to run databases in docker +db: + source: jdbc + connection: + username: user + password: changeit + poolName: "mysql" + 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." + services: + tracing: + - enabled: true + metrics: + - type: TIMER + health-check: + type: "query" + statementName: "health-check" + statements: + health-check: "SELECT 0" + create-types: "CREATE TABLE PokeTypes (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL)" + create-pokemons: "CREATE TABLE Pokemons (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL, id_type INTEGER NOT NULL REFERENCES PokeTypes(id))" + select-all-types: "SELECT id, name FROM PokeTypes" + select-all-pokemons: "SELECT id, name, id_type FROM Pokemons" + select-pokemon-by-id: "SELECT id, name, id_type FROM Pokemons WHERE id = :id" + select-pokemon-by-name: "SELECT id, name, id_type FROM Pokemons WHERE name = ?" + insert-type: "INSERT INTO PokeTypes(id, name) VALUES(?, ?)" + insert-pokemon: "INSERT INTO Pokemons(id, name, id_type) VALUES(?, ?, ?)" + update-pokemon-by-id: "UPDATE Pokemons SET name = :name, id_type = :idType WHERE id = :id" + delete-pokemon-by-id: "DELETE FROM Pokemons WHERE id = :id" + delete-all-types: "DELETE FROM PokeTypes" + delete-all-pokemons: "DELETE FROM Pokemons" diff --git a/examples/dbclient/pokemons/src/test/resources/application-oracle-test.yaml b/examples/dbclient/pokemons/src/test/resources/application-oracle-test.yaml new file mode 100644 index 000000000..66d62ec06 --- /dev/null +++ b/examples/dbclient/pokemons/src/test/resources/application-oracle-test.yaml @@ -0,0 +1,58 @@ +# +# Copyright (c) 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + +tracing: + service: jdbc-db + +# see README.md for details how to run databases in docker +db: + source: jdbc + connection: + username: "system" + password: "oracle" + 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." + services: + tracing: + - enabled: true + metrics: + - type: TIMER + health-check: + type: "query" + statementName: "health-check" + statements: + health-check: "SELECT 1 FROM DUAL" + create-types: "CREATE TABLE PokeTypes (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL)" + create-pokemons: "CREATE TABLE Pokemons (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL, id_type INTEGER NOT NULL REFERENCES PokeTypes(id))" + select-all-types: "SELECT id, name FROM PokeTypes" + select-all-pokemons: "SELECT id, name, id_type FROM Pokemons" + select-pokemon-by-id: "SELECT id, name, id_type FROM Pokemons WHERE id = :id" + select-pokemon-by-name: "SELECT id, name, id_type FROM Pokemons WHERE name = ?" + insert-type: "INSERT INTO PokeTypes(id, name) VALUES(?, ?)" + insert-pokemon: "INSERT INTO Pokemons(id, name, id_type) VALUES(?, ?, ?)" + update-pokemon-by-id: "UPDATE Pokemons SET name = :name, id_type = :idType WHERE id = :id" + delete-pokemon-by-id: "DELETE FROM Pokemons WHERE id = :id" + delete-all-types: "DELETE FROM PokeTypes" + delete-all-pokemons: "DELETE FROM Pokemons" diff --git a/examples/dbclient/pom.xml b/examples/dbclient/pom.xml new file mode 100644 index 000000000..34c3f28ee --- /dev/null +++ b/examples/dbclient/pom.xml @@ -0,0 +1,46 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + + pom + + io.helidon.examples.dbclient + helidon-examples-dbclient-project + 1.0.0-SNAPSHOT + Helidon Examples DB Client + + + Examples of Helidon DB Client + + + + + jdbc + mongodb + common + pokemons + + diff --git a/examples/employee-app/.dockerignore b/examples/employee-app/.dockerignore new file mode 100644 index 000000000..c8b241f22 --- /dev/null +++ b/examples/employee-app/.dockerignore @@ -0,0 +1 @@ +target/* \ No newline at end of file diff --git a/examples/employee-app/Dockerfile b/examples/employee-app/Dockerfile new file mode 100644 index 000000000..fc8d64243 --- /dev/null +++ b/examples/employee-app/Dockerfile @@ -0,0 +1,54 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -DskipTests + +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-examples-employee-app.jar ./ +COPY --from=build /helidon/target/libs ./libs + +CMD ["java", "-jar", "helidon-examples-employee-app.jar"] + +EXPOSE 8080 diff --git a/examples/employee-app/README.md b/examples/employee-app/README.md new file mode 100644 index 000000000..9a1779c45 --- /dev/null +++ b/examples/employee-app/README.md @@ -0,0 +1,293 @@ +# Helidon Quickstart SE - Employee Directory Example + +This project implements an employee directory REST service using Helidon SE. + The application is composed of a Helidon REST Microservice backend along with + an HTML/JavaScript front end. The source for both application is included with + the Maven project. + +By default, the service uses a ArrayList backend with sample data. You can connect + the backend application to an Oracle database by changing the values in the + `resources/application.yaml` file. + +The service uses Helidon DB Client to access the database. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-employee-app.jar +``` + +## Create script +If you do not have the employee table in your database, you can create it and required resources as follows: + +```sql +CREATE TABLE EMPLOYEE (ID NUMBER(6), + FIRSTNAME VARCHAR2(20), + LASTNAME VARCHAR2(25), + EMAIL VARCHAR2(30), + PHONE VARCHAR2(30), + BIRTHDATE VARCHAR2(15), + TITLE VARCHAR2(20), + DEPARTMENT VARCHAR2(20)); + +ALTER TABLE EMPLOYEE ADD (CONSTRAINT emp_id_pk PRIMARY KEY (ID)); + +CREATE SEQUENCE EMPLOYEE_SEQ INCREMENT BY 1 NOCACHE NOCYCLE; +``` + +## Exercise the application +Get all employees. +```shell +curl -X GET curl -X GET http://localhost:8080/employees +``` + +Only 1 output record is shown for brevity: +```json +[ + { + "birthDate": "1970-11-28T08:28:48.078Z", + "department": "Mobility", + "email": "Hugh.Jast@example.com", + "firstName": "Hugh", + "id": "48cf06ad-6ed4-47e6-ac44-3ea9c67cbe2d", + "lastName": "Jast", + "phone": "730-715-4446", + "title": "National Data Strategist" + } +] +``` + + +Get all employees whose last name contains "S". +```shell +curl -X GET http://localhost:8080/employees/lastname/S +``` + +Only 1 output record is shown for brevity: +```json +[ + { + "birthDate": "1978-03-18T17:00:12.938Z", + "department": "Security", + "email": "Zora.Sawayn@example.com", + "firstName": "Zora", + "id": "d7b583a2-f068-40d9-aec0-6f87899c5d8a", + "lastName": "Sawayn", + "phone": "923-814-0502", + "title": "Dynamic Marketing Designer" + } +] +``` + +Get an individual record. +```shell +curl -X GET http://localhost:8080/employees/48cf06ad-6ed4-47e6-ac44-3ea9c67cbe2d +``` +Output: +```json +[ + { + "birthDate": "1970-11-28T08:28:48.078Z", + "department": "Mobility", + "email": "Hugh.Jast@example.com", + "firstName": "Hugh", + "id": "48cf06ad-6ed4-47e6-ac44-3ea9c67cbe2d", + "lastName": "Jast", + "phone": "730-715-4446", + "title": "National Data Strategist" + } +] +``` + +Connect with a web browser at: +```txt +http://localhost:8080/public/index.html +``` + + +## Try health and metrics + +```shell +curl -s -X GET http://localhost:8080/health +``` + +```json +{ + "outcome": "UP", + "checks": [ + { + "name": "deadlock", + "state": "UP" + }, + { + "name": "diskSpace", + "state": "UP", + "data": { + "free": "306.61 GB", + "freeBytes": 329225338880, + "percentFree": "65.84%", + "total": "465.72 GB", + "totalBytes": 500068036608 + } + }, + { + "name": "heapMemory", + "state": "UP", + "data": { + "free": "239.35 MB", + "freeBytes": 250980656, + "max": "4.00 GB", + "maxBytes": 4294967296, + "percentFree": "99.59%", + "total": "256.00 MB", + "totalBytes": 268435456 + } + } + ] +} +``` + +### Prometheus Format + +```shell +curl -s -X GET http://localhost:8080/metrics +``` + +Only 1 output item is shown for brevity: +```txt +# TYPE base:classloader_current_loaded_class_count counter +# HELP base:classloader_current_loaded_class_count Displays the number of classes that are currently loaded in the Java virtual machine. +base:classloader_current_loaded_class_count 3995 +``` + +### JSON Format +```shell +curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics +``` + +Output: +```json +{ + "base": { + "classloader.currentLoadedClass.count": 4011, + "classloader.totalLoadedClass.count": 4011, + "classloader.totalUnloadedClass.count": 0, + "cpu.availableProcessors": 8, + "cpu.systemLoadAverage": 1.65283203125, + "gc.G1 Old Generation.count": 0, + "gc.G1 Old Generation.time": 0, + "gc.G1 Young Generation.count": 2, + "gc.G1 Young Generation.time": 8, + "jvm.uptime": 478733, + "memory.committedHeap": 268435456, + "memory.maxHeap": 4294967296, + "memory.usedHeap": 18874368, + "thread.count": 11, + "thread.daemon.count": 4, + "thread.max.count": 11 + }, + "vendor": { + "grpc.requests.count": 0, + "grpc.requests.meter": { + "count": 0, + "meanRate": 0, + "oneMinRate": 0, + "fiveMinRate": 0, + "fifteenMinRate": 0 + }, + "requests.count": 5, + "requests.meter": { + "count": 5, + "meanRate": 0.01046407983617782, + "oneMinRate": 0.0023897243038835964, + "fiveMinRate": 0.003944597070306631, + "fifteenMinRate": 0.0023808575122958794 + } + } +} +``` + +## Build the Docker Image + +```shell +docker build -t employee-app . +``` + +## Start the application with Docker + +```shell +docker run --rm -p 8080:8080 employee-app:latest +``` + +Exercise the application as described above. + +## Deploy the application to Kubernetes + +```txt +kubectl cluster-info # Verify which cluster +kubectl get pods # Verify connectivity to cluster +kubectl create -f app.yaml # Deply application +kubectl get service employee-app # Get service info +``` + + +### Oracle DB Credentials +You can connect to two different datastores for the back end application. + Just fill in the application.yaml files. To use an ArrayList as the data store, + simply set `drivertype` to `Array`. To connect to an Oracle database, you must + set all the values: `user`, `password`, `hosturl`, and `drivertype`. + For Oracle, the `drivertype` should be set to `Oracle`. + +**Sample `application.yaml`** +```yaml +app: + user: + password: + hosturl: :/. + drivertype: Array + + server: + port: 8080 + host: 0.0.0.0 +``` + +## Create the database objects + +1. Create a connection to your Oracle Database using sqlplus or SQL Developer. + See https://docs.cloud.oracle.com/iaas/Content/Database/Tasks/connectingDB.htm. +2. Create the database objects: + +```sql +CREATE TABLE EMPLOYEE ( + ID INTEGER NOT NULL, + FIRSTNAME VARCHAR(100), + LASTNAME VARCHAR(100), + EMAIL VARCHAR(100), + PHONE VARCHAR(100), + BIRTHDATE VARCHAR(10), + TITLE VARCHAR(100), + DEPARTMENT VARCHAR(100), + PRIMARY KEY (ID) + ); +``` + +```sql +CREATE SEQUENCE EMPLOYEE_SEQ + START WITH 100 + INCREMENT BY 1; +``` + +```sql +INSERT INTO EMPLOYEE (ID, FIRSTNAME, LASTNAME, EMAIL, PHONE, BIRTHDATE, TITLE, DEPARTMENT) VALUES (EMPLOYEE_SEQ.nextVal, 'Hugh', 'Jast', 'Hugh.Jast@example.com', '730-555-0100', '1970-11-28', 'National Data Strategist', 'Mobility'); + +INSERT INTO EMPLOYEE (ID, FIRSTNAME, LASTNAME, EMAIL, PHONE, BIRTHDATE, TITLE, DEPARTMENT) VALUES (EMPLOYEE_SEQ.nextVal, 'Toy', 'Herzog', 'Toy.Herzog@example.com', '769-555-0102', '1961-08-08', 'Dynamic Operations Manager', 'Paradigm'); + +INSERT INTO EMPLOYEE (ID, FIRSTNAME, LASTNAME, EMAIL, PHONE, BIRTHDATE, TITLE, DEPARTMENT) VALUES (EMPLOYEE_SEQ.nextVal, 'Reed', 'Hahn', 'Reed.Hahn@example.com', '429-555-0153', '1977-02-05', 'Future Directives Facilitator', 'Quality'); + +INSERT INTO EMPLOYEE (ID, FIRSTNAME, LASTNAME, EMAIL, PHONE, BIRTHDATE, TITLE, DEPARTMENT) VALUES (EMPLOYEE_SEQ.nextVal, 'Novella', 'Bahringer', 'Novella.Bahringer@example.com', '293-596-3547', '1961-07-25', 'Principal Factors Architect', 'Division'); + +INSERT INTO EMPLOYEE (ID, FIRSTNAME, LASTNAME, EMAIL, PHONE, BIRTHDATE, TITLE, DEPARTMENT) VALUES (EMPLOYEE_SEQ.nextVal, 'Zora', 'Sawayn', 'Zora.Sawayn@example.com', '923-555-0161', '1978-03-18', 'Dynamic Marketing Designer', 'Security'); + +INSERT INTO EMPLOYEE (ID, FIRSTNAME, LASTNAME, EMAIL, PHONE, BIRTHDATE, TITLE, DEPARTMENT) VALUES (EMPLOYEE_SEQ.nextVal, 'Cordia', 'Willms', 'Cordia.Willms@example.com', '778-555-0187', '1989-03-31', 'Human Division Representative', 'Optimization'); +``` \ No newline at end of file diff --git a/examples/employee-app/app.yaml b/examples/employee-app/app.yaml new file mode 100644 index 000000000..7f3d18e65 --- /dev/null +++ b/examples/employee-app/app.yaml @@ -0,0 +1,63 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +kind: Service +apiVersion: v1 +metadata: + name: helidon-examples-employee-app + labels: + app: helidon-examples-employee-app +spec: + type: NodePort + selector: + app: helidon-examples-employee-app + ports: + - port: 8080 + targetPort: 8080 + name: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helidon-examples-employee-app +spec: + replicas: 1 + template: + metadata: + labels: + app: helidon-examples-employee-app + version: v1 + spec: + containers: + - name: helidon-examples-employee-app + image: helidon-examples-employee-app + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: helidon-examples-employee-app + labels: + app: helidon-examples-employee-app +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: 8080 + selector: + app: helidon-examples-employee-app \ No newline at end of file diff --git a/examples/employee-app/pom.xml b/examples/employee-app/pom.xml new file mode 100644 index 000000000..d72b905ec --- /dev/null +++ b/examples/employee-app/pom.xml @@ -0,0 +1,121 @@ + + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.employee + helidon-examples-employee-app + 1.0.0-SNAPSHOT + Helidon Examples Employee App + + + io.helidon.examples.employee.Main + + + + + io.helidon.integrations.db + ojdbc + + + com.oracle.database.jdbc + ucp + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver + helidon-webserver-static-content + + + io.helidon.http.media + helidon-http-media-jsonb + + + io.helidon.config + helidon-config-yaml + + + io.helidon.health + helidon-health-checks + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.dbclient + helidon-dbclient-jdbc + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + + diff --git a/examples/employee-app/src/main/java/io/helidon/examples/employee/Employee.java b/examples/employee-app/src/main/java/io/helidon/examples/employee/Employee.java new file mode 100644 index 000000000..b15b9d5af --- /dev/null +++ b/examples/employee-app/src/main/java/io/helidon/examples/employee/Employee.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.employee; + +import java.util.UUID; + +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; +/** + * Represents an employee. + */ +public final class Employee { + + private final String id; + private final String firstName; + private final String lastName; + private final String email; + private final String phone; + private final String birthDate; + private final String title; + private final String department; + + /**Creates a new Employee. + * @param id The employee ID. + * @param firstName The employee first name. + * @param lastName The employee lastName. + * @param email The employee email. + * @param phone The employee phone. + * @param birthDate The employee birthDatee. + * @param title The employee title. + * @param department The employee department.*/ + @SuppressWarnings("checkstyle:ParameterNumber") + private Employee(String id, String firstName, String lastName, String email, String phone, String birthDate, + String title, String department) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.phone = phone; + this.birthDate = birthDate; + this.title = title; + this.department = department; + } + + /** + * Creates a new employee. This method helps to parse the json parameters in the requests. + * @param id The employee ID. If the employee ID is null or empty generates a new ID. + * @param firstName The employee first name. + * @param lastName The employee lastName. + * @param email The employee email. + * @param phone The employee phone. + * @param birthDate The employee birthDatee. + * @param title The employee title. + * @param department The employee department. + * @return A new employee object + */ + @JsonbCreator + @SuppressWarnings("checkstyle:ParameterNumber") + public static Employee of(@JsonbProperty("id") String id, @JsonbProperty("firstName") String firstName, + @JsonbProperty("lastName") String lastName, @JsonbProperty("email") String email, + @JsonbProperty("phone") String phone, @JsonbProperty("birthDate") String birthDate, + @JsonbProperty("title") String title, @JsonbProperty("department") String department) { + if (id == null || id.trim().equals("")) { + id = UUID.randomUUID().toString(); + } + Employee e = new Employee(id, firstName, lastName, email, phone, birthDate, title, department); + return e; + } + + /** + * Returns the employee ID. + * @return the ID + */ + public String getId() { + return this.id; + } + + /** + * Returns the employee first name. + * @return The first name + */ + public String getFirstName() { + return this.firstName; + } + + /** + * Returns the employee last name. + * @return The last name + */ + public String getLastName() { + return this.lastName; + } + + /** + * Returns the employee e-mail. + * @return The email + */ + public String getEmail() { + return this.email; + } + + /** + * Returns the employee phone. + * @return The phone + */ + public String getPhone() { + return this.phone; + } + + /** + * Returns the employee birthdate. + * @return The birthdate + */ + public String getBirthDate() { + return this.birthDate; + } + + /** + * Returns the employee title. + * @return The title + */ + public String getTitle() { + return this.title; + } + + /** + * Returns the employee department. + * @return The department + */ + public String getDepartment() { + return this.department; + } + + @Override + public String toString() { + return "ID: " + id + " First Name: " + firstName + " Last Name: " + lastName + " EMail: " + email + " Phone: " + + phone + " Birth Date: " + birthDate + " Title: " + title + " Department: " + department; + } + +} diff --git a/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeRepository.java b/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeRepository.java new file mode 100644 index 000000000..1ce65c59f --- /dev/null +++ b/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeRepository.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.employee; + +import java.util.List; +import java.util.Optional; + +import io.helidon.config.Config; + +/** + * Interface for Data Access Objects. + */ +public interface EmployeeRepository { + + /** + * Create a new employeeRepository instance using one of the two implementations + * {@link EmployeeRepositoryImpl} or {@link EmployeeRepositoryImplDB} depending + * on the specified driver type. + * + * @param driverType Represents the driver type. It can be Array or Oracle. + * @param config Contains the application configuration specified in the + * application.yaml file. + * @return The employee repository implementation. + */ + static EmployeeRepository create(String driverType, Config config) { + return switch (driverType) { + case "Database" -> new EmployeeRepositoryImplDB(config); + default -> + // Array is default + new EmployeeRepositoryImpl(); + }; + } + + /** + * Returns the list of the employees. + * + * @return The collection of all the employee objects + */ + List getAll(); + + /** + * Returns the list of the employees that match with the specified lastName. + * + * @param lastName Represents the last name value for the search. + * @return The collection of the employee objects that match with the specified + * lastName + */ + List getByLastName(String lastName); + + /** + * Returns the list of the employees that match with the specified title. + * + * @param title Represents the title value for the search + * @return The collection of the employee objects that match with the specified + * title + */ + List getByTitle(String title); + + /** + * Returns the list of the employees that match with the specified department. + * + * @param department Represents the department value for the search. + * @return The collection of the employee objects that match with the specified + * department + */ + List getByDepartment(String department); + + /** + * Add a new employee. + * + * @param employee returns the employee object including the ID generated. + * @return the employee object including the ID generated + */ + Employee save(Employee employee); // Add new employee + + /** + * Update an existing employee. + * + * @param updatedEmployee The employee object with the values to update + * @param id The employee ID + * @return number of updated records + */ + long update(Employee updatedEmployee, String id); + + /** + * Delete an employee by ID. + * + * @param id The employee ID + * @return number of deleted records + */ + long deleteById(String id); + + /** + * Get an employee by ID. + * + * @param id The employee ID + * @return The employee object if the employee is found + */ + Optional getById(String id); +} diff --git a/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeRepositoryImpl.java b/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeRepositoryImpl.java new file mode 100644 index 000000000..2a9498025 --- /dev/null +++ b/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeRepositoryImpl.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.employee; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; +import jakarta.json.bind.JsonbConfig; + +/** + * Implementation of the {@link EmployeeRepository}. This implementation uses a + * mock database written with in-memory ArrayList classes. + * The strings id, name, and other search strings are validated before being + * passed to the methods in this class. + */ +public final class EmployeeRepositoryImpl implements EmployeeRepository { + + private final CopyOnWriteArrayList eList = new CopyOnWriteArrayList<>(); + + /** + * To load the initial data, parses the content of employee.json + * file located in the resources directory to a list of Employee + * objects. + */ + public EmployeeRepositoryImpl() { + JsonbConfig config = new JsonbConfig().withFormatting(Boolean.TRUE); + try (Jsonb jsonb = JsonbBuilder.create(config); + InputStream jsonFile = EmployeeRepositoryImpl.class.getResourceAsStream("/employees.json")) { + Employee[] employees = jsonb.fromJson(jsonFile, Employee[].class); + eList.addAll(Arrays.asList(employees)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public List getByLastName(String name) { + return eList.stream() + .filter((e) -> (e.getLastName().contains(name))) + .toList(); + } + + @Override + public List getByTitle(String title) { + return eList.stream() + .filter((e) -> (e.getTitle().contains(title))) + .toList(); + } + + @Override + public List getByDepartment(String department) { + return eList.stream() + .filter((e) -> (e.getDepartment().contains(department))) + .toList(); + } + + @Override + public List getAll() { + return eList; + } + + @Override + public Optional getById(String id) { + return eList.stream().filter(e -> e.getId().equals(id)).findFirst(); + } + + @Override + public Employee save(Employee employee) { + Employee nextEmployee = Employee.of(null, + employee.getFirstName(), + employee.getLastName(), + employee.getEmail(), + employee.getPhone(), + employee.getBirthDate(), + employee.getTitle(), + employee.getDepartment()); + eList.add(nextEmployee); + return nextEmployee; + } + + @Override + public long update(Employee updatedEmployee, String id) { + deleteById(id); + Employee e = Employee.of(id, updatedEmployee.getFirstName(), updatedEmployee.getLastName(), + updatedEmployee.getEmail(), updatedEmployee.getPhone(), updatedEmployee.getBirthDate(), + updatedEmployee.getTitle(), updatedEmployee.getDepartment()); + eList.add(e); + return 1L; + } + + @Override + public long deleteById(String id) { + return eList.stream() + .filter(e -> e.getId().equals(id)) + .findFirst() + .map(eList::remove) + .map(it -> 1L) + .orElse(0L); + } +} diff --git a/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeRepositoryImplDB.java b/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeRepositoryImplDB.java new file mode 100644 index 000000000..e95694c0b --- /dev/null +++ b/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeRepositoryImplDB.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.employee; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.jdbc.JdbcClientBuilder; + +/** + * Implementation of the {@link EmployeeRepository}. This implementation uses an + * Oracle database to persist the Employee objects. + */ +final class EmployeeRepositoryImplDB implements EmployeeRepository { + + private final DbClient dbClient; + + /** + * Creates the database connection using the parameters specified in the + * application.yaml file located in the resources directory. + * + * @param config Represents the application configuration. + */ + EmployeeRepositoryImplDB(Config config) { + String url = "jdbc:oracle:thin:@"; + String driver = "oracle.jdbc.driver.OracleDriver"; + + String dbUserName = config.get("app.user").asString().orElse("sys as SYSDBA"); + String dbUserPassword = config.get("app.password").asString().orElse("changeit"); + String dbHostURL = config.get("app.hosturl").asString().orElse("localhost:1521/xe"); + + try { + Class.forName(driver); + } catch (Exception sqle) { + sqle.printStackTrace(); + } + + // now we create the DB Client - explicitly use JDBC, so we can + // configure JDBC specific configuration + dbClient = JdbcClientBuilder.create() + .url(url + dbHostURL) + .username(dbUserName) + .password(dbUserPassword) + .build(); + } + + @Override + public List getAll() { + String queryStr = "SELECT * FROM EMPLOYEE"; + + return toEmployeeList(dbClient.execute().query(queryStr)); + } + + @Override + public List getByLastName(String name) { + String queryStr = "SELECT * FROM EMPLOYEE WHERE LASTNAME LIKE ?"; + + return toEmployeeList(dbClient.execute().query(queryStr, name)); + } + + @Override + public List getByTitle(String title) { + String queryStr = "SELECT * FROM EMPLOYEE WHERE TITLE LIKE ?"; + + return toEmployeeList(dbClient.execute().query(queryStr, title)); + } + + @Override + public List getByDepartment(String department) { + String queryStr = "SELECT * FROM EMPLOYEE WHERE DEPARTMENT LIKE ?"; + + return toEmployeeList(dbClient.execute().query(queryStr, department)); + } + + @Override + public Employee save(Employee employee) { + String insertTableSQL = "INSERT INTO EMPLOYEE " + + "(ID, FIRSTNAME, LASTNAME, EMAIL, PHONE, BIRTHDATE, TITLE, DEPARTMENT) " + + "VALUES(EMPLOYEE_SEQ.NEXTVAL,?,?,?,?,?,?,?)"; + + dbClient.execute() + .createInsert(insertTableSQL) + .addParam(employee.getFirstName()) + .addParam(employee.getLastName()) + .addParam(employee.getEmail()) + .addParam(employee.getPhone()) + .addParam(employee.getBirthDate()) + .addParam(employee.getTitle()) + .addParam(employee.getDepartment()) + .execute(); + // let's always return the employee once the insert finishes + return employee; + } + + @Override + public long deleteById(String id) { + String deleteRowSQL = "DELETE FROM EMPLOYEE WHERE ID=?"; + + return dbClient.execute().delete(deleteRowSQL, id); + } + + @Override + public Optional getById(String id) { + String queryStr = "SELECT * FROM EMPLOYEE WHERE ID =?"; + + return dbClient.execute() + .get(queryStr, id) + .map(row -> row.as(Employee.class)); + } + + @Override + public long update(Employee updatedEmployee, String id) { + String updateTableSQL = "UPDATE EMPLOYEE SET FIRSTNAME=?, LASTNAME=?, EMAIL=?, PHONE=?, BIRTHDATE=?, TITLE=?, " + + "DEPARTMENT=? WHERE ID=?"; + + return dbClient.execute() + .createUpdate(updateTableSQL) + .addParam(updatedEmployee.getFirstName()) + .addParam(updatedEmployee.getLastName()) + .addParam(updatedEmployee.getEmail()) + .addParam(updatedEmployee.getPhone()) + .addParam(updatedEmployee.getBirthDate()) + .addParam(updatedEmployee.getTitle()) + .addParam(updatedEmployee.getDepartment()) + .addParam(Integer.parseInt(id)) + .execute(); + } + + private static List toEmployeeList(Stream rows) { + return rows.map(EmployeeDbMapper::read).toList(); + } + + private static final class EmployeeDbMapper { + private EmployeeDbMapper() { + } + + static Employee read(DbRow row) { + // map named columns to an object + return Employee.of( + row.column("ID").get(String.class), + row.column("FIRSTNAME").get(String.class), + row.column("LASTNAME").get(String.class), + row.column("EMAIL").get(String.class), + row.column("PHONE").get(String.class), + row.column("BIRTHDATE").get(String.class), + row.column("TITLE").get(String.class), + row.column("DEPARTMENT").get(String.class) + ); + } + } +} diff --git a/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeService.java b/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeService.java new file mode 100644 index 000000000..8bec0e7ba --- /dev/null +++ b/examples/employee-app/src/main/java/io/helidon/examples/employee/EmployeeService.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.employee; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +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; + +/** + * The Employee service endpoints. + *
    + *
  • Get all employees: {@code curl -X GET http://localhost:8080/employees}
  • + *
  • Get employee by id: {@code curl -X GET http://localhost:8080/employees/{id}}
  • + *
  • Add employee {@code curl -X POST http://localhost:8080/employees/{id}}
  • + *
  • Update employee by id {@code curl -X PUT http://localhost:8080/employees/{id}}
  • + *
  • Delete employee by id {@code curl -X DELETE http://localhost:8080/employees/{id}}
  • + *
+ * The message is returned as a JSON object + */ +public class EmployeeService implements HttpService { + private final EmployeeRepository employees; + private static final Logger LOGGER = Logger.getLogger(EmployeeService.class.getName()); + + EmployeeService(Config config) { + String driverType = config.get("app.drivertype").asString().orElse("Array"); + employees = EmployeeRepository.create(driverType, config); + } + + /** + * A service registers itself by updating the routine rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.get("/", this::getAll) + .get("/lastname/{name}", this::getByLastName) + .get("/department/{name}", this::getByDepartment) + .get("/title/{name}", this::getByTitle) + .post("/", this::save) + .get("/{id}", this::getEmployeeById) + .put("/{id}", this::update) + .delete("/{id}", this::delete); + } + + /** + * Gets all the employees. + * + * @param request the server request + * @param response the server response + */ + private void getAll(ServerRequest request, ServerResponse response) { + LOGGER.fine("getAll"); + + response.send(employees.getAll()); + } + + /** + * Gets the employees by the last name specified in the parameter. + * + * @param request the server request + * @param response the server response + */ + private void getByLastName(ServerRequest request, ServerResponse response) { + LOGGER.fine("getByLastName"); + + String name = request.path().pathParameters().get("name"); + // Invalid query strings handled in isValidQueryStr. Keeping DRY + if (isValidQueryStr(response, name)) { + response.send(employees.getByLastName(name)); + } + } + + /** + * Gets the employees by the title specified in the parameter. + * + * @param request the server request + * @param response the server response + */ + private void getByTitle(ServerRequest request, ServerResponse response) { + LOGGER.fine("getByTitle"); + + String title = request.path().pathParameters().get("name"); + if (isValidQueryStr(response, title)) { + response.send(employees.getByTitle(title)); + } + } + + /** + * Gets the employees by the department specified in the parameter. + * + * @param request the server request + * @param response the server response + */ + private void getByDepartment(ServerRequest request, ServerResponse response) { + LOGGER.fine("getByDepartment"); + + String department = request.path().pathParameters().get("name"); + if (isValidQueryStr(response, department)) { + response.send(employees.getByDepartment(department)); + } + } + + /** + * Gets the employees by the ID specified in the parameter. + * + * @param request the server request + * @param response the server response + */ + private void getEmployeeById(ServerRequest request, ServerResponse response) { + LOGGER.fine("getEmployeeById"); + + String id = request.path().pathParameters().get("id"); + // If invalid, response handled in isValidId. Keeping DRY + if (isValidQueryStr(response, id)) { + employees.getById(id) + .ifPresentOrElse(response::send, () -> response.status(404).send()); + } + } + + /** + * Saves a new employee. + * + * @param request the server request + * @param response the server response + */ + private void save(ServerRequest request, ServerResponse response) { + LOGGER.fine("save"); + + Employee employee = request.content().as(Employee.class); + employees.save(Employee.of(null, + employee.getFirstName(), + employee.getLastName(), + employee.getEmail(), + employee.getPhone(), + employee.getBirthDate(), + employee.getTitle(), + employee.getDepartment())); + response.status(201).send(); + } + + /** + * Updates an existing employee. + * + * @param request the server request + * @param response the server response + */ + private void update(ServerRequest request, ServerResponse response) { + LOGGER.fine("update"); + + String id = request.path().pathParameters().get("id"); + if (isValidQueryStr(response, id)) { + if (employees.update(request.content().as(Employee.class), id) == 0) { + response.status(404).send(); + } else { + response.status(204).send(); + } + } + } + + /** + * Deletes an existing employee. + * + * @param request the server request + * @param response the server response + */ + private void delete(ServerRequest request, ServerResponse response) { + LOGGER.fine("delete"); + + String id = request.path().pathParameters().get("id"); + if (isValidQueryStr(response, id)) { + if (employees.deleteById(id) == 0) { + response.status(404).send(); + } else { + response.status(204).send(); + } + } + } + + /** + * Validates the parameter. + * + * @param response the server response + * @param name employee name + * @return true if valid, false otherwise + */ + private boolean isValidQueryStr(ServerResponse response, String name) { + Map errorMessage = new HashMap<>(); + if (name == null || name.isEmpty() || name.length() > 100) { + errorMessage.put("errorMessage", "Invalid query string"); + response.status(400).send(errorMessage); + return false; + } else { + return true; + } + } +} diff --git a/examples/employee-app/src/main/java/io/helidon/examples/employee/Main.java b/examples/employee-app/src/main/java/io/helidon/examples/employee/Main.java new file mode 100644 index 000000000..77b8d0929 --- /dev/null +++ b/examples/employee-app/src/main/java/io/helidon/examples/employee/Main.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.employee; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.staticcontent.StaticContentService; + +/** + * Simple Employee rest application. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(final String[] args) { + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build().start(); + System.out.printf(""" + WEB server is up! + Web client at: http://localhost:%1$d/public/index.html + """, server.port()); + } + + /** + * Set up the server. + * + * @param server server builder + */ + static void setup(WebServerConfig.Builder server) { + + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + + // Get webserver config from the "server" section of application.yaml and JSON support registration + server.config(config.get("server")) + .routing(r -> routing(r, config)); + } + + /** + * Setup routing. + * + * @param routing routing builder + * @param config configuration of this server + */ + static void routing(HttpRouting.Builder routing, Config config) { + routing.register("/public", StaticContentService.builder("public") + .welcomeFileName("index.html")) + .register("/employees", new EmployeeService(config)); + } + +} diff --git a/examples/employee-app/src/main/java/io/helidon/examples/employee/package-info.java b/examples/employee-app/src/main/java/io/helidon/examples/employee/package-info.java new file mode 100644 index 000000000..aa7f6ef0c --- /dev/null +++ b/examples/employee-app/src/main/java/io/helidon/examples/employee/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Employee example application + *

+ * Start with {@link io.helidon.examples.employee.Main} class. + * + * @see io.helidon.examples.employee.Main + */ +package io.helidon.examples.employee; diff --git a/examples/employee-app/src/main/resources/application.yaml b/examples/employee-app/src/main/resources/application.yaml new file mode 100644 index 000000000..946d4854a --- /dev/null +++ b/examples/employee-app/src/main/resources/application.yaml @@ -0,0 +1,29 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + # database user, such as "system" + user: + # database password + password: + # host url - remove to use the default of Oracle XE (localhost:1521/XE) + hosturl: :/. + # switch to "Database" to use database. Uses in-memory structure otherwise + drivertype: Array + +server: + port: 8080 + host: 0.0.0.0 diff --git a/examples/employee-app/src/main/resources/employees.json b/examples/employee-app/src/main/resources/employees.json new file mode 100644 index 000000000..bce81a67c --- /dev/null +++ b/examples/employee-app/src/main/resources/employees.json @@ -0,0 +1,402 @@ +[ + { + "id": "84240085-7c68-4930-8fb3-2f9be11b6810", + "firstName": "Hugh", + "lastName": "Jast", + "email": "Hugh.Jast@example.com", + "phone": "730-715-4446", + "birthDate": "1970-11-28", + "title": "National Data Strategist", + "department": "Mobility" + }, + { + "id": "f9971d4f-5b30-4553-8d5c-2116f9a58264", + "firstName": "Toy", + "lastName": "Herzog", + "email": "Toy.Herzog@example.com", + "phone": "769-569-1789", + "birthDate": "1961-08-08", + "title": "Dynamic Operations Manager", + "department": "Paradigm" + }, + { + "id": "5b8a19f4-8ccf-49b2-9d68-9644c29eb0f0", + "firstName": "Reed", + "lastName": "Hahn", + "email": "Reed.Hahn@example.com", + "phone": "429-071-2018", + "birthDate": "1977-02-05", + "title": "Future Directives Facilitator", + "department": "Quality" + }, + { + "id": "07700c37-a418-4762-9d7e-831dc1ea797e", + "firstName": "Novella", + "lastName": "Bahringer", + "email": "Novella.Bahringer@example.com", + "phone": "293-596-3547", + "birthDate": "1961-07-25", + "title": "Principal Factors Architect", + "department": "Division" + }, + { + "id": "11c9cf10-fbbd-4ffa-8ef6-038a4bce9713", + "firstName": "Zora", + "lastName": "Sawayn", + "email": "Zora.Sawayn@example.com", + "phone": "923-814-0502", + "birthDate": "1978-03-18", + "title": "Dynamic Marketing Designer", + "department": "Security" + }, + { + "id": "19737839-97a8-4b07-b52b-828db9f98e0a", + "firstName": "Cordia", + "lastName": "Willms", + "email": "Cordia.Willms@example.com", + "phone": "778-821-3941", + "birthDate": "1989-03-31", + "title": "Human Division Representative", + "department": "Optimization" + }, + { + "id": "3e8822ea-6a3d-4855-9e79-d918c7733ec5", + "firstName": "Clair", + "lastName": "Bartoletti", + "email": "Clair.Bartoletti@example.com", + "phone": "647-896-8993", + "birthDate": "1986-01-04", + "title": "Customer Marketing Executive", + "department": "Factors" + }, + { + "id": "2937368c-2dd9-4e86-908c-4a39a27bde39", + "firstName": "Joe", + "lastName": "Beahan", + "email": "Joe.Beahan@example.com", + "phone": "548-890-6181", + "birthDate": "1990-07-11", + "title": "District Group Specialist", + "department": "Program" + }, + { + "id": "e74f77d9-69f2-48ac-8a7f-b90a70bdeeea", + "firstName": "Daphney", + "lastName": "Feest", + "email": "Daphney.Feest@example.com", + "phone": "143-967-7041", + "birthDate": "1984-03-26", + "title": "Dynamic Mobility Consultant", + "department": "Metrics" + }, + { + "id": "154df9ce-1e86-47e1-a770-306a26cd62e9", + "firstName": "Janessa", + "lastName": "Wyman", + "email": "Janessa.Wyman@example.com", + "phone": "498-186-9009", + "birthDate": "1985-09-06", + "title": "Investor Brand Associate", + "department": "Markets" + }, + { + "id": "92c8b250-786d-47eb-a7f5-eabe3d6e39c2", + "firstName": "Mara", + "lastName": "Roob", + "email": "Mara.Roob@example.com", + "phone": "962-540-9884", + "birthDate": "1975-05-11", + "title": "Legacy Assurance Engineer", + "department": "Usability" + }, + { + "id": "9e046988-c561-417f-9696-f14608c00bd4", + "firstName": "Brennon", + "lastName": "Bernhard", + "email": "Brennon.Bernhard@example.com", + "phone": "395-224-9853", + "birthDate": "1963-12-05", + "title": "Product Web Officer", + "department": "Interactions" + }, + { + "id": "490a1262-15a7-4606-84ae-6e02f6671c13", + "firstName": "Amya", + "lastName": "Bernier", + "email": "Amya.Bernier@example.com", + "phone": "563-562-4171", + "birthDate": "1972-06-23", + "title": "Global Tactics Planner", + "department": "Program" + }, + { + "id": "60bad2bd-a71d-4494-bff1-09940809c51c", + "firstName": "Claud", + "lastName": "Boehm", + "email": "Claud.Boehm@example.com", + "phone": "089-073-7399", + "birthDate": "1965-02-27", + "title": "National Marketing Associate", + "department": "Directives" + }, + { + "id": "0f48efc9-a820-42c0-9d8f-2ae962d0ce7b", + "firstName": "Nyah", + "lastName": "Schowalter", + "email": "Nyah.Schowalter@example.com", + "phone": "823-063-7120", + "birthDate": "1969-02-19", + "title": "Dynamic Communications Assistant", + "department": "Metrics" + }, + { + "id": "3323fc7a-dcb9-4cfe-9e59-4160290c1ccc", + "firstName": "Imogene", + "lastName": "Bernhard", + "email": "Imogene.Bernhard@example.com", + "phone": "747-970-6062", + "birthDate": "1958-02-09", + "title": "Dynamic Assurance Supervisor", + "department": "Paradigm" + }, + { + "id": "44c5fe14-d1a3-416a-aab8-f569c3ed2eca", + "firstName": "Chanel", + "lastName": "Kuhlman", + "email": "Chanel.Kuhlman@example.com", + "phone": "882-145-9513", + "birthDate": "1985-03-03", + "title": "District Paradigm Representative", + "department": "Integration" + }, + { + "id": "8235e91e-3024-47f8-afd8-577617cfb8bf", + "firstName": "Cierra", + "lastName": "Morar", + "email": "Cierra.Morar@example.com", + "phone": "273-607-2209", + "birthDate": "1965-01-25", + "title": "Dynamic Data Planner", + "department": "Paradigm" + }, + { + "id": "5733bed9-ce41-4ed5-8b29-9288318dd5a7", + "firstName": "Faye", + "lastName": "Grimes", + "email": "Faye.Grimes@example.com", + "phone": "750-139-1344", + "birthDate": "1962-08-21", + "title": "Central Applications Analyst", + "department": "Tactics" + }, + { + "id": "34956311-26fb-46b1-abf0-a95a08d929ea", + "firstName": "Doyle", + "lastName": "Rohan", + "email": "Doyle.Rohan@example.com", + "phone": "093-457-5621", + "birthDate": "1991-11-29", + "title": "Corporate Accountability Supervisor", + "department": "Applications" + }, + { + "id": "2e121065-431a-4ba2-aeb4-3919fcb7969a", + "firstName": "Jonathan", + "lastName": "Barrows", + "email": "Jonathan.Barrows@example.com", + "phone": "262-503-2161", + "birthDate": "1963-12-15", + "title": "Regional Configuration Liason", + "department": "Implementation" + }, + { + "id": "99d0603f-03cc-4dca-bebe-a711eaf14d77", + "firstName": "Myriam", + "lastName": "Luettgen", + "email": "Myriam.Luettgen@example.com", + "phone": "951-924-8295", + "birthDate": "1962-02-08", + "title": "Central Functionality Specialist", + "department": "Accountability" + }, + { + "id": "50653f57-3379-4bfd-9999-2ec86ab1131d", + "firstName": "Johnnie", + "lastName": "Schiller", + "email": "Johnnie.Schiller@example.com", + "phone": "534-025-2200", + "birthDate": "1965-04-11", + "title": "Principal Creative Developer", + "department": "Interactions" + }, + { + "id": "d93c7987-69c8-4333-99fe-d1561b338722", + "firstName": "Laila", + "lastName": "White", + "email": "Laila.White@example.com", + "phone": "468-939-2298", + "birthDate": "1956-01-04", + "title": "Corporate Optimization Engineer", + "department": "Assurance" + }, + { + "id": "ac90bb96-79e1-424a-94f6-90f7c01ea4b3", + "firstName": "Alessandra", + "lastName": "Becker", + "email": "Alessandra.Becker@example.com", + "phone": "081-724-0866", + "birthDate": "1984-08-12", + "title": "Central Identity Associate", + "department": "Quality" + }, + { + "id": "215f86fb-8434-4b75-8cc2-4bf731b71f6f", + "firstName": "Shannon", + "lastName": "McCullough", + "email": "Shannon.McCullough@example.com", + "phone": "101-995-1089", + "birthDate": "1989-02-25", + "title": "Global Data Engineer", + "department": "Division" + }, + { + "id": "f09f164c-7fc2-4afe-b9ae-29bc1c48b529", + "firstName": "Garnet", + "lastName": "Labadie", + "email": "Garnet.Labadie@example.com", + "phone": "147-954-6624", + "birthDate": "1990-01-01", + "title": "Senior Communications Producer", + "department": "Program" + }, + { + "id": "b16d7d13-e4ee-4293-bb2d-c79d98485ef0", + "firstName": "Mark", + "lastName": "Graham", + "email": "Mark.Graham@example.com", + "phone": "462-746-7388", + "birthDate": "1991-08-23", + "title": "Legacy Directives Agent", + "department": "Assurance" + }, + { + "id": "3ff0adc3-09e0-4490-900c-9d59dd622bd8", + "firstName": "Tristin", + "lastName": "Bayer", + "email": "Tristin.Bayer@example.com", + "phone": "882-044-3996", + "birthDate": "1964-03-26", + "title": "Internal Marketing Officer", + "department": "Intranet" + }, + { + "id": "c1e5e2ad-2ee9-4eb1-af9e-e9e17fe694bc", + "firstName": "Maurice", + "lastName": "Renner", + "email": "Maurice.Renner@example.com", + "phone": "383-435-0943", + "birthDate": "1973-11-05", + "title": "National Accountability Planner", + "department": "Accounts" + }, + { + "id": "06a0ac23-ff09-4f27-971d-1b901e5ff1c8", + "firstName": "Preston", + "lastName": "Stark", + "email": "Preston.Stark@example.com", + "phone": "080-698-9552", + "birthDate": "1994-02-02", + "title": "Corporate Program Orchestrator", + "department": "Integration" + }, + { + "id": "2443a803-39ec-435b-9bda-1bfdee716308", + "firstName": "Mabelle", + "lastName": "Herman", + "email": "Mabelle.Herman@example.com", + "phone": "778-672-2787", + "birthDate": "1956-11-30", + "title": "Human Identity Officer", + "department": "Integration" + }, + { + "id": "45d87394-7fb2-430a-9b65-714b5266b8a1", + "firstName": "Juvenal", + "lastName": "Swaniawski", + "email": "Juvenal.Swaniawski@example.com", + "phone": "349-906-2745", + "birthDate": "1963-11-17", + "title": "Future Program Planner", + "department": "Response" + }, + { + "id": "f0b4ec27-af12-4694-aee2-77a620c6fb5e", + "firstName": "Rachelle", + "lastName": "Okuneva", + "email": "Rachelle.Okuneva@example.com", + "phone": "134-565-3868", + "birthDate": "1992-05-27", + "title": "District Creative Architect", + "department": "Paradigm" + }, + { + "id": "7c4dc3be-6141-4ce2-8513-cee11e2708d1", + "firstName": "Macey", + "lastName": "Weissnat", + "email": "Macey.Weissnat@example.com", + "phone": "210-461-3749", + "birthDate": "1978-06-24", + "title": "Product Accountability Facilitator", + "department": "Data" + }, + { + "id": "c4282cf5-f10b-4752-b201-a1f6cb469212", + "firstName": "Ena", + "lastName": "Gerlach", + "email": "Ena.Gerlach@example.com", + "phone": "429-925-7634", + "birthDate": "1976-04-09", + "title": "Human Tactics Agent", + "department": "Creative" + }, + { + "id": "2b32bbbd-c4bb-426c-a759-1664f40db4c8", + "firstName": "Darrick", + "lastName": "Deckow", + "email": "Darrick.Deckow@example.com", + "phone": "549-222-4121", + "birthDate": "1956-10-25", + "title": "Lead Solutions Producer", + "department": "Metrics" + }, + { + "id": "65389ce1-d930-48ae-9347-49741496dc4e", + "firstName": "Palma", + "lastName": "Torp", + "email": "Palma.Torp@example.com", + "phone": "346-556-3517", + "birthDate": "1967-06-24", + "title": "Product Infrastructure Consultant", + "department": "Response" + }, + { + "id": "9cbaf350-0e00-4aaf-84a4-b348f75257ca", + "firstName": "Lucious", + "lastName": "Steuber", + "email": "Lucious.Steuber@example.com", + "phone": "977-372-2840", + "birthDate": "1961-11-24", + "title": "District Creative Supervisor", + "department": "Mobility" + }, + { + "id": "72de2181-3f3d-4b0d-a528-bc60a1aa0a12", + "firstName": "Kacey", + "lastName": "Kilback", + "email": "Kacey.Kilback@example.com", + "phone": "268-777-2011", + "birthDate": "1957-09-06", + "title": "Corporate Mobility Agent", + "department": "Infrastructure" + } +] \ No newline at end of file diff --git a/examples/employee-app/src/main/resources/logging.properties b/examples/employee-app/src/main/resources/logging.properties new file mode 100644 index 000000000..110246353 --- /dev/null +++ b/examples/employee-app/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/employee-app/src/main/resources/public/EmployeeController.js b/examples/employee-app/src/main/resources/public/EmployeeController.js new file mode 100644 index 000000000..26d2d0e56 --- /dev/null +++ b/examples/employee-app/src/main/resources/public/EmployeeController.js @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var server = "/"; + +function search (){ + var searchTerm = $("#searchText").val().trim(); + if (searchTerm != "") { + $("#people").show(); + $("#people").html("SEARCHING..."); + $.ajax({ + url: server + "employees/" + + $("#searchType").val() + "/" + + encodeURIComponent(searchTerm), + method: "GET" + }).done( + function(data) { + $("#people").empty(); + $("#people").hide(); + if (data.length == 0) { + $("#people").html(""); + $("#notFound").show(); + $("#notFound").html("No people found matching your search criteria"); + } else { + showResults(data); + } + $("#people").show(400, "swing"); + }); + } else { + loadEmployees(); + } +} + +$(function() { + $("#searchText").on("keyup", function(e) { + if (e.keyCode == 13) { + search (); + } + }); +}); + +function showResults(data){ + $("#people").hide(); + $("#people").empty(); + $("#notFound").hide(); + data.forEach(function(employee) { + var item = $(renderEmployees(employee)); + item.on("click", function() { + var detailItem = $(renderDetailEmployee(employee)); + $("#home").hide(); + $("#detail").empty(); + $("#notFound").hide(); + $("#detail").append(detailItem); + $("#people").hide( + 400, + "swing", + function() { + $("#detail").show() + }); + }); + $("#people").append(item); + }); +} + +function showEmployeeForm() { + $("#notFound").hide(); + $("#editForm").hide(); + $("#deleteButton").hide(); + $("#employeeForm").show(); + $("#formTitle").text("Add Employee"); + $("#home").hide(); + $("#people").hide(); +} + +function loadEmployees() { + $("#notFound").hide(); + $("#searchText").val(""); + $("#employeeForm").hide(); + $("#editForm").hide(); + $("#home").show(); + $("#people").show(); + $("#people").html("LOADING..."); + $.ajax({ + dataType: "json", + url: server + "employees", + method: "GET" + }).done(function(data) { + showResults(data); + $("#people").show(400, "swing"); + }); +} + + +function renderEmployees(employee){ + var template = $('#employees_tpl').html(); + Mustache.parse(template); + var rendered = Mustache.render(template, { + "firstName" : employee.firstName, + "lastName" : employee.lastName, + "title" : employee.title, + "department" : employee.department + }); + return rendered; +} + +function renderDetailEmployee(employee){ + var template = $('#detail_tpl').html(); + Mustache.parse(template); + var rendered = Mustache.render(template,{ + "id" : employee.id, + "firstName" : employee.firstName, + "lastName" : employee.lastName, + "email" : employee.email, + "birthDate" : employee.birthDate, + "phone" : employee.phone, + "title" : employee.title, + "department" : employee.department + }); + return rendered; +} + +function save() { + var employee = { + id: "", + firstName: $("#firstName").val(), + lastName: $("#lastName").val(), + email: $("#email").val(), + phone: $("#phone").val(), + birthDate: $("#birthDate").val(), + title: $("#title").val(), + department: $("#department").val() + }; + $.ajax({ + url: server + "employees", + method: "POST", + data: JSON.stringify(employee) + }).done(function(data) { + $("#detail").hide(); + $("#firstName").val(""); + $("#lastName").val(""); + $("#email").val(""); + $("#phone").val(""); + $("#birthDate").val(""); + $("#title").val(""); + $("#department").val(""); + loadEmployees(); + }); + +} + +function updateEmployee() { + var employee = { + id: $("#editId").val(), + firstName: $("#editFirstName").val(), + lastName: $("#editLastName").val(), + email: $("#editEmail").val(), + phone: $("#editPhone").val(), + birthDate: $("#editBirthDate").val(), + title: $("#editTitle").val(), + department: $("#editDepartment").val() + }; + $("#detail").html("UPDATING..."); + $.ajax({ + url: server + "employees/" + employee.id, + method: "PUT", + data: JSON.stringify(employee) + }).done(function(data) { + $("#detail").hide(); + loadEmployees(); + }); +} + +function deleteEmployee() { + var employee = { + firstName: $("#editFirstName").val(), + lastName: $("#editLastName").val(), + id: $("#editId").val() + }; + $('

').dialog({ + modal: true, + title: "Confirm Delete", + open: function() { + var markup = 'Are you sure you want to delete ' + + employee.firstName + ' ' + employee.lastName + + " employee?"; + $(this).html(markup); + }, + buttons: { + Ok: function() { + $("#detail").html("DELETING..."); + $(this).dialog("close"); + $.ajax({ + url: server + "employees/" + employee.id, + method: "DELETE" + }).done(function(data) { + $("#detail").hide(); + loadEmployees(); + }); + }, + Cancel: function() { + $(this).dialog("close"); + } + } + }); + +} diff --git a/examples/employee-app/src/main/resources/public/index.html b/examples/employee-app/src/main/resources/public/index.html new file mode 100644 index 000000000..7351e33c0 --- /dev/null +++ b/examples/employee-app/src/main/resources/public/index.html @@ -0,0 +1,399 @@ + + + + + + + + + + + + + + + + + + + + + + +

Cloud Employee App

+ + +
+
+
LOADING...
+
+
+ + + + diff --git a/examples/employee-app/src/main/resources/public/nopic.png b/examples/employee-app/src/main/resources/public/nopic.png new file mode 100644 index 000000000..5967653cf Binary files /dev/null and b/examples/employee-app/src/main/resources/public/nopic.png differ diff --git a/examples/employee-app/src/test/java/io/helidon/examples/employee/MainTest.java b/examples/employee-app/src/test/java/io/helidon/examples/employee/MainTest.java new file mode 100644 index 000000000..60ba7eb89 --- /dev/null +++ b/examples/employee-app/src/test/java/io/helidon/examples/employee/MainTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.employee; + +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.DirectClient; +import io.helidon.webserver.testing.junit5.RoutingTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import jakarta.json.JsonArray; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@RoutingTest +public class MainTest { + + private final DirectClient client; + + public MainTest(DirectClient client) { + this.client = client; + } + + @SetUpRoute + public static void setup(HttpRouting.Builder routing) { + Main.routing(routing, Config.empty()); + } + + @Test + public void testEmployees() { + try (Http1ClientResponse response = client.get("/employees") + .request()) { + assertThat("HTTP response2", response.status(), is(Status.OK_200)); + assertThat(response.as(JsonArray.class).size(), is(40)); + } + } + +} diff --git a/examples/graphql/basics/README.md b/examples/graphql/basics/README.md new file mode 100644 index 000000000..133e06f71 --- /dev/null +++ b/examples/graphql/basics/README.md @@ -0,0 +1,35 @@ +# Helidon GraphQL Basic Example + +This example shows the basics of using Helidon SE GraphQL. The example +manually creates a GraphQL Schema using the [GraphQL Java](https://github.com/graphql-java/graphql-java) API. + +## Build and run + +Start the application: + +```shell +mvn package +java -jar target/helidon-examples-graphql-basics.jar +``` + +Note the port number reported by the application. + +Probe the GraphQL endpoints: + +1. Hello word endpoint: + + ```shell + export PORT='41667' + curl -X POST http://127.0.0.1:${PORT}/graphql -d '{"query":"query { hello }"}' + + #"data":{"hello":"world"}} + ``` + +1. Hello in different languages + + ```shell + export PORT='41667' + curl -X POST http://127.0.0.1:${PORT}/graphql -d '{"query":"query { helloInDifferentLanguages }"}' + + #{"data":{"helloInDifferentLanguages":["Bonjour","Hola","Zdravstvuyte","Nǐn hǎo","Salve","Gudday","Konnichiwa","Guten Tag"]}} + ``` diff --git a/examples/graphql/basics/pom.xml b/examples/graphql/basics/pom.xml new file mode 100644 index 000000000..0797fc6da --- /dev/null +++ b/examples/graphql/basics/pom.xml @@ -0,0 +1,69 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.graphql + helidon-examples-graphql-basics + 1.0.0-SNAPSHOT + Helidon Examples GraphQL Basics + + + Basic usage of GraphQL in helidon SE + + + + io.helidon.examples.graphql.basics.Main + + + + + io.helidon.webserver + helidon-webserver-graphql + + + io.helidon.webserver + helidon-webserver + + + io.helidon.common + helidon-common + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + + diff --git a/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/Main.java b/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/Main.java new file mode 100644 index 000000000..1be76d499 --- /dev/null +++ b/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/Main.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.graphql.basics; + +import java.util.List; + +import io.helidon.webserver.WebServer; +import io.helidon.webserver.graphql.GraphQlService; + +import graphql.schema.DataFetcher; +import graphql.schema.GraphQLSchema; +import graphql.schema.StaticDataFetcher; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; + +/** + * Main class of Graphql SE integration example. + */ +@SuppressWarnings("SpellCheckingInspection") +public class Main { + + private Main() { + } + + /** + * Start the example. Prints endpoints to standard output. + * + * @param args not used + */ + public static void main(String[] args) { + WebServer server = WebServer.builder() + .routing(routing -> routing + .register(GraphQlService.create(buildSchema()))) + .build(); + server.start(); + String endpoint = "http://localhost:" + server.port(); + System.out.printf(""" + GraphQL started on %1$s/graphql + GraphQL schema available on %1$s/graphql/schema.graphql + """, endpoint); + } + + /** + * Generate a {@link GraphQLSchema}. + * + * @return a {@link GraphQLSchema} + */ + private static GraphQLSchema buildSchema() { + String schema = """ + type Query{ + hello: String\s + helloInDifferentLanguages: [String]\s + + }"""; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + // DataFetcher to return various hello's in difference languages + DataFetcher> dataFetcher = environment -> + List.of("Bonjour", "Hola", "Zdravstvuyte", "Nǐn hǎo", "Salve", "Gudday", "Konnichiwa", "Guten Tag"); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type("Query", builder -> builder.dataFetcher("hello", new StaticDataFetcher("world"))) + .type("Query", builder -> builder.dataFetcher("helloInDifferentLanguages", dataFetcher)) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + } +} diff --git a/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/package-info.java b/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/package-info.java new file mode 100644 index 000000000..23b3ca705 --- /dev/null +++ b/examples/graphql/basics/src/main/java/io/helidon/examples/graphql/basics/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Example of healthchecks in helidon SE. + */ +package io.helidon.examples.graphql.basics; diff --git a/examples/graphql/pom.xml b/examples/graphql/pom.xml new file mode 100644 index 000000000..5df98496d --- /dev/null +++ b/examples/graphql/pom.xml @@ -0,0 +1,36 @@ + + + + + 4.0.0 + + helidon-examples-project + io.helidon.examples + 1.0.0-SNAPSHOT + + io.helidon.examples.graphql + helidon-examples-graphql-project + 1.0.0-SNAPSHOT + pom + Helidon Examples GraphQL + + + basics + + diff --git a/examples/health/basics/README.md b/examples/health/basics/README.md new file mode 100644 index 000000000..17c690051 --- /dev/null +++ b/examples/health/basics/README.md @@ -0,0 +1,23 @@ +# Helidon Health Basic Example + +This example shows the basics of using Helidon SE Health. It uses the +set of built-in health checks that Helidon provides plus defines a +custom health check. + +## Build and run + +Start the application: + +```shell +mvn package +java -jar target/helidon-examples-health-basics.jar +``` + +Note the port number reported by the application. + +Probe the health endpoints: + +```shell +curl -i -X GET http://localhost:8080/observe/health/ready +curl -i -X GET http://localhost:8080/observe/health/ +``` diff --git a/examples/health/basics/pom.xml b/examples/health/basics/pom.xml new file mode 100644 index 000000000..1d7c48b12 --- /dev/null +++ b/examples/health/basics/pom.xml @@ -0,0 +1,79 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.health + helidon-examples-health-basics + 1.0.0-SNAPSHOT + Helidon Examples Health Basics + + + Basic usage of health checks in helidon SE + + + + io.helidon.examples.health.basics.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.health + helidon-health-checks + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + + diff --git a/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java b/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java new file mode 100644 index 000000000..e3b5ef5d7 --- /dev/null +++ b/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.health.basics; + +import java.time.Duration; + +import io.helidon.health.HealthCheckResponse; +import io.helidon.health.HealthCheckType; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.health.HealthObserver; + +/** + * Main class of health check integration example. + */ +public final class Main { + + private static long serverStartTime; + + private Main() { + } + + /** + * Start the example. Prints endpoints to standard output. + * + * @param args not used + */ + public static void main(String[] args) { + serverStartTime = System.currentTimeMillis(); + + // load logging + LogConfig.configureRuntime(); + + ObserveFeature observe = ObserveFeature.builder() + .observersDiscoverServices(true) + .addObserver(HealthObserver.builder() + .details(true) + .useSystemServices(true) + .addCheck(() -> HealthCheckResponse.builder() + .status(HealthCheckResponse.Status.UP) + .detail("time", System.currentTimeMillis()) + .build(), HealthCheckType.READINESS) + .addCheck(() -> HealthCheckResponse.builder() + .status(isStarted()) + .detail("time", System.currentTimeMillis()) + .build(), HealthCheckType.STARTUP) + .build()) + .build(); + + WebServer server = WebServer.builder() + .featuresDiscoverServices(false) + .addFeature(observe) + .routing(Main::routing) + .port(8080) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + } + + /** + * Set up HTTP routing. + * This method is used from tests as well. + * + * @param router HTTP routing builder + */ + static void routing(HttpRouting.Builder router) { + router.get("/hello", (req, res) -> res.send("Hello World!")); + } + + private static boolean isStarted() { + return Duration.ofMillis(System.currentTimeMillis() - serverStartTime).getSeconds() >= 8; + } +} diff --git a/examples/health/basics/src/main/java/io/helidon/examples/health/basics/package-info.java b/examples/health/basics/src/main/java/io/helidon/examples/health/basics/package-info.java new file mode 100644 index 000000000..f59115091 --- /dev/null +++ b/examples/health/basics/src/main/java/io/helidon/examples/health/basics/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Example of healthchecks in helidon SE. + */ +package io.helidon.examples.health.basics; diff --git a/examples/health/pom.xml b/examples/health/pom.xml new file mode 100644 index 000000000..bb494a032 --- /dev/null +++ b/examples/health/pom.xml @@ -0,0 +1,36 @@ + + + + + 4.0.0 + + helidon-examples-project + io.helidon.examples + 1.0.0-SNAPSHOT + + io.helidon.examples.health + helidon-examples-health-project + 1.0.0-SNAPSHOT + pom + Helidon Examples Health + + + basics + + diff --git a/examples/integrations/README.md b/examples/integrations/README.md new file mode 100644 index 000000000..05fd10c9d --- /dev/null +++ b/examples/integrations/README.md @@ -0,0 +1 @@ +# Helidon Integrations Examples diff --git a/examples/integrations/cdi/README.md b/examples/integrations/cdi/README.md new file mode 100644 index 000000000..64fad7955 --- /dev/null +++ b/examples/integrations/cdi/README.md @@ -0,0 +1 @@ +# Helidon Integrations CDI Examples diff --git a/examples/integrations/cdi/datasource-hikaricp-h2/README.md b/examples/integrations/cdi/datasource-hikaricp-h2/README.md new file mode 100644 index 000000000..d630d6a6b --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-h2/README.md @@ -0,0 +1,19 @@ +# H2 Integration Example + +## Overview + +This example shows a trivial Helidon MicroProfile application that +uses the Hikari connection pool CDI integration and an H2 in-memory +database. + +## Build and run + +```shell +mvn package +java -jar target/helidon-integrations-examples-datasource-hikaricp-h2.jar +``` + +Try the endpoint: +```shell +curl http://localhost:8080/tables +``` diff --git a/examples/integrations/cdi/datasource-hikaricp-h2/pom.xml b/examples/integrations/cdi/datasource-hikaricp-h2/pom.xml new file mode 100644 index 000000000..9f1f3275e --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-h2/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.integrations.cdi + helidon-integrations-examples-datasource-hikaricp-h2 + 1.0.0-SNAPSHOT + Helidon Examples CDI Extensions DataSource/HikariCP H2 + + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + compile + + + jakarta.ws.rs + jakarta.ws.rs-api + compile + + + org.eclipse.microprofile.config + microprofile-config-api + compile + + + + + com.h2database + h2 + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-datasource-hikaricp + runtime + + + io.smallrye + jandex + runtime + true + + + io.helidon.microprofile.server + helidon-microprofile-server + runtime + + + io.helidon.microprofile.config + helidon-microprofile-config + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/integrations/cdi/datasource-hikaricp-h2/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/TablesResource.java b/examples/integrations/cdi/datasource-hikaricp-h2/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/TablesResource.java new file mode 100644 index 000000000..4410a44cd --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-h2/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/TablesResource.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.datasource.hikaricp.jaxrs; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; + +import javax.sql.DataSource; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * A JAX-RS resource class in {@linkplain ApplicationScoped + * application scope} rooted at {@code /tables}. + * + * @see #get() + */ +@Path("/tables") +@ApplicationScoped +public class TablesResource { + + private final DataSource dataSource; + + /** + * Creates a new {@link TablesResource}. + * + * @param dataSource the {@link DataSource} to use to acquire + * database table names; must not be {@code null} + * + * @exception NullPointerException if {@code dataSource} is {@code + * null} + */ + @Inject + public TablesResource(@Named("example") final DataSource dataSource) { + super(); + this.dataSource = Objects.requireNonNull(dataSource); + } + + /** + * Returns a {@link Response} which, if successful, contains a + * newline-separated list of Oracle database table names. + * + *

This method never returns {@code null}.

+ * + * @return a non-{@code null} {@link Response} + * + * @exception SQLException if a database error occurs + */ + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response get() throws SQLException { + final StringBuilder sb = new StringBuilder(); + try (Connection connection = this.dataSource.getConnection(); + PreparedStatement ps = + connection.prepareStatement(" SELECT TABLE_NAME" + + " FROM INFORMATION_SCHEMA.TABLES " + + "ORDER BY TABLE_NAME ASC"); + ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + sb.append(rs.getString(1)).append("\n"); + } + } + final Response returnValue = Response.ok() + .entity(sb.toString()) + .build(); + return returnValue; + } + +} diff --git a/examples/integrations/cdi/datasource-hikaricp-h2/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/package-info.java b/examples/integrations/cdi/datasource-hikaricp-h2/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/package-info.java new file mode 100644 index 000000000..1f72fcff0 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-h2/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides JAX-RS-related classes and interfaces for this example + * project. + */ +package io.helidon.examples.integrations.datasource.hikaricp.jaxrs; diff --git a/examples/integrations/cdi/datasource-hikaricp-h2/src/main/resources/META-INF/beans.xml b/examples/integrations/cdi/datasource-hikaricp-h2/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..a0938bff7 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-h2/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/cdi/datasource-hikaricp-h2/src/main/resources/META-INF/microprofile-config.properties b/examples/integrations/cdi/datasource-hikaricp-h2/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..dcb470b29 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-h2/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Default database properties. +javax.sql.DataSource.example.dataSourceClassName = org.h2.jdbcx.JdbcDataSource +javax.sql.DataSource.example.dataSource.url = jdbc:h2:mem:sample +javax.sql.DataSource.example.dataSource.user = sa +javax.sql.DataSource.example.dataSource.password = + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 diff --git a/examples/integrations/cdi/datasource-hikaricp-mysql/README.md b/examples/integrations/cdi/datasource-hikaricp-mysql/README.md new file mode 100644 index 000000000..f1497e866 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-mysql/README.md @@ -0,0 +1,60 @@ +# MySQL Integration Example + +## Overview + +This example shows a trivial Helidon MicroProfile application that +uses the MySQL CDI integration. It also shows how to run MySQL in a +Docker container and connect to it using the application. + +## Notes + +To run MySQL's `mysql:8` Docker image in a Docker container named +`mysql` that publishes its port 3306 to the host machine's port 3306 +and uses `tiger` as the MySQL root password and that will +automatically be removed when it is stopped: + +```sh +docker container run --rm -d -p 3306:3306 \ + --env MYSQL_ROOT_PASSWORD=tiger \ + --name mysql \ + mysql:8 +``` + +(Note that in the `3306:3306` option value above the first port number +is the port number on the host (i.e. your physical machine running +`docker`) and the second number (after the colon) is the port number +on the Docker container.) + +To ensure that the sample application is configured to talk to MySQL +running in this Docker container, verify that the following lines +(among others) are present in +`src/main/resources/META-INF/microprofile-config.properties`: + +```properties +javax.sql.DataSource.example.dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource +javax.sql.DataSource.example.dataSource.url = jdbc:mysql://localhost:3306 +javax.sql.DataSource.example.dataSource.user = root +javax.sql.DataSource.example.dataSource.password = tiger +``` + + +## Build and run + +```shell +mvn package +java -jar target/helidon-integrations-examples-datasource-hikaricp-mysql.jar +``` + +Try the endpoint: +```shell +curl http://localhost:8080/tables +``` + +Stop the docker container: +```shell +docker stop mysql +``` + +## References + +- [MySQL Docker documentation](https://hub.docker.com/_/mysql?tab=description) diff --git a/examples/integrations/cdi/datasource-hikaricp-mysql/pom.xml b/examples/integrations/cdi/datasource-hikaricp-mysql/pom.xml new file mode 100644 index 000000000..d45df7492 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-mysql/pom.xml @@ -0,0 +1,104 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.integrations.cdi + helidon-integrations-examples-datasource-hikaricp-mysql + 1.0.0-SNAPSHOT + Helidon Examples CDI Extensions DataSource/HikariCP MySQL + + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + compile + + + jakarta.ws.rs + jakarta.ws.rs-api + compile + + + org.eclipse.microprofile.config + microprofile-config-api + compile + + + + + com.mysql + mysql-connector-j + runtime + true + + + io.helidon.integrations.cdi + helidon-integrations-cdi-datasource-hikaricp + runtime + + + io.smallrye + jandex + runtime + true + + + io.helidon.microprofile.server + helidon-microprofile-server + runtime + + + io.helidon.microprofile.config + helidon-microprofile-config + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/TablesResource.java b/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/TablesResource.java new file mode 100644 index 000000000..4410a44cd --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/TablesResource.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.datasource.hikaricp.jaxrs; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; + +import javax.sql.DataSource; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * A JAX-RS resource class in {@linkplain ApplicationScoped + * application scope} rooted at {@code /tables}. + * + * @see #get() + */ +@Path("/tables") +@ApplicationScoped +public class TablesResource { + + private final DataSource dataSource; + + /** + * Creates a new {@link TablesResource}. + * + * @param dataSource the {@link DataSource} to use to acquire + * database table names; must not be {@code null} + * + * @exception NullPointerException if {@code dataSource} is {@code + * null} + */ + @Inject + public TablesResource(@Named("example") final DataSource dataSource) { + super(); + this.dataSource = Objects.requireNonNull(dataSource); + } + + /** + * Returns a {@link Response} which, if successful, contains a + * newline-separated list of Oracle database table names. + * + *

This method never returns {@code null}.

+ * + * @return a non-{@code null} {@link Response} + * + * @exception SQLException if a database error occurs + */ + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response get() throws SQLException { + final StringBuilder sb = new StringBuilder(); + try (Connection connection = this.dataSource.getConnection(); + PreparedStatement ps = + connection.prepareStatement(" SELECT TABLE_NAME" + + " FROM INFORMATION_SCHEMA.TABLES " + + "ORDER BY TABLE_NAME ASC"); + ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + sb.append(rs.getString(1)).append("\n"); + } + } + final Response returnValue = Response.ok() + .entity(sb.toString()) + .build(); + return returnValue; + } + +} diff --git a/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/package-info.java b/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/package-info.java new file mode 100644 index 000000000..1f72fcff0 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides JAX-RS-related classes and interfaces for this example + * project. + */ +package io.helidon.examples.integrations.datasource.hikaricp.jaxrs; diff --git a/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/resources/META-INF/beans.xml b/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..a0938bff7 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/resources/META-INF/microprofile-config.properties b/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..2d984f717 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp-mysql/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Default database properties. +javax.sql.DataSource.example.dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource +javax.sql.DataSource.example.dataSource.url = jdbc:mysql://localhost:3306 +javax.sql.DataSource.example.dataSource.user = root +javax.sql.DataSource.example.dataSource.password = tiger + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 diff --git a/examples/integrations/cdi/datasource-hikaricp/.dockerignore b/examples/integrations/cdi/datasource-hikaricp/.dockerignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp/.dockerignore @@ -0,0 +1 @@ +target/ diff --git a/examples/integrations/cdi/datasource-hikaricp/Dockerfile b/examples/integrations/cdi/datasource-hikaricp/Dockerfile new file mode 100644 index 000000000..5caee647f --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp/Dockerfile @@ -0,0 +1,53 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -DskipTests +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-examples-integrations-datasource-hikaricp.jar ./ +COPY --from=build /helidon/target/libs ./libs + +CMD [ "java", "-jar", "helidon-examples-integrations-datasource-hikaricp.jar" ] + +EXPOSE 8080 diff --git a/examples/integrations/cdi/datasource-hikaricp/README.md b/examples/integrations/cdi/datasource-hikaricp/README.md new file mode 100644 index 000000000..cb72a0dee --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp/README.md @@ -0,0 +1,85 @@ +# Hikari Connection Pool Integration Example + +## Overview + +This example shows a trivial Helidon MicroProfile application that +uses the Hikari connection pool CDI integration. It also shows how to +run the Oracle database in a Docker container and connect the +application to it. + +## Prerequisites + +You'll need an Oracle account in order to log in to the Oracle +Container Registry. The Oracle Container Registry is where the Docker +image housing the Oracle database is located. To set up an Oracle +account if you don't already have one, see +[the Oracle account creation website](https://profile.oracle.com/myprofile/account/create-account.jspx). + +## Notes + +To log in to the Oracle Container Registry (which you will need to do +in order to download Oracle database Docker images from it): + +```shell +docker login -u username -p password container-registry.oracle.com +``` + +For more information on the Oracle Container Registry, please visit +its [website](https://container-registry.oracle.com/). + +To run Oracle's `database/standard` Docker image in a Docker container +named `oracle` that publishes ports 1521 and 5500 to +the host while relying on the defaults for all other settings: + +```shell +docker container run -d -it -p 1521:1521 -p 5500:5500 --shm-size=3g \ + --name oracle \ + container-registry.oracle.com/database/standard:latest +``` + +It will take about ten minutes before the database is ready. + +For more information on the Oracle database image used by this +example, you can visit the relevant section of the + [Oracle Container Registry website](https://container-registry.oracle.com/). + +To ensure that the sample application is configured to talk to the +Oracle database running in this Docker container, verify that the +following lines (among others) are present in +`src/main/resources/META-INF/microprofile-config.properties`: + +```properties +javax.sql.DataSource.example.dataSourceClassName=oracle.jdbc.pool.OracleDataSource +javax.sql.DataSource.example.dataSource.url = jdbc:oracle:thin:@localhost:1521:ORCL +javax.sql.DataSource.example.dataSource.user = sys as sysoper +javax.sql.DataSource.example.dataSource.password = Oracle +``` + +## Build and run + +With Docker: +```shell +docker build -t helidon-examples-integrations-datasource-hikaricp . +docker run --rm -d \ + --link oracle \ + -e javax_sql_DataSource_example_dataSource_url="jdbc:oracle:thin:@oracle:1521:ORCL" \ + --name helidon-examples-integrations-datasource-hikaricp \ + -p 8080:8080 helidon-examples-integrations-datasource-hikaricp:latest +``` +OR + +With Maven: +```shell +mvn package +java -jar target/helidon-examples-integrations-datasource-hikaricp.jar +``` + +Try the endpoint: +```shell +curl http://localhost:8080/tables +``` + +Stop the docker containers: +```shell +docker stop oracle helidon-examples-integrations-datasource-hikaricp +``` diff --git a/examples/integrations/cdi/datasource-hikaricp/pom.xml b/examples/integrations/cdi/datasource-hikaricp/pom.xml new file mode 100644 index 000000000..6e0d78b80 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.integrations.cdi + helidon-examples-integrations-datasource-hikaricp + 1.0.0-SNAPSHOT + Helidon Examples CDI Extensions DataSource/HikariCP + + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + compile + + + jakarta.ws.rs + jakarta.ws.rs-api + compile + + + org.eclipse.microprofile.config + microprofile-config-api + compile + + + + + io.helidon.integrations.db + ojdbc + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-datasource-hikaricp + runtime + + + io.smallrye + jandex + runtime + true + + + io.helidon.microprofile.server + helidon-microprofile-server + runtime + + + io.helidon.microprofile.config + helidon-microprofile-config + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/TablesResource.java b/examples/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/TablesResource.java new file mode 100644 index 000000000..52ff77118 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/TablesResource.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.datasource.hikaricp.jaxrs; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; + +import javax.sql.DataSource; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * A JAX-RS resource class in {@linkplain ApplicationScoped + * application scope} rooted at {@code /tables}. + * + * @see #get() + */ +@Path("/tables") +@ApplicationScoped +public class TablesResource { + + private final DataSource dataSource; + + /** + * Creates a new {@link TablesResource}. + * + * @param dataSource the {@link DataSource} to use to acquire + * database table names; must not be {@code null} + * + * @exception NullPointerException if {@code dataSource} is {@code + * null} + */ + @Inject + public TablesResource(@Named("example") final DataSource dataSource) { + super(); + this.dataSource = Objects.requireNonNull(dataSource); + } + + /** + * Returns a {@link Response} which, if successful, contains a + * newline-separated list of Oracle database table names. + * + *

This method never returns {@code null}.

+ * + * @return a non-{@code null} {@link Response} + * + * @exception SQLException if a database error occurs + */ + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response get() throws SQLException { + final StringBuilder sb = new StringBuilder(); + try (Connection connection = this.dataSource.getConnection(); + PreparedStatement ps = + connection.prepareStatement(" SELECT TABLE_NAME" + + " FROM ALL_TABLES " + + "ORDER BY TABLE_NAME ASC"); + ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + sb.append(rs.getString(1)).append("\n"); + } + } + final Response returnValue = Response.ok() + .entity(sb.toString()) + .build(); + return returnValue; + } + +} diff --git a/examples/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/package-info.java b/examples/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/package-info.java new file mode 100644 index 000000000..7dbf84052 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/examples/integrations/datasource/hikaricp/jaxrs/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides JAX-RS-related classes and interfaces for this example + * project. + */ +package io.helidon.examples.integrations.datasource.hikaricp.jaxrs; diff --git a/examples/integrations/cdi/datasource-hikaricp/src/main/resources/META-INF/beans.xml b/examples/integrations/cdi/datasource-hikaricp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..ddb8316e3 --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/cdi/datasource-hikaricp/src/main/resources/META-INF/microprofile-config.properties b/examples/integrations/cdi/datasource-hikaricp/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..0ce3463ec --- /dev/null +++ b/examples/integrations/cdi/datasource-hikaricp/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,29 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Default database properties. +# See +# https://container-registry.oracle.com/pls/apex/f?p=113:4:102624334361221::NO::: +# and +# https://technology.amis.nl/2017/11/18/run-oracle-database-in-docker-using-prebaked-image-from-oracle-container-registry-a-two-minute-guide/ +javax.sql.DataSource.example.dataSourceClassName=oracle.jdbc.pool.OracleDataSource +javax.sql.DataSource.example.dataSource.url = jdbc:oracle:thin:@localhost:1521:ORCL +javax.sql.DataSource.example.dataSource.user = sys as sysoper +javax.sql.DataSource.example.dataSource.password = Oracle + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 diff --git a/examples/integrations/cdi/jpa/README.md b/examples/integrations/cdi/jpa/README.md new file mode 100644 index 000000000..5a202ed5f --- /dev/null +++ b/examples/integrations/cdi/jpa/README.md @@ -0,0 +1,13 @@ +# JPA Integration Example + +With Java: +```shell +mvn package +java -jar target/helidon-integrations-examples-jpa.jar +``` + +Try the endpoint: +```shell +curl -X POST -H "Content-Type: text/plain" http://localhost:8080/foo -d 'bar' +curl http://localhost:8080/foo +``` diff --git a/examples/integrations/cdi/jpa/pom.xml b/examples/integrations/cdi/jpa/pom.xml new file mode 100644 index 000000000..644c1bac9 --- /dev/null +++ b/examples/integrations/cdi/jpa/pom.xml @@ -0,0 +1,187 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.integrations.cdi + helidon-integrations-examples-jpa + 1.0.0-SNAPSHOT + Helidon Examples CDI Extensions JPA + + + + + + jakarta.annotation + jakarta.annotation-api + compile + + + jakarta.enterprise + jakarta.enterprise.cdi-api + compile + + + jakarta.inject + jakarta.inject-api + compile + + + jakarta.ws.rs + jakarta.ws.rs-api + compile + + + jakarta.persistence + jakarta.persistence-api + compile + + + jakarta.transaction + jakarta.transaction-api + compile + + + + + com.h2database + h2 + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-eclipselink + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-datasource-hikaricp + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jpa + runtime + + + io.smallrye + jandex + runtime + true + + + io.helidon.microprofile.server + helidon-microprofile-server + runtime + + + io.helidon.microprofile.config + helidon-microprofile-config + runtime + + + org.eclipse.microprofile.config + microprofile-config-api + runtime + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.hibernate.orm + hibernate-jpamodelgen + ${version.lib.hibernate} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + org.codehaus.mojo + exec-maven-plugin + + + weave + process-classes + + java + + + org.eclipse.persistence.tools.weaving.jpa.StaticWeave + + -loglevel + INFO + ${project.build.outputDirectory} + ${project.build.outputDirectory} + + + + + + + + diff --git a/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/Greeting.java b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/Greeting.java new file mode 100644 index 000000000..4ca2788e9 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/Greeting.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.cdi.jpa; + +import java.util.Objects; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * A contrived representation for example purposes only of a two-part + * greeting as might be stored in a database. + */ +@Access(AccessType.FIELD) +@Entity(name = "Greeting") +@Table(name = "GREETING") +public class Greeting { + + @Id + @Column(name = "FIRSTPART", insertable = true, nullable = false, updatable = false) + private String firstPart; + + @Basic(optional = false) + @Column(name = "SECONDPART", insertable = true, nullable = false, updatable = true) + private String secondPart; + + /** + * Creates a new {@link Greeting}; required by the JPA + * specification and for no other purpose. + * + * @deprecated Please use the {@link #Greeting(String, + * String)} constructor instead. + * + * @see #Greeting(String, String) + */ + @Deprecated + protected Greeting() { + super(); + } + + /** + * Creates a new {@link Greeting}. + * + * @param firstPart the first part of the greeting; must not be + * {@code null} + * + * @param secondPart the second part of the greeting; must not be + * {@code null} + * + * @exception NullPointerException if {@code firstPart} or {@code + * secondPart} is {@code null} + */ + public Greeting(final String firstPart, final String secondPart) { + super(); + this.firstPart = Objects.requireNonNull(firstPart); + this.secondPart = Objects.requireNonNull(secondPart); + } + + /** + * Sets the second part of this greeting. + * + * @param secondPart the second part of this greeting; must not be + * {@code null} + * + * @exception NullPointerException if {@code secondPart} is {@code + * null} + */ + public void setSecondPart(final String secondPart) { + this.secondPart = Objects.requireNonNull(secondPart); + } + + /** + * Returns a {@link String} representation of the second part of + * this {@link Greeting}. + * + *

This method never returns {@code null}.

+ * + * @return a non-{@code null} {@link String} representation of the + * second part of this {@link Greeting} + */ + @Override + public String toString() { + return this.secondPart; + } + +} diff --git a/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldApplication.java b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldApplication.java new file mode 100644 index 000000000..0b536de4b --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldApplication.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.cdi.jpa; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.core.Application; + +/** + * An example {@link Application} demonstrating the modular + * integration of JPA and JTA with Helidon MicroProfile. + */ +@ApplicationScoped +public class HelloWorldApplication extends Application { + + private final Set> classes; + + /** + * Creates a new {@link HelloWorldApplication}. + */ + public HelloWorldApplication() { + super(); + final Set> classes = new HashSet<>(); + classes.add(HelloWorldResource.class); + classes.add(JPAExceptionMapper.class); + this.classes = Collections.unmodifiableSet(classes); + } + + /** + * Returns a non-{@code null} {@link Set} of {@link Class}es that + * comprise this JAX-RS application. + * + * @return a non-{@code null}, {@linkplain + * Collections#unmodifiableSet(Set) unmodifiable Set} + * + * @see HelloWorldResource + */ + @Override + public Set> getClasses() { + return this.classes; + } + +} diff --git a/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldResource.java b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldResource.java new file mode 100644 index 000000000..efd85913a --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldResource.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.cdi.jpa; + +import java.net.URI; +import java.util.Objects; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceException; +import jakarta.transaction.Status; +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; +import jakarta.transaction.Transactional; +import jakarta.transaction.Transactional.TxType; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * A JAX-RS root resource class that manipulates greetings in a + * database. + * + * @see #get(String) + * + * @see #post(String, String) + */ +@Path("") +@RequestScoped +public class HelloWorldResource { + + /** + * The {@link EntityManager} used by this class. + * + *

Note that it behaves as though there is a transaction manager + * in effect, because there is.

+ */ + @PersistenceContext(unitName = "test") + private EntityManager entityManager; + + /** + * A {@link Transaction} that is guaranteed to be non-{@code null} + * only when a transactional method is executing. + * + * @see #post(String, String) + */ + @Inject + private Transaction transaction; + + /** + * Creates a new {@link HelloWorldResource}. + */ + public HelloWorldResource() { + super(); + } + + /** + * Returns a {@link Response} with a status of {@code 404} when + * invoked. + * + * @return a non-{@code null} {@link Response} + */ + @GET + @Path("favicon.ico") + public Response getFavicon() { + return Response.status(404).build(); + } + + /** + * When handed a {@link String} like, say, "{@code hello}", responds + * with the second part of the composite greeting as found via an + * {@link EntityManager}. + * + * @param firstPart the first part of the greeting; must not be + * {@code null} + * + * @return the second part of the greeting; never {@code null} + * + * @exception NullPointerException if {@code firstPart} was {@code + * null} + * + * @exception PersistenceException if the {@link EntityManager} + * encountered an error + */ + @GET + @Path("{firstPart}") + @Produces(MediaType.TEXT_PLAIN) + public String get(@PathParam("firstPart") final String firstPart) { + Objects.requireNonNull(firstPart); + assert this.entityManager != null; + final Greeting greeting = this.entityManager.find(Greeting.class, firstPart); + assert greeting != null; + return greeting.toString(); + } + + /** + * When handed two parts of a greeting, like, say, "{@code hello}" + * and "{@code world}", stores a new {@link Greeting} entity in the + * database appropriately. + * + * @param firstPart the first part of the greeting; must not be + * {@code null} + * + * @param secondPart the second part of the greeting; must not be + * {@code null} + * + * @return the {@link String} representation of the resulting {@link + * Greeting}'s identifier; never {@code null} + * + * @exception NullPointerException if {@code firstPart} or {@code + * secondPart} was {@code null} + * + * @exception PersistenceException if the {@link EntityManager} + * encountered an error + * + * @exception SystemException if something went wrong with the + * transaction + */ + @POST + @Path("{firstPart}") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Transactional(TxType.REQUIRED) + public Response post(@PathParam("firstPart") final String firstPart, + final String secondPart) + throws SystemException { + Objects.requireNonNull(firstPart); + Objects.requireNonNull(secondPart); + assert this.transaction != null; + assert this.transaction.getStatus() == Status.STATUS_ACTIVE; + assert this.entityManager != null; + assert this.entityManager.isJoinedToTransaction(); + Greeting greeting = this.entityManager.find(Greeting.class, firstPart); + final boolean created; + if (greeting == null) { + greeting = new Greeting(firstPart, secondPart); + this.entityManager.persist(greeting); + created = true; + } else { + greeting.setSecondPart(secondPart); + created = false; + } + assert this.entityManager.contains(greeting); + if (created) { + return Response.created(URI.create(firstPart)).build(); + } else { + return Response.ok(firstPart).build(); + } + } + +} diff --git a/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/JPAExceptionMapper.java b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/JPAExceptionMapper.java new file mode 100644 index 000000000..d302a29e5 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/JPAExceptionMapper.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.cdi.jpa; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.NoResultException; +import jakarta.persistence.PersistenceException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** + * An {@link ExceptionMapper} that handles {@link + * PersistenceException}s. + * + * @see ExceptionMapper + */ +@ApplicationScoped +@Provider +public class JPAExceptionMapper implements ExceptionMapper { + + /** + * Creates a new {@link JPAExceptionMapper}. + */ + public JPAExceptionMapper() { + super(); + } + + /** + * Returns an appropriate non-{@code null} {@link Response} for the + * supplied {@link PersistenceException}. + * + * @param persistenceException the {@link PersistenceException} that + * caused this {@link JPAExceptionMapper} to be invoked; may be + * {@code null} + * + * @return a non-{@code null} {@link Response} representing the + * error + */ + @Override + public Response toResponse(final PersistenceException persistenceException) { + final Response returnValue; + if (persistenceException instanceof NoResultException + || persistenceException instanceof EntityNotFoundException) { + returnValue = Response.status(404).build(); + } else { + returnValue = null; + throw persistenceException; + } + return returnValue; + } + +} diff --git a/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/package-info.java b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/package-info.java new file mode 100644 index 000000000..7c80b84a3 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides classes and interfaces demonstrating the usage of JPA and + * JTA integration within Helidon MicroProfile. + */ +package io.helidon.examples.integrations.cdi.jpa; diff --git a/examples/integrations/cdi/jpa/src/main/resources/META-INF/beans.xml b/examples/integrations/cdi/jpa/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..a0938bff7 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/cdi/jpa/src/main/resources/META-INF/microprofile-config.properties b/examples/integrations/cdi/jpa/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..4e2866d89 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +javax.sql.DataSource.test.dataSourceClassName=org.h2.jdbcx.JdbcDataSource +javax.sql.DataSource.test.dataSource.url=jdbc:h2:mem:test;INIT=CREATE TABLE IF NOT EXISTS GREETING (FIRSTPART VARCHAR NOT NULL, SECONDPART VARCHAR NOT NULL, PRIMARY KEY (FIRSTPART))\\;MERGE INTO GREETING (FIRSTPART, SECONDPART) VALUES ('hello', 'world') +javax.sql.DataSource.test.dataSource.user=sa +javax.sql.DataSource.test.dataSource.password=${EMPTY} + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 diff --git a/examples/integrations/cdi/jpa/src/main/resources/META-INF/persistence.xml b/examples/integrations/cdi/jpa/src/main/resources/META-INF/persistence.xml new file mode 100644 index 000000000..42fae4fd7 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,52 @@ + + + + + test + io.helidon.examples.integrations.cdi.jpa.Greeting + + + + + + + + + + + + + + + + + + + diff --git a/examples/integrations/cdi/pokemons/README.md b/examples/integrations/cdi/pokemons/README.md new file mode 100644 index 000000000..132dd40af --- /dev/null +++ b/examples/integrations/cdi/pokemons/README.md @@ -0,0 +1,25 @@ +# JPA Pokemons Example + +With Java: +```shell +mvn package +java -jar target/helidon-integrations-examples-pokemons.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/pokemon +#Output: [{"id":1,"type":12,"name":"Bulbasaur"}, ...] +``` +```shell +curl -X GET http://localhost:8080/type +#Output: [{"id":1,"name":"Normal"}, ...] +``` +```shell +curl -H "Content-Type: application/json" --request POST --data '{"id":100, "type":1, "name":"Test"}' http://localhost:8080/pokemon +``` + +--- + +Pokémon, and Pokémon character names are trademarks of Nintendo. diff --git a/examples/integrations/cdi/pokemons/pom.xml b/examples/integrations/cdi/pokemons/pom.xml new file mode 100644 index 000000000..0aed392b6 --- /dev/null +++ b/examples/integrations/cdi/pokemons/pom.xml @@ -0,0 +1,185 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.integrations.cdi + helidon-integrations-examples-pokemons + 1.0.0-SNAPSHOT + Helidon Examples CDI Extensions Pokemons JPA + + + + + jakarta.annotation + jakarta.annotation-api + + + jakarta.enterprise + jakarta.enterprise.cdi-api + + + jakarta.inject + jakarta.inject-api + + + jakarta.ws.rs + jakarta.ws.rs-api + + + jakarta.json.bind + jakarta.json.bind-api + + + jakarta.persistence + jakarta.persistence-api + + + jakarta.transaction + jakarta.transaction-api + + + + + + com.h2database + h2 + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-hibernate + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-datasource-hikaricp + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jpa + runtime + + + io.smallrye + jandex + runtime + true + + + io.helidon.microprofile.server + helidon-microprofile-server + runtime + + + io.helidon.microprofile.metrics + helidon-microprofile-metrics + runtime + + + org.glassfish.jersey.media + jersey-media-json-binding + runtime + + + io.helidon.microprofile.config + helidon-microprofile-config + runtime + + + org.eclipse.microprofile.config + microprofile-config-api + runtime + + + org.hibernate.validator + hibernate-validator-cdi + runtime + + + org.glassfish + jakarta.el + runtime + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + org.hibernate.orm.tooling + hibernate-enhance-maven-plugin + + + + true + true + true + + + enhance + + + + + + + diff --git a/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/Pokemon.java b/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/Pokemon.java new file mode 100644 index 000000000..fb1cd99e2 --- /dev/null +++ b/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/Pokemon.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.cdi.pokemon; + +import jakarta.json.bind.annotation.JsonbTransient; +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; + +/** + * A Pokemon entity class. A Pokemon is represented as a triple of an + * ID, a name and a type. + */ +@Entity(name = "Pokemon") +@Table(name = "POKEMON") +@Access(AccessType.PROPERTY) +@NamedQueries({ + @NamedQuery(name = "getPokemons", + query = "SELECT p FROM Pokemon p"), + @NamedQuery(name = "getPokemonByName", + query = "SELECT p FROM Pokemon p WHERE p.name = :name") +}) +public class Pokemon { + + private int id; + + private String name; + + @JsonbTransient + private PokemonType pokemonType; + + private int type; + + /** + * Creates a new pokemon. + */ + public Pokemon() { + } + + @Id + @Column(name = "ID", nullable = false, updatable = false) + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Basic(optional = false) + @Column(name = "NAME", nullable = false) + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * Returns pokemon's type. + * + * @return Pokemon's type. + */ + @ManyToOne + public PokemonType getPokemonType() { + return pokemonType; + } + + /** + * Sets pokemon's type. + * + * @param pokemonType Pokemon's type. + */ + public void setPokemonType(PokemonType pokemonType) { + this.pokemonType = pokemonType; + this.type = pokemonType.getId(); + } + + @Transient + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } +} diff --git a/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/PokemonResource.java b/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/PokemonResource.java new file mode 100644 index 000000000..918567c37 --- /dev/null +++ b/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/PokemonResource.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.cdi.pokemon; + +import java.util.List; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.TypedQuery; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * This class implements REST endpoints to interact with Pokemons. The following + * operations are supported: + * + * GET /pokemon: Retrieve list of all pokemons + * GET /pokemon/{id}: Retrieve single pokemon by ID + * GET /pokemon/name/{name}: Retrieve single pokemon by name + * DELETE /pokemon/{id}: Delete a pokemon by ID + * POST /pokemon: Create a new pokemon + */ +@Path("pokemon") +public class PokemonResource { + + @PersistenceContext(unitName = "test") + private EntityManager entityManager; + + /** + * Retrieves list of all pokemons. + * + * @return List of pokemons. + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getPokemons() { + return entityManager.createNamedQuery("getPokemons", Pokemon.class).getResultList(); + } + + /** + * Retrieves single pokemon by ID. + * + * @param id The ID. + * @return A pokemon that matches the ID. + * @throws NotFoundException If no pokemon found for the ID. + */ + @GET + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + public Pokemon getPokemonById(@PathParam("id") String id) { + try { + return entityManager.find(Pokemon.class, Integer.valueOf(id)); + } catch (IllegalArgumentException e) { + throw new NotFoundException("Unable to find pokemon with ID " + id); + } + } + + /** + * Deletes a single pokemon by ID. + * + * @param id The ID. + */ + @DELETE + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + @Transactional(Transactional.TxType.REQUIRED) + public void deletePokemon(@PathParam("id") String id) { + Pokemon pokemon = getPokemonById(id); + entityManager.remove(pokemon); + } + + /** + * Retrieves a pokemon by name. + * + * @param name The name. + * @return A pokemon that matches the name. + * @throws NotFoundException If no pokemon found for the name. + */ + @GET + @Path("name/{name}") + @Produces(MediaType.APPLICATION_JSON) + public Pokemon getPokemonByName(@PathParam("name") String name) { + TypedQuery query = entityManager.createNamedQuery("getPokemonByName", Pokemon.class); + List list = query.setParameter("name", name).getResultList(); + if (list.isEmpty()) { + throw new NotFoundException("Unable to find pokemon with name " + name); + } + return list.get(0); + } + + /** + * Creates a new pokemon. + * + * @param pokemon New pokemon. + * @throws BadRequestException If a problem was found. + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Transactional(Transactional.TxType.REQUIRED) + public void createPokemon(Pokemon pokemon) { + try { + PokemonType pokemonType = entityManager.createNamedQuery("getPokemonTypeById", PokemonType.class) + .setParameter("id", pokemon.getType()).getSingleResult(); + pokemon.setPokemonType(pokemonType); + entityManager.persist(pokemon); + } catch (Exception e) { + throw new BadRequestException("Unable to create pokemon with ID " + pokemon.getId()); + } + } +} + diff --git a/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/PokemonType.java b/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/PokemonType.java new file mode 100644 index 000000000..7e19af87e --- /dev/null +++ b/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/PokemonType.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.cdi.pokemon; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; + +/** + * A Pokemon Type entity. A type is represented by an ID and a name. + */ +@Entity(name = "PokemonType") +@Table(name = "POKEMONTYPE") +@Access(AccessType.FIELD) +@NamedQueries({ + @NamedQuery(name = "getPokemonTypes", + query = "SELECT t FROM PokemonType t"), + @NamedQuery(name = "getPokemonTypeById", + query = "SELECT t FROM PokemonType t WHERE t.id = :id") +}) +public class PokemonType { + + @Id + @Column(name = "ID", nullable = false, updatable = false) + private int id; + + @Basic(optional = false) + @Column(name = "NAME") + private String name; + + /** + * Creates a new type. + */ + public PokemonType() { + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/PokemonTypeResource.java b/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/PokemonTypeResource.java new file mode 100644 index 000000000..70bc9c981 --- /dev/null +++ b/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/PokemonTypeResource.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.cdi.pokemon; + +import java.util.List; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * This class implements a REST endpoint to retrieve Pokemon types. + * + * GET /type: Retrieve list of all pokemon types + */ +@Path("type") +public class PokemonTypeResource { + + @PersistenceContext(unitName = "test") + private EntityManager entityManager; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getPokemonTypes() { + return entityManager.createNamedQuery("getPokemonTypes", PokemonType.class).getResultList(); + } +} diff --git a/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/package-info.java b/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/package-info.java new file mode 100644 index 000000000..85a2ffde6 --- /dev/null +++ b/examples/integrations/cdi/pokemons/src/main/java/io/helidon/examples/integrations/cdi/pokemon/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Pokemon JPA integration sample. + */ +package io.helidon.examples.integrations.cdi.pokemon; diff --git a/examples/integrations/cdi/pokemons/src/main/resources/META-INF/beans.xml b/examples/integrations/cdi/pokemons/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..676e09a2d --- /dev/null +++ b/examples/integrations/cdi/pokemons/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/cdi/pokemons/src/main/resources/META-INF/init_script.sql b/examples/integrations/cdi/pokemons/src/main/resources/META-INF/init_script.sql new file mode 100644 index 000000000..8d3d00c78 --- /dev/null +++ b/examples/integrations/cdi/pokemons/src/main/resources/META-INF/init_script.sql @@ -0,0 +1,25 @@ +INSERT INTO POKEMONTYPE VALUES (1, 'Normal'); +INSERT INTO POKEMONTYPE VALUES (2, 'Fighting'); +INSERT INTO POKEMONTYPE VALUES (3, 'Flying'); +INSERT INTO POKEMONTYPE VALUES (4, 'Poison'); +INSERT INTO POKEMONTYPE VALUES (5, 'Ground'); +INSERT INTO POKEMONTYPE VALUES (6, 'Rock'); +INSERT INTO POKEMONTYPE VALUES (7, 'Bug'); +INSERT INTO POKEMONTYPE VALUES (8, 'Ghost'); +INSERT INTO POKEMONTYPE VALUES (9, 'Steel'); +INSERT INTO POKEMONTYPE VALUES (10, 'Fire'); +INSERT INTO POKEMONTYPE VALUES (11, 'Water'); +INSERT INTO POKEMONTYPE VALUES (12, 'Grass'); +INSERT INTO POKEMONTYPE VALUES (13, 'Electric'); +INSERT INTO POKEMONTYPE VALUES (14, 'Psychic'); +INSERT INTO POKEMONTYPE VALUES (15, 'Ice'); +INSERT INTO POKEMONTYPE VALUES (16, 'Dragon'); +INSERT INTO POKEMONTYPE VALUES (17, 'Dark'); +INSERT INTO POKEMONTYPE VALUES (18, 'Fairy'); + +INSERT INTO POKEMON VALUES (1, 'Bulbasaur', 12); +INSERT INTO POKEMON VALUES (2, 'Charmander', 10); +INSERT INTO POKEMON VALUES (3, 'Squirtle', 11); +INSERT INTO POKEMON VALUES (4, 'Caterpie', 7); +INSERT INTO POKEMON VALUES (5, 'Weedle', 7); +INSERT INTO POKEMON VALUES (6, 'Pidgey', 3); diff --git a/examples/integrations/cdi/pokemons/src/main/resources/META-INF/microprofile-config.properties b/examples/integrations/cdi/pokemons/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..4559b00f7 --- /dev/null +++ b/examples/integrations/cdi/pokemons/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,24 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +javax.sql.DataSource.test.dataSourceClassName=org.h2.jdbcx.JdbcDataSource +#javax.sql.DataSource.test.dataSource.url=jdbc:h2:tcp://localhost:1521/test;IFEXISTS=FALSE +javax.sql.DataSource.test.dataSource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1 +javax.sql.DataSource.test.dataSource.user=sa +javax.sql.DataSource.test.dataSource.password=${EMPTY} + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 diff --git a/examples/integrations/cdi/pokemons/src/main/resources/META-INF/persistence.xml b/examples/integrations/cdi/pokemons/src/main/resources/META-INF/persistence.xml new file mode 100644 index 000000000..cc9103e8a --- /dev/null +++ b/examples/integrations/cdi/pokemons/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,35 @@ + + + + + test + io.helidon.examples.integrations.cdi.pokemon.Pokemon + io.helidon.examples.integrations.cdi.pokemon.PokemonType + + + + + + + + diff --git a/examples/integrations/cdi/pokemons/src/test/java/io/helidon/examples/integrations/cdi/pokemon/MainTest.java b/examples/integrations/cdi/pokemons/src/test/java/io/helidon/examples/integrations/cdi/pokemon/MainTest.java new file mode 100644 index 000000000..ec72ed297 --- /dev/null +++ b/examples/integrations/cdi/pokemons/src/test/java/io/helidon/examples/integrations/cdi/pokemon/MainTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.cdi.pokemon; + +import io.helidon.microprofile.server.Server; + +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.json.JsonArray; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class MainTest { + + private static Server server; + private static Client client; + + @BeforeAll + public static void startTheServer() { + client = ClientBuilder.newClient(); + server = Server.create().start(); + } + + @AfterAll + static void destroyClass() { + CDI current = CDI.current(); + ((SeContainer) current).close(); + } + + @Test + void testPokemonTypes() { + JsonArray types = client.target(getConnectionString("/type")) + .request() + .get(JsonArray.class); + assertThat(types.size(), is(18)); + } + + @Test + void testPokemon() { + assertThat(getPokemonCount(), is(6)); + + Pokemon pokemon = client.target(getConnectionString("/pokemon/1")) + .request() + .get(Pokemon.class); + assertThat(pokemon.getName(), is("Bulbasaur")); + + pokemon = client.target(getConnectionString("/pokemon/name/Charmander")) + .request() + .get(Pokemon.class); + assertThat(pokemon.getType(), is(10)); + + try (Response response = client.target(getConnectionString("/pokemon/1")) + .request() + .get()) { + assertThat(response.getStatus(), is(200)); + } + + Pokemon test = new Pokemon(); + test.setType(1); + test.setId(100); + test.setName("Test"); + try (Response response = client.target(getConnectionString("/pokemon")) + .request() + .post(Entity.entity(test, MediaType.APPLICATION_JSON))) { + assertThat(response.getStatus(), is(204)); + assertThat(getPokemonCount(), is(7)); + } + + try (Response response = client.target(getConnectionString("/pokemon/100")) + .request() + .delete()) { + assertThat(response.getStatus(), is(204)); + assertThat(getPokemonCount(), is(6)); + } + } + + private int getPokemonCount() { + JsonArray pokemons = client.target(getConnectionString("/pokemon")) + .request() + .get(JsonArray.class); + return pokemons.size(); + } + + private String getConnectionString(String path) { + return "http://localhost:" + server.port() + path; + } +} diff --git a/examples/integrations/cdi/pom.xml b/examples/integrations/cdi/pom.xml new file mode 100644 index 000000000..98f65ed15 --- /dev/null +++ b/examples/integrations/cdi/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + io.helidon.examples.integrations + helidon-examples-integrations-project + 1.0.0-SNAPSHOT + + io.helidon.examples.integrations.cdi + helidon-examples-integrations-cdi-project + 1.0.0-SNAPSHOT + pom + Helidon Examples CDI Extensions + + + datasource-hikaricp + datasource-hikaricp-h2 + datasource-hikaricp-mysql + jpa + pokemons + + diff --git a/examples/integrations/micrometer/mp/README.md b/examples/integrations/micrometer/mp/README.md new file mode 100644 index 000000000..57032897c --- /dev/null +++ b/examples/integrations/micrometer/mp/README.md @@ -0,0 +1,80 @@ +# Helidon MP Micrometer Example + +This example shows a simple greeting application, similar to the one from the Helidon MP +QuickStart, but enhanced with Helidon MP Micrometer support to +* time all accesses to the two `GET` endpoints, and + +* count the accesses to the `GET` endpoint which returns a personalized + greeting. + +The example is similar to the one from the Helidon MP QuickStart with these differences: +* The `pom.xml` file contains this additional dependency on the Helidon Micrometer integration + module: +```xml + + io.helidon.integrations + helidon-integrations-micrometer + +``` +* The `GreetingService` includes additional annotations: + * `@Timed` on the two `GET` methods. + * `@Counted` on the `GET` method that returns a personalized greeting. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-integrations-micrometer-mp.jar +``` + +## Using the app endpoints as with the "classic" greeting app + +These normal greeting app endpoints work just as in the original greeting app: + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} +``` + +```shell +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} +``` + +```shell +curl -X PUT -H "Content-Type: application/json" -d '{"message" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} +``` + +## Using Micrometer + +Access the `/micrometer` endpoint which reports the newly-added timer and counter. + +```shell +curl http://localhost:8080/micrometer +``` +Because the `@Timer` annotation specifies a histogram, +the actual timer output includes a lengthy histogram (only part of which is shown below). +Your output might show the `personalizedGets` output before the `allGets` output, +rather than after as shown here. + +```shell +curl http://localhost:8080/micrometer +``` +```listing +# HELP allGets_seconds_max Tracks all GET operations +# TYPE allGets_seconds_max gauge +allGets_seconds_max 0.004840005 +# HELP allGets_seconds Tracks all GET operations +# TYPE allGets_seconds histogram +allGets_seconds_bucket{le="0.001",} 2.0 +allGets_seconds_bucket{le="30.0",} 3.0 +allGets_seconds_bucket{le="+Inf",} 3.0 +allGets_seconds_count 3.0 +allGets_seconds_sum 0.005098119 +# HELP personalizedGets_total Counts personalized GET operations +# TYPE personalizedGets_total counter +personalizedGets_total 2.0 +``` diff --git a/examples/integrations/micrometer/mp/pom.xml b/examples/integrations/micrometer/mp/pom.xml new file mode 100644 index 000000000..07036bad9 --- /dev/null +++ b/examples/integrations/micrometer/mp/pom.xml @@ -0,0 +1,107 @@ + + + + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + 4.0.0 + + Helidon Examples Integration Micrometer MP + + + Basic illustration of Micrometer integration in Helidon MP + + + io.helidon.examples.integrations.micrometer + helidon-examples-integrations-micrometer-mp + 1.0.0-SNAPSHOT + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.integrations.micrometer + helidon-integrations-micrometer-cdi + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.eclipse.microprofile.openapi + microprofile-openapi-api + + + org.glassfish.jersey.media + jersey-media-json-binding + runtime + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + io.helidon.webclient + helidon-webclient + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + + \ No newline at end of file diff --git a/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/GreetResource.java b/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/GreetResource.java new file mode 100644 index 000000000..063d9e686 --- /dev/null +++ b/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/GreetResource.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micrometer.mp; + +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; + +/** + * A simple JAX-RS resource to greet you. Examples: + * + * Get default greeting message: + * curl -X GET http://localhost:8080/greet + * + * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + * + * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + * + * The message is returned as a JSON object. + */ +@Path("/greet") +@RequestScoped +public class GreetResource { + + static final String PERSONALIZED_GETS_COUNTER_NAME = "personalizedGets"; + private static final String PERSONALIZED_GETS_COUNTER_DESCRIPTION = "Counts personalized GET operations"; + static final String GETS_TIMER_NAME = "allGets"; + private static final String GETS_TIMER_DESCRIPTION = "Tracks all GET operations"; + + /** + * The greeting message provider. + */ + private final GreetingProvider greetingProvider; + + /** + * Using constructor injection to get a configuration property. + * By default this gets the value from META-INF/microprofile-config + * + * @param greetingConfig the configured greeting message + */ + @Inject + public GreetResource(GreetingProvider greetingConfig) { + this.greetingProvider = greetingConfig; + } + + /** + * Return a worldly greeting message. + * + * @return {@link GreetingMessage} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Timed(value = GETS_TIMER_NAME, description = GETS_TIMER_DESCRIPTION, histogram = true) + public GreetingMessage getDefaultMessage() { + return createResponse("World"); + } + + /** + * Return a greeting message using the name that was provided. + * + * @param name the name to greet + * @return {@link GreetingMessage} + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + @Counted(value = PERSONALIZED_GETS_COUNTER_NAME, description = PERSONALIZED_GETS_COUNTER_DESCRIPTION) + @Timed(value = GETS_TIMER_NAME, description = GETS_TIMER_DESCRIPTION, histogram = true) + public GreetingMessage getMessage(@PathParam("name") String name) { + return createResponse(name); + } + + /** + * Set the greeting to use in future messages. + * + * @param message {@link GreetingMessage} containing the new greeting + * @return {@link Response} + */ + @Path("/greeting") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @RequestBody(name = "greeting", + required = true, + content = @Content(mediaType = "application/json", + schema = @Schema(type = SchemaType.OBJECT, requiredProperties = { "greeting" }))) + @APIResponses({ + @APIResponse(name = "normal", responseCode = "204", description = "Greeting updated"), + @APIResponse(name = "missing 'greeting'", responseCode = "400", + description = "JSON did not contain setting for 'greeting'")}) + public Response updateGreeting(GreetingMessage message) { + if (message.getMessage() == null) { + GreetingMessage entity = new GreetingMessage("No greeting provided"); + return Response.status(Response.Status.BAD_REQUEST).entity(entity).build(); + } + + greetingProvider.setMessage(message.getMessage()); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + private GreetingMessage createResponse(String who) { + String msg = String.format("%s %s!", greetingProvider.getMessage(), who); + return new GreetingMessage(msg); + } +} diff --git a/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/GreetingMessage.java b/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/GreetingMessage.java new file mode 100644 index 000000000..e7a993ea7 --- /dev/null +++ b/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/GreetingMessage.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micrometer.mp; + +/** + * POJO defining the greeting message content. + */ +@SuppressWarnings("unused") +public class GreetingMessage { + private String message; + + /** + * Create a new GreetingMessage instance. + */ + public GreetingMessage() { + } + + /** + * Create a new GreetingMessage instance. + * + * @param message message + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Gets the message value. + * + * @return message value + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message message value to set + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/GreetingProvider.java b/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/GreetingProvider.java new file mode 100644 index 000000000..3630b1a3b --- /dev/null +++ b/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/GreetingProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.micrometer.mp; + +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Provider for greeting message. + */ +@ApplicationScoped +public class GreetingProvider { + private final AtomicReference message = new AtomicReference<>(); + + /** + * Create a new greeting provider, reading the message from configuration. + * + * @param message greeting to use + */ + @Inject + public GreetingProvider(@ConfigProperty(name = "app.greeting") String message) { + this.message.set(message); + } + + String getMessage() { + return message.get(); + } + + void setMessage(String message) { + this.message.set(message); + } +} diff --git a/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/package-info.java b/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/package-info.java new file mode 100644 index 000000000..cbcea4b58 --- /dev/null +++ b/examples/integrations/micrometer/mp/src/main/java/io/helidon/examples/integrations/micrometer/mp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example for Micrometer integration. + */ +package io.helidon.examples.integrations.micrometer.mp; diff --git a/examples/integrations/micrometer/mp/src/main/resources/META-INF/beans.xml b/examples/integrations/micrometer/mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e149ced7d --- /dev/null +++ b/examples/integrations/micrometer/mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/micrometer/mp/src/main/resources/META-INF/microprofile-config.properties b/examples/integrations/micrometer/mp/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..4c7757ab7 --- /dev/null +++ b/examples/integrations/micrometer/mp/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Application properties. This is the default greeting +app.greeting=Hello + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +micrometer.builtin-registries.0.type=prometheus +micrometer.builtin-registries.0.prefix=myPrefix diff --git a/examples/integrations/micrometer/mp/src/main/resources/application.yaml b/examples/integrations/micrometer/mp/src/main/resources/application.yaml new file mode 100644 index 000000000..040ee86dd --- /dev/null +++ b/examples/integrations/micrometer/mp/src/main/resources/application.yaml @@ -0,0 +1,20 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +micrometer: + builtin-registries: + - type: prometheus + prefix: myPrefix \ No newline at end of file diff --git a/examples/integrations/micrometer/mp/src/main/resources/logging.properties b/examples/integrations/micrometer/mp/src/main/resources/logging.properties new file mode 100644 index 000000000..c5299aba0 --- /dev/null +++ b/examples/integrations/micrometer/mp/src/main/resources/logging.properties @@ -0,0 +1,36 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.microprofile.level=INFO +#io.helidon.common.level=INFO +#org.glassfish.jersey.level=INFO +#org.jboss.weld=INFO diff --git a/examples/integrations/micrometer/mp/src/test/java/io/helidon/examples/integrations/micrometer/mp/TestEndpoint.java b/examples/integrations/micrometer/mp/src/test/java/io/helidon/examples/integrations/micrometer/mp/TestEndpoint.java new file mode 100644 index 000000000..9ddafa675 --- /dev/null +++ b/examples/integrations/micrometer/mp/src/test/java/io/helidon/examples/integrations/micrometer/mp/TestEndpoint.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.micrometer.mp; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.Test; + +import static io.helidon.examples.integrations.micrometer.mp.GreetResource.PERSONALIZED_GETS_COUNTER_NAME; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +public class TestEndpoint { + + @Inject + private WebTarget webTarget; + + @Inject + private MeterRegistry registry; + + @Test + public void pingGreet() { + + GreetingMessage message = webTarget + .path("/greet/Joe") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(GreetingMessage.class); + + String responseString = message.getMessage(); + + assertThat("Response string", responseString, is("Hello Joe!")); + Counter counter = registry.counter(PERSONALIZED_GETS_COUNTER_NAME); + double before = counter.count(); + + message = webTarget + .path("/greet/Jose") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(GreetingMessage.class); + + responseString = message.getMessage(); + + assertThat("Response string", responseString, is("Hello Jose!")); + double after = counter.count(); + assertThat("Difference in personalized greeting counter between successive calls", after - before, is(1d)); + + } +} diff --git a/examples/integrations/micrometer/pom.xml b/examples/integrations/micrometer/pom.xml new file mode 100644 index 000000000..da7cb8006 --- /dev/null +++ b/examples/integrations/micrometer/pom.xml @@ -0,0 +1,43 @@ + + + + + 4.0.0 + + io.helidon.examples.integrations + helidon-examples-integrations-project + 1.0.0-SNAPSHOT + + + helidon-examples-micrometer-project + 1.0.0-SNAPSHOT + Helidon Examples Integration Micrometer + + pom + + + se + mp + + + + Basic illustrations of Micrometer integration in Helidon + + + diff --git a/examples/integrations/micrometer/se/README.md b/examples/integrations/micrometer/se/README.md new file mode 100644 index 000000000..2358b4b55 --- /dev/null +++ b/examples/integrations/micrometer/se/README.md @@ -0,0 +1,94 @@ +# Helidon SE Micrometer Example + +This example shows a simple greeting application, similar to the one from the +Helidon SE QuickStart, but enhanced with Micrometer support to: + +* time all accesses to the two `GET` endpoints, and + +* count the accesses to the `GET` endpoint which returns a personalized + greeting. + +The Helidon Micrometer integration creates a Micrometer `MeterRegistry` automatically for you. +The `registry()` instance method on `MicrometerSupport` returns that meter registry. + +The `Main` class +1. Uses `MicrometerSupport` to obtain the Micrometer `MeterRegistry` which Helidon SE + automatically provides. + +1. Uses the `MeterRegistry` to: + * Create a Micrometer `Timer` for recording accesses to all `GET` endpoints. + * Create a Micrometer `Counter` for counting accesses to the `GET` endpoint that + returns a personalized greeting. + +1. Registers the built-in support for the `/micrometer` endpoint. + +1. Passes the `Timer` and `Counter` to the `GreetingService` constructor. + +The `GreetingService` class +1. Accepts in the constructor the `Timer` and `Counter` and saves them. +1. Adds routing rules to: + * Update the `Timer` with every `GET` access. + * Increment `Counter` (in addition to returning a personalized greeting) for every + personalized `GET` access. + + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-integrations-micrometer-se.jar +``` + +## Using the app endpoints as with the "classic" greeting app + +These normal greeting app endpoints work just as in the original greeting app: + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} +``` + +```shell +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} +``` + +```shell +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} +``` + +## Using Micrometer + +Access the `/micrometer` endpoint which reports the newly-added timer and counter. +```shell +curl http://localhost:8080/micrometer +``` + +Because we created the `Timer` with a histogram, +the actual timer output includes a lengthy histogram (only part of which is shown below). +Your output might show the `personalizedGets` output before the `allGets` output, +rather than after as shown here. + +```shell +curl http://localhost:8080/micrometer +``` +```listing +# HELP allGets_seconds_max +# TYPE allGets_seconds_max gauge +allGets_seconds_max 0.04341847 +# HELP allGets_seconds +# TYPE allGets_seconds histogram +allGets_seconds_bucket{le="0.001",} 0.0 +... +allGets_seconds_bucket{le="30.0",} 3.0 +allGets_seconds_bucket{le="+Inf",} 3.0 +allGets_seconds_count 3.0 +allGets_seconds_sum 0.049222592 +# HELP personalizedGets_total +# TYPE personalizedGets_total counter +personalizedGets_total 2.0 + +``` \ No newline at end of file diff --git a/examples/integrations/micrometer/se/pom.xml b/examples/integrations/micrometer/se/pom.xml new file mode 100644 index 000000000..e0bf3789e --- /dev/null +++ b/examples/integrations/micrometer/se/pom.xml @@ -0,0 +1,94 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.integrations.micrometer-project + helidon-examples-integrations-micrometer-se + 1.0.0-SNAPSHOT + + Helidon Examples Integration Micrometer SE + + + Basic illustration of Micrometer integration in Helidon SE + + + + io.helidon.examples.integrations.micrometer.se.Main + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.config + helidon-config-yaml + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.integrations.micrometer + helidon-integrations-micrometer + + + io.helidon.webclient + helidon-webclient + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/GreetService.java b/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/GreetService.java new file mode 100644 index 000000000..b5049622b --- /dev/null +++ b/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/GreetService.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micrometer.se; + +import java.util.Collections; + +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.webserver.http.HttpRequest; +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.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Timer; +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple service to greet you. + *

+ * Examples: + *

{@code
+ * Get default greeting message:
+ * curl -X GET http://localhost:8080/greet
+ *
+ * Get greeting message for Joe:
+ * curl -X GET http://localhost:8080/greet/Joe
+ *
+ * Change greeting
+ * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting
+ *
+ * }
+ * The greeting message is returned as a JSON object. + * + *

+ */ + +public class GreetService implements HttpService { + + /** + * The config value for the key {@code greeting}. + */ + private String greeting; + + private final Timer getTimer; + private final Counter personalizedGetCounter; + + private static final JsonBuilderFactory JSON_BF = Json.createBuilderFactory(Collections.emptyMap()); + + GreetService(Timer getTimer, Counter personalizedGetCounter) { + Config config = Config.global(); + this.greeting = config.get("app.greeting").asString().orElse("Ciao"); + this.getTimer = getTimer; + this.personalizedGetCounter = personalizedGetCounter; + } + + /** + * A service registers itself by updating the routine rules. + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules + .get((req, resp) -> getTimer.record(resp::next)) // Update the timer with every GET. + .get("/", this::getDefaultMessageHandler) + .get("/{name}", + (req, resp) -> { + personalizedGetCounter.increment(); + resp.next(); + }, // Count personalized GETs... + this::getMessageHandler) // ...and process them. + .put("/greeting", this::updateGreetingHandler); + } + + /** + * Return a worldly greeting message. + * @param request the server request + * @param response the server response + */ + private void getDefaultMessageHandler(HttpRequest 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().first("name").get(); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + GreetingMessage msg = new GreetingMessage(String.format("%s %s!", greeting, name)); + response.send(msg.forRest()); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + + if (!jo.containsKey(GreetingMessage.JSON_LABEL)) { + JsonObject jsonErrorObject = JSON_BF.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting = GreetingMessage.fromRest(jo).getMessage(); + response.status(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) { + JsonObject obj = request.content().as(JsonObject.class); + updateGreetingFromJson(obj, response); + } +} diff --git a/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/GreetingMessage.java b/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/GreetingMessage.java new file mode 100644 index 000000000..e05d9db24 --- /dev/null +++ b/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/GreetingMessage.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.micrometer.se; + +import java.util.Collections; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; + +/** + * POJO for the greeting message exchanged between the server and the client. + */ +public class GreetingMessage { + + /** + * Label for tagging a {@code GreetingMessage} instance in JSON. + */ + public static final String JSON_LABEL = "greeting"; + + private static final JsonBuilderFactory JSON_BF = Json.createBuilderFactory(Collections.emptyMap()); + + private String message; + + /** + * Create a new greeting with the specified message content. + * + * @param message the message to store in the greeting + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Returns the message value. + * + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message value to be set + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Converts a JSON object (typically read from the request payload) + * into a {@code GreetingMessage}. + * + * @param jsonObject the {@link JsonObject} to convert. + * @return {@code GreetingMessage} set according to the provided object + */ + public static GreetingMessage fromRest(JsonObject jsonObject) { + return new GreetingMessage(jsonObject.getString(JSON_LABEL)); + } + + /** + * Prepares a {@link JsonObject} corresponding to this instance. + * + * @return {@code JsonObject} representing this {@code GreetingMessage} instance + */ + public JsonObject forRest() { + JsonObjectBuilder builder = JSON_BF.createObjectBuilder(); + return builder.add(JSON_LABEL, message) + .build(); + } +} diff --git a/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/Main.java b/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/Main.java new file mode 100644 index 000000000..47f3bba60 --- /dev/null +++ b/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/Main.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micrometer.se; + +import io.helidon.config.Config; +import io.helidon.integrations.micrometer.MicrometerFeature; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Timer; + +/** + * Simple Hello World rest application. + */ +public final class Main { + + static final String PERSONALIZED_GETS_COUNTER_NAME = "personalizedGets"; + static final String ALL_GETS_TIMER_NAME = "allGets"; + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(final String[] args) { + startServer(); + } + + /** + * Start the server. + */ + static WebServer startServer() { + + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + // and initialize global config + Config config = Config.create(); + Config.global(config); + + WebServer server = WebServer.builder() + .config(config.get("server")) + .routing(Main::setupRouting) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + return server; + } + + /** + * Setup routing. + * + * @param routing routing builder + */ + static void setupRouting(HttpRouting.Builder routing) { + + Config config = Config.global(); + + MicrometerFeature micrometerSupport = MicrometerFeature.create(config); + Counter personalizedGetCounter = micrometerSupport.registry() + .counter(PERSONALIZED_GETS_COUNTER_NAME); + Timer getTimer = Timer.builder(ALL_GETS_TIMER_NAME) + .publishPercentileHistogram() + .register(micrometerSupport.registry()); + + GreetService greetService = new GreetService(getTimer, personalizedGetCounter); + + routing.register("/greet", greetService) + .addFeature(micrometerSupport); + } +} diff --git a/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/package-info.java b/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/package-info.java new file mode 100644 index 000000000..e58edb7ca --- /dev/null +++ b/examples/integrations/micrometer/se/src/main/java/io/helidon/examples/integrations/micrometer/se/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example application showing Micrometer support in Helidon SE + *

+ * Start with {@link io.helidon.examples.integrations.micrometer.se.Main} class. + * + * @see io.helidon.examples.integrations.micrometer.se.Main + */ +package io.helidon.examples.integrations.micrometer.se; diff --git a/examples/integrations/micrometer/se/src/main/resources/application.yaml b/examples/integrations/micrometer/se/src/main/resources/application.yaml new file mode 100644 index 000000000..92e9f51e4 --- /dev/null +++ b/examples/integrations/micrometer/se/src/main/resources/application.yaml @@ -0,0 +1,24 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + greeting: "Hello" + +server: + port: 8080 + host: 0.0.0.0 + + diff --git a/examples/integrations/micrometer/se/src/test/java/io/helidon/examples/integrations/micrometer/se/MainTest.java b/examples/integrations/micrometer/se/src/test/java/io/helidon/examples/integrations/micrometer/se/MainTest.java new file mode 100644 index 000000000..5e179a824 --- /dev/null +++ b/examples/integrations/micrometer/se/src/test/java/io/helidon/examples/integrations/micrometer/se/MainTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.micrometer.se; + +import java.util.Collections; + +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServerConfig.Builder; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +// we need to first call the methods, before validating metrics +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@ServerTest +public class MainTest { + + private static final JsonBuilderFactory JSON_BF = Json.createBuilderFactory(Collections.emptyMap()); + private static final JsonObject TEST_JSON_OBJECT; + + private static double expectedPersonalizedGets; + private static double expectedAllGets; + private final Http1Client client; + + static { + TEST_JSON_OBJECT = JSON_BF.createObjectBuilder() + .add("greeting", "Hola") + .build(); + } + + public MainTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + public static void setup(Builder builder) { + builder.routing(Main::setupRouting); + } + + @Test + @Order(1) + void testDefaultGreeting() { + JsonObject jsonObject = get(); + assertThat(jsonObject.getString("greeting"), is("Hello World!")); + } + + @Test + @Order(2) + void testNamedGreeting() { + JsonObject jsonObject = personalizedGet("Joe"); + Assertions.assertEquals("Hello Joe!", jsonObject.getString("greeting")); + } + + @Test + @Order(3) + void testUpdateGreeting() { + try (Http1ClientResponse response = client.put() + .path("/greet/greeting") + .submit(TEST_JSON_OBJECT)) { + + assertThat(response.status(), is(Status.NO_CONTENT_204)); + } + + JsonObject jsonObject = personalizedGet("Joe"); + assertThat(jsonObject.getString("greeting"), is("Hola Joe!")); + } + + @Test + @Order(4) + void testMicrometer() { + Http1ClientResponse response = client.get() + .path("/micrometer") + .request(); + + assertThat(response.status().code(), is(200)); + + String output = response.as(String.class); + String expected = Main.ALL_GETS_TIMER_NAME + "_seconds_count " + expectedAllGets; + assertThat("Unable to find expected all-gets timer count " + expected + "; output is " + output, + output, containsString(expected)); // all gets; the put + // is not counted + assertThat("Unable to find expected all-gets timer sum", output, + containsString(Main.ALL_GETS_TIMER_NAME + "_seconds_sum")); + expected = Main.PERSONALIZED_GETS_COUNTER_NAME + "_total " + expectedPersonalizedGets; + assertThat("Unable to find expected counter result " + expected + "; output is " + output, + output, containsString(expected)); + response.close(); + } + + private JsonObject get() { + return get("/greet"); + } + + private JsonObject get(String path) { + JsonObject jsonObject = client.get() + .path(path) + .requestEntity(JsonObject.class); + expectedAllGets++; + return jsonObject; + } + + private JsonObject personalizedGet(String name) { + JsonObject result = get("/greet/" + name); + expectedPersonalizedGets++; + return result; + } +} diff --git a/examples/integrations/micronaut/data/README.md b/examples/integrations/micronaut/data/README.md new file mode 100644 index 000000000..52b780e84 --- /dev/null +++ b/examples/integrations/micronaut/data/README.md @@ -0,0 +1,146 @@ +# Helidon Micronaut Data Example + +This example shows integration with Micronaut Data into Helidon MP. + +## Sources + +This example combines Micronaut Data and CDI. + +### CDI classes + +The following classes are CDI and JAX-RS classes that use injected Micronaut beans: + +- `PetResource` - JAX-RS resource exposing Pet REST API +- `OwnerResource` - JAX-RS resource exposing Owner REST API +- `BeanValidationExceptionMapper` - JAX-RS exception mapper to return correct status code + in case of validation failure + +### Micronaut classes + +The following classes are pure Micronaut beans (and cannot have CDI injected into them) + +- `DbPetRepository` - Micronaut Data repository extending an abstract class +- `DbOwnerRepository` - Micronaut Data repository implementing an interface +- `DbPopulateData` - Micronaut startup event listener to initialize the database +- package `model` - data model of the database + +## Build and run + +Start the application: + +```shell +mvn package +java -jar target/helidon-examples-integrations-micronaut-data.jar +``` + +Access endpoints + +```shell +# Get all pets +curl -i http://localhost:8080/pets +# Get all owners +curl -i http://localhost:8080/owners +# Get a single pet +curl -i http://localhost:8080/pets/Dino +# Get a single owner +curl -i http://localhost:8080/owners/Barney +# To fail input validation +curl -i http://localhost:8080/pets/s +``` + +# To use Oracle XE instead of H2 + +- Update ./pom.xml to replace dependency on micronaut-jdbc-hikari with following +```xml + + io.micronaut.sql + micronaut-jdbc-ucp + runtime + +``` + +- Update ./pom.xml to replace dependency on com.h2database with following +```xml + + io.helidon.integrations.db + ojdbc + runtime + + + com.oracle.database.jdbc + ucp + runtime + +``` + +- Update ./src/main/java/io/helidon/examples/integrations/micronaut/data/DbOwnerRepository.java and + ./src/main/java/io/helidon/examples/integrations/micronaut/data/DbOwnerRepository.java to change from + Dialect.H2 to Dialect.ORACLE + +- Update ./src/main/java/io/helidon/examples/integrations/micronaut/data/DbPopulateData.java to change Typehint to + @TypeHint(typeNames = {"oracle.jdbc.OracleDriver"}) + +- Install Oracle XE + Instructions for running XE in a docker container can be found here: https://github.com/oracle/docker-images/tree/master/OracleDatabase/SingleInstance + +- Update ./src/main/resources/META-INF/microprofile-config.properties to comment out h2 related datasource.* properties and uncomment+update following ones related to XE. +```properties +#datasources.default.url=jdbc:oracle:thin:@localhost:/ +#datasources.default.driverClassName=oracle.jdbc.OracleDriver +#datasources.default.username=system +#datasources.default.password= +#datasources.default.schema-generate=CREATE_DROP +#datasources.default.dialect=oracle +``` + +# To use Oracle ATP cloud service instead of H2 + +- Update ./pom.xml to replace dependency on micronaut-jdbc-hikari with following +```xml + + io.micronaut.sql + micronaut-jdbc-ucp + runtime + +``` + +- Update ./pom.xml to replace dependency on com.h2database with following +```xml + + io.helidon.integrations.db + ojdbc + runtime + + + com.oracle.database.jdbc + ucp + runtime + +``` + +- Update ./src/main/java/io/helidon/examples/integrations/micronaut/data/DbOwnerRepository.java and + ./src/main/java/io/helidon/examples/integrations/micronaut/data/DbOwnerRepository.java to change from + Dialect.H2 to Dialect.ORACLE + +- Update ./src/main/java/io/helidon/examples/integrations/micronaut/data/DbPopulateData.java to change Typehint to + @TypeHint(typeNames = {"oracle.jdbc.OracleDriver"}) + +- Setup ATP + Instructions for ATP setup can be found here: https://blogs.oracle.com/developers/the-complete-guide-to-getting-up-and-running-with-autonomous-database-in-the-cloud + +- Create Schema used by test +```sql +CREATE TABLE "PET" ("ID" VARCHAR(36),"OWNER_ID" NUMBER(19) NOT NULL,"NAME" VARCHAR(255) NOT NULL,"TYPE" VARCHAR(255) NOT NULL); +CREATE SEQUENCE "OWNER_SEQ" MINVALUE 1 START WITH 1 NOCACHE NOCYCLE; +CREATE TABLE "OWNER" ("ID" NUMBER(19) PRIMARY KEY NOT NULL,"AGE" NUMBER(10) NOT NULL,"NAME" VARCHAR(255) NOT NULL); +``` + +- Update ./src/main/resources/META-INF/microprofile-config.properties to comment out h2 related datasource.* properties and add uncomment+update following ones related to ATP. +```properties +#datasources.default.url=jdbc:oracle:thin:@?TNS_ADMIN= +#datasources.default.driverClassName=oracle.jdbc.OracleDriver +#datasources.default.username= +#datasources.default.password= +#datasources.default.schema-generate=NONE +#datasources.default.dialect=oracle +``` \ No newline at end of file diff --git a/examples/integrations/micronaut/data/pom.xml b/examples/integrations/micronaut/data/pom.xml new file mode 100644 index 000000000..3b09d250e --- /dev/null +++ b/examples/integrations/micronaut/data/pom.xml @@ -0,0 +1,162 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + helidon-examples-integrations-micronaut-data + 1.0.0-SNAPSHOT + Helidon Examples Integration Micronaut Data + + + + jakarta.persistence + jakarta.persistence-api + provided + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.micronaut.data + micronaut-data-jdbc + + + io.micronaut + micronaut-runtime + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.integrations.micronaut + helidon-integrations-micronaut-cdi + runtime + + + io.helidon.integrations.micronaut + helidon-integrations-micronaut-data + runtime + + + io.micronaut.data + micronaut-data-tx + runtime + + + io.micronaut.sql + micronaut-jdbc-hikari + runtime + + + com.h2database + + + h2 + runtime + + + io.smallrye + jandex + runtime + true + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + io.micronaut + micronaut-inject-java + ${version.lib.micronaut} + + + io.micronaut + micronaut-validation + ${version.lib.micronaut} + + + io.micronaut.data + micronaut-data-processor + ${version.lib.micronaut.data} + + + io.helidon.integrations.micronaut + helidon-integrations-micronaut-cdi-processor + ${helidon.version} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/BeanValidationExceptionMapper.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/BeanValidationExceptionMapper.java new file mode 100644 index 000000000..4d184c4bc --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/BeanValidationExceptionMapper.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micronaut.data; + +import javax.validation.ConstraintViolationException; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** + * A JAX-RS provider that maps {@link jakarta.validation.ConstraintViolationException} from bean validation + * to a proper JAX-RS response with {@link jakarta.ws.rs.core.Response.Status#BAD_REQUEST} status. + * If this provider is not present, validation exception from Micronaut would end with an internal server + * error. + */ +@Provider +public class BeanValidationExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(ConstraintViolationException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(exception.getMessage()) + .build(); + } +} diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/DbOwnerRepository.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/DbOwnerRepository.java new file mode 100644 index 000000000..84a6f55d2 --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/DbOwnerRepository.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micronaut.data; + +import java.util.List; +import java.util.Optional; + +import io.helidon.examples.integrations.micronaut.data.model.Owner; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.CrudRepository; + +/** + * Micronaut Data repository for pet owners. + */ +@JdbcRepository(dialect = Dialect.H2) +public interface DbOwnerRepository extends CrudRepository { + /** + * Get all owners from the database. + * + * @return all owners + */ + @Override + List findAll(); + + /** + * Find an owner by name. + * + * @param name name of owner + * @return owner if found + */ + Optional findByName(String name); +} diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/DbPetRepository.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/DbPetRepository.java new file mode 100644 index 000000000..a02814655 --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/DbPetRepository.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micronaut.data; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import io.helidon.examples.integrations.micronaut.data.model.NameDTO; +import io.helidon.examples.integrations.micronaut.data.model.Pet; + +import io.micronaut.data.annotation.Join; +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.PageableRepository; + +/** + * Micronaut data repository for pets. + */ +@JdbcRepository(dialect = Dialect.H2) +public abstract class DbPetRepository implements PageableRepository { + + /** + * Get all pets. + * + * @param pageable pageable instance + * @return list of pets + */ + public abstract List list(Pageable pageable); + + /** + * Find a pet by its name. + * + * @param name pet name + * @return pet if it was found + */ + @Join("owner") + public abstract Optional findByName(String name); +} diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/DbPopulateData.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/DbPopulateData.java new file mode 100644 index 000000000..dcd86a40f --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/DbPopulateData.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + This class is almost exactly copied from Micronaut examples. + */ +package io.helidon.examples.integrations.micronaut.data; + +import java.util.Arrays; + +import io.helidon.examples.integrations.micronaut.data.model.Owner; +import io.helidon.examples.integrations.micronaut.data.model.Pet; + +import io.micronaut.context.event.StartupEvent; +import io.micronaut.core.annotation.TypeHint; +import io.micronaut.runtime.event.annotation.EventListener; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +/** + * A Micronaut bean that listens on startup event and populates database with data. + */ +@Singleton +@TypeHint(typeNames = {"org.h2.Driver", "org.h2.mvstore.db.MVTableEngine"}) +public class DbPopulateData { + private final DbOwnerRepository ownerRepository; + private final DbPetRepository petRepository; + + @Inject + DbPopulateData(DbOwnerRepository ownerRepository, DbPetRepository petRepository) { + this.ownerRepository = ownerRepository; + this.petRepository = petRepository; + } + + @EventListener + void init(StartupEvent event) { + Owner fred = new Owner("Fred"); + fred.setAge(45); + Owner barney = new Owner("Barney"); + barney.setAge(40); + ownerRepository.saveAll(Arrays.asList(fred, barney)); + + Pet dino = new Pet("Dino", fred); + Pet bp = new Pet("Baby Puss", fred); + bp.setType(Pet.PetType.CAT); + Pet hoppy = new Pet("Hoppy", barney); + + petRepository.saveAll(Arrays.asList(dino, bp, hoppy)); + } +} diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/OwnerResource.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/OwnerResource.java new file mode 100644 index 000000000..58aaca488 --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/OwnerResource.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micronaut.data; + +import io.helidon.examples.integrations.micronaut.data.model.Owner; + +import jakarta.inject.Inject; +import jakarta.validation.constraints.Pattern; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import org.eclipse.microprofile.metrics.annotation.Timed; + +/** + * JAX-RS resource, and the MicroProfile entry point to manage pet owners. + * This resource used Micronaut data beans (repositories) to query database, and + * bean validation as implemented by Micronaut. + */ +@Path("/owners") +public class OwnerResource { + private final DbOwnerRepository ownerRepository; + + /** + * Create a new instance with repository. + * + * @param ownerRepo owner repository from Micronaut data + */ + @Inject + public OwnerResource(DbOwnerRepository ownerRepo) { + this.ownerRepository = ownerRepo; + } + + /** + * Gets all owners from the database. + * @return all owners, using JSON-B to map them to JSON + */ + @GET + public Iterable getAll() { + return ownerRepository.findAll(); + } + + /** + * Get a named owner from the database. + * + * @param name name of the owner to find, must be at least two characters long, may contain whitespace + * @return a single owner + * @throws jakarta.ws.rs.NotFoundException in case the owner is not in the database (to return 404 status) + */ + @Path("/{name}") + @GET + @Timed + public Owner owner(@PathParam("name") @Pattern(regexp = "\\w+[\\w+\\s?]*\\w") String name) { + return ownerRepository.findByName(name) + .orElseThrow(() -> new NotFoundException("Owner by name " + name + " does not exist")); + } +} diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/PetResource.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/PetResource.java new file mode 100644 index 000000000..a5fea526e --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/PetResource.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micronaut.data; + +import javax.validation.constraints.Pattern; + +import io.helidon.examples.integrations.micronaut.data.model.Pet; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import org.eclipse.microprofile.metrics.annotation.Timed; + +/** + * JAX-RS resource, and the MicroProfile entry point to manage pets. + * This resource used Micronaut data beans (repositories) to query database, and + * bean validation as implemented by Micronaut. + */ +@Path("/pets") +public class PetResource { + private final DbPetRepository petRepository; + + /** + * Create a new instance with pet repository. + * + * @param petRepo Pet repository from Micronaut data + */ + @Inject + public PetResource(DbPetRepository petRepo) { + this.petRepository = petRepo; + } + + /** + * Gets all pets from the database. + * @return all pets, using JSON-B to map them to JSON + */ + @GET + public Iterable getAll() { + return petRepository.findAll(); + } + + /** + * Get a named pet from the database. + * + * @param name name of the pet to find, must be at least two characters long, may contain whitespace + * @return a single pet + * @throws jakarta.ws.rs.NotFoundException in case the pet is not in the database (to return 404 status) + */ + @Path("/{name}") + @GET + @Timed + public Pet pet(@PathParam("name") @Pattern(regexp = "\\w+[\\w+\\s?]*\\w") String name) { + return petRepository.findByName(name) + .orElseThrow(() -> new NotFoundException("Pet by name " + name + " does not exist")); + } +} diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/NameDTO.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/NameDTO.java new file mode 100644 index 000000000..35a119c27 --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/NameDTO.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micronaut.data.model; + +import io.micronaut.core.annotation.Introspected; + +/** + * Used in list of names of pets. + */ +@Introspected +public class NameDTO { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/Owner.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/Owner.java new file mode 100644 index 000000000..37591b873 --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/Owner.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micronaut.data.model; + +import io.micronaut.core.annotation.Creator; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +/** + * Owner database entity. + */ +@Entity +public class Owner { + + @Id + @GeneratedValue + private Long id; + private String name; + private int age; + + /** + * Create a named owner. + * + * @param name name of the owner + */ + @Creator + public Owner(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + +} diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/Pet.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/Pet.java new file mode 100644 index 000000000..2b8858bc4 --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/Pet.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micronaut.data.model; + +import java.util.UUID; + +import io.micronaut.core.annotation.Creator; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.data.annotation.AutoPopulated; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +/** + * Pet database entity. + */ +@Entity +public class Pet { + + @Id + @AutoPopulated + private UUID id; + private String name; + @ManyToOne + private Owner owner; + private PetType type = PetType.DOG; + + /** + * Creates a new pet. + * @param name name of the pet + * @param owner owner of the pet (optional) + */ + // NOTE - please use Nullable from this package, jakarta.annotation.Nullable will fail with JPMS, + // as it is declared in the same package as is used by annother module (jakarta.annotation-api) + @Creator + public Pet(String name, @Nullable Owner owner) { + this.name = name; + this.owner = owner; + } + + public Owner getOwner() { + return owner; + } + + public String getName() { + return name; + } + + public UUID getId() { + return id; + } + + public PetType getType() { + return type; + } + + public void setType(PetType type) { + this.type = type; + } + + public void setId(UUID id) { + this.id = id; + } + + /** + * Type of pet. + */ + public enum PetType { + /** + * Dog. + */ + DOG, + /** + * Cat. + */ + CAT + } +} diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/package-info.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/package-info.java new file mode 100644 index 000000000..c5cd8a9a3 --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/model/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Model classes for entities and transfer objects. + */ +package io.helidon.examples.integrations.micronaut.data.model; diff --git a/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/package-info.java b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/package-info.java new file mode 100644 index 000000000..d11096061 --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/java/io/helidon/examples/integrations/micronaut/data/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example showing use of Micronaut Data in Helidon MicroProfile server. + */ +package io.helidon.examples.integrations.micronaut.data; diff --git a/examples/integrations/micronaut/data/src/main/resources/META-INF/beans.xml b/examples/integrations/micronaut/data/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..676e09a2d --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/micronaut/data/src/main/resources/META-INF/microprofile-config.properties b/examples/integrations/micronaut/data/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..6b196be88 --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,40 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server.port=8080 + +#When native image is used, we must use remote h2 +#datasources.default.url=jdbc:h2:tcp://localhost:9092/test +datasources.default.url=jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE +datasources.default.driverClassName=org.h2.Driver +datasources.default.username=sa +datasources.default.password=${EMPTY} +datasources.default.schema-generate=CREATE_DROP +datasources.default.dialect=h2 + +#datasources.default.url=jdbc:oracle:thin:@localhost:/ +#datasources.default.driverClassName=oracle.jdbc.OracleDriver +#datasources.default.username=system +#datasources.default.password= +#datasources.default.schema-generate=CREATE_DROP +#datasources.default.dialect=oracle + +#datasources.default.url=jdbc:oracle:thin:@?TNS_ADMIN= +#datasources.default.driverClassName=oracle.jdbc.OracleDriver +#datasources.default.username= +#datasources.default.password= +#datasources.default.schema-generate=NONE +#datasources.default.dialect=oracle diff --git a/examples/integrations/micronaut/data/src/main/resources/logging.properties b/examples/integrations/micronaut/data/src/main/resources/logging.properties new file mode 100644 index 000000000..d1f6eb1e5 --- /dev/null +++ b/examples/integrations/micronaut/data/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO diff --git a/examples/integrations/micronaut/data/src/test/java/io/helidon/examples/integrations/micronaut/data/MicronautExampleTest.java b/examples/integrations/micronaut/data/src/test/java/io/helidon/examples/integrations/micronaut/data/MicronautExampleTest.java new file mode 100644 index 000000000..e725cbb1f --- /dev/null +++ b/examples/integrations/micronaut/data/src/test/java/io/helidon/examples/integrations/micronaut/data/MicronautExampleTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.micronaut.data; + +import io.helidon.examples.integrations.micronaut.data.model.Pet; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +class MicronautExampleTest { + @Inject + private WebTarget webTarget; + + @Test + void testAllPets() { + JsonArray jsonValues = webTarget.path("/pets") + .request() + .get(JsonArray.class); + + assertThat("We should get all pets", jsonValues.size(), is(3)); + } + + @Test + void testGetPet() { + JsonObject pet = webTarget.path("/pets/Dino") + .request() + .get(JsonObject.class); + + assertThat(pet.getString("name"), is("Dino")); + assertThat(pet.getString("type"), is(Pet.PetType.DOG.toString())); + } + + @Test + void testNotFound() { + try (Response response = webTarget.path("/pets/Fino") + .request() + .get()) { + assertThat("Should be not found: 404", response.getStatus(), is(404)); + } + } + + @Test + void testValidationError() { + try (Response response = webTarget.path("/pets/a") + .request() + .get()) { + assertThat("Should be bad request: 400", response.getStatus(), is(400)); + } + } + +} \ No newline at end of file diff --git a/examples/integrations/micronaut/pom.xml b/examples/integrations/micronaut/pom.xml new file mode 100644 index 000000000..1eb9088ec --- /dev/null +++ b/examples/integrations/micronaut/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + io.helidon.examples.integrations + helidon-examples-integrations-project + 1.0.0-SNAPSHOT + + io.helidon.examples.integrations.micronaut + helidon-examples-integrations-micronaut-project + 1.0.0-SNAPSHOT + pom + Helidon Examples Integration Micronaut + + + data + + diff --git a/examples/integrations/microstream/README.md b/examples/integrations/microstream/README.md new file mode 100644 index 000000000..d48be2072 --- /dev/null +++ b/examples/integrations/microstream/README.md @@ -0,0 +1 @@ +# Microstream Integrations Examples diff --git a/examples/integrations/microstream/greetings-mp/README.md b/examples/integrations/microstream/greetings-mp/README.md new file mode 100644 index 000000000..b298250df --- /dev/null +++ b/examples/integrations/microstream/greetings-mp/README.md @@ -0,0 +1,28 @@ +# Microstream integration example + +This example uses Microstream to persist the greetings supplied + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-integrations-microstream-greetings-mp.jar +``` + +## Endpoints + +Get default greeting message: +```shell +curl -X GET http://localhost:7001/greet +``` + +Get greeting message for Joe: + +```shell +curl -X GET http://localhost:7001/greet/Joe +``` + +Add a greeting: +```shell +curl -X PUT -H "Content-Type: application/json" -d '{"message" : "Howdy"}' http://localhost:7001/greet/greeting +``` \ No newline at end of file diff --git a/examples/integrations/microstream/greetings-mp/pom.xml b/examples/integrations/microstream/greetings-mp/pom.xml new file mode 100644 index 000000000..1983f45e7 --- /dev/null +++ b/examples/integrations/microstream/greetings-mp/pom.xml @@ -0,0 +1,84 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + + helidon-examples-integrations-microstream-greetings-mp + 1.0.0-SNAPSHOT + Helidon Examples Integration Microstream Greetings mp + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.integrations.microstream + helidon-integrations-microstream-cdi + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + + \ No newline at end of file diff --git a/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/GreetResource.java b/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/GreetResource.java new file mode 100644 index 000000000..56ed25715 --- /dev/null +++ b/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/GreetResource.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.microstream.greetings.mp; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * A simple service to greet you. Examples: + * + * Get default greeting message: + * curl -X GET http://localhost:7001/greet + * + * Get greeting message for Joe: + * curl -X GET http://localhost:7001/greet/Joe + * + * add a greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:7001/greet/greeting + * + * The message is returned as a JSON object + */ +@Path("/greet") +@RequestScoped +public class GreetResource { + + private final GreetingProvider greetingProvider; + + /** + * Using constructor injection to get a configuration property. + * By default this gets the value from META-INF/microprofile-config + * + * @param greetingConfig the configured greeting message + */ + @Inject + public GreetResource(GreetingProvider greetingConfig) { + this.greetingProvider = greetingConfig; + } + + /** + * Return a default greeting message. + * + * @return {@link GreetingMessage} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getDefaultMessage() { + return createResponse("World"); + } + + private GreetingMessage createResponse(String who) { + String msg = String.format("%s %s!", greetingProvider.getGreeting(), who); + + return new GreetingMessage(msg); + } + + /** + * Return a greeting message using the name that was provided. + * + * @param name the name to greet + * @return {@link GreetingMessage} + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getMessage(@PathParam("name") String name) { + return createResponse(name); + } + + /** + * Set the greeting to use in future messages. + * + * @param message JSON containing the new greeting + * @return {@link Response} + */ + @Path("/greeting") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response updateGreeting(GreetingMessage message) { + if (message.getMessage() == null) { + GreetingMessage entity = new GreetingMessage("No greeting provided"); + return Response.status(Response.Status.BAD_REQUEST).entity(entity).build(); + } + + greetingProvider.addGreeting(message.getMessage()); + return Response.status(Response.Status.NO_CONTENT).build(); + } + +} diff --git a/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/GreetingMessage.java b/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/GreetingMessage.java new file mode 100644 index 000000000..389424b85 --- /dev/null +++ b/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/GreetingMessage.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.microstream.greetings.mp; + +/** + * POJO defining the greeting message content. + */ +@SuppressWarnings("unused") +public class GreetingMessage { + private String message; + + /** + * Create a new GreetingMessage instance. + */ + public GreetingMessage() { + } + + /** + * Create a new GreetingMessage instance. + * + * @param message message + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Gets the message value. + * + * @return message value + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message message value to set + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/GreetingProvider.java b/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/GreetingProvider.java new file mode 100644 index 000000000..777c32960 --- /dev/null +++ b/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/GreetingProvider.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.microstream.greetings.mp; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import io.helidon.integrations.microstream.cdi.MicrostreamStorage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import one.microstream.storage.embedded.types.EmbeddedStorageManager; + +/** + * Provider for greeting message that are persisted by microstream. + */ +@ApplicationScoped +public class GreetingProvider { + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + private final EmbeddedStorageManager storage; + private final Random rnd = new Random(); + + private List greetingMessages; + + /** + * Creates new GreetingProvider using a microstream EmbeddedStorageManager. + * + * @param storage the used EmbeddedStorageManager. + */ + @SuppressWarnings("unchecked") + @Inject + public GreetingProvider(@MicrostreamStorage(configNode = "one.microstream.storage.greetings") + EmbeddedStorageManager storage) { + super(); + this.storage = storage; + + // load stored data + greetingMessages = (List) storage.root(); + + // Initialize storage if empty + if (greetingMessages == null) { + greetingMessages = new ArrayList<>(); + storage.setRoot(greetingMessages); + storage.storeRoot(); + addGreeting("Hello"); + } + } + + /** + * Add a new greeting to the available greetings and persist it. + * + * @param newGreeting the new greeting to be added and persisted. + */ + public void addGreeting(String newGreeting) { + try { + lock.writeLock().lock(); + greetingMessages.add(newGreeting); + storage.store(greetingMessages); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * returns a random greeting. + * + * @return a greeting. + */ + public String getGreeting() { + try { + lock.readLock().lock(); + return greetingMessages.get(rnd.nextInt(greetingMessages.size())); + } finally { + lock.readLock().unlock(); + } + } + +} diff --git a/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/package-info.java b/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/package-info.java new file mode 100644 index 000000000..e43f651b0 --- /dev/null +++ b/examples/integrations/microstream/greetings-mp/src/main/java/io/helidon/examples/integrations/microstream/greetings/mp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * An example that uses Microstream to persist the greetings. + */ +package io.helidon.examples.integrations.microstream.greetings.mp; diff --git a/examples/integrations/microstream/greetings-mp/src/main/resources/META-INF/beans.xml b/examples/integrations/microstream/greetings-mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e149ced7d --- /dev/null +++ b/examples/integrations/microstream/greetings-mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/microstream/greetings-mp/src/main/resources/META-INF/microprofile-config.properties b/examples/integrations/microstream/greetings-mp/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..d14f5354e --- /dev/null +++ b/examples/integrations/microstream/greetings-mp/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +one.microstream.storage.greetings.storage-directory=./greetingsStorage diff --git a/examples/integrations/microstream/greetings-mp/src/test/java/io/helidon/examples/integrations/microstream/greetings/mp/MicrostreamExampleGreetingsMpTest.java b/examples/integrations/microstream/greetings-mp/src/test/java/io/helidon/examples/integrations/microstream/greetings/mp/MicrostreamExampleGreetingsMpTest.java new file mode 100644 index 000000000..bcae47416 --- /dev/null +++ b/examples/integrations/microstream/greetings-mp/src/test/java/io/helidon/examples/integrations/microstream/greetings/mp/MicrostreamExampleGreetingsMpTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.microstream.greetings.mp; + +import java.nio.file.Path; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@HelidonTest +class MicrostreamExampleGreetingsMpTest { + + @Inject + private WebTarget webTarget; + + @TempDir + static Path tempDir; + + @BeforeAll + static void beforeAll() { + System.setProperty("one.microstream.storage.greetings.storage-directory", tempDir.toString()); + } + + @Test + void testGreeting() { + GreetingMessage response = webTarget.path("/greet").request().get(GreetingMessage.class); + + assertEquals("Hello World!", response.getMessage(), "response should be 'Hello World' "); + } + +} diff --git a/examples/integrations/microstream/greetings-se/README.md b/examples/integrations/microstream/greetings-se/README.md new file mode 100644 index 000000000..358ca8939 --- /dev/null +++ b/examples/integrations/microstream/greetings-se/README.md @@ -0,0 +1,32 @@ +# Microstream integration example + +This example uses Microstream to persist a log entry for every greeting + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-integrations-microstream-greetings-se.jar +``` + +## Endpoints + +Get default greeting message: +```shell +curl -X GET http://localhost:8080/greet +``` + +Get greeting message for Joe: +```shell +curl -X GET http://localhost:8080/greet/Joe +``` + +Change greeting: +```shell +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting +``` + +Get the logs: +```shell +curl -X GET http://localhost:8080/greet/logs +``` \ No newline at end of file diff --git a/examples/integrations/microstream/greetings-se/pom.xml b/examples/integrations/microstream/greetings-se/pom.xml new file mode 100644 index 000000000..e1ab07b07 --- /dev/null +++ b/examples/integrations/microstream/greetings-se/pom.xml @@ -0,0 +1,106 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + helidon-examples-integrations-microstream-greetings-se + 1.0.0-SNAPSHOT + Helidon Examples Integration Microstream Greetings se + + + io.helidon.examples.integrations.microstream.greetings.se.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.integrations.microstream + helidon-integrations-microstream + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.config + helidon-config-yaml + + + io.helidon.health + helidon-health-checks + + + io.helidon.metrics + helidon-metrics-api + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/GreetingService.java b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/GreetingService.java new file mode 100644 index 000000000..809862139 --- /dev/null +++ b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/GreetingService.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.microstream.greetings.se; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.integrations.microstream.core.EmbeddedStorageManagerBuilder; +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.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple service to greet you. Examples: + *

+ * Get default greeting message: + * curl -X GET http://localhost:8080/greet + *

+ * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + *

+ * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + *

+ * Get the logs: + * curl -X GET http://localhost:8080/greet/logs + *

+ * The message is returned as a JSON object + */ + +public class GreetingService implements HttpService { + + private final AtomicReference greeting = new AtomicReference<>(); + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + private static final Logger LOGGER = Logger.getLogger(GreetingService.class.getName()); + + private final GreetingServiceMicrostreamContext mctx; + + GreetingService(Config config) { + greeting.set(config.get("app.greeting").asString().orElse("Ciao")); + + mctx = new GreetingServiceMicrostreamContext(EmbeddedStorageManagerBuilder.create(config.get("microstream"))); + // we need to initialize the root element first + // if we do not wait here, we have a race where HTTP method may be invoked before we initialize root + mctx.start(); + mctx.initRootElement(); + } + + @Override + public void routing(HttpRules rules) { + rules.get("/", this::getDefaultMessageHandler) + .get("/logs", this::getLog) + .get("/{name}", this::getMessageHandler) + .put("/greeting", this::updateGreetingHandler); + } + + private void getLog(ServerRequest request, ServerResponse response) { + JsonArrayBuilder arrayBuilder = JSON.createArrayBuilder(); + mctx.getLogs().forEach((entry) -> arrayBuilder.add( + JSON.createObjectBuilder() + .add("name", entry.getName()) + .add("time", entry.getDateTime().toString()))); + response.send(arrayBuilder.build()); + } + + /** + * 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().get("name"); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + String msg = String.format("%s %s!", greeting.get(), name); + + mctx.addLogEntry(name); + + JsonObject returnObject = JSON.createObjectBuilder() + .add("message", msg) + .build(); + response.send(returnObject); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + if (!jo.containsKey("greeting")) { + JsonObject jsonErrorObject = JSON.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting.set(jo.getString("greeting")); + response.status(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) { + JsonObject jsonObject = request.content().as(JsonObject.class); + updateGreetingFromJson(jsonObject, response); + } + +} diff --git a/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/GreetingServiceMicrostreamContext.java b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/GreetingServiceMicrostreamContext.java new file mode 100644 index 000000000..8edcedbd5 --- /dev/null +++ b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/GreetingServiceMicrostreamContext.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.microstream.greetings.se; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import one.microstream.storage.embedded.types.EmbeddedStorageManager; + +/** + * This class extends {@link MicrostreamExecutionContext} and provides data access methods. + */ +public class GreetingServiceMicrostreamContext extends MicrostreamExecutionContext { + + /** + * Create a new instance. + * + * @param storageManager the EmbeddedStorageManager used. + */ + public GreetingServiceMicrostreamContext(EmbeddedStorageManager storageManager) { + super(storageManager); + } + + /** + * Add and store a new log entry. + * + * @param name parameter for log text. + */ + @SuppressWarnings({"unchecked", "resource"}) + public void addLogEntry(String name) { + List logs = (List) storageManager().root(); + logs.add(new LogEntry(name, LocalDateTime.now())); + storageManager().store(logs); + } + + /** + * initialize the storage root with a new, empty List. + */ + @SuppressWarnings("resource") + public void initRootElement() { + if (storageManager().root() == null) { + storageManager().setRoot(new ArrayList()); + storageManager().storeRoot(); + } + } + + /** + * returns a List of all stored LogEntries. + * + * @return all LogEntries. + */ + @SuppressWarnings({"unchecked", "resource"}) + public List getLogs() { + return (List) storageManager().root(); + } + +} diff --git a/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/LogEntry.java b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/LogEntry.java new file mode 100644 index 000000000..83e67f942 --- /dev/null +++ b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/LogEntry.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.microstream.greetings.se; + +import java.time.LocalDateTime; + +/** + * + * simple POJO that represents a Log entry that is stored by microstream in this example. + * + */ +public class LogEntry { + private String name; + private LocalDateTime dateTime; + + /** + * The Constructor. + * + * @param name name to be logged. + * + * @param dateTime dateTime date and time to be logged + */ + public LogEntry(String name, LocalDateTime dateTime) { + super(); + this.name = name; + this.dateTime = dateTime; + } + + public String getName() { + return name; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + +} diff --git a/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/Main.java b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/Main.java new file mode 100644 index 000000000..8c03369a5 --- /dev/null +++ b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/Main.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.microstream.greetings.se; + +import io.helidon.config.ClasspathConfigSource; +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * Microstream demo with a simple rest application. + */ +public class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build().start(); + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + static void setup(WebServerConfig.Builder server) { + LogConfig.configureRuntime(); + Config config = Config.builder() + .addSource(ClasspathConfigSource.create("/application.yaml")) + .build(); + + // Build server with JSONP support + server.config(config.get("server")) + .routing(r -> routing(r, config)); + } + + /** + * Setup routing. + * + * @param routing routing builder + * @param config configuration of this server + */ + static void routing(HttpRouting.Builder routing, Config config) { + GreetingService greetService = new GreetingService(config); + routing.register("/greet", greetService); + } +} diff --git a/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/MicrostreamExecutionContext.java b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/MicrostreamExecutionContext.java new file mode 100644 index 000000000..4497cf1b5 --- /dev/null +++ b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/MicrostreamExecutionContext.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.microstream.greetings.se; + +import one.microstream.reference.LazyReferenceManager; +import one.microstream.storage.embedded.types.EmbeddedStorageManager; + +/** + * Provides a very simply way to access a Microstream storage, and it's associated data. + */ +public class MicrostreamExecutionContext { + + private final EmbeddedStorageManager storage; + + /** + * Creates a new instance. + * + * @param storageManager the used EmbeddedStorageManager. + */ + public MicrostreamExecutionContext(EmbeddedStorageManager storageManager) { + this.storage = storageManager; + } + + /** + * returns the used storageManager. + * + * @return the used EmbeddedStorageManager. + */ + public EmbeddedStorageManager storageManager() { + return storage; + } + + /** + * Start the storage. + * + * @return the started EmbeddedStorageManager. + */ + public EmbeddedStorageManager start() { + return storage.start(); + } + + /** + * Shutdown the storage. + * + * @return the stopped EmbeddedStorageManager. + */ + public EmbeddedStorageManager shutdown() { + storage.shutdown(); + LazyReferenceManager.get().stop(); + return storage; + } + + /** + * Return the persistent object graph's root object. + * + * @param type of the root object + * @return the graph's root object casted to + */ + @SuppressWarnings("unchecked") + public T root() { + return (T) storage.root(); + } + + /** + * Sets the passed instance as the new root for the persistent object graph. + * + * @param object the new root object + * @return the new root object + */ + public Object setRoot(Object object) { + return storage.setRoot(object); + } + + /** + * Stores the registered root instance. + * + * @return the root instance's objectId. + */ + public long storeRoot() { + return storage.storeRoot(); + } + + /** + * Stores the passed object. + * + * @param object object to store + * @return the object id representing the passed instance. + */ + public long store(Object object) { + return storage.store(object); + } +} diff --git a/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/package-info.java b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/package-info.java new file mode 100644 index 000000000..5fa972bf3 --- /dev/null +++ b/examples/integrations/microstream/greetings-se/src/main/java/io/helidon/examples/integrations/microstream/greetings/se/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * An example that uses Microstream to persist a log entry for every greeting. + */ +package io.helidon.examples.integrations.microstream.greetings.se; diff --git a/examples/integrations/microstream/greetings-se/src/main/resources/application.yaml b/examples/integrations/microstream/greetings-se/src/main/resources/application.yaml new file mode 100644 index 000000000..d22a1bae2 --- /dev/null +++ b/examples/integrations/microstream/greetings-se/src/main/resources/application.yaml @@ -0,0 +1,26 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + greeting: "Hello" + +server: + port: 8080 + host: 0.0.0.0 + +microstream: + channel-count: 4 + housekeeping-interval: 2000ms diff --git a/examples/integrations/microstream/greetings-se/src/test/java/io/helidon/examples/integrations/microstream/greetings/se/MicrostreamExampleGreetingsSeTest.java b/examples/integrations/microstream/greetings-se/src/test/java/io/helidon/examples/integrations/microstream/greetings/se/MicrostreamExampleGreetingsSeTest.java new file mode 100644 index 000000000..caebeb120 --- /dev/null +++ b/examples/integrations/microstream/greetings-se/src/test/java/io/helidon/examples/integrations/microstream/greetings/se/MicrostreamExampleGreetingsSeTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.microstream.greetings.se; + +import java.nio.file.Path; + +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 jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +public class MicrostreamExampleGreetingsSeTest { + + @TempDir + static Path tempDir; + + private final Http1Client client; + + public MicrostreamExampleGreetingsSeTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + System.setProperty("microstream.storage-directory", tempDir.toString()); + Main.setup(server); + } + + @Test + void testExample() { + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(JsonObject.class).getString("message"), is("Hello Joe!")); + } + + try (Http1ClientResponse response = client.get("/greet/logs").request()) { + JsonArray jsonArray = response.as(JsonArray.class); + assertThat(jsonArray.get(0).asJsonObject().getString("name"), is("Joe")); + assertThat(jsonArray.get(0).asJsonObject().getString("time"), notNullValue()); + } + } +} diff --git a/examples/integrations/microstream/pom.xml b/examples/integrations/microstream/pom.xml new file mode 100644 index 000000000..ee19232c2 --- /dev/null +++ b/examples/integrations/microstream/pom.xml @@ -0,0 +1,39 @@ + + + + 4.0.0 + + io.helidon.examples.integrations + helidon-examples-integrations-project + 1.0.0-SNAPSHOT + + + io.helidon.examples.integrations.microstream + helidon-examples-integrations-microstream-project + 1.0.0-SNAPSHOT + Helidon Examples Integration Microstreams + pom + + + greetings-se + greetings-mp + + \ No newline at end of file diff --git a/examples/integrations/neo4j/README.md b/examples/integrations/neo4j/README.md new file mode 100644 index 000000000..887e2f240 --- /dev/null +++ b/examples/integrations/neo4j/README.md @@ -0,0 +1,40 @@ +# Helidon SE integration with Neo4J example + +## Build and run + +Bring up a Neo4j instance via Docker + +```shell +docker run --publish=7474:7474 --publish=7687:7687 -e 'NEO4J_AUTH=neo4j/secret' neo4j:4.0 +``` + +Goto the Neo4j browser and play the first step of the movies graph: [`:play movies`](http://localhost:7474/browser/?cmd=play&arg=movies). + +Build and run with JDK20 +```shell +mvn package +java -jar target/helidon-examples-integration-neo4j.jar +``` + +Then access the rest API like this: + +````shell +export PORT=38837 +curl localhost:${PORT}/api/movies +```` + +# Health and metrics + +Neo4jSupport provides health checks and metrics reading from Neo4j. + +Enable them in the driver: +```yaml + pool: + metricsEnabled: true +``` + +```shell +export PORT=38837 +curl localhost:${PORT}/observe/health +curl localhost:${PORT}/observe/metrics +``` diff --git a/examples/integrations/neo4j/pom.xml b/examples/integrations/neo4j/pom.xml new file mode 100644 index 000000000..9ca3792c5 --- /dev/null +++ b/examples/integrations/neo4j/pom.xml @@ -0,0 +1,133 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + helidon-examples-integration-neo4j + 1.0.0-SNAPSHOT + Helidon Examples Integrations Neo4j + + + io.helidon.examples.integrations.neo4j.Main + 5.12.0 + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.health + helidon-health-checks + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.http.media + helidon-http-media-jsonb + + + io.helidon.config + helidon-config-yaml + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + + + io.helidon.metrics + helidon-metrics + + + io.helidon.integrations.neo4j + helidon-integrations-neo4j + + + io.helidon.integrations.neo4j + helidon-integrations-neo4j-health + + + io.helidon.integrations.neo4j + helidon-integrations-neo4j-metrics + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + org.neo4j.test + neo4j-harness + ${neo4j-harness.version} + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + diff --git a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/Main.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/Main.java new file mode 100644 index 000000000..03c47d55d --- /dev/null +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/Main.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.neo4j; + +import java.util.List; + +import io.helidon.config.Config; +import io.helidon.examples.integrations.neo4j.domain.MovieRepository; +import io.helidon.health.checks.DeadlockHealthCheck; +import io.helidon.health.checks.DiskSpaceHealthCheck; +import io.helidon.health.checks.HeapMemoryHealthCheck; +import io.helidon.integrations.neo4j.Neo4j; +import io.helidon.integrations.neo4j.health.Neo4jHealthCheck; +import io.helidon.integrations.neo4j.metrics.Neo4jMetricsSupport; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.health.HealthObserver; +import io.helidon.webserver.spi.ServerFeature; + +import org.neo4j.driver.Driver; + +import static io.helidon.webserver.http.HttpRouting.Builder; + +/** + * The application main class. + */ +public class Main { + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + // load logging configuration + LogConfig.configureRuntime(); + + startServer(); + } + + static void startServer() { + Neo4j neo4j = Neo4j.create(Config.create().get("neo4j")); + Driver neo4jDriver = neo4j.driver(); + + WebServer server = WebServer.builder() + .featuresDiscoverServices(false) + .features(features(neo4jDriver)) + .routing(it -> routing(it, neo4jDriver)) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/api/movies"); + } + + static List features(Driver neo4jDriver) { + Neo4jHealthCheck healthCheck = Neo4jHealthCheck.create(neo4jDriver); + return List.of(ObserveFeature.just(HealthObserver.builder() + .useSystemServices(false) + .addCheck(HeapMemoryHealthCheck.create()) + .addCheck(DiskSpaceHealthCheck.create()) + .addCheck(DeadlockHealthCheck.create()) + .addCheck(healthCheck) + .build())); + } + + /** + * Updates HTTP Routing. + */ + static void routing(Builder routing, Driver neo4jDriver) { + + + Neo4jMetricsSupport.builder() + .driver(neo4jDriver) + .build() + .initialize(); + + + MovieService movieService = new MovieService(new MovieRepository(neo4jDriver)); + + + + routing.register(movieService); + } +} + diff --git a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/MovieService.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/MovieService.java new file mode 100644 index 000000000..5297b0e27 --- /dev/null +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/MovieService.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.neo4j; + +import io.helidon.examples.integrations.neo4j.domain.MovieRepository; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * The Movie service. + */ +public class MovieService implements HttpService { + + private final MovieRepository movieRepository; + + /** + * The movies service. + * + * @param movieRepository a movie repository. + */ + public MovieService(MovieRepository movieRepository) { + this.movieRepository = movieRepository; + } + + @Override + public void routing(HttpRules rules) { + rules.get("/api/movies", this::findMoviesHandler); + } + + private void findMoviesHandler(ServerRequest request, ServerResponse response) { + response.send(this.movieRepository.findAll()); + } +} diff --git a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Actor.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Actor.java new file mode 100644 index 000000000..8de20701c --- /dev/null +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Actor.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2002-2020 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * This file is part of Neo4j. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.helidon.examples.integrations.neo4j.domain; + +import java.util.ArrayList; +import java.util.List; + +/* + * Helidon changes are under the copyright of: + * + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * The Actor class. + * + * @author Michael Simons + */ +public class Actor { + + private final String name; + + private final List roles; + + /** + * Constructor for actor. + * + * @param name + * @param roles + */ + public Actor(String name, final List roles) { + this.name = name; + this.roles = new ArrayList<>(roles); + } + + public String getName() { + return name; + } + + public List getRoles() { + return roles; + } +} diff --git a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Movie.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Movie.java new file mode 100644 index 000000000..a091f41ef --- /dev/null +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Movie.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2002-2020 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * This file is part of Neo4j. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.helidon.examples.integrations.neo4j.domain; + +import java.util.ArrayList; +import java.util.List; + +/* + * Helidon changes are under the copyright of: + * + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * The Movie class. + * + * @author Michael Simons + */ +public class Movie { + + private final String title; + + private final String description; + + private List actors = new ArrayList<>(); + + private List directors = new ArrayList<>(); + + private Integer released; + + /** + * Constructor for Movie. + * + * @param title + * @param description + */ + public Movie(String title, String description) { + this.title = title; + this.description = description; + } + + public String getTitle() { + return title; + } + + public List getActors() { + return actors; + } + + public void setActors(List actors) { + this.actors = actors; + } + + public String getDescription() { + return description; + } + + public List getDirectors() { + return directors; + } + + public void setDirectorss(List directors) { + this.directors = directors; + } + + public Integer getReleased() { + return released; + } + + public void setReleased(Integer released) { + this.released = released; + } +} diff --git a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/MovieRepository.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/MovieRepository.java new file mode 100644 index 000000000..c7b6e3bcf --- /dev/null +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/MovieRepository.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2002-2020 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * This file is part of Neo4j. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.helidon.examples.integrations.neo4j.domain; + +import java.util.List; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Value; + +/* + * Helidon changes are under the copyright of: + * + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * The Movie repository. + * + * @author Michael Simons + */ +public final class MovieRepository { + + private final Driver driver; + + /** + * Constructor for the repo. + * + * @param driver + */ + public MovieRepository(Driver driver) { + this.driver = driver; + } + + /** + * Returns all the movies. + * @return List with movies + */ + public List findAll(){ + + try (var session = driver.session()) { + + var query = "" + + "match (m:Movie) " + + "match (m) <- [:DIRECTED] - (d:Person) " + + "match (m) <- [r:ACTED_IN] - (a:Person) " + + "return m, collect(d) as directors, collect({name:a.name, roles: r.roles}) as actors"; + + return session.executeRead(tx -> tx.run(query).list(r -> { + var movieNode = r.get("m").asNode(); + + var directors = r.get("directors").asList(v -> { + var personNode = v.asNode(); + return new Person(personNode.get("born").asInt(), personNode.get("name").asString()); + }); + + var actors = r.get("actors").asList(v -> { + return new Actor(v.get("name").asString(), v.get("roles").asList(Value::asString)); + }); + + var m = new Movie(movieNode.get("title").asString(), movieNode.get("tagline").asString()); + m.setReleased(movieNode.get("released").asInt()); + m.setDirectorss(directors); + m.setActors(actors); + return m; + })); + } + } +} diff --git a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Person.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Person.java new file mode 100644 index 000000000..a2db40933 --- /dev/null +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Person.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2002-2020 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * This file is part of Neo4j. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.helidon.examples.integrations.neo4j.domain; + +/* + * Helidon changes are under the copyright of: + * + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * The Person class. + * + * @author Michael Simons + */ +public class Person { + + private final String name; + + private Integer born; + + /** + * Constrictor for person. + * @param born + * @param name + */ + public Person(Integer born, String name) { + this.born = born; + this.name = name; + } + + public String getName() { + return name; + } + + public Integer getBorn() { + return born; + } + + public void setBorn(Integer born) { + this.born = born; + } + + @SuppressWarnings("checkstyle:OperatorWrap") + @Override + public String toString() { + return "Person{" + + "name='" + name + '\'' + + ", born=" + born + + '}'; + } +} diff --git a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/package-info.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/package-info.java new file mode 100644 index 000000000..0c5b784ac --- /dev/null +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Domain objects for movies. + */ +package io.helidon.examples.integrations.neo4j.domain; diff --git a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/package-info.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/package-info.java new file mode 100644 index 000000000..40e08b886 --- /dev/null +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon Integrations Neo4j Example. + *

+ * + * @see io.helidon.examples.integrations.neo4j.Main + */ +package io.helidon.examples.integrations.neo4j; diff --git a/examples/integrations/neo4j/src/main/resources/application.yaml b/examples/integrations/neo4j/src/main/resources/application.yaml new file mode 100644 index 000000000..74153354d --- /dev/null +++ b/examples/integrations/neo4j/src/main/resources/application.yaml @@ -0,0 +1,29 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +server: + port: 8080 + host: 0.0.0.0 + + +neo4j: + uri: bolt://localhost:7687 + authentication: + username: neo4j + password: secret + pool: + metricsEnabled: true diff --git a/examples/integrations/neo4j/src/main/resources/logging.properties b/examples/integrations/neo4j/src/main/resources/logging.properties new file mode 100644 index 000000000..c916a0505 --- /dev/null +++ b/examples/integrations/neo4j/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/MainTest.java b/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/MainTest.java new file mode 100644 index 000000000..1ee1f61dd --- /dev/null +++ b/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/MainTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.neo4j; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +import jakarta.json.JsonArray; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Config; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.harness.Neo4j; +import org.neo4j.harness.Neo4jBuilders; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Main test class for Neo4j Helidon SE application. + */ +@ServerTest +public class MainTest { + + static final String FIXTURE = """ + CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'}) + CREATE (Keanu:Person {name:'Keanu Reeves', born:1964}) + CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967}) + CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961}) + CREATE (Hugo:Person {name:'Hugo Weaving', born:1960}) + CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967}) + CREATE (LanaW:Person {name:'Lana Wachowski', born:1965}) + CREATE (JoelS:Person {name:'Joel Silver', born:1952}) + CREATE (KevinB:Person {name:'Kevin Bacon', born:1958}) + CREATE + (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix), + (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix), + (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix), + (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix), + (LillyW)-[:DIRECTED]->(TheMatrix), + (LanaW)-[:DIRECTED]->(TheMatrix), + (JoelS)-[:PRODUCED]->(TheMatrix) + + CREATE (Emil:Person {name:"Emil Eifrem", born:1978}) + CREATE (Emil)-[:ACTED_IN {roles:["Emil"]}]->(TheMatrix) + + CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'}) + CREATE + (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded), + (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded), + (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded), + (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded), + (LillyW)-[:DIRECTED]->(TheMatrixReloaded), + (LanaW)-[:DIRECTED]->(TheMatrixReloaded), + (JoelS)-[:PRODUCED]->(TheMatrixReloaded) + + CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, + tagline:'Everything that has a beginning has an end'}) + CREATE + (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions), + (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions), + (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions), + (KevinB)-[:ACTED_IN {roles:['Unknown']}]->(TheMatrixRevolutions), + (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions), + (LillyW)-[:DIRECTED]->(TheMatrixRevolutions), + (LanaW)-[:DIRECTED]->(TheMatrixRevolutions), + (JoelS)-[:PRODUCED]->(TheMatrixRevolutions)"""; + private static Neo4j embeddedDatabaseServer; + private final Http1Client webClient; + + public MainTest(Http1Client webClient) { + this.webClient = webClient; + } + + @SetUpServer + static void server(WebServerConfig.Builder server) { + //Setup embedded Neo4j Server and inject in routing + embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() + .withDisabledServer() + .withFixture(FIXTURE) + .build(); + + Driver driver = GraphDatabase.driver(embeddedDatabaseServer.boltURI(), Config.builder() + .withDriverMetrics() + .build()); + server.features(Main.features(driver)) + .routing(it -> Main.routing(it, driver)); + + } + + @BeforeAll + static void startServer() { + //Setup embedded Neo4j Server and inject in routing + embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() + .withDisabledServer() + .withFixture(FIXTURE) + .build(); + + System.setProperty("neo4j.uri", embeddedDatabaseServer.boltURI().toString()); + } + + @AfterAll + static void stopServer() { + if (embeddedDatabaseServer != null) { + embeddedDatabaseServer.close(); + } + } + + @Test + public void testHealth() { + try (Http1ClientResponse response = webClient.get("/observe/health").request()) { + assertThat(response.status(), is(Status.NO_CONTENT_204)); + } + } + + @Test + void testMovies() { + JsonArray result = webClient.get("/api/movies").requestEntity(JsonArray.class); + assertThat(result.getJsonObject(0).getString("title"), containsString("The Matrix")); + } +} \ No newline at end of file diff --git a/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/package-info.java b/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/package-info.java new file mode 100644 index 000000000..c52e32679 --- /dev/null +++ b/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests for Helidon Integrations Neo4j Example. + */ +package io.helidon.examples.integrations.neo4j; diff --git a/examples/integrations/oci/README.md b/examples/integrations/oci/README.md new file mode 100644 index 000000000..fe566dd16 --- /dev/null +++ b/examples/integrations/oci/README.md @@ -0,0 +1,5 @@ +# OCI Java SDK Examples + +```shell +mvn package +``` diff --git a/examples/integrations/oci/atp-cdi/README.md b/examples/integrations/oci/atp-cdi/README.md new file mode 100644 index 000000000..2af418749 --- /dev/null +++ b/examples/integrations/oci/atp-cdi/README.md @@ -0,0 +1,28 @@ +# Helidon ATP MP Examples + +This example demonstrates how user can easily retrieve wallet from their ATP instance running in OCI and use information from that wallet to setup DataSource to do Database operations. + +It requires a running OCI ATP instance. + +Before running the test, make sure to update required properties in `application.yaml` + +- oci.atp.ocid: This is OCID of your running ATP instance. +- oci.atp.walletPassword: password to encrypt the keys inside the wallet. The password must be at least 8 characters long and must include at least 1 letter and either 1 numeric character or 1 special character. +- oracle.ucp.jdbc.PoolDataSource.atp.tnsNetServiceName: netServiceName of your database running inside OCI ATP as can be found in `tnsnames.ora` file. +- oracle.ucp.jdbc.PoolDataSource.atp.user: User to access your database running inside OCI ATP. +- oracle.ucp.jdbc.PoolDataSource.atp.password: Password of user to access your database running inside OCI ATP. + +Once you have updated required properties, you can run the example: + +```shell +mvn package +java -jar ./target/helidon-examples-integrations-oci-atp-cdi.jar +``` + +To verify that, you can retrieve wallet and do database operation: + +```shell +curl http://localhost:8080/atp/wallet +``` + +You should see `Hello world!!` \ No newline at end of file diff --git a/examples/integrations/oci/atp-cdi/pom.xml b/examples/integrations/oci/atp-cdi/pom.xml new file mode 100644 index 000000000..89b421456 --- /dev/null +++ b/examples/integrations/oci/atp-cdi/pom.xml @@ -0,0 +1,95 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + + io.helidon.examples.integrations.oci + helidon-examples-integrations-oci-atp-cdi + 1.0.0-SNAPSHOT + Helidon Examples Integration OCI ATP CDI + CDI integration with OCI ATP. + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.integrations.cdi + helidon-integrations-cdi-datasource-ucp + + + io.helidon.integrations.db + ojdbc + + + com.oracle.database.jdbc + ucp + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-cdi + runtime + + + com.oracle.oci.sdk + oci-java-sdk-database + + + io.helidon.config + helidon-config-yaml-mp + + + io.smallrye + jandex + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/integrations/oci/atp-cdi/src/main/java/io/helidon/examples/integrations/oci/atp/cdi/AtpResource.java b/examples/integrations/oci/atp-cdi/src/main/java/io/helidon/examples/integrations/oci/atp/cdi/AtpResource.java new file mode 100644 index 000000000..cedb2bf6a --- /dev/null +++ b/examples/integrations/oci/atp-cdi/src/main/java/io/helidon/examples/integrations/oci/atp/cdi/AtpResource.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.atp.cdi; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import com.oracle.bmc.database.Database; +import com.oracle.bmc.database.model.GenerateAutonomousDatabaseWalletDetails; +import com.oracle.bmc.database.requests.GenerateAutonomousDatabaseWalletRequest; +import com.oracle.bmc.database.responses.GenerateAutonomousDatabaseWalletResponse; +import com.oracle.bmc.http.client.Options; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import oracle.security.pki.OraclePKIProvider; +import oracle.ucp.jdbc.PoolDataSource; +import org.eclipse.microprofile.config.inject.ConfigProperty; +/** + * JAX-RS resource - REST API for the atp example. + */ +@Path("/atp") +public class AtpResource { + private static final Logger LOGGER = Logger.getLogger(AtpResource.class.getName()); + + private final Database databaseClient; + private final PoolDataSource atpDataSource; + private final String atpTnsNetServiceName; + + private final String atpOcid; + private final String walletPassword; + + @Inject + AtpResource(Database databaseClient, @Named("atp") PoolDataSource atpDataSource, + @ConfigProperty(name = "oracle.ucp.jdbc.PoolDataSource.atp.tnsNetServiceName") String atpTnsNetServiceName, + @ConfigProperty(name = "oci.atp.ocid") String atpOcid, + @ConfigProperty(name = "oci.atp.walletPassword") String walletPassword) { + this.databaseClient = databaseClient; + this.atpDataSource = Objects.requireNonNull(atpDataSource); + this.atpTnsNetServiceName = atpTnsNetServiceName; + this.atpOcid = atpOcid; + this.walletPassword = walletPassword; + } + + /** + * Generate wallet file for the configured ATP. + * + * @return response containing wallet file + */ + @GET + @Path("/wallet") + public Response generateWallet() { + Options.shouldAutoCloseResponseInputStream(false); + GenerateAutonomousDatabaseWalletResponse walletResponse = + databaseClient.generateAutonomousDatabaseWallet( + GenerateAutonomousDatabaseWalletRequest.builder() + .autonomousDatabaseId(this.atpOcid) + .generateAutonomousDatabaseWalletDetails( + GenerateAutonomousDatabaseWalletDetails.builder() + .password(this.walletPassword) + .build()) + .build()); + + if (walletResponse.getContentLength() == 0) { + LOGGER.log(Level.SEVERE, "GenerateAutonomousDatabaseWalletResponse is empty"); + return Response.status(Response.Status.NOT_FOUND).build(); + } + + byte[] walletContent = null; + try { + walletContent = walletResponse.getInputStream().readAllBytes(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error processing GenerateAutonomousDatabaseWalletResponse", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + String returnEntity = null; + try { + this.atpDataSource.setSSLContext(getSSLContext(walletContent)); + this.atpDataSource.setURL(getJdbcUrl(walletContent, this.atpTnsNetServiceName)); + try ( + Connection connection = this.atpDataSource.getConnection(); + PreparedStatement ps = connection.prepareStatement("SELECT 'Hello world!!' FROM DUAL"); + ResultSet rs = ps.executeQuery() + ){ + rs.next(); + returnEntity = rs.getString(1); + } + } catch (SQLException e) { + LOGGER.log(Level.SEVERE, "Error setting up DataSource", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + + return Response.status(Response.Status.OK).entity(returnEntity).build(); + } + + /** + * Returns SSLContext based on cwallet.sso in wallet. + * + * @param walletContent + * @return SSLContext + */ + private static SSLContext getSSLContext(byte[] walletContent) throws IllegalStateException { + SSLContext sslContext = null; + try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new ByteArrayInputStream(walletContent)))) { + ZipEntry entry = null; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("cwallet.sso")) { + KeyStore keyStore = KeyStore.getInstance("SSO", new OraclePKIProvider()); + keyStore.load(zis, null); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX"); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX"); + trustManagerFactory.init(keyStore); + keyManagerFactory.init(keyStore, null); + sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); + } + zis.closeEntry(); + } + } catch (RuntimeException | Error throwMe) { + throw throwMe; + } catch (Exception e) { + throw new IllegalStateException("Error while getting SSLContext from wallet.", e); + } + return sslContext; + } + + /** + * Returns JDBC URL with connection description for the given service based on tnsnames.ora in wallet. + * + * @param walletContent + * @param tnsNetServiceName + * @return String + */ + private static String getJdbcUrl(byte[] walletContent, String tnsNetServiceName) throws IllegalStateException { + String jdbcUrl = null; + try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new ByteArrayInputStream(walletContent)))) { + ZipEntry entry = null; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("tnsnames.ora")) { + jdbcUrl = new String(zis.readAllBytes(), StandardCharsets.UTF_8) + .replaceFirst(tnsNetServiceName + "\\s*=\\s*", "jdbc:oracle:thin:@") + .replaceAll("\\n[^\\n]+", ""); + } + zis.closeEntry(); + } + } catch (IOException e) { + throw new IllegalStateException("Error while getting JDBC URL from wallet.", e); + } + return jdbcUrl; + } +} + diff --git a/examples/integrations/oci/atp-cdi/src/main/java/io/helidon/examples/integrations/oci/atp/cdi/package-info.java b/examples/integrations/oci/atp-cdi/src/main/java/io/helidon/examples/integrations/oci/atp/cdi/package-info.java new file mode 100644 index 000000000..93ab02108 --- /dev/null +++ b/examples/integrations/oci/atp-cdi/src/main/java/io/helidon/examples/integrations/oci/atp/cdi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of integration with OCI ATP in CDI application. + */ +package io.helidon.examples.integrations.oci.atp.cdi; diff --git a/examples/integrations/oci/atp-cdi/src/main/resources/META-INF/beans.xml b/examples/integrations/oci/atp-cdi/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e149ced7d --- /dev/null +++ b/examples/integrations/oci/atp-cdi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/oci/atp-cdi/src/main/resources/application.yaml b/examples/integrations/oci/atp-cdi/src/main/resources/application.yaml new file mode 100644 index 000000000..d7073eb23 --- /dev/null +++ b/examples/integrations/oci/atp-cdi/src/main/resources/application.yaml @@ -0,0 +1,37 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# The values are read from +# ~/helidon/conf/examples.yaml +# or you can just update them here + +server: + port: 8080 + +oracle: + ucp: + jdbc: + PoolDataSource: + atp: + connectionFactoryClassName: oracle.jdbc.pool.OracleDataSource + tnsNetServiceName: "${atp.db.tnsNetServiceName}" + user: "${atp.db.user}" + password: "${atp.db.password}" + +oci: + atp: + ocid: "${oci.properties.atp-ocid}" + walletPassword: "${oci.properties.atp-walletPassword}" \ No newline at end of file diff --git a/examples/integrations/oci/atp-cdi/src/main/resources/logging.properties b/examples/integrations/oci/atp-cdi/src/main/resources/logging.properties new file mode 100644 index 000000000..40dc82ff1 --- /dev/null +++ b/examples/integrations/oci/atp-cdi/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO +io.helidon.webclient.level=INFO diff --git a/examples/integrations/oci/atp/README.md b/examples/integrations/oci/atp/README.md new file mode 100644 index 000000000..da82d2aa9 --- /dev/null +++ b/examples/integrations/oci/atp/README.md @@ -0,0 +1,28 @@ +# Helidon ATP SE Examples + +This example demonstrates how user can easily retrieve wallet from their ATP instance running in OCI and use information from that wallet to setup DataSource to do Database operations. + +It requires a running OCI ATP instance. + +Before running the test, make sure to update required properties in `application.yaml` + +- oci.atp.ocid: This is OCID of your running ATP instance. +- oci.atp.walletPassword: password to encrypt the keys inside the wallet. The password must be at least 8 characters long and must include at least 1 letter and either 1 numeric character or 1 special character. +- db.tnsNetServiceName: netServiceName of your database running inside OCI ATP as can be found in `tnsnames.ora` file. +- db.userName: User to access your database running inside OCI ATP. +- db.password: Password of user to access your database running inside OCI ATP. + +Once you have updated required properties, you can run the example: + +```shell +mvn package +java -jar ./target/helidon-examples-integrations-oci-atp.jar +``` + +To verify that, you can retrieve wallet and do database operation: + +```shell +curl http://localhost:8080/atp/wallet +``` + +You should see `Hello world!!` diff --git a/examples/integrations/oci/atp/pom.xml b/examples/integrations/oci/atp/pom.xml new file mode 100644 index 000000000..0e3a3c339 --- /dev/null +++ b/examples/integrations/oci/atp/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.integrations.oci + helidon-examples-integrations-oci-atp + 1.0.0-SNAPSHOT + Helidon Examples Integration OCI ATP + Integration with OCI ATP. + + + io.helidon.examples.integrations.oci.atp.OciAtpMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.dbclient + helidon-dbclient-jdbc + + + io.helidon.integrations.db + ojdbc + + + com.oracle.database.jdbc + ucp + + + io.helidon.config + helidon-config-yaml + + + com.oracle.oci.sdk + oci-java-sdk-database + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey3 + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/AtpService.java b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/AtpService.java new file mode 100644 index 000000000..125e9f437 --- /dev/null +++ b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/AtpService.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.atp; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.sql.SQLException; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.jdbc.JdbcClientProvider; +import io.helidon.http.Status; +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 com.oracle.bmc.database.Database; +import com.oracle.bmc.database.model.GenerateAutonomousDatabaseWalletDetails; +import com.oracle.bmc.database.requests.GenerateAutonomousDatabaseWalletRequest; +import com.oracle.bmc.database.responses.GenerateAutonomousDatabaseWalletResponse; +import oracle.jdbc.pool.OracleDataSource; +import oracle.security.pki.OraclePKIProvider; +import oracle.ucp.jdbc.PoolDataSource; +import oracle.ucp.jdbc.PoolDataSourceFactory; + +class AtpService implements HttpService { + private static final Logger LOGGER = Logger.getLogger(AtpService.class.getName()); + + private final Database databaseClient; + private final Config config; + + AtpService(Database databaseClient, Config config) { + this.databaseClient = databaseClient; + this.config = config; + } + + @Override + public void routing(HttpRules rules) { + rules.get("/wallet", this::generateWallet); + } + + /** + * Generate wallet file for the configured ATP. + */ + private void generateWallet(ServerRequest req, ServerResponse res) { + String ocid = config.get("oci.atp.ocid").asString().get(); + GenerateAutonomousDatabaseWalletDetails walletDetails = + GenerateAutonomousDatabaseWalletDetails.builder() + .password(ocid) + .build(); + GenerateAutonomousDatabaseWalletResponse walletResponse = databaseClient + .generateAutonomousDatabaseWallet( + GenerateAutonomousDatabaseWalletRequest.builder() + .autonomousDatabaseId(ocid) + .generateAutonomousDatabaseWalletDetails(walletDetails) + .build()); + + if (walletResponse.getContentLength() == 0) { + LOGGER.log(Level.SEVERE, "GenerateAutonomousDatabaseWalletResponse is empty"); + res.status(Status.NOT_FOUND_404).send(); + return; + } + + byte[] walletContent; + try { + walletContent = walletResponse.getInputStream().readAllBytes(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error processing GenerateAutonomousDatabaseWalletResponse", e); + res.status(Status.INTERNAL_SERVER_ERROR_500).send(); + return; + } + + DbClient dbClient = createDbClient(walletContent); + Optional row = dbClient.execute().query("SELECT 'Hello world!!' FROM DUAL").findFirst(); + if (row.isPresent()) { + res.send(row.get().column(1).as(String.class)); + } else { + res.status(404).send(); + } + } + + DbClient createDbClient(byte[] walletContent) { + PoolDataSource pds = PoolDataSourceFactory.getPoolDataSource(); + try { + pds.setSSLContext(getSSLContext(walletContent)); + pds.setURL(getJdbcUrl(walletContent, config.get("db.tnsNetServiceName") + .as(String.class) + .orElseThrow(() -> new IllegalStateException("Missing tnsNetServiceName!!")))); + pds.setUser(config.get("db.userName").as(String.class).orElse("ADMIN")); + pds.setPassword(config.get("db.password") + .as(String.class) + .orElseThrow(() -> new IllegalStateException("Missing password!!"))); + pds.setConnectionFactoryClassName(OracleDataSource.class.getName()); + } catch (SQLException e) { + LOGGER.log(Level.SEVERE, "Error setting up PoolDataSource", e); + throw new RuntimeException(e); + } + return new JdbcClientProvider().builder() + .connectionPool(() -> { + try { + return pds.getConnection(); + } catch (SQLException e) { + throw new IllegalStateException("Error while setting up new connection", e); + } + }) + .build(); + } + + /** + * Returns SSLContext based on cwallet.sso in wallet. + * + * @return SSLContext + */ + private static SSLContext getSSLContext(byte[] walletContent) throws IllegalStateException { + SSLContext sslContext = null; + try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new ByteArrayInputStream(walletContent)))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("cwallet.sso")) { + KeyStore keyStore = KeyStore.getInstance("SSO", new OraclePKIProvider()); + keyStore.load(zis, null); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX"); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX"); + trustManagerFactory.init(keyStore); + keyManagerFactory.init(keyStore, null); + sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); + } + zis.closeEntry(); + } + } catch (RuntimeException | Error throwMe) { + throw throwMe; + } catch (Exception e) { + throw new IllegalStateException("Error while getting SSLContext from wallet.", e); + } + return sslContext; + } + + /** + * Returns JDBC URL with connection description for the given service based on {@code tnsnames.ora} in wallet. + * + * @param walletContent walletContent + * @param tnsNetServiceName tnsNetServiceName + * @return String + */ + private static String getJdbcUrl(byte[] walletContent, String tnsNetServiceName) throws IllegalStateException { + String jdbcUrl = null; + try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new ByteArrayInputStream(walletContent)))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("tnsnames.ora")) { + jdbcUrl = new String(zis.readAllBytes(), StandardCharsets.UTF_8) + .replaceFirst(tnsNetServiceName + "\\s*=\\s*", "jdbc:oracle:thin:@") + .replaceAll("\\n[^\\n]+", ""); + } + zis.closeEntry(); + } + } catch (IOException e) { + throw new IllegalStateException("Error while getting JDBC URL from wallet.", e); + } + return jdbcUrl; + } +} diff --git a/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/OciAtpMain.java b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/OciAtpMain.java new file mode 100644 index 000000000..0835e651d --- /dev/null +++ b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/OciAtpMain.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.atp; + +import java.io.IOException; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; + +import com.oracle.bmc.ConfigFileReader; +import com.oracle.bmc.auth.AuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.database.Database; +import com.oracle.bmc.database.DatabaseClient; +import com.oracle.bmc.model.BmcException; + +/** + * Main class of the example. + * This example sets up a web server to serve REST API to retrieve ATP wallet. + */ +public final class OciAtpMain { + /** + * Cannot be instantiated. + */ + private OciAtpMain() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) throws IOException { + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + + // this requires OCI configuration in the usual place + // ~/.oci/config + ConfigFileReader.ConfigFile configFile = ConfigFileReader.parseDefault(); + AuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(configFile); + Database databaseClient = DatabaseClient.builder().build(authProvider); + + // Prepare routing for the server + WebServer server = WebServer.builder() + .config(config.get("server")) + .routing(routing -> routing + .register("/atp", new AtpService(databaseClient, config)) + // OCI SDK error handling + .error(BmcException.class, (req, res, ex) -> + res.status(ex.getStatusCode()) + .send(ex.getMessage()))) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/"); + } +} diff --git a/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/package-info.java b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/package-info.java new file mode 100644 index 000000000..aa9dfb8a1 --- /dev/null +++ b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of integration with OCI ATP in a Helidon SE application. + */ +package io.helidon.examples.integrations.oci.atp; diff --git a/examples/integrations/oci/atp/src/main/resources/application.yaml b/examples/integrations/oci/atp/src/main/resources/application.yaml new file mode 100644 index 000000000..a5261a306 --- /dev/null +++ b/examples/integrations/oci/atp/src/main/resources/application.yaml @@ -0,0 +1,32 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# The values are read from +# ~/helidon/conf/examples.yaml +# or you can just update them here + +server: + port: 8080 + +db: + userName: "${atp.db.userName}" + password: "${atp.db.password}" + tnsNetServiceName: "${atp.db.tnsNetServiceName}" + +oci: + atp: + ocid: "${oci.properties.atp-ocid}" + walletPassword: "${oci.properties.atp-walletPassword}" diff --git a/examples/integrations/oci/atp/src/main/resources/logging.properties b/examples/integrations/oci/atp/src/main/resources/logging.properties new file mode 100644 index 000000000..40dc82ff1 --- /dev/null +++ b/examples/integrations/oci/atp/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO +io.helidon.webclient.level=INFO diff --git a/examples/integrations/oci/metrics/README.md b/examples/integrations/oci/metrics/README.md new file mode 100644 index 000000000..53a113400 --- /dev/null +++ b/examples/integrations/oci/metrics/README.md @@ -0,0 +1,9 @@ +The metrics example. + +The example requires OCI config in some default place like ``.oci/config`` + +Build and run the example by +```shell +mvn package +java -jar ./target/helidon-examples-integrations-oci-metrics.jar +``` diff --git a/examples/integrations/oci/metrics/pom.xml b/examples/integrations/oci/metrics/pom.xml new file mode 100644 index 000000000..1394845b3 --- /dev/null +++ b/examples/integrations/oci/metrics/pom.xml @@ -0,0 +1,73 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + + io.helidon.examples.integrations.oci.telemetry.OciMetricsMain + + + io.helidon.examples.integrations.oci + helidon-examples-integrations-oci-metrics + 1.0.0-SNAPSHOT + Helidon Examples Integration OCI Metrics + Integration with OCI Metrics. + + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + + + com.oracle.oci.sdk + oci-java-sdk-monitoring + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey3 + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/OciMetricsMain.java b/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/OciMetricsMain.java new file mode 100644 index 000000000..dc25fcab0 --- /dev/null +++ b/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/OciMetricsMain.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.telemetry; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; + +import com.oracle.bmc.ConfigFileReader; +import com.oracle.bmc.auth.AuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.monitoring.Monitoring; +import com.oracle.bmc.monitoring.MonitoringClient; +import com.oracle.bmc.monitoring.model.Datapoint; +import com.oracle.bmc.monitoring.model.FailedMetricRecord; +import com.oracle.bmc.monitoring.model.MetricDataDetails; +import com.oracle.bmc.monitoring.model.PostMetricDataDetails; +import com.oracle.bmc.monitoring.model.PostMetricDataResponseDetails; +import com.oracle.bmc.monitoring.requests.PostMetricDataRequest; +import com.oracle.bmc.monitoring.responses.PostMetricDataResponse; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * OCI Metrics example. + */ +public final class OciMetricsMain { + + private OciMetricsMain() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) throws Exception { + LogConfig.configureRuntime(); + // as I cannot share my configuration of OCI, let's combine the configuration + // from my home directory with the one compiled into the jar + // when running this example, you can either update the application.yaml in resources directory + // or use the same approach + Config config = buildConfig(); + + // this requires OCI configuration in the usual place + // ~/.oci/config + AuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(ConfigFileReader.parseDefault()); + try (Monitoring monitoringClient = MonitoringClient.builder().build(authProvider)) { + monitoringClient.setEndpoint(monitoringClient.getEndpoint().replace("telemetry.", "telemetry-ingestion.")); + + PostMetricDataRequest postMetricDataRequest = PostMetricDataRequest.builder() + .postMetricDataDetails(getPostMetricDataDetails(config)) + .build(); + + // Invoke the API call. + PostMetricDataResponse postMetricDataResponse = monitoringClient.postMetricData(postMetricDataRequest); + PostMetricDataResponseDetails postMetricDataResponseDetails = postMetricDataResponse + .getPostMetricDataResponseDetails(); + int count = postMetricDataResponseDetails.getFailedMetricsCount(); + System.out.println("Failed count: " + count); + if (count > 0) { + System.out.println("Failed metrics:"); + for (FailedMetricRecord failedMetric : postMetricDataResponseDetails.getFailedMetrics()) { + System.out.println("\t" + failedMetric.getMessage() + ": " + failedMetric.getMetricData()); + } + } + } + } + + private static PostMetricDataDetails getPostMetricDataDetails(Config config) { + String compartmentId = config.get("oci.metrics.compartment-ocid").asString().get(); + Instant now = Instant.now(); + return PostMetricDataDetails.builder() + .metricData(List.of( + MetricDataDetails.builder() + .compartmentId(compartmentId) + // Add a few data points to see something in the console + .datapoints(List.of( + Datapoint.builder() + .timestamp(Date.from(now.minus(10, ChronoUnit.SECONDS))) + .value(101.00) + .build(), + Datapoint.builder() + .timestamp(Date.from(now)) + .value(149.00) + .build() + )) + .dimensions(Map.of( + "resourceId", "myresourceid", + "unit", "cm")) + .name("my_app.jump") + .namespace("helidon_examples") + .build() + )) + .batchAtomicity(PostMetricDataDetails.BatchAtomicity.NonAtomic).build(); + } + + private static Config buildConfig() { + return Config.builder() + .sources( + // you can use this file to override the defaults that are built-in + file(System.getProperty("user.home") + "/helidon/conf/examples.yaml").optional(), + // in jar file (see src/main/resources/application.yaml) + classpath("application.yaml")) + .build(); + } +} diff --git a/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/package-info.java b/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/package-info.java new file mode 100644 index 000000000..939bfb6ea --- /dev/null +++ b/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example using OCI metrics blocking API. + */ +package io.helidon.examples.integrations.oci.telemetry; diff --git a/examples/integrations/oci/metrics/src/main/resources/application.yaml b/examples/integrations/oci/metrics/src/main/resources/application.yaml new file mode 100644 index 000000000..d8d676047 --- /dev/null +++ b/examples/integrations/oci/metrics/src/main/resources/application.yaml @@ -0,0 +1,23 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# The values are read from +# ~/helidon/conf/examples.yaml +# or you can just update them here +oci: + metrics: + compartment-ocid: "${oci.properties.compartment-ocid}" + diff --git a/examples/integrations/oci/metrics/src/main/resources/logging.properties b/examples/integrations/oci/metrics/src/main/resources/logging.properties new file mode 100644 index 000000000..8e28f63f9 --- /dev/null +++ b/examples/integrations/oci/metrics/src/main/resources/logging.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO diff --git a/examples/integrations/oci/objectstorage-cdi/README.md b/examples/integrations/oci/objectstorage-cdi/README.md new file mode 100644 index 000000000..aa08fbe0c --- /dev/null +++ b/examples/integrations/oci/objectstorage-cdi/README.md @@ -0,0 +1,11 @@ +The object storage (CDI) example. + +The example requires OCI config in some default place like ``.oci/config`` +Also properties from the ``src/main/resources/application.yaml`` shall be configured. +Like ``oci.objectstorage.bucketName`` + +Build and run the example by +```shell +mvn package +java -jar ./target/helidon-examples-integrations-oci-objectstorage-cdi.jar +``` diff --git a/examples/integrations/oci/objectstorage-cdi/pom.xml b/examples/integrations/oci/objectstorage-cdi/pom.xml new file mode 100644 index 000000000..6dbfa582c --- /dev/null +++ b/examples/integrations/oci/objectstorage-cdi/pom.xml @@ -0,0 +1,87 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + + io.helidon.examples.integrations.oci + helidon-examples-integrations-oci-objectstorage-cdi + 1.0.0-SNAPSHOT + Helidon Examples Integration OCI Object Storage CDI + CDI integration with OCI Object Storage. + + + io.helidon.examples.integrations.oci.objectstorage.cdi.ObjectStorageCdiMain + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-cdi + + + com.oracle.oci.sdk + oci-java-sdk-objectstorage + + + io.helidon.config + helidon-config-yaml-mp + + + io.smallrye + jandex + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + + diff --git a/examples/integrations/oci/objectstorage-cdi/src/main/java/io/helidon/examples/integrations/oci/objectstorage/cdi/ObjectStorageCdiMain.java b/examples/integrations/oci/objectstorage-cdi/src/main/java/io/helidon/examples/integrations/oci/objectstorage/cdi/ObjectStorageCdiMain.java new file mode 100644 index 000000000..f80182564 --- /dev/null +++ b/examples/integrations/oci/objectstorage-cdi/src/main/java/io/helidon/examples/integrations/oci/objectstorage/cdi/ObjectStorageCdiMain.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.objectstorage.cdi; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import io.helidon.config.yaml.mp.YamlMpConfigSource; +import io.helidon.microprofile.cdi.Main; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.eclipse.microprofile.config.spi.ConfigSource; + +/** + * Main class of the example. + * This is only used to merge configuration from home directory with the one embedded on classpath. + */ +public final class ObjectStorageCdiMain { + private ObjectStorageCdiMain() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + ConfigProviderResolver configProvider = ConfigProviderResolver.instance(); + + Config mpConfig = configProvider.getBuilder() + .addDefaultSources() + .withSources(examplesConfig()) + .addDiscoveredSources() + .addDiscoveredConverters() + .build(); + + // configure + configProvider.registerConfig(mpConfig, null); + + // start CDI + Main.main(args); + } + + private static ConfigSource[] examplesConfig() { + Path path = Paths.get(System.getProperty("user.home") + "/helidon/conf/examples.yaml"); + if (Files.exists(path)) { + return new ConfigSource[] {YamlMpConfigSource.create(path)}; + } + return new ConfigSource[0]; + } +} diff --git a/examples/integrations/oci/objectstorage-cdi/src/main/java/io/helidon/examples/integrations/oci/objectstorage/cdi/ObjectStorageResource.java b/examples/integrations/oci/objectstorage-cdi/src/main/java/io/helidon/examples/integrations/oci/objectstorage/cdi/ObjectStorageResource.java new file mode 100644 index 000000000..3db4c5a9e --- /dev/null +++ b/examples/integrations/oci/objectstorage-cdi/src/main/java/io/helidon/examples/integrations/oci/objectstorage/cdi/ObjectStorageResource.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.objectstorage.cdi; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.http.HeaderNames; + +import com.oracle.bmc.objectstorage.ObjectStorage; +import com.oracle.bmc.objectstorage.requests.DeleteObjectRequest; +import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest; +import com.oracle.bmc.objectstorage.requests.GetObjectRequest; +import com.oracle.bmc.objectstorage.requests.PutObjectRequest; +import com.oracle.bmc.objectstorage.responses.DeleteObjectResponse; +import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; +import com.oracle.bmc.objectstorage.responses.GetObjectResponse; +import com.oracle.bmc.objectstorage.responses.PutObjectResponse; +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * JAX-RS resource - REST API for the objecstorage example. + */ +@Path("/files") +public class ObjectStorageResource { + private static final Logger LOGGER = Logger.getLogger(ObjectStorageResource.class.getName()); + private final ObjectStorage objectStorageClient; + private final String namespaceName; + private final String bucketName; + + @Inject + ObjectStorageResource(ObjectStorage objectStorageClient, + @ConfigProperty(name = "oci.objectstorage.bucketName") + String bucketName) { + this.objectStorageClient = objectStorageClient; + this.bucketName = bucketName; + GetNamespaceResponse namespaceResponse = + this.objectStorageClient.getNamespace(GetNamespaceRequest.builder().build()); + this.namespaceName = namespaceResponse.getValue(); + } + + /** + * Download a file from object storage. + * + * @param fileName name of the object + * @return response + */ + @GET + @Path("/file/{file-name}") + public Response download(@PathParam("file-name") String fileName) { + GetObjectResponse getObjectResponse = + objectStorageClient.getObject( + GetObjectRequest.builder() + .namespaceName(namespaceName) + .bucketName(bucketName) + .objectName(fileName) + .build()); + + if (getObjectResponse.getContentLength() == 0) { + LOGGER.log(Level.SEVERE, "GetObjectResponse is empty"); + return Response.status(Response.Status.NOT_FOUND).build(); + } + + try (InputStream fileStream = getObjectResponse.getInputStream()) { + byte[] objectContent = fileStream.readAllBytes(); + Response.ResponseBuilder ok = Response.ok(objectContent) + .header(HeaderNames.CONTENT_DISPOSITION.defaultCase(), "attachment; filename=\"" + fileName + "\"") + .header("opc-request-id", getObjectResponse.getOpcRequestId()) + .header("request-id", getObjectResponse.getOpcClientRequestId()) + .header(HeaderNames.CONTENT_LENGTH.defaultCase(), getObjectResponse.getContentLength()); + + return ok.build(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error processing GetObjectResponse", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Upload a file to object storage. + * + * @param fileName name of the object + * @return response + */ + @POST + @Path("/file/{fileName}") + public Response upload(@PathParam("fileName") String fileName) { + + PutObjectRequest putObjectRequest = null; + try (InputStream stream = new FileInputStream(System.getProperty("user.dir") + File.separator + fileName)) { + byte[] contents = stream.readAllBytes(); + putObjectRequest = + PutObjectRequest.builder() + .namespaceName(namespaceName) + .bucketName(bucketName) + .objectName(fileName) + .putObjectBody(new ByteArrayInputStream(contents)) + .contentLength(Long.valueOf(contents.length)) + .build(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error creating PutObjectRequest", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + PutObjectResponse putObjectResponse = objectStorageClient.putObject(putObjectRequest); + + Response.ResponseBuilder ok = Response.ok() + .header("opc-request-id", putObjectResponse.getOpcRequestId()) + .header("request-id", putObjectResponse.getOpcClientRequestId()); + + return ok.build(); + } + + /** + * Delete a file from object storage. + * + * @param fileName object name + * @return response + */ + @DELETE + @Path("/file/{file-name}") + public Response delete(@PathParam("file-name") String fileName) { + DeleteObjectResponse deleteObjectResponse = objectStorageClient.deleteObject(DeleteObjectRequest.builder() + .namespaceName(namespaceName) + .bucketName(bucketName) + .objectName(fileName) + .build()); + Response.ResponseBuilder ok = Response.ok() + .header("opc-request-id", deleteObjectResponse.getOpcRequestId()) + .header("request-id", deleteObjectResponse.getOpcClientRequestId()); + + return ok.build(); + } +} diff --git a/examples/integrations/oci/objectstorage-cdi/src/main/java/io/helidon/examples/integrations/oci/objectstorage/cdi/package-info.java b/examples/integrations/oci/objectstorage-cdi/src/main/java/io/helidon/examples/integrations/oci/objectstorage/cdi/package-info.java new file mode 100644 index 000000000..20453fd0c --- /dev/null +++ b/examples/integrations/oci/objectstorage-cdi/src/main/java/io/helidon/examples/integrations/oci/objectstorage/cdi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of integration with OCI object storage in a CDI application. + */ +package io.helidon.examples.integrations.oci.objectstorage.cdi; diff --git a/examples/integrations/oci/objectstorage-cdi/src/main/resources/META-INF/beans.xml b/examples/integrations/oci/objectstorage-cdi/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e149ced7d --- /dev/null +++ b/examples/integrations/oci/objectstorage-cdi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/oci/objectstorage-cdi/src/main/resources/application.yaml b/examples/integrations/oci/objectstorage-cdi/src/main/resources/application.yaml new file mode 100644 index 000000000..097e80035 --- /dev/null +++ b/examples/integrations/oci/objectstorage-cdi/src/main/resources/application.yaml @@ -0,0 +1,26 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + +# +# The following properties under oci are accessed by Helidon. Values +# under oci.properties.* are read from ~/helidon/conf/examples.yaml. +# +oci: + objectstorage: + bucketName: "${oci.properties.objectstorage-bucketName}" diff --git a/examples/integrations/oci/objectstorage-cdi/src/main/resources/logging.properties b/examples/integrations/oci/objectstorage-cdi/src/main/resources/logging.properties new file mode 100644 index 000000000..40dc82ff1 --- /dev/null +++ b/examples/integrations/oci/objectstorage-cdi/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO +io.helidon.webclient.level=INFO diff --git a/examples/integrations/oci/objectstorage/README.md b/examples/integrations/oci/objectstorage/README.md new file mode 100644 index 000000000..06c5c52ba --- /dev/null +++ b/examples/integrations/oci/objectstorage/README.md @@ -0,0 +1,9 @@ +The object storage example. + +The example requires OCI config in some default place like ``.oci/config`` + +Build and run the example by +```shell +mvn package +java -jar ./target/helidon-examples-integrations-oci-objectstorage.jar +``` diff --git a/examples/integrations/oci/objectstorage/pom.xml b/examples/integrations/oci/objectstorage/pom.xml new file mode 100644 index 000000000..28af1a635 --- /dev/null +++ b/examples/integrations/oci/objectstorage/pom.xml @@ -0,0 +1,73 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.integrations.oci + helidon-examples-integrations-oci-objectstorage + 1.0.0-SNAPSHOT + Helidon Examples Integration OCI Object Storage + Integration with OCI Object Storage. + + + io.helidon.examples.integrations.oci.objecstorage.OciObjectStorageMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.config + helidon-config-yaml + + + com.oracle.oci.sdk + oci-java-sdk-objectstorage + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey3 + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/ObjectStorageService.java b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/ObjectStorageService.java new file mode 100644 index 000000000..d336f43f3 --- /dev/null +++ b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/ObjectStorageService.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.objecstorage; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.config.ConfigException; +import io.helidon.http.HeaderNames; +import io.helidon.http.Status; +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 com.oracle.bmc.ConfigFileReader; +import com.oracle.bmc.auth.AuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.objectstorage.ObjectStorage; +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.objectstorage.requests.DeleteObjectRequest; +import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest; +import com.oracle.bmc.objectstorage.requests.GetObjectRequest; +import com.oracle.bmc.objectstorage.requests.PutObjectRequest; +import com.oracle.bmc.objectstorage.responses.DeleteObjectResponse; +import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; +import com.oracle.bmc.objectstorage.responses.GetObjectResponse; +import com.oracle.bmc.objectstorage.responses.PutObjectResponse; + +/** + * REST API for the objecstorage example. + */ +public class ObjectStorageService implements HttpService { + private static final Logger LOGGER = Logger.getLogger(ObjectStorageService.class.getName()); + private final ObjectStorage objectStorageClient; + private final String namespaceName; + private final String bucketName; + + + ObjectStorageService(Config config) { + try { + ConfigFileReader.ConfigFile configFile = ConfigFileReader.parseDefault(); + AuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(configFile); + this.objectStorageClient = ObjectStorageClient.builder().build(authProvider); + this.bucketName = config.get("oci.objectstorage.bucketName") + .asString() + .orElseThrow(() -> new IllegalStateException("Missing bucket name!!")); + GetNamespaceResponse namespaceResponse = + this.objectStorageClient.getNamespace(GetNamespaceRequest.builder().build()); + this.namespaceName = namespaceResponse.getValue(); + } catch (IOException e) { + throw new ConfigException("Failed to read configuration properties", e); + } + } + + /** + * A service registers itself by updating the routine rules. + * + * @param rules the routing rules. + */ + public void routing(HttpRules rules) { + rules.get("/file/{file-name}", this::download); + rules.post("/file/{fileName}", this::upload); + rules.delete("/file/{file-name}", this::delete); + } + + /** + * Download a file from object storage. + * + * @param request request + * @param response response + */ + public void download(ServerRequest request, ServerResponse response) { + String fileName = request.path().pathParameters().get("file-name"); + GetObjectResponse getObjectResponse = + objectStorageClient.getObject( + GetObjectRequest.builder() + .namespaceName(namespaceName) + .bucketName(bucketName) + .objectName(fileName) + .build()); + + if (getObjectResponse.getContentLength() == 0) { + LOGGER.log(Level.SEVERE, "GetObjectResponse is empty"); + response.status(Status.NOT_FOUND_404).send(); + return; + } + + try (InputStream fileStream = getObjectResponse.getInputStream()) { + byte[] objectContent = fileStream.readAllBytes(); + response + .status(Status.OK_200) + .header(HeaderNames.CONTENT_DISPOSITION.defaultCase(), "attachment; filename=\"" + fileName + "\"") + .header("opc-request-id", getObjectResponse.getOpcRequestId()) + .header(HeaderNames.CONTENT_LENGTH.defaultCase(), getObjectResponse.getContentLength().toString()); + + response.send(objectContent); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error processing GetObjectResponse", e); + response.status(Status.INTERNAL_SERVER_ERROR_500).send(); + } + } + + /** + * Upload a file to object storage. + * + * @param request request + * @param response response + */ + public void upload(ServerRequest request, ServerResponse response) { + String fileName = request.path().pathParameters().get("fileName"); + PutObjectRequest putObjectRequest; + try (InputStream stream = new FileInputStream(System.getProperty("user.dir") + File.separator + fileName)) { + byte[] contents = stream.readAllBytes(); + putObjectRequest = + PutObjectRequest.builder() + .namespaceName(namespaceName) + .bucketName(bucketName) + .objectName(fileName) + .putObjectBody(new ByteArrayInputStream(contents)) + .contentLength((long) contents.length) + .build(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error creating PutObjectRequest", e); + response.status(Status.INTERNAL_SERVER_ERROR_500).send(); + return; + } + PutObjectResponse putObjectResponse = objectStorageClient.putObject(putObjectRequest); + + response.status(Status.OK_200).header("opc-request-id", putObjectResponse.getOpcRequestId()); + + response.send(); + } + + /** + * Delete a file from object storage. + * + * @param request request + * @param response response + */ + public void delete(ServerRequest request, ServerResponse response) { + String fileName = request.path().pathParameters().get("file-name"); + DeleteObjectResponse deleteObjectResponse = objectStorageClient.deleteObject(DeleteObjectRequest.builder() + .namespaceName(namespaceName) + .bucketName(bucketName) + .objectName(fileName) + .build()); + response.status(Status.OK_200).header("opc-request-id", deleteObjectResponse.getOpcRequestId()); + + response.send(); + } +} diff --git a/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/OciObjectStorageMain.java b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/OciObjectStorageMain.java new file mode 100644 index 000000000..9453bab0f --- /dev/null +++ b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/OciObjectStorageMain.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.objecstorage; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.config.Config; +import io.helidon.config.spi.ConfigSource; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * Main class of the example. + * This example sets up a web server to serve REST API to upload/download/delete objects. + */ +public final class OciObjectStorageMain { + + private OciObjectStorageMain() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + Config config = Config + .builder() + .sources(examplesConfig()) + .build(); + WebServer.builder() + .routing(r -> routing(r, config)) + .config(config.get("server")) + .build() + .start(); + } + + /** + * Updates HTTP Routing. + * + * @param routing routing builder + * @param config config + */ + static void routing(HttpRouting.Builder routing, Config config) { + ObjectStorageService objectStorageService = new ObjectStorageService(config); + routing.register("/files", objectStorageService); + } + + private static List> examplesConfig() { + List> suppliers = new ArrayList<>(); + Path path = Paths.get(System.getProperty("user.home") + "/helidon/conf/examples.yaml"); + if (Files.exists(path)) { + suppliers.add(file(path).build()); + } + suppliers.add(classpath("application.yaml").build()); + return suppliers; + } +} diff --git a/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/package-info.java b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/package-info.java new file mode 100644 index 000000000..ffa5134cb --- /dev/null +++ b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of integration with OCI object storage in a Helidon SE application. + */ +package io.helidon.examples.integrations.oci.objecstorage; diff --git a/examples/integrations/oci/objectstorage/src/main/resources/application.yaml b/examples/integrations/oci/objectstorage/src/main/resources/application.yaml new file mode 100644 index 000000000..2e4b34c10 --- /dev/null +++ b/examples/integrations/oci/objectstorage/src/main/resources/application.yaml @@ -0,0 +1,26 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# The values are read from +# ~/helidon/conf/examples.yaml +# or you can just update them here + +server: + port: 8080 + +oci: + objectstorage: + bucketName: "${oci.properties.objectstorage-bucketName}" diff --git a/examples/integrations/oci/objectstorage/src/main/resources/logging.properties b/examples/integrations/oci/objectstorage/src/main/resources/logging.properties new file mode 100644 index 000000000..40dc82ff1 --- /dev/null +++ b/examples/integrations/oci/objectstorage/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO +io.helidon.webclient.level=INFO diff --git a/examples/integrations/oci/pom.xml b/examples/integrations/oci/pom.xml new file mode 100644 index 000000000..4ecd5ad83 --- /dev/null +++ b/examples/integrations/oci/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + io.helidon.examples.integrations + helidon-examples-integrations-project + 1.0.0-SNAPSHOT + + io.helidon.examples.integrations.oci + helidon-examples-integrations-oci-project + 1.0.0-SNAPSHOT + pom + Helidon Examples Integration OCI + Examples of integration with OCI (Oracle Cloud). + + + atp + atp-cdi + metrics + objectstorage + objectstorage-cdi + vault + vault-cdi + + + diff --git a/examples/integrations/oci/vault-cdi/README.md b/examples/integrations/oci/vault-cdi/README.md new file mode 100644 index 000000000..aba4193e1 --- /dev/null +++ b/examples/integrations/oci/vault-cdi/README.md @@ -0,0 +1,12 @@ +The vault (CDI) example. + +The example requires OCI config in some default place like ``.oci/config`` + +Also properties from the ``src/main/resources/application.yaml`` shall be configured. +Like ``oci.properties.compartment-ocid`` + +Build and run the example by +```shell +mvn package +java -jar ./target/helidon-examples-integrations-oci-vault-cdi.jar +``` \ No newline at end of file diff --git a/examples/integrations/oci/vault-cdi/pom.xml b/examples/integrations/oci/vault-cdi/pom.xml new file mode 100644 index 000000000..6b689cf5f --- /dev/null +++ b/examples/integrations/oci/vault-cdi/pom.xml @@ -0,0 +1,101 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + + io.helidon.examples.integrations.oci + helidon-examples-integrations-oci-vault-cdi + 1.0.0-SNAPSHOT + Helidon Examples Integration OCI Object Storage CDI + CDI integration with OCI Object Storage. + + + io.helidon.examples.integrations.oci.vault.cdi.VaultCdiMain + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-cdi + + + com.oracle.oci.sdk + oci-java-sdk-keymanagement + + + com.oracle.oci.sdk + oci-java-sdk-secrets + + + com.oracle.oci.sdk + oci-java-sdk-vault + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + + diff --git a/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/CryptoClientProducer.java b/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/CryptoClientProducer.java new file mode 100644 index 000000000..cb122b112 --- /dev/null +++ b/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/CryptoClientProducer.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.vault.cdi; + +import com.oracle.bmc.keymanagement.KmsCryptoClient; +import com.oracle.bmc.keymanagement.KmsCryptoClientBuilder; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * KMS crypto client (used for encryption, decryption and signatures) requires additional configuration, that cannot + * be done automatically by the SDK. + */ +@ApplicationScoped +class CryptoClientProducer { + private final String cryptoEndpoint; + + @Inject + CryptoClientProducer(@ConfigProperty(name = "app.vault.cryptographic-endpoint") + String cryptoEndpoint) { + this.cryptoEndpoint = cryptoEndpoint; + } + + @Produces + KmsCryptoClientBuilder clientBuilder() { + return KmsCryptoClient.builder() + .endpoint(cryptoEndpoint); + } +} diff --git a/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/ErrorHandlerProvider.java b/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/ErrorHandlerProvider.java new file mode 100644 index 000000000..a90d2cd88 --- /dev/null +++ b/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/ErrorHandlerProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.vault.cdi; + +import com.oracle.bmc.model.BmcException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** + * Maps SDK errors to HTTP errors, as otherwise any exception is manifested as an internal server error. + * This mapper is not part of integration with OCI SDK, as each application may require a different entity format. + * This mapper simply uses the response code as HTTP status code, and error message as entity. + */ +@Provider +class ErrorHandlerProvider implements ExceptionMapper { + @Override + public Response toResponse(BmcException e) { + return Response.status(e.getStatusCode()) + .entity(e.getMessage()) + .build(); + } +} diff --git a/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/VaultCdiMain.java b/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/VaultCdiMain.java new file mode 100644 index 000000000..641934080 --- /dev/null +++ b/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/VaultCdiMain.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.integrations.oci.vault.cdi; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import io.helidon.config.yaml.mp.YamlMpConfigSource; +import io.helidon.microprofile.cdi.Main; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.eclipse.microprofile.config.spi.ConfigSource; + +/** + * Main class of the example. + * Used only to set up configuration. + */ +public final class VaultCdiMain { + private VaultCdiMain() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + ConfigProviderResolver configProvider = ConfigProviderResolver.instance(); + + Config mpConfig = configProvider.getBuilder() + .addDefaultSources() + .withSources(examplesConfig()) + .addDiscoveredSources() + .addDiscoveredConverters() + .build(); + + // configure + configProvider.registerConfig(mpConfig, null); + + // start CDI + Main.main(args); + } + + private static ConfigSource[] examplesConfig() { + Path path = Paths.get(System.getProperty("user.home") + "/helidon/conf/examples.yaml"); + if (Files.exists(path)) { + return new ConfigSource[] {YamlMpConfigSource.create(path)}; + } + return new ConfigSource[0]; + } +} diff --git a/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/VaultResource.java b/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/VaultResource.java new file mode 100644 index 000000000..c2a1c4312 --- /dev/null +++ b/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/VaultResource.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.vault.cdi; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +import io.helidon.common.Base64Value; + +import com.oracle.bmc.keymanagement.KmsCrypto; +import com.oracle.bmc.keymanagement.model.DecryptDataDetails; +import com.oracle.bmc.keymanagement.model.EncryptDataDetails; +import com.oracle.bmc.keymanagement.model.SignDataDetails; +import com.oracle.bmc.keymanagement.model.VerifyDataDetails; +import com.oracle.bmc.keymanagement.requests.DecryptRequest; +import com.oracle.bmc.keymanagement.requests.EncryptRequest; +import com.oracle.bmc.keymanagement.requests.SignRequest; +import com.oracle.bmc.keymanagement.requests.VerifyRequest; +import com.oracle.bmc.secrets.Secrets; +import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails; +import com.oracle.bmc.secrets.model.SecretBundleContentDetails; +import com.oracle.bmc.secrets.requests.GetSecretBundleRequest; +import com.oracle.bmc.vault.Vaults; +import com.oracle.bmc.vault.model.Base64SecretContentDetails; +import com.oracle.bmc.vault.model.CreateSecretDetails; +import com.oracle.bmc.vault.model.ScheduleSecretDeletionDetails; +import com.oracle.bmc.vault.model.SecretContentDetails; +import com.oracle.bmc.vault.requests.CreateSecretRequest; +import com.oracle.bmc.vault.requests.ScheduleSecretDeletionRequest; +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * JAX-RS resource - REST API of the example. + */ +@Path("/vault") +public class VaultResource { + private final Secrets secrets; + private final KmsCrypto crypto; + private final Vaults vaults; + private final String vaultOcid; + private final String compartmentOcid; + private final String encryptionKeyOcid; + private final String signatureKeyOcid; + + @Inject + VaultResource(Secrets secrets, + KmsCrypto crypto, + Vaults vaults, + @ConfigProperty(name = "app.vault.vault-ocid") + String vaultOcid, + @ConfigProperty(name = "app.vault.compartment-ocid") + String compartmentOcid, + @ConfigProperty(name = "app.vault.encryption-key-ocid") + String encryptionKeyOcid, + @ConfigProperty(name = "app.vault.signature-key-ocid") + String signatureKeyOcid) { + this.secrets = secrets; + this.crypto = crypto; + this.vaults = vaults; + this.vaultOcid = vaultOcid; + this.compartmentOcid = compartmentOcid; + this.encryptionKeyOcid = encryptionKeyOcid; + this.signatureKeyOcid = signatureKeyOcid; + } + + /** + * Encrypt a string. + * + * @param secret secret to encrypt + * @return cipher text + */ + @GET + @Path("/encrypt/{text}") + public String encrypt(@PathParam("text") String secret) { + return crypto.encrypt(EncryptRequest.builder() + .encryptDataDetails(EncryptDataDetails.builder() + .keyId(encryptionKeyOcid) + .plaintext(Base64Value.create(secret).toBase64()) + .build()) + .build()) + .getEncryptedData() + .getCiphertext(); + } + + /** + * Decrypt a cipher text. + * + * @param cipherText cipher text to decrypt + * @return original secret + */ + @GET + @Path("/decrypt/{text: .*}") + public String decrypt(@PathParam("text") String cipherText) { + return Base64Value.createFromEncoded(crypto.decrypt(DecryptRequest.builder() + .decryptDataDetails(DecryptDataDetails.builder() + .keyId(encryptionKeyOcid) + .ciphertext(cipherText) + .build()) + .build()) + .getDecryptedData() + .getPlaintext()) + .toDecodedString(); + } + + /** + * Sign data. + * + * @param dataToSign data to sign (must be a String) + * @return signature text + */ + @GET + @Path("/sign/{text}") + public String sign(@PathParam("text") String dataToSign) { + return crypto.sign(SignRequest.builder() + .signDataDetails(SignDataDetails.builder() + .keyId(signatureKeyOcid) + .signingAlgorithm(SignDataDetails.SigningAlgorithm.Sha224RsaPkcsPss) + .message(Base64Value.create(dataToSign).toBase64()) + .build()) + .build()) + .getSignedData() + .getSignature(); + } + + /** + * Verify a signature. The base64 encoded signature is the entity + * + * @param dataToVerify data that was signed + * @param signature signature text + * @return whether the signature is valid or not + */ + @POST + @Path("/verify/{text}") + public String verify(@PathParam("text") String dataToVerify, + String signature) { + VerifyDataDetails.SigningAlgorithm algorithm = VerifyDataDetails.SigningAlgorithm.Sha224RsaPkcsPss; + + boolean valid = crypto.verify(VerifyRequest.builder() + .verifyDataDetails(VerifyDataDetails.builder() + .keyId(signatureKeyOcid) + .signingAlgorithm(algorithm) + .message(Base64Value.create(dataToVerify).toBase64()) + .signature(signature) + .build()) + .build()) + .getVerifiedData() + .getIsSignatureValid(); + + return valid ? "Signature valid" : "Signature not valid"; + } + + /** + * Get secret content from Vault. + * + * @param secretOcid OCID of the secret to get + * @return content of the secret + */ + @GET + @Path("/secret/{id}") + public String getSecret(@PathParam("id") String secretOcid) { + SecretBundleContentDetails content = secrets.getSecretBundle(GetSecretBundleRequest.builder() + .secretId(secretOcid) + .build()) + .getSecretBundle() + .getSecretBundleContent(); + + if (content instanceof Base64SecretBundleContentDetails) { + // the only supported type + return Base64Value.createFromEncoded(((Base64SecretBundleContentDetails) content).getContent()).toDecodedString(); + } else { + throw new InternalServerErrorException("Invalid secret content type"); + } + } + + /** + * Delete a secret from Vault. + * This operation actually marks a secret for deletion, and the minimal time is 30 days. + * + * @param secretOcid OCID of the secret to delete + * @return short message + */ + @DELETE + @Path("/secret/{id}") + public String deleteSecret(@PathParam("id") String secretOcid) { + // has to be for quite a long period of time - did not work with less than 30 days + Date deleteTime = Date.from(Instant.now().plus(30, ChronoUnit.DAYS)); + + vaults.scheduleSecretDeletion(ScheduleSecretDeletionRequest.builder() + .secretId(secretOcid) + .scheduleSecretDeletionDetails(ScheduleSecretDeletionDetails.builder() + .timeOfDeletion(deleteTime) + .build()) + .build()); + + return "Secret " + secretOcid + " was marked for deletion"; + } + + /** + * Create a new secret. + * + * @param name name of the secret + * @param secretText secret content + * @return OCID of the created secret + */ + @POST + @Path("/secret/{name}") + public String createSecret(@PathParam("name") String name, + String secretText) { + SecretContentDetails content = Base64SecretContentDetails.builder() + .content(Base64Value.create(secretText).toBase64()) + .build(); + + return vaults.createSecret(CreateSecretRequest.builder() + .createSecretDetails(CreateSecretDetails.builder() + .secretName(name) + .vaultId(vaultOcid) + .compartmentId(compartmentOcid) + .keyId(encryptionKeyOcid) + .secretContent(content) + .build()) + .build()) + .getSecret() + .getId(); + } +} diff --git a/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/package-info.java b/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/package-info.java new file mode 100644 index 000000000..2cdf8f233 --- /dev/null +++ b/examples/integrations/oci/vault-cdi/src/main/java/io/helidon/examples/integrations/oci/vault/cdi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of OCI Vault integration in CDI. + */ +package io.helidon.examples.integrations.oci.vault.cdi; diff --git a/examples/integrations/oci/vault-cdi/src/main/resources/META-INF/beans.xml b/examples/integrations/oci/vault-cdi/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e149ced7d --- /dev/null +++ b/examples/integrations/oci/vault-cdi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/oci/vault-cdi/src/main/resources/application.yaml b/examples/integrations/oci/vault-cdi/src/main/resources/application.yaml new file mode 100644 index 000000000..c0fe46401 --- /dev/null +++ b/examples/integrations/oci/vault-cdi/src/main/resources/application.yaml @@ -0,0 +1,31 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + +# +# The following properties under oci are accessed by Helidon. Values +# under oci.properties.* are read from ~/helidon/conf/examples.yaml. +# +app: + vault: + # Vault OCID (the vault you want to use for this example) + vault-ocid: "${oci.properties.vault-ocid}" + compartment-ocid: "${oci.properties.compartment-ocid}" + encryption-key-ocid: "${oci.properties.vault-key-ocid}" + signature-key-ocid: "${oci.properties.vault-rsa-key-ocid}" + cryptographic-endpoint: "${oci.properties.cryptographic-endpoint}" diff --git a/examples/integrations/oci/vault-cdi/src/main/resources/logging.properties b/examples/integrations/oci/vault-cdi/src/main/resources/logging.properties new file mode 100644 index 000000000..8e28f63f9 --- /dev/null +++ b/examples/integrations/oci/vault-cdi/src/main/resources/logging.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO diff --git a/examples/integrations/oci/vault/README.md b/examples/integrations/oci/vault/README.md new file mode 100644 index 000000000..42fa85511 --- /dev/null +++ b/examples/integrations/oci/vault/README.md @@ -0,0 +1,9 @@ +The vault example. + +The example requires OCI config in some default place like ``.oci/config`` + +Build and run the example by +```shell +mvn package +java -jar ./target/helidon-examples-integrations-oci-vault.jar +``` diff --git a/examples/integrations/oci/vault/pom.xml b/examples/integrations/oci/vault/pom.xml new file mode 100644 index 000000000..a46ab8376 --- /dev/null +++ b/examples/integrations/oci/vault/pom.xml @@ -0,0 +1,82 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.integrations.oci + helidon-examples-integrations-oci-vault + 1.0.0-SNAPSHOT + Helidon Examples Integration OCI Vault + Integration with OCI Vault. + + + io.helidon.examples.integrations.oci.vault.OciVaultMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.config + helidon-config-yaml + + + com.oracle.oci.sdk + oci-java-sdk-keymanagement + + + com.oracle.oci.sdk + oci-java-sdk-secrets + + + com.oracle.oci.sdk + oci-java-sdk-vault + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey3 + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + + diff --git a/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/OciVaultMain.java b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/OciVaultMain.java new file mode 100644 index 000000000..90566314b --- /dev/null +++ b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/OciVaultMain.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.vault; + +import java.io.IOException; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; + +import com.oracle.bmc.ConfigFileReader; +import com.oracle.bmc.auth.AuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.keymanagement.KmsCrypto; +import com.oracle.bmc.keymanagement.KmsCryptoClient; +import com.oracle.bmc.model.BmcException; +import com.oracle.bmc.secrets.Secrets; +import com.oracle.bmc.secrets.SecretsClient; +import com.oracle.bmc.vault.Vaults; +import com.oracle.bmc.vault.VaultsClient; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * Main class of the example. + * Boots a web server and provides REST API for Vault interactions. + */ +public final class OciVaultMain { + private OciVaultMain() { + } + + /** + * Main method. + * @param args ignored + */ + public static void main(String[] args) throws IOException { + LogConfig.configureRuntime(); + + // as I cannot share my configuration of OCI, let's combine the configuration + // from my home directory with the one compiled into the jar + // when running this example, you can either update the application.yaml in resources directory + // or use the same approach + Config config = buildConfig(); + + Config vaultConfig = config.get("oci.vault"); + // the following three parameters are required + String vaultOcid = vaultConfig.get("vault-ocid").asString().get(); + String compartmentOcid = vaultConfig.get("compartment-ocid").asString().get(); + String encryptionKey = vaultConfig.get("encryption-key-ocid").asString().get(); + String signatureKey = vaultConfig.get("signature-key-ocid").asString().get(); + String cryptoEndpoint = vaultConfig.get("cryptographic-endpoint").asString().get(); + + // this requires OCI configuration in the usual place + // ~/.oci/config + AuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(ConfigFileReader.parseDefault()); + + Secrets secrets = SecretsClient.builder().build(authProvider); + KmsCrypto crypto = KmsCryptoClient.builder() + .endpoint(cryptoEndpoint) + .build(authProvider); + Vaults vaults = VaultsClient.builder().build(authProvider); + + WebServer server = WebServer.builder() + .routing(routing -> routing + .register("/vault", new VaultService(secrets, + vaults, + crypto, + vaultOcid, + compartmentOcid, + encryptionKey, + signatureKey)) + .error(BmcException.class, (req, res, ex) -> res.status( + ex.getStatusCode()).send(ex.getMessage()))) + .config(config.get("server")) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + + } + + private static Config buildConfig() { + return Config.builder() + .sources( + // you can use this file to override the defaults that are built-in + file(System.getProperty("user.home") + "/helidon/conf/examples.yaml").optional(), + // in jar file (see src/main/resources/application.yaml) + classpath("application.yaml")) + .build(); + } +} diff --git a/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/VaultService.java b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/VaultService.java new file mode 100644 index 000000000..7f47ac72c --- /dev/null +++ b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/VaultService.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.vault; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.Base64Value; +import io.helidon.http.Status; +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 com.oracle.bmc.keymanagement.KmsCrypto; +import com.oracle.bmc.keymanagement.model.DecryptDataDetails; +import com.oracle.bmc.keymanagement.model.EncryptDataDetails; +import com.oracle.bmc.keymanagement.model.SignDataDetails; +import com.oracle.bmc.keymanagement.model.VerifyDataDetails; +import com.oracle.bmc.keymanagement.requests.DecryptRequest; +import com.oracle.bmc.keymanagement.requests.EncryptRequest; +import com.oracle.bmc.keymanagement.requests.SignRequest; +import com.oracle.bmc.keymanagement.requests.VerifyRequest; +import com.oracle.bmc.keymanagement.responses.DecryptResponse; +import com.oracle.bmc.keymanagement.responses.EncryptResponse; +import com.oracle.bmc.keymanagement.responses.SignResponse; +import com.oracle.bmc.keymanagement.responses.VerifyResponse; +import com.oracle.bmc.secrets.Secrets; +import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails; +import com.oracle.bmc.secrets.model.SecretBundleContentDetails; +import com.oracle.bmc.secrets.requests.GetSecretBundleRequest; +import com.oracle.bmc.secrets.responses.GetSecretBundleResponse; +import com.oracle.bmc.vault.Vaults; +import com.oracle.bmc.vault.model.Base64SecretContentDetails; +import com.oracle.bmc.vault.model.CreateSecretDetails; +import com.oracle.bmc.vault.model.ScheduleSecretDeletionDetails; +import com.oracle.bmc.vault.model.SecretContentDetails; +import com.oracle.bmc.vault.requests.CreateSecretRequest; +import com.oracle.bmc.vault.requests.ScheduleSecretDeletionRequest; +import com.oracle.bmc.vault.responses.CreateSecretResponse; + +class VaultService implements HttpService { + + private static final Logger LOGGER = Logger.getLogger(VaultService.class.getName()); + private final Secrets secrets; + private final Vaults vaults; + private final KmsCrypto crypto; + private final String vaultOcid; + private final String compartmentOcid; + private final String encryptionKeyOcid; + private final String signatureKeyOcid; + + VaultService(Secrets secrets, + Vaults vaults, + KmsCrypto crypto, + String vaultOcid, + String compartmentOcid, + String encryptionKeyOcid, + String signatureKeyOcid) { + this.secrets = secrets; + this.vaults = vaults; + this.crypto = crypto; + this.vaultOcid = vaultOcid; + this.compartmentOcid = compartmentOcid; + this.encryptionKeyOcid = encryptionKeyOcid; + this.signatureKeyOcid = signatureKeyOcid; + } + + /** + * A service registers itself by updating the routine rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.get("/encrypt/{text:.*}", this::encrypt) + .get("/decrypt/{text:.*}", this::decrypt) + .get("/sign/{text}", this::sign) + .post("/verify/{text}", this::verify) + .get("/secret/{id}", this::getSecret) + .post("/secret/{name}", this::createSecret) + .delete("/secret/{id}", this::deleteSecret); + } + + private void getSecret(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + GetSecretBundleResponse id = secrets.getSecretBundle(GetSecretBundleRequest.builder() + .secretId(req.path().pathParameters().get("id")) + .build()); + SecretBundleContentDetails content = id.getSecretBundle().getSecretBundleContent(); + if (content instanceof Base64SecretBundleContentDetails) { + // the only supported type + res.send(Base64Value.createFromEncoded(((Base64SecretBundleContentDetails) content).getContent()) + .toDecodedString()); + } else { + res.status(Status.INTERNAL_SERVER_ERROR_500).send("Invalid secret content type"); + } + }, res); + + } + + private void deleteSecret(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + // has to be for quite a long period of time - did not work with less than 30 days + Date deleteTime = Date.from(Instant.now().plus(30, ChronoUnit.DAYS)); + String secretOcid = req.path().pathParameters().get("id"); + vaults.scheduleSecretDeletion(ScheduleSecretDeletionRequest.builder() + .secretId(secretOcid) + .scheduleSecretDeletionDetails(ScheduleSecretDeletionDetails.builder() + .timeOfDeletion(deleteTime) + .build()) + .build() + ); + response.send(String.format("Secret %s was marked for deletion", secretOcid)); + }, res); + } + + private void createSecret(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + String secretText = req.content().as(String.class); + SecretContentDetails content = Base64SecretContentDetails.builder() + .content(Base64Value.create(secretText).toBase64()) + .build(); + CreateSecretResponse vaultsSecret = vaults.createSecret(CreateSecretRequest.builder() + .createSecretDetails(CreateSecretDetails.builder() + .secretName(req.path().pathParameters().get("name")) + .vaultId(vaultOcid) + .compartmentId(compartmentOcid) + .keyId(encryptionKeyOcid) + .secretContent(content) + .build()) + .build()); + response.send(vaultsSecret.getSecret().getId()); + }, res); + } + + private void verify(ServerRequest req, ServerResponse res) { + + + ociHandler(response -> { + String text = req.path().pathParameters().get("text"); + String signature = req.content().as(String.class); + VerifyDataDetails.SigningAlgorithm algorithm = VerifyDataDetails.SigningAlgorithm.Sha224RsaPkcsPss; + VerifyResponse verifyResponse = crypto.verify(VerifyRequest.builder() + .verifyDataDetails(VerifyDataDetails.builder() + .keyId(signatureKeyOcid) + .signingAlgorithm(algorithm) + .message(Base64Value.create(text).toBase64()) + .signature(signature) + .build()) + .build()); + boolean valid = verifyResponse.getVerifiedData().getIsSignatureValid(); + response.send(valid ? "Signature valid" : "Signature not valid"); + }, res); + } + + private void sign(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + SignResponse signResponse = crypto.sign(SignRequest.builder() + .signDataDetails(SignDataDetails.builder() + .keyId(signatureKeyOcid) + .signingAlgorithm(SignDataDetails.SigningAlgorithm.Sha224RsaPkcsPss) + .message(Base64Value.create(req.path() + .pathParameters().get("text")).toBase64()) + .build()) + .build()); + response.send(signResponse.getSignedData().getSignature()); + }, res); + } + + private void encrypt(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + EncryptResponse encryptResponse = crypto.encrypt(EncryptRequest.builder() + .encryptDataDetails(EncryptDataDetails.builder() + .keyId(encryptionKeyOcid) + .plaintext(Base64Value.create(req.path() + .pathParameters().get("text")).toBase64()) + .build()) + .build()); + response.send(encryptResponse.getEncryptedData().getCiphertext()); + }, res); + } + + private void decrypt(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + DecryptResponse decryptResponse = crypto.decrypt(DecryptRequest.builder() + .decryptDataDetails(DecryptDataDetails.builder() + .keyId(encryptionKeyOcid) + .ciphertext(req.path() + .pathParameters().get("text")) + .build()) + .build()); + response.send(Base64Value.createFromEncoded(decryptResponse.getDecryptedData().getPlaintext()) + .toDecodedString()); + }, res); + } + + private void ociHandler(Consumer consumer, ServerResponse response) { + try { + consumer.accept(response); + } catch (Throwable error) { + LOGGER.log(Level.WARNING, "OCI Exception", error); + response.status(Status.INTERNAL_SERVER_ERROR_500).send(error.getMessage()); + } + } +} diff --git a/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/package-info.java b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/package-info.java new file mode 100644 index 000000000..5a5aae8d2 --- /dev/null +++ b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of OCI Vault integration in a SE application. + */ +package io.helidon.examples.integrations.oci.vault; diff --git a/examples/integrations/oci/vault/src/main/resources/application.yaml b/examples/integrations/oci/vault/src/main/resources/application.yaml new file mode 100644 index 000000000..eaf0fff5b --- /dev/null +++ b/examples/integrations/oci/vault/src/main/resources/application.yaml @@ -0,0 +1,31 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + +# +# The following properties under oci are accessed by Helidon. Values +# under oci.properties.* are read from ~/helidon/conf/examples.yaml. +# +oci: + vault: + # Vault OCID (the vault you want to use for this example + vault-ocid: "${oci.properties.vault-ocid}" + compartment-ocid: "${oci.properties.compartment-ocid}" + encryption-key-ocid: "${oci.properties.vault-key-ocid}" + signature-key-ocid: "${oci.properties.vault-rsa-key-ocid}" + cryptographic-endpoint: "${oci.properties.cryptographic-endpoint}" diff --git a/examples/integrations/oci/vault/src/main/resources/logging.properties b/examples/integrations/oci/vault/src/main/resources/logging.properties new file mode 100644 index 000000000..8e28f63f9 --- /dev/null +++ b/examples/integrations/oci/vault/src/main/resources/logging.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO diff --git a/examples/integrations/pom.xml b/examples/integrations/pom.xml new file mode 100644 index 000000000..13c652090 --- /dev/null +++ b/examples/integrations/pom.xml @@ -0,0 +1,44 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + io.helidon.examples.integrations + helidon-examples-integrations-project + 1.0.0-SNAPSHOT + Helidon Examples Integrations + pom + + + cdi + micronaut + neo4j + micrometer + oci + vault + microstream + + + diff --git a/examples/integrations/vault/hcp-cdi/README.md b/examples/integrations/vault/hcp-cdi/README.md new file mode 100644 index 000000000..764dabb49 --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/README.md @@ -0,0 +1,26 @@ +HCP Vault Integration with Helidon APIs +--- + +This example expects an empty Vault. It uses the token to create all required resources. + +To run this example: + +1. Run a docker image with a known root token + +```shell +docker run --cap-add=IPC_LOCK -e VAULT_DEV_ROOT_TOKEN_ID=myroot -d --name=vault -p8200:8200 vault +``` + +2. Build this application + +```shell +mvn clean package +``` + +3. Start this application + +```shell +java -jar ./target/helidon-examples-integrations-vault-hcp-cdi.jar +``` + +4. Exercise the endpoints \ No newline at end of file diff --git a/examples/integrations/vault/hcp-cdi/pom.xml b/examples/integrations/vault/hcp-cdi/pom.xml new file mode 100644 index 000000000..a6e6833d8 --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + + io.helidon.examples.integrations.vault + helidon-examples-integrations-vault-hcp-cdi + 1.0.0-SNAPSHOT + Helidon Examples Integration Vault CDI + CDI integration with Vault. + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.integrations.vault + helidon-integrations-vault-cdi + + + io.helidon.config + helidon-config-yaml + + + io.helidon.integrations.vault.auths + helidon-integrations-vault-auths-token + + + io.helidon.integrations.vault.auths + helidon-integrations-vault-auths-approle + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-kv1 + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-kv2 + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-cubbyhole + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-transit + + + io.helidon.integrations.vault.sys + helidon-integrations-vault-sys + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/CubbyholeResource.java b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/CubbyholeResource.java new file mode 100644 index 000000000..a7943421b --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/CubbyholeResource.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp.cdi; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.integrations.vault.Secret; +import io.helidon.integrations.vault.secrets.cubbyhole.CreateCubbyhole; +import io.helidon.integrations.vault.secrets.cubbyhole.CubbyholeSecrets; +import io.helidon.integrations.vault.secrets.cubbyhole.DeleteCubbyhole; + +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; + +/** + * JAX-RS resource for Cubbyhole secrets engine operations. + */ +@Path("/cubbyhole") +public class CubbyholeResource { + private final CubbyholeSecrets secrets; + + @Inject + CubbyholeResource(CubbyholeSecrets secrets) { + this.secrets = secrets; + } + + /** + * Create a secret from request entity, the name of the value is {@code secret}. + * + * @param path path of the secret taken from request path + * @param secret secret from the entity + * @return response + */ + @POST + @Path("/secrets/{path: .*}") + public Response createSecret(@PathParam("path") String path, String secret) { + CreateCubbyhole.Response response = secrets.create(path, Map.of("secret", secret)); + + return Response.ok() + .entity("Created secret on path: " + path + ", key is \"secret\", original status: " + response.status().code()) + .build(); + } + + /** + * Delete the secret on a specified path. + * + * @param path path of the secret taken from request path + * @return response + */ + @DELETE + @Path("/secrets/{path: .*}") + public Response deleteSecret(@PathParam("path") String path) { + DeleteCubbyhole.Response response = secrets.delete(path); + + return Response.ok() + .entity("Deleted secret on path: " + path + ". Original status: " + response.status().code()) + .build(); + } + + /** + * Get the secret on a specified path. + * + * @param path path of the secret taken from request path + * @return response + */ + @GET + @Path("/secrets/{path: .*}") + public Response getSecret(@PathParam("path") String path) { + Optional secret = secrets.get(path); + + if (secret.isPresent()) { + return Response.ok() + .entity("Secret: " + secret.get().values().toString()) + .build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } +} diff --git a/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/Kv1Resource.java b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/Kv1Resource.java new file mode 100644 index 000000000..50f03a997 --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/Kv1Resource.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp.cdi; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.integrations.vault.Secret; +import io.helidon.integrations.vault.secrets.kv1.CreateKv1; +import io.helidon.integrations.vault.secrets.kv1.DeleteKv1; +import io.helidon.integrations.vault.secrets.kv1.Kv1Secrets; +import io.helidon.integrations.vault.sys.DisableEngine; +import io.helidon.integrations.vault.sys.EnableEngine; +import io.helidon.integrations.vault.sys.Sys; + +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; + +/** + * JAX-RS resource for Key/Value version 1 secrets engine operations. + */ +@Path("/kv1") +public class Kv1Resource { + private final Sys sys; + private final Kv1Secrets secrets; + + @Inject + Kv1Resource(Sys sys, Kv1Secrets secrets) { + this.sys = sys; + this.secrets = secrets; + } + + /** + * Enable the secrets engine on the default path. + * + * @return response + */ + @Path("/engine") + @GET + public Response enableEngine() { + EnableEngine.Response response = sys.enableEngine(Kv1Secrets.ENGINE); + + return Response.ok() + .entity("Key/value version 1 secret engine is now enabled. Original status: " + response.status().code()) + .build(); + } + + /** + * Disable the secrets engine on the default path. + * @return response + */ + @Path("/engine") + @DELETE + public Response disableEngine() { + DisableEngine.Response response = sys.disableEngine(Kv1Secrets.ENGINE); + return Response.ok() + .entity("Key/value version 1 secret engine is now disabled. Original status: " + response.status().code()) + .build(); + } + + /** + * Create a secret from request entity, the name of the value is {@code secret}. + * + * @param path path of the secret taken from request path + * @param secret secret from the entity + * @return response + */ + @POST + @Path("/secrets/{path: .*}") + public Response createSecret(@PathParam("path") String path, String secret) { + CreateKv1.Response response = secrets.create(path, Map.of("secret", secret)); + + return Response.ok() + .entity("Created secret on path: " + path + ", key is \"secret\", original status: " + response.status().code()) + .build(); + } + + /** + * Delete the secret on a specified path. + * + * @param path path of the secret taken from request path + * @return response + */ + @DELETE + @Path("/secrets/{path: .*}") + public Response deleteSecret(@PathParam("path") String path) { + DeleteKv1.Response response = secrets.delete(path); + + return Response.ok() + .entity("Deleted secret on path: " + path + ". Original status: " + response.status().code()) + .build(); + } + + /** + * Get the secret on a specified path. + * + * @param path path of the secret taken from request path + * @return response + */ + @GET + @Path("/secrets/{path: .*}") + public Response getSecret(@PathParam("path") String path) { + Optional secret = secrets.get(path); + + if (secret.isPresent()) { + Secret kv1Secret = secret.get(); + return Response.ok() + .entity("Secret: " + secret.get().values().toString()) + .build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } +} diff --git a/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/Kv2Resource.java b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/Kv2Resource.java new file mode 100644 index 000000000..095d23997 --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/Kv2Resource.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp.cdi; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.integrations.vault.secrets.kv2.CreateKv2; +import io.helidon.integrations.vault.secrets.kv2.DeleteAllKv2; +import io.helidon.integrations.vault.secrets.kv2.Kv2Secret; +import io.helidon.integrations.vault.secrets.kv2.Kv2Secrets; + +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; + +/** + * JAX-RS resource for Key/Value version 2 secrets engine operations. + */ +@Path("/kv2") +public class Kv2Resource { + private final Kv2Secrets secrets; + + @Inject + Kv2Resource(Kv2Secrets secrets) { + this.secrets = secrets; + } + + /** + * Create a secret from request entity, the name of the value is {@code secret}. + * + * @param path path of the secret taken from request path + * @param secret secret from the entity + * @return response + */ + @POST + @Path("/secrets/{path: .*}") + public Response createSecret(@PathParam("path") String path, String secret) { + CreateKv2.Response response = secrets.create(path, Map.of("secret", secret)); + + return Response.ok() + .entity("Created secret on path: " + path + ", key is \"secret\", original status: " + response.status().code()) + .build(); + } + + /** + * Delete the secret on a specified path. + * + * @param path path of the secret taken from request path + * @return response + */ + @DELETE + @Path("/secrets/{path: .*}") + public Response deleteSecret(@PathParam("path") String path) { + DeleteAllKv2.Response response = secrets.deleteAll(path); + + return Response.ok() + .entity("Deleted secret on path: " + path + ". Original status: " + response.status().code()) + .build(); + } + + /** + * Get the secret on a specified path. + * + * @param path path of the secret taken from request path + * @return response + */ + @GET + @Path("/secrets/{path: .*}") + public Response getSecret(@PathParam("path") String path) { + + Optional secret = secrets.get(path); + + if (secret.isPresent()) { + Kv2Secret kv2Secret = secret.get(); + return Response.ok() + .entity("Version " + kv2Secret.metadata().version() + ", secret: " + kv2Secret.values().toString()) + .build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } +} diff --git a/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/TransitResource.java b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/TransitResource.java new file mode 100644 index 000000000..358ab8f9f --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/TransitResource.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp.cdi; + +import io.helidon.common.Base64Value; +import io.helidon.integrations.vault.secrets.transit.CreateKey; +import io.helidon.integrations.vault.secrets.transit.Decrypt; +import io.helidon.integrations.vault.secrets.transit.DeleteKey; +import io.helidon.integrations.vault.secrets.transit.Encrypt; +import io.helidon.integrations.vault.secrets.transit.Hmac; +import io.helidon.integrations.vault.secrets.transit.Sign; +import io.helidon.integrations.vault.secrets.transit.TransitSecrets; +import io.helidon.integrations.vault.secrets.transit.UpdateKeyConfig; +import io.helidon.integrations.vault.secrets.transit.Verify; +import io.helidon.integrations.vault.sys.DisableEngine; +import io.helidon.integrations.vault.sys.EnableEngine; +import io.helidon.integrations.vault.sys.Sys; + +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; + +/** + * JAX-RS resource for Transit secrets engine operations. + */ +@Path("/transit") +public class TransitResource { + private static final String ENCRYPTION_KEY = "encryption-key"; + private static final String SIGNATURE_KEY = "signature-key"; + + private final Sys sys; + private final TransitSecrets secrets; + + @Inject + TransitResource(Sys sys, TransitSecrets secrets) { + this.sys = sys; + this.secrets = secrets; + } + + /** + * Enable the secrets engine on the default path. + * + * @return response + */ + @Path("/engine") + @GET + public Response enableEngine() { + EnableEngine.Response response = sys.enableEngine(TransitSecrets.ENGINE); + + return Response.ok() + .entity("Transit secret engine is now enabled. Original status: " + response.status().code()) + .build(); + } + + /** + * Disable the secrets engine on the default path. + * @return response + */ + @Path("/engine") + @DELETE + public Response disableEngine() { + DisableEngine.Response response = sys.disableEngine(TransitSecrets.ENGINE); + + return Response.ok() + .entity("Transit secret engine is now disabled. Original status: " + response.status()) + .build(); + } + + /** + * Create the encrypting and signature keys. + * + * @return response + */ + @Path("/keys") + @GET + public Response createKeys() { + secrets.createKey(CreateKey.Request.builder() + .name(ENCRYPTION_KEY)); + + secrets.createKey(CreateKey.Request.builder() + .name(SIGNATURE_KEY) + .type("rsa-2048")); + + return Response.ok() + .entity("Created encryption (and HMAC), and signature keys") + .build(); + } + + /** + * Delete the encryption and signature keys. + * + * @return response + */ + @Path("/keys") + @DELETE + public Response deleteKeys() { + // we must first enable deletion of the key (by default it cannot be deleted) + secrets.updateKeyConfig(UpdateKeyConfig.Request.builder() + .name(ENCRYPTION_KEY) + .allowDeletion(true)); + + secrets.updateKeyConfig(UpdateKeyConfig.Request.builder() + .name(SIGNATURE_KEY) + .allowDeletion(true)); + + secrets.deleteKey(DeleteKey.Request.create(ENCRYPTION_KEY)); + secrets.deleteKey(DeleteKey.Request.create(SIGNATURE_KEY)); + + return Response.ok() + .entity("Deleted encryption (and HMAC), and signature keys") + .build(); + } + + /** + * Encrypt a secret. + * + * @param secret provided as part of the path + * @return cipher text + */ + @Path("/encrypt/{secret: .*}") + @GET + public String encryptSecret(@PathParam("secret") String secret) { + return secrets.encrypt(Encrypt.Request.builder() + .encryptionKeyName(ENCRYPTION_KEY) + .data(Base64Value.create(secret))) + .encrypted() + .cipherText(); + } + + /** + * Decrypt a secret. + * + * @param cipherText provided as part of the path + * @return decrypted secret text + */ + @Path("/decrypt/{cipherText: .*}") + @GET + public String decryptSecret(@PathParam("cipherText") String cipherText) { + return secrets.decrypt(Decrypt.Request.builder() + .encryptionKeyName(ENCRYPTION_KEY) + .cipherText(cipherText)) + .decrypted() + .toDecodedString(); + } + + /** + * Create an HMAC for text. + * + * @param text text to do HMAC for + * @return hmac string that can be used to {@link #verifyHmac(String, String)} + */ + @Path("/hmac/{text}") + @GET + public String hmac(@PathParam("text") String text) { + return secrets.hmac(Hmac.Request.builder() + .hmacKeyName(ENCRYPTION_KEY) + .data(Base64Value.create(text))) + .hmac(); + } + + /** + * Create a signature for text. + * + * @param text text to sign + * @return signature string that can be used to {@link #verifySignature(String, String)} + */ + @Path("/sign/{text}") + @GET + public String sign(@PathParam("text") String text) { + return secrets.sign(Sign.Request.builder() + .signatureKeyName(SIGNATURE_KEY) + .data(Base64Value.create(text))) + .signature(); + } + + /** + * Verify HMAC. + * + * @param secret secret that was used to {@link #hmac(String)} + * @param hmac HMAC text + * @return {@code HMAC Valid} or {@code HMAC Invalid} + */ + @Path("/verify/hmac/{secret}/{hmac: .*}") + @GET + public String verifyHmac(@PathParam("secret") String secret, @PathParam("hmac") String hmac) { + boolean isValid = secrets.verify(Verify.Request.builder() + .digestKeyName(ENCRYPTION_KEY) + .data(Base64Value.create(secret)) + .hmac(hmac)) + .isValid(); + + return (isValid ? "HMAC Valid" : "HMAC Invalid"); + } + + /** + * Verify signature. + * + * @param secret secret that was used to {@link #sign(String)} + * @param signature signature + * @return {@code Signature Valid} or {@code Signature Invalid} + */ + @Path("/verify/sign/{secret}/{signature: .*}") + @GET + public String verifySignature(@PathParam("secret") String secret, @PathParam("signature") String signature) { + boolean isValid = secrets.verify(Verify.Request.builder() + .digestKeyName(SIGNATURE_KEY) + .data(Base64Value.create(secret)) + .signature(signature)) + .isValid(); + + return (isValid ? "Signature Valid" : "Signature Invalid"); + } +} diff --git a/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/VaultCdiMain.java b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/VaultCdiMain.java new file mode 100644 index 000000000..0d14ae1c4 --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/VaultCdiMain.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp.cdi; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import io.helidon.config.yaml.mp.YamlMpConfigSource; +import io.helidon.microprofile.cdi.Main; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.eclipse.microprofile.config.spi.ConfigSource; + +/** + * Main class of example. + */ +public final class VaultCdiMain { + private VaultCdiMain() { + } + + /** + * Main method of example. + * + * @param args ignored + */ + public static void main(String[] args) { + ConfigProviderResolver configProvider = ConfigProviderResolver.instance(); + + Config mpConfig = configProvider.getBuilder() + .addDefaultSources() + .withSources(examplesConfig()) + .addDiscoveredSources() + .addDiscoveredConverters() + .build(); + + // configure + configProvider.registerConfig(mpConfig, null); + + // start CDI + Main.main(args); + } + + private static ConfigSource[] examplesConfig() { + Path path = Paths.get(System.getProperty("user.home") + "/helidon/conf/examples.yaml"); + if (Files.exists(path)) { + return new ConfigSource[] {YamlMpConfigSource.create(path)}; + } + return new ConfigSource[0]; + } +} diff --git a/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/package-info.java b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/package-info.java new file mode 100644 index 000000000..34e9215f2 --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/src/main/java/io/helidon/examples/integrations/vault/hcp/cdi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of Helidon integration with Hashicorp Vault within CDI. + */ +package io.helidon.examples.integrations.vault.hcp.cdi; diff --git a/examples/integrations/vault/hcp-cdi/src/main/resources/META-INF/beans.xml b/examples/integrations/vault/hcp-cdi/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e149ced7d --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/vault/hcp-cdi/src/main/resources/application.yaml b/examples/integrations/vault/hcp-cdi/src/main/resources/application.yaml new file mode 100644 index 000000000..67fdf52e8 --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/src/main/resources/application.yaml @@ -0,0 +1,27 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server.port: 8080 + +vault: + default: + address: "http://localhost:8200" + token: "myroot" + auth: + app-role: + enabled: false + token: + enabled: true diff --git a/examples/integrations/vault/hcp-cdi/src/main/resources/logging.properties b/examples/integrations/vault/hcp-cdi/src/main/resources/logging.properties new file mode 100644 index 000000000..8e28f63f9 --- /dev/null +++ b/examples/integrations/vault/hcp-cdi/src/main/resources/logging.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO diff --git a/examples/integrations/vault/hcp/README.md b/examples/integrations/vault/hcp/README.md new file mode 100644 index 000000000..1bcbac31d --- /dev/null +++ b/examples/integrations/vault/hcp/README.md @@ -0,0 +1,26 @@ +HCP Vault Integration +--- + +This example expects an empty Vault. It uses the token to create all required resources. + +To run this example: + +1. Run a docker image with a known root token + +```shell +docker run --cap-add=IPC_LOCK -e VAULT_DEV_ROOT_TOKEN_ID=myroot -d --name=vault -p8200:8200 vault +``` + +2. Build this application + +```shell +mvn clean package +``` + +3. Start this application + +```shell +java -jar ./target/helidon-examples-integrations-vault-hcp.jar +``` + +4. Exercise the endpoints \ No newline at end of file diff --git a/examples/integrations/vault/hcp/pom.xml b/examples/integrations/vault/hcp/pom.xml new file mode 100644 index 000000000..c4c44fd7a --- /dev/null +++ b/examples/integrations/vault/hcp/pom.xml @@ -0,0 +1,99 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.integrations.vault + helidon-examples-integrations-vault-hcp + 1.0.0-SNAPSHOT + Helidon Examples Integration Vault + Helidon integration with Vault. + + + io.helidon.examples.integrations.vault.hcp.VaultMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.integrations.vault + helidon-integrations-vault + + + io.helidon.config + helidon-config-yaml + + + io.helidon.integrations.vault.auths + helidon-integrations-vault-auths-token + + + io.helidon.integrations.vault.auths + helidon-integrations-vault-auths-approle + + + io.helidon.integrations.vault.auths + helidon-integrations-vault-auths-k8s + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-kv1 + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-kv2 + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-cubbyhole + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-transit + + + io.helidon.integrations.vault.sys + helidon-integrations-vault-sys + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/AppRoleExample.java b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/AppRoleExample.java new file mode 100644 index 000000000..1298561c2 --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/AppRoleExample.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; + +import io.helidon.config.Config; +import io.helidon.integrations.vault.Vault; +import io.helidon.integrations.vault.auths.approle.AppRoleAuth; +import io.helidon.integrations.vault.auths.approle.AppRoleVaultAuth; +import io.helidon.integrations.vault.auths.approle.CreateAppRole; +import io.helidon.integrations.vault.auths.approle.GenerateSecretId; +import io.helidon.integrations.vault.secrets.kv2.Kv2Secret; +import io.helidon.integrations.vault.secrets.kv2.Kv2Secrets; +import io.helidon.integrations.vault.sys.EnableAuth; +import io.helidon.integrations.vault.sys.Sys; + +class AppRoleExample { + private static final String SECRET_PATH = "approle/example/secret"; + private static final String ROLE_NAME = "approle_role"; + private static final String POLICY_NAME = "approle_policy"; + private static final String CUSTOM_APP_ROLE_PATH = "customapprole"; + + private final Vault tokenVault; + private final Config config; + private final Sys sys; + + private Vault appRoleVault; + + AppRoleExample(Vault tokenVault, Config config) { + this.tokenVault = tokenVault; + this.config = config; + + this.sys = tokenVault.sys(Sys.API); + } + + public String run() { + /* + The following tasks must be run before we authenticate + */ + enableAppRoleAuth(); + workWithSecrets(); + disableAppRoleAuth(); + return "AppRole example finished successfully."; + } + + private void workWithSecrets() { + Kv2Secrets secrets = appRoleVault.secrets(Kv2Secrets.ENGINE); + secrets.create(SECRET_PATH, Map.of("secret-key", "secretValue", + "secret-user", "username")); + Optional secret = secrets.get(SECRET_PATH); + if (secret.isPresent()) { + Kv2Secret kv2Secret = secret.get(); + System.out.println("appRole first secret: " + kv2Secret.value("secret-key")); + System.out.println("appRole second secret: " + kv2Secret.value("secret-user")); + } else { + System.out.println("appRole secret not found"); + } + secrets.deleteAll(SECRET_PATH); + } + + private void disableAppRoleAuth() { + sys.deletePolicy(POLICY_NAME); + sys.disableAuth(CUSTOM_APP_ROLE_PATH); + } + + private void enableAppRoleAuth() { + + // enable the method + sys.enableAuth(EnableAuth.Request.builder() + .auth(AppRoleAuth.AUTH_METHOD) + // must be aligned with path configured in application.yaml + .path(CUSTOM_APP_ROLE_PATH)); + + // add policy + sys.createPolicy(POLICY_NAME, VaultPolicy.POLICY); + + tokenVault.auth(AppRoleAuth.AUTH_METHOD, CUSTOM_APP_ROLE_PATH) + .createAppRole(CreateAppRole.Request.builder() + .roleName(ROLE_NAME) + .addTokenPolicy(POLICY_NAME) + .tokenExplicitMaxTtl(Duration.ofMinutes(1))); + + String roleId = tokenVault.auth(AppRoleAuth.AUTH_METHOD, CUSTOM_APP_ROLE_PATH) + .readRoleId(ROLE_NAME) + .orElseThrow(); + + + GenerateSecretId.Response response = tokenVault.auth(AppRoleAuth.AUTH_METHOD, CUSTOM_APP_ROLE_PATH) + .generateSecretId(GenerateSecretId.Request.builder() + .roleName(ROLE_NAME) + .addMetadata("name", "helidon")); + + String secretId = response.secretId(); + + System.out.println("roleId: " + roleId); + System.out.println("secretId: " + secretId); + appRoleVault = Vault.builder() + .config(config) + .addVaultAuth(AppRoleVaultAuth.builder() + .path(CUSTOM_APP_ROLE_PATH) + .appRoleId(roleId) + .secretId(secretId) + .build()) + .build(); + } +} diff --git a/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/CubbyholeService.java b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/CubbyholeService.java new file mode 100644 index 000000000..71c15ff68 --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/CubbyholeService.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.http.Status; +import io.helidon.integrations.vault.Secret; +import io.helidon.integrations.vault.secrets.cubbyhole.CubbyholeSecrets; +import io.helidon.integrations.vault.sys.Sys; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +class CubbyholeService implements HttpService { + private final Sys sys; + private final CubbyholeSecrets secrets; + + CubbyholeService(Sys sys, CubbyholeSecrets secrets) { + this.sys = sys; + this.secrets = secrets; + } + + @Override + public void routing(HttpRules rules) { + rules.get("/create", this::createSecrets) + .get("/secrets/{path:.*}", this::getSecret); + } + + private void createSecrets(ServerRequest req, ServerResponse res) { + secrets.create("first/secret", Map.of("key", "secretValue")); + res.send("Created secret on path /first/secret"); + } + + private void getSecret(ServerRequest req, ServerResponse res) { + String path = req.path().pathParameters().get("path"); + Optional secret = secrets.get(path); + if (secret.isPresent()) { + // using toString so we do not need to depend on JSON-B + res.send(secret.get().values().toString()); + } else { + res.status(Status.NOT_FOUND_404); + res.send(); + } + } +} diff --git a/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/K8sExample.java b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/K8sExample.java new file mode 100644 index 000000000..7462c8a01 --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/K8sExample.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.config.Config; +import io.helidon.integrations.vault.Vault; +import io.helidon.integrations.vault.auths.k8s.ConfigureK8s; +import io.helidon.integrations.vault.auths.k8s.CreateRole; +import io.helidon.integrations.vault.auths.k8s.K8sAuth; +import io.helidon.integrations.vault.secrets.kv2.Kv2Secret; +import io.helidon.integrations.vault.secrets.kv2.Kv2Secrets; +import io.helidon.integrations.vault.sys.Sys; + +class K8sExample { + private static final String SECRET_PATH = "k8s/example/secret"; + private static final String POLICY_NAME = "k8s_policy"; + + private final Vault tokenVault; + private final String k8sAddress; + private final Config config; + private final Sys sys; + + private Vault k8sVault; + + K8sExample(Vault tokenVault, Config config) { + this.tokenVault = tokenVault; + this.sys = tokenVault.sys(Sys.API); + this.k8sAddress = config.get("cluster-address").asString().get(); + this.config = config; + } + + public String run() { + /* + The following tasks must be run before we authenticate + */ + enableK8sAuth(); + // Now we can login using k8s - must run within a k8s cluster (or you need the k8s configuration files locally) + workWithSecrets(); + // Now back to token based Vault, as we will clean up + disableK8sAuth(); + return "k8s example finished successfully."; + } + + private void workWithSecrets() { + Kv2Secrets secrets = k8sVault.secrets(Kv2Secrets.ENGINE); + + secrets.create(SECRET_PATH, Map.of("secret-key", "secretValue", + "secret-user", "username")); + + Optional secret = secrets.get(SECRET_PATH); + if (secret.isPresent()) { + Kv2Secret kv2Secret = secret.get(); + System.out.println("k8s first secret: " + kv2Secret.value("secret-key")); + System.out.println("k8s second secret: " + kv2Secret.value("secret-user")); + } else { + System.out.println("k8s secret not found"); + } + secrets.deleteAll(SECRET_PATH); + } + + private void disableK8sAuth() { + sys.deletePolicy(POLICY_NAME); + sys.disableAuth(K8sAuth.AUTH_METHOD.defaultPath()); + } + + private void enableK8sAuth() { + // enable the method + sys.enableAuth(K8sAuth.AUTH_METHOD); + sys.createPolicy(POLICY_NAME, VaultPolicy.POLICY); + tokenVault.auth(K8sAuth.AUTH_METHOD) + .configure(ConfigureK8s.Request.builder() + .address(k8sAddress)); + tokenVault.auth(K8sAuth.AUTH_METHOD) + // this must be the same role name as is defined in application.yaml + .createRole(CreateRole.Request.builder() + .roleName("my-role") + .addBoundServiceAccountName("*") + .addBoundServiceAccountNamespace("default") + .addTokenPolicy(POLICY_NAME)); + k8sVault = Vault.create(config); + } +} diff --git a/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/Kv1Service.java b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/Kv1Service.java new file mode 100644 index 000000000..1b0c9f998 --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/Kv1Service.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.http.Status; +import io.helidon.integrations.vault.Secret; +import io.helidon.integrations.vault.secrets.kv1.Kv1Secrets; +import io.helidon.integrations.vault.sys.Sys; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +class Kv1Service implements HttpService { + private final Sys sys; + private final Kv1Secrets secrets; + + Kv1Service(Sys sys, Kv1Secrets secrets) { + this.sys = sys; + this.secrets = secrets; + } + + @Override + public void routing(HttpRules rules) { + rules.get("/enable", this::enableEngine) + .get("/create", this::createSecrets) + .get("/secrets/{path:.*}", this::getSecret) + .delete("/secrets/{path:.*}", this::deleteSecret) + .get("/disable", this::disableEngine); + } + + private void disableEngine(ServerRequest req, ServerResponse res) { + sys.disableEngine(Kv1Secrets.ENGINE); + res.send("KV1 Secret engine disabled"); + } + + private void enableEngine(ServerRequest req, ServerResponse res) { + sys.enableEngine(Kv1Secrets.ENGINE); + res.send("KV1 Secret engine enabled"); + } + + private void createSecrets(ServerRequest req, ServerResponse res) { + secrets.create("first/secret", Map.of("key", "secretValue")); + res.send("Created secret on path /first/secret"); + } + + private void deleteSecret(ServerRequest req, ServerResponse res) { + String path = req.path().pathParameters().get("path"); + secrets.delete(path); + res.send("Deleted secret on path " + path); + } + + private void getSecret(ServerRequest req, ServerResponse res) { + String path = req.path().pathParameters().get("path"); + + Optional secret = secrets.get(path); + if (secret.isPresent()) { + // using toString so we do not need to depend on JSON-B + res.send(secret.get().values().toString()); + } else { + res.status(Status.NOT_FOUND_404); + res.send(); + } + } +} diff --git a/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/Kv2Service.java b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/Kv2Service.java new file mode 100644 index 000000000..c46b510ac --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/Kv2Service.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.http.Status; +import io.helidon.integrations.vault.secrets.kv2.Kv2Secret; +import io.helidon.integrations.vault.secrets.kv2.Kv2Secrets; +import io.helidon.integrations.vault.sys.Sys; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +class Kv2Service implements HttpService { + private final Sys sys; + private final Kv2Secrets secrets; + + Kv2Service(Sys sys, Kv2Secrets secrets) { + this.sys = sys; + this.secrets = secrets; + } + + @Override + public void routing(HttpRules rules) { + rules.get("/create", this::createSecrets) + .get("/secrets/{path:.*}", this::getSecret) + .delete("/secrets/{path:.*}", this::deleteSecret); + } + + private void createSecrets(ServerRequest req, ServerResponse res) { + secrets.create("first/secret", Map.of("key", "secretValue")); + res.send("Created secret on path /first/secret"); + } + + private void deleteSecret(ServerRequest req, ServerResponse res) { + String path = req.path().pathParameters().get("path"); + secrets.deleteAll(path); + res.send("Deleted secret on path " + path); + } + + private void getSecret(ServerRequest req, ServerResponse res) { + String path = req.path().pathParameters().get("path"); + + Optional secret = secrets.get(path); + if (secret.isPresent()) { + // using toString so we do not need to depend on JSON-B + Kv2Secret kv2Secret = secret.get(); + res.send("Version " + kv2Secret.metadata().version() + ", secret: " + kv2Secret.values().toString()); + } else { + res.status(Status.NOT_FOUND_404); + res.send(); + } + } +} diff --git a/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/TransitService.java b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/TransitService.java new file mode 100644 index 000000000..745d48e6b --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/TransitService.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp; + +import java.util.List; + +import io.helidon.common.Base64Value; +import io.helidon.integrations.vault.secrets.transit.CreateKey; +import io.helidon.integrations.vault.secrets.transit.Decrypt; +import io.helidon.integrations.vault.secrets.transit.DecryptBatch; +import io.helidon.integrations.vault.secrets.transit.DeleteKey; +import io.helidon.integrations.vault.secrets.transit.Encrypt; +import io.helidon.integrations.vault.secrets.transit.EncryptBatch; +import io.helidon.integrations.vault.secrets.transit.Hmac; +import io.helidon.integrations.vault.secrets.transit.Sign; +import io.helidon.integrations.vault.secrets.transit.TransitSecrets; +import io.helidon.integrations.vault.secrets.transit.UpdateKeyConfig; +import io.helidon.integrations.vault.secrets.transit.Verify; +import io.helidon.integrations.vault.sys.Sys; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +class TransitService implements HttpService { + private static final String ENCRYPTION_KEY = "encryption-key"; + private static final String SIGNATURE_KEY = "signature-key"; + private static final Base64Value SECRET_STRING = Base64Value.create("Hello World"); + private final Sys sys; + private final TransitSecrets secrets; + + TransitService(Sys sys, TransitSecrets secrets) { + this.sys = sys; + this.secrets = secrets; + } + + @Override + public void routing(HttpRules rules) { + rules.get("/enable", this::enableEngine) + .get("/keys", this::createKeys) + .delete("/keys", this::deleteKeys) + .get("/batch", this::batch) + .get("/encrypt/{text:.*}", this::encryptSecret) + .get("/decrypt/{text:.*}", this::decryptSecret) + .get("/sign", this::sign) + .get("/hmac", this::hmac) + .get("/verify/sign/{text:.*}", this::verify) + .get("/verify/hmac/{text:.*}", this::verifyHmac) + .get("/disable", this::disableEngine); + } + + private void enableEngine(ServerRequest req, ServerResponse res) { + sys.enableEngine(TransitSecrets.ENGINE); + res.send("Transit Secret engine enabled"); + } + + private void disableEngine(ServerRequest req, ServerResponse res) { + sys.disableEngine(TransitSecrets.ENGINE); + res.send("Transit Secret engine disabled"); + } + + private void createKeys(ServerRequest req, ServerResponse res) { + CreateKey.Request request = CreateKey.Request.builder() + .name(ENCRYPTION_KEY); + + secrets.createKey(request); + secrets.createKey(CreateKey.Request.builder() + .name(SIGNATURE_KEY) + .type("rsa-2048")); + + res.send("Created keys"); + } + + private void deleteKeys(ServerRequest req, ServerResponse res) { + secrets.updateKeyConfig(UpdateKeyConfig.Request.builder() + .name(ENCRYPTION_KEY) + .allowDeletion(true)); + System.out.println("Updated key config"); + + secrets.deleteKey(DeleteKey.Request.create(ENCRYPTION_KEY)); + + res.send("Deleted key."); + } + + private void decryptSecret(ServerRequest req, ServerResponse res) { + String encrypted = req.path().pathParameters().get("text"); + + Decrypt.Response decryptResponse = secrets.decrypt(Decrypt.Request.builder() + .encryptionKeyName(ENCRYPTION_KEY) + .cipherText(encrypted)); + + res.send(String.valueOf(decryptResponse.decrypted().toDecodedString())); + } + + private void encryptSecret(ServerRequest req, ServerResponse res) { + String secret = req.path().pathParameters().get("text"); + + Encrypt.Response encryptResponse = secrets.encrypt(Encrypt.Request.builder() + .encryptionKeyName(ENCRYPTION_KEY) + .data(Base64Value.create(secret))); + + res.send(encryptResponse.encrypted().cipherText()); + } + + private void hmac(ServerRequest req, ServerResponse res) { + Hmac.Response hmacResponse = secrets.hmac(Hmac.Request.builder() + .hmacKeyName(ENCRYPTION_KEY) + .data(SECRET_STRING)); + + res.send(hmacResponse.hmac()); + } + + private void sign(ServerRequest req, ServerResponse res) { + Sign.Response signResponse = secrets.sign(Sign.Request.builder() + .signatureKeyName(SIGNATURE_KEY) + .data(SECRET_STRING)); + + res.send(signResponse.signature()); + } + + private void verifyHmac(ServerRequest req, ServerResponse res) { + String hmac = req.path().pathParameters().get("text"); + + Verify.Response verifyResponse = secrets.verify(Verify.Request.builder() + .digestKeyName(ENCRYPTION_KEY) + .data(SECRET_STRING) + .hmac(hmac)); + + res.send("Valid: " + verifyResponse.isValid()); + } + + private void verify(ServerRequest req, ServerResponse res) { + String signature = req.path().pathParameters().get("text"); + + Verify.Response verifyResponse = secrets.verify(Verify.Request.builder() + .digestKeyName(SIGNATURE_KEY) + .data(SECRET_STRING) + .signature(signature)); + + res.send("Valid: " + verifyResponse.isValid()); + } + + private void batch(ServerRequest req, ServerResponse res) { + String[] data = new String[]{"one", "two", "three", "four"}; + EncryptBatch.Request request = EncryptBatch.Request.builder() + .encryptionKeyName(ENCRYPTION_KEY); + DecryptBatch.Request decryptRequest = DecryptBatch.Request.builder() + .encryptionKeyName(ENCRYPTION_KEY); + + for (String item : data) { + request.addEntry(EncryptBatch.BatchEntry.create(Base64Value.create(item))); + } + List batchResult = secrets.encrypt(request).batchResult(); + for (Encrypt.Encrypted encrypted : batchResult) { + System.out.println("Encrypted: " + encrypted.cipherText()); + decryptRequest.addEntry(DecryptBatch.BatchEntry.create(encrypted.cipherText())); + } + + List base64Values = secrets.decrypt(decryptRequest).batchResult(); + for (int i = 0; i < data.length; i++) { + String decryptedValue = base64Values.get(i).toDecodedString(); + if (!data[i].equals(decryptedValue)) { + res.send("Data at index " + i + " is invalid. Decrypted " + decryptedValue + + ", expected: " + data[i]); + return; + } + } + res.send("Batch encryption/decryption completed"); + } +} diff --git a/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/VaultMain.java b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/VaultMain.java new file mode 100644 index 000000000..b336359e1 --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/VaultMain.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp; + +import java.time.Duration; + +import io.helidon.config.Config; +import io.helidon.integrations.vault.Vault; +import io.helidon.integrations.vault.secrets.cubbyhole.CubbyholeSecrets; +import io.helidon.integrations.vault.secrets.kv1.Kv1Secrets; +import io.helidon.integrations.vault.secrets.kv2.Kv2Secrets; +import io.helidon.integrations.vault.secrets.transit.TransitSecrets; +import io.helidon.integrations.vault.sys.Sys; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * Main class of example. + */ +public final class VaultMain { + private VaultMain() { + } + + /** + * Main method of example. + * + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + // as I cannot share my secret configuration, let's combine the configuration + // from my home directory with the one compiled into the jar + // when running this example, you can either update the application.yaml in resources directory + // or use the same approach + Config config = buildConfig(); + + // we have three configurations available + // 1. Token based authentication + Vault tokenVault = Vault.builder() + .config(config.get("vault.token")) + .updateWebClient(it -> it.connectTimeout(Duration.ofSeconds(5)) + .readTimeout(Duration.ofSeconds(5))) + .build(); + + // 2. App role based authentication - must be created after we obtain the role id an token + // 3. Kubernetes (k8s) based authentication (requires to run on k8s) - must be created after we create + // the authentication method + + // the tokenVault is using the root token and can be used to enable engines and + // other authentication mechanisms + + System.out.println(new K8sExample(tokenVault, config.get("vault.k8s")).run()); + System.out.println(new AppRoleExample(tokenVault, config.get("vault.approle")).run()); + + /* + We do not need to block here for our examples, as the server started below will keep the process running + */ + + Sys sys = tokenVault.sys(Sys.API); + // we use await for webserver, as we do not care if we block the main thread - it is not used + // for anything + WebServer webServer = WebServer.builder() + .config(config.get("server")) + .routing(routing -> routing + .register("/cubbyhole", new CubbyholeService(sys, tokenVault.secrets(CubbyholeSecrets.ENGINE))) + .register("/kv1", new Kv1Service(sys, tokenVault.secrets(Kv1Secrets.ENGINE))) + .register("/kv2", new Kv2Service(sys, tokenVault.secrets(Kv2Secrets.ENGINE))) + .register("/transit", new TransitService(sys, tokenVault.secrets(TransitSecrets.ENGINE)))) + .build() + .start(); + + String baseAddress = "http://localhost:" + webServer.port() + "/"; + System.out.println("Server started on " + baseAddress); + System.out.println(); + System.out.println("Key/Value Version 1 Secrets Engine"); + System.out.println("\t" + baseAddress + "kv1/enable"); + System.out.println("\t" + baseAddress + "kv1/create"); + System.out.println("\t" + baseAddress + "kv1/secrets/first/secret"); + System.out.println("\tcurl -i -X DELETE " + baseAddress + "kv1/secrets/first/secret"); + System.out.println("\t" + baseAddress + "kv1/disable"); + System.out.println(); + System.out.println("Key/Value Version 2 Secrets Engine"); + System.out.println("\t" + baseAddress + "kv2/create"); + System.out.println("\t" + baseAddress + "kv2/secrets/first/secret"); + System.out.println("\tcurl -i -X DELETE " + baseAddress + "kv2/secrets/first/secret"); + System.out.println(); + System.out.println("Transit Secrets Engine"); + System.out.println("\t" + baseAddress + "transit/enable"); + System.out.println("\t" + baseAddress + "transit/keys"); + System.out.println("\t" + baseAddress + "transit/encrypt/secret_text"); + System.out.println("\t" + baseAddress + "transit/decrypt/cipher_text"); + System.out.println("\t" + baseAddress + "transit/sign"); + System.out.println("\t" + baseAddress + "transit/verify/sign/signature_text"); + System.out.println("\t" + baseAddress + "transit/hmac"); + System.out.println("\t" + baseAddress + "transit/verify/hmac/hmac_text"); + System.out.println("\tcurl -i -X DELETE " + baseAddress + "transit/keys"); + System.out.println("\t" + baseAddress + "transit/disable"); + } + + private static Config buildConfig() { + return Config.builder() + .sources( + // you can use this file to override the defaults that are built-in + file(System.getProperty("user.home") + "/helidon/conf/examples.yaml").optional(), + // in jar file (see src/main/resources/application.yaml) + classpath("application.yaml")) + .build(); + } +} diff --git a/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/VaultPolicy.java b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/VaultPolicy.java new file mode 100644 index 000000000..21fd349ed --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/VaultPolicy.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.vault.hcp; + +final class VaultPolicy { + private VaultPolicy() { + } + static final String POLICY = "# Enable and manage authentication methods\n" + + "path \"auth/*\"\n" + + "{\n" + + " capabilities = [\"create\", \"update\", \"delete\", \"sudo\"]\n" + + "}\n" + + "\n" + + "# Create, update, and delete auth methods\n" + + "path \"sys/auth/*\"\n" + + "{\n" + + " capabilities = [\"create\", \"update\", \"delete\", \"sudo\"]\n" + + "}\n" + + "\n" + + "# List auth methods\n" + + "path \"sys/auth\"\n" + + "{\n" + + " capabilities = [\"read\"]\n" + + "}\n" + + "\n" + + "# Enable and manage the key/value secrets engine at `secret/` path\n" + + "\n" + + "# List, create, update, and delete key/value secrets\n" + + "path \"secret/*\"\n" + + "{\n" + + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" + + "}\n" + + "\n" + + "path \"kv1/*\"\n" + + "{\n" + + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" + + "}\n" + + "\n" + + "path \"cubbyhole/*\"\n" + + "{\n" + + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" + + "}\n" + + "\n" + + "path \"database/*\"\n" + + "{\n" + + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" + + "}\n" + + "\n" + + "path \"kv/*\"\n" + + "{\n" + + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" + + "}\n" + + "\n" + + "# Manage secrets engines\n" + + "path \"sys/mounts/*\"\n" + + "{\n" + + " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\", \"sudo\"]\n" + + "}\n" + + "\n" + + "# List existing secrets engines.\n" + + "path \"sys/mounts\"\n" + + "{\n" + + " capabilities = [\"read\"]\n" + + "}\n"; +} diff --git a/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/package-info.java b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/package-info.java new file mode 100644 index 000000000..6d423ad33 --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/java/io/helidon/examples/integrations/vault/hcp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of Helidon integration with Hashicorp Vault. + */ +package io.helidon.examples.integrations.vault.hcp; diff --git a/examples/integrations/vault/hcp/src/main/resources/application.yaml b/examples/integrations/vault/hcp/src/main/resources/application.yaml new file mode 100644 index 000000000..718109f31 --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/resources/application.yaml @@ -0,0 +1,59 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server.port: 8080 + +vault: + properties: + address: "http://localhost:8200" + k8s: + address: "${vault.properties.address}" + # please change this to your k8s cluster address + # cluster-address: "https://kubernetes.docker.internal:6443" + cluster-address: "https://10.96.0.1" + auth: + k8s: + enabled: true + # this role is created in the code, must be the same value + token-role: my-role + service-account-token: "${vault.properties.k8s.service-account-token}" + app-role: + enabled: false + token: + enabled: false + token: + token: "myroot" + address: "${vault.properties.address}" + auth: + k8s: + enabled: false + app-role: + enabled: false + token: + enabled: true + approle: + address: "${vault.properties.address}" + auth: + k8s: + enabled: false + app-role: + # this is not needed, as we use a builder, + # it is here to show how this could be used to define a + # custom path for vault authentication (same can be done for k8s) + path: "customapprole" + enabled: true + token: + enabled: false \ No newline at end of file diff --git a/examples/integrations/vault/hcp/src/main/resources/logging.properties b/examples/integrations/vault/hcp/src/main/resources/logging.properties new file mode 100644 index 000000000..8e28f63f9 --- /dev/null +++ b/examples/integrations/vault/hcp/src/main/resources/logging.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO diff --git a/examples/integrations/vault/pom.xml b/examples/integrations/vault/pom.xml new file mode 100644 index 000000000..d6135601b --- /dev/null +++ b/examples/integrations/vault/pom.xml @@ -0,0 +1,39 @@ + + + + + 4.0.0 + + io.helidon.examples.integrations + helidon-examples-integrations-project + 1.0.0-SNAPSHOT + + io.helidon.examples.integrations.vault + helidon-examples-integrations-vault-project + 1.0.0-SNAPSHOT + pom + Helidon Examples Integration Vault + Examples of integration with Vault. + + + hcp + hcp-cdi + + diff --git a/examples/jbatch/README.md b/examples/jbatch/README.md new file mode 100644 index 000000000..551fcdef8 --- /dev/null +++ b/examples/jbatch/README.md @@ -0,0 +1,16 @@ +# Helidon + jBatch + +Minimal Helidon MP + jBatch PoC. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-jbatch.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/batch +``` \ No newline at end of file diff --git a/examples/jbatch/pom.xml b/examples/jbatch/pom.xml new file mode 100644 index 000000000..85a3fa68d --- /dev/null +++ b/examples/jbatch/pom.xml @@ -0,0 +1,133 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.jbatch + helidon-examples-jbatch + 1.0.0-SNAPSHOT + Helidon Examples JBatch + + + 2.1.0 + 2.1.0 + 10.14.2.0 + 3.0.1 + + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.logging + helidon-logging-jul + runtime + + + jakarta.json + jakarta.json-api + + + jakarta.transaction + jakarta.transaction-api + + + jakarta.batch + jakarta.batch-api + ${version.lib.jbatch-api} + + + com.ibm.jbatch + com.ibm.jbatch.spi + ${version.lib.jbatch.container} + + + org.glassfish.jaxb + jaxb-runtime + ${version.lib.jaxb-api} + + + + + io.smallrye + jandex + runtime + true + + + com.ibm.jbatch + com.ibm.jbatch.container + ${version.lib.jbatch.container} + runtime + + + org.apache.derby + derby + ${version.lib.derby} + runtime + + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + org.hamcrest + hamcrest-core + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/jbatch/src/main/java/io/helidon/examples/jbatch/BatchResource.java b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/BatchResource.java new file mode 100644 index 000000000..df5674237 --- /dev/null +++ b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/BatchResource.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.jbatch; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import com.ibm.jbatch.spi.BatchSPIManager; +import jakarta.batch.operations.JobOperator; +import jakarta.batch.runtime.JobExecution; +import jakarta.batch.runtime.StepExecution; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import static jakarta.batch.runtime.BatchRuntime.getJobOperator; + + +/** + * Trigger a batch process using resource. + */ +@Path("/batch") +@ApplicationScoped +public class BatchResource { + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + private JobOperator jobOperator; + + /** + * Run a JBatch process when endpoint called. + * @return JsonObject with the result. + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public JsonObject executeBatch() { + + BatchSPIManager batchSPIManager = BatchSPIManager.getInstance(); + batchSPIManager.registerPlatformMode(BatchSPIManager.PlatformMode.SE); + batchSPIManager.registerExecutorServiceProvider(new HelidonExecutorServiceProvider()); + + jobOperator = getJobOperator(); + Long executionId = jobOperator.start("myJob", new Properties()); + + return JSON.createObjectBuilder() + .add("Started a job with Execution ID: ", executionId) + .build(); + } + + /** + * Check the job status. + * @param executionId the job ID. + * @return JsonObject with status. + */ + @GET + @Path("/status/{execution-id}") + public JsonObject status(@PathParam("execution-id") Long executionId){ + JobExecution jobExecution = jobOperator.getJobExecution(executionId); + + List stepExecutions = jobOperator.getStepExecutions(executionId); + List executedSteps = new ArrayList<>(); + for (StepExecution stepExecution : stepExecutions) { + executedSteps.add(stepExecution.getStepName()); + } + + return JSON.createObjectBuilder() + .add("Steps executed", Arrays.toString(executedSteps.toArray())) + .add("Status", jobExecution.getBatchStatus().toString()) + .build(); + } +} diff --git a/examples/jbatch/src/main/java/io/helidon/examples/jbatch/HelidonExecutorServiceProvider.java b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/HelidonExecutorServiceProvider.java new file mode 100644 index 000000000..f7fc5949b --- /dev/null +++ b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/HelidonExecutorServiceProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.jbatch; + +import java.util.concurrent.ExecutorService; + +import io.helidon.common.configurable.ThreadPoolSupplier; + +import com.ibm.jbatch.spi.ExecutorServiceProvider; + + +/** + * Executor service for batch processing. + */ +public class HelidonExecutorServiceProvider implements ExecutorServiceProvider { + @Override + public ExecutorService getExecutorService() { + return ThreadPoolSupplier.builder().corePoolSize(2).build().get(); + } +} diff --git a/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyBatchlet.java b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyBatchlet.java new file mode 100644 index 000000000..fab5f0fd5 --- /dev/null +++ b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyBatchlet.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.jbatch.jobs; + +import jakarta.batch.api.AbstractBatchlet; + +/** + * Batchlet example. + */ +public class MyBatchlet extends AbstractBatchlet { + + /** + * Run inside a batchlet. + * + * @return String with status. + */ + @Override + public String process() { + System.out.println("Running inside a batchlet"); + return "COMPLETED"; + } + +} diff --git a/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyInputRecord.java b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyInputRecord.java new file mode 100644 index 000000000..b9f73a438 --- /dev/null +++ b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyInputRecord.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.jbatch.jobs; + +/** + * Example of an Input Record. + */ +public class MyInputRecord { + private int id; + + /** + * Constructor for Input Record. + * @param id + */ + public MyInputRecord(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public String toString() { + return "MyInputRecord: " + id; + } +} diff --git a/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyItemProcessor.java b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyItemProcessor.java new file mode 100644 index 000000000..f39c15868 --- /dev/null +++ b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyItemProcessor.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.jbatch.jobs; + +import jakarta.batch.api.chunk.ItemProcessor; + +/** + * Example Item Processor. + */ +public class MyItemProcessor implements ItemProcessor { + + @Override + public MyOutputRecord processItem(Object t) { + System.out.println("processItem: " + t); + + return (((MyInputRecord) t).getId() % 2 == 0) ? null : new MyOutputRecord(((MyInputRecord) t).getId() * 2); + } +} diff --git a/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyItemReader.java b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyItemReader.java new file mode 100644 index 000000000..e6d12ea75 --- /dev/null +++ b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyItemReader.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.jbatch.jobs; + +import java.util.StringTokenizer; + +import jakarta.batch.api.chunk.AbstractItemReader; + + +/** + * Example Item Reader. + */ +public class MyItemReader extends AbstractItemReader { + + private final StringTokenizer tokens; + + /** + * Constructor for Item Reader. + */ + public MyItemReader() { + tokens = new StringTokenizer("1,2,3,4,5,6,7,8,9,10", ","); + } + + /** + * Perform read Item. + * @return Stage result. + */ + @Override + public MyInputRecord readItem() { + if (tokens.hasMoreTokens()) { + return new MyInputRecord(Integer.valueOf(tokens.nextToken())); + } + return null; + } +} diff --git a/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyItemWriter.java b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyItemWriter.java new file mode 100644 index 000000000..66572dd30 --- /dev/null +++ b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyItemWriter.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.jbatch.jobs; + +import java.util.List; + +import jakarta.batch.api.chunk.AbstractItemWriter; + + +/** + * Example Item Writer. + */ +public class MyItemWriter extends AbstractItemWriter { + + @Override + public void writeItems(List list) { + System.out.println("writeItems: " + list); + } +} diff --git a/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyOutputRecord.java b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyOutputRecord.java new file mode 100644 index 000000000..24e0b11a6 --- /dev/null +++ b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/MyOutputRecord.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.jbatch.jobs; + +/** + * Example Output Processor. + */ +public class MyOutputRecord { + + private int id; + + /** + * Constructor for Output Record. + * @param id + */ + public MyOutputRecord(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public String toString() { + return "MyOutputRecord: " + id; + } +} diff --git a/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/package-info.java b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/package-info.java new file mode 100644 index 000000000..fef1a02b6 --- /dev/null +++ b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/jobs/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * JBatch specific classes. + */ +package io.helidon.examples.jbatch.jobs; diff --git a/examples/jbatch/src/main/java/io/helidon/examples/jbatch/package-info.java b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/package-info.java new file mode 100644 index 000000000..f49bf46eb --- /dev/null +++ b/examples/jbatch/src/main/java/io/helidon/examples/jbatch/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example application showing support for JBatch and HelidonMP. + */ +package io.helidon.examples.jbatch; diff --git a/examples/jbatch/src/main/resources/META-INF/batch-jobs/myJob.xml b/examples/jbatch/src/main/resources/META-INF/batch-jobs/myJob.xml new file mode 100644 index 000000000..754a82135 --- /dev/null +++ b/examples/jbatch/src/main/resources/META-INF/batch-jobs/myJob.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/examples/jbatch/src/main/resources/META-INF/beans.xml b/examples/jbatch/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..292cd41a4 --- /dev/null +++ b/examples/jbatch/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/jbatch/src/main/resources/META-INF/helidon/serial-config.properties b/examples/jbatch/src/main/resources/META-INF/helidon/serial-config.properties new file mode 100644 index 000000000..88030d7f9 --- /dev/null +++ b/examples/jbatch/src/main/resources/META-INF/helidon/serial-config.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# The JBatch uses Serialization a lot, and these are all required +pattern=com.ibm.jbatch.**;jakarta.batch.runtime.BatchStatus;java.lang.Enum;\ + java.util.Properties;java.util.Hashtable;java.util.Map$Entry diff --git a/examples/jbatch/src/main/resources/META-INF/microprofile-config.properties b/examples/jbatch/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..bdaf6133e --- /dev/null +++ b/examples/jbatch/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +# Change the following to true to enable the optional MicroProfile Metrics REST.request metrics +metrics.rest-request.enabled=false diff --git a/examples/jbatch/src/main/resources/logging.properties b/examples/jbatch/src/main/resources/logging.properties new file mode 100644 index 000000000..ef323f89b --- /dev/null +++ b/examples/jbatch/src/main/resources/logging.properties @@ -0,0 +1,30 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Quiet Weld +org.jboss.level=WARNING diff --git a/examples/jbatch/src/test/java/io/helidon/examples/jbatch/TestJBatchEndpoint.java b/examples/jbatch/src/test/java/io/helidon/examples/jbatch/TestJBatchEndpoint.java new file mode 100644 index 000000000..dfdcb1a39 --- /dev/null +++ b/examples/jbatch/src/test/java/io/helidon/examples/jbatch/TestJBatchEndpoint.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.jbatch; + +import java.util.Collections; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +public class TestJBatchEndpoint { + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + @Inject + private WebTarget webTarget; + + @Test + public void runJob() throws InterruptedException { + + JsonObject expectedJson = JSON.createObjectBuilder() + .add("Steps executed", "[step1, step2]") + .add("Status", "COMPLETED") + .build(); + + //Start the job + JsonObject jsonObject = webTarget + .path("/batch") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(JsonObject.class); + + Integer responseJobId = jsonObject.getInt("Started a job with Execution ID: "); + assertThat(responseJobId, is(notNullValue())); + JsonObject result = null; + for (int i = 1; i < 10; i++) { + //Wait a bit for it to complete + Thread.sleep(i*1000); + + //Examine the results + result = webTarget + .path("batch/status/" + responseJobId) + .request(MediaType.APPLICATION_JSON_TYPE) + .get(JsonObject.class); + + if (result.equals(expectedJson)){ + break; + } + + } + + assertThat(result, equalTo(expectedJson)); + } +} diff --git a/examples/k8s/zipkin.yaml b/examples/k8s/zipkin.yaml new file mode 100644 index 000000000..5b632ff78 --- /dev/null +++ b/examples/k8s/zipkin.yaml @@ -0,0 +1,73 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: zipkin + labels: + app: zipkin +spec: + selector: + matchLabels: + app: zipkin + replicas: 1 + template: + metadata: + labels: + app: zipkin + spec: + containers: + - name: zipkin + image: openzipkin/zipkin:2 + imagePullPolicy: Always + ports: + - containerPort: 9411 +--- + +apiVersion: v1 +kind: Service +metadata: + name: zipkin + labels: + app: zipkin +spec: + type: ClusterIP + selector: + app: zipkin + ports: + - port: 9411 + targetPort: 9411 + name: http + +--- + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: zipkin-ingress +spec: + rules: + - host: localhost + http: + paths: + - path: /zipkin + pathType: Prefix + backend: + service: + name: zipkin + port: + number: 9411 diff --git a/examples/logging/jul/README.md b/examples/logging/jul/README.md new file mode 100644 index 000000000..44a10a6a2 --- /dev/null +++ b/examples/logging/jul/README.md @@ -0,0 +1,52 @@ +JUL Example +--- + +This example shows how to use Java Util Logging with MDC + using Helidon API. + +The example can be built using GraalVM native image as well. + +# Running as jar + +Build this application: +```shell +mvn clean package +``` + +Run from command line: +```shell +java -jar target/helidon-examples-logging-jul.jar +``` + +Expected output should be similar to the following: +```text +2020.11.19 15:37:28 INFO io.helidon.logging.jul.JulProvider Thread[#1,main,5,main]: Logging at initialization configured using classpath: /logging.properties "" +2020.11.19 15:37:28 INFO io.helidon.examples.logging.jul.Main Thread[main,5,main]: Starting up "startup" +2020.11.19 15:37:28 INFO io.helidon.examples.logging.jul.Main Thread[pool-1-thread-1,5,main]: Running on another thread "propagated" +2020.11.19 15:37:28 INFO io.helidon.common.features.HelidonFeatures Thread[#23,features-thread,5,main]: Helidon 4.0.0-SNAPSHOT features: [Config, Encoding, Media, WebServer] "" +2020.11.19 15:37:28 INFO io.helidon.webserver.LoomServer Thread[#1,main,5,main]: Started all channels in 46 milliseconds. 577 milliseconds since JVM startup. Java 20.0.1+9-29 "propagated" +``` + +# Running as native image +You must use GraalVM with native image installed as your JDK, +or you can specify an environment variable `GRAALVM_HOME` that points +to such an installation. + +Build this application: +```shell +mvn clean package -Pnative-image +``` + +Run from command line: +```shell +./target/helidon-examples-logging-jul +``` + +Expected output should be similar to the following: +```text +2020.11.19 15:38:14 INFO io.helidon.logging.common.LogConfig Thread[main,5,main]: Logging at runtime configured using classpath: /logging.properties "" +2020.11.19 15:38:14 INFO io.helidon.examples.logging.jul.Main Thread[main,5,main]: Starting up "startup" +2020.11.19 15:38:14 INFO io.helidon.examples.logging.jul.Main Thread[pool-1-thread-1,5,main]: Running on another thread "propagated" +2020.11.19 15:38:14 INFO io.helidon.common.features.HelidonFeatures Thread[features-thread,5,main]: Helidon SE 2.2.0 features: [Config, WebServer] "" +2020.11.19 15:38:14 INFO io.helidon.reactive.webserver.NettyWebServer Thread[nioEventLoopGroup-2-1,10,main]: Channel '@default' started: [id: 0x2b929906, L:/0:0:0:0:0:0:0:0:8080] "" +``` \ No newline at end of file diff --git a/examples/logging/jul/pom.xml b/examples/logging/jul/pom.xml new file mode 100644 index 000000000..0e348e947 --- /dev/null +++ b/examples/logging/jul/pom.xml @@ -0,0 +1,62 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.logging + helidon-examples-logging-jul + 1.0.0-SNAPSHOT + Helidon Examples Logging Java Util Logging + + + Example of logging and MDC using JUL + + + + io.helidon.examples.logging.jul.Main + + + + + io.helidon.webserver + helidon-webserver + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/logging/jul/src/main/java/io/helidon/examples/logging/jul/Main.java b/examples/logging/jul/src/main/java/io/helidon/examples/logging/jul/Main.java new file mode 100644 index 000000000..c30a03f15 --- /dev/null +++ b/examples/logging/jul/src/main/java/io/helidon/examples/logging/jul/Main.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.logging.jul; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.logging.Logger; + +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; +import io.helidon.logging.common.HelidonMdc; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +/** + * Main class of the example, runnable from command line. + */ +public final class Main { + private static final Logger LOGGER = Logger.getLogger(Main.class.getName()); + + private Main() { + } + + /** + * Starts the example. + * + * @param args not used + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + // the Helidon context is used to propagate MDC across threads + // if running within Helidon WebServer, you do not need to runInContext, as that is already + // done by the webserver + Contexts.runInContext(Context.create(), Main::logging); + + WebServer server = WebServer.builder() + .port(8080) + .routing(Main::routing) + .build() + .start(); + } + + private static void routing(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> { + HelidonMdc.set("name", String.valueOf(req.id())); + LOGGER.info("Running in webserver, id:"); + res.send("Hello"); + }); + } + + private static void logging() { + HelidonMdc.set("name", "startup"); + LOGGER.info("Starting up"); + + // now let's see propagation across executor service boundary + HelidonMdc.set("name", "propagated"); + // wrap executor so it supports Helidon context, this is done for all built-in executors in Helidon + ExecutorService es = Contexts.wrap(Executors.newSingleThreadExecutor()); + + Future submit = es.submit(() -> { + LOGGER.info("Running on another thread"); + }); + try { + submit.get(); + } catch (Exception e) { + e.printStackTrace(); + } + es.shutdown(); + } +} diff --git a/examples/logging/jul/src/main/java/io/helidon/examples/logging/jul/package-info.java b/examples/logging/jul/src/main/java/io/helidon/examples/logging/jul/package-info.java new file mode 100644 index 000000000..a7fe560e7 --- /dev/null +++ b/examples/logging/jul/src/main/java/io/helidon/examples/logging/jul/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Java util logging example with Mapped diagnostics context in Helidon. + */ +package io.helidon.examples.logging.jul; diff --git a/examples/logging/jul/src/main/resources/logging.properties b/examples/logging/jul/src/main/resources/logging.properties new file mode 100644 index 000000000..90de19dd6 --- /dev/null +++ b/examples/logging/jul/src/main/resources/logging.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# !thread! is replaced by Helidon with the thread name +# any %X{...} is replaced by a value from MDC +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s "%X{name}"%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO diff --git a/examples/logging/log4j/README.md b/examples/logging/log4j/README.md new file mode 100644 index 000000000..bf7f61892 --- /dev/null +++ b/examples/logging/log4j/README.md @@ -0,0 +1,55 @@ +Log4j Example +--- + +This example shows how to use log4j with MDC (`ThreadContext`) + using Helidon API. + +The example moves all Java Util Logging to log4j. + +The example can be built using GraalVM native image as well. + +# Running as jar + +Build this application: +```shell +mvn clean package +``` + +Run from command line: +```shell +java -jar target/helidon-examples-logging-log4j.jar +``` + +Expected output should be similar to the following: +```text +15:44:48.596 INFO [main] io.helidon.examples.logging.log4j.Main - Starting up "startup" +15:44:48.598 INFO [main] io.helidon.examples.logging.log4j.Main - Using System logger "startup" +15:44:48.600 INFO [pool-2-thread-1] io.helidon.examples.logging.log4j.Main - Running on another thread "propagated" +15:44:48.704 INFO [features-thread] io.helidon.common.features.HelidonFeatures - Helidon 4.0.0-SNAPSHOT features: [Config, Encoding, Media, WebServer] "" +15:44:48.801 INFO [main] io.helidon.webserver.LoomServer - Started all channels in 12 milliseconds. 746 milliseconds since JVM startup. Java 20.0.1+9-29 "propagated" +``` + +# Running as native image +You must use GraalVM with native image installed as your JDK, +or you can specify an environment variable `GRAALVM_HOME` that points +to such an installation. + +Build this application: +```shell +mvn clean package -Pnative-image +``` + +Run from command line: +```shell +./target/helidon-examples-logging-log4j +``` + +*In native image, we can only replace loggers initialized after reconfiguration of logging system +This unfortunately means that Helidon logging would not be available* + +Expected output should be similar to the following: +```text +15:47:53.033 INFO [main] io.helidon.examples.logging.log4j.Main - Starting up "startup" +15:47:53.033 INFO [main] io.helidon.examples.logging.log4j.Main - Using JUL logger "startup" +15:47:53.033 INFO [pool-2-thread-1] io.helidon.examples.logging.log4j.Main - Running on another thread "propagated" +``` \ No newline at end of file diff --git a/examples/logging/log4j/pom.xml b/examples/logging/log4j/pom.xml new file mode 100644 index 000000000..776392a49 --- /dev/null +++ b/examples/logging/log4j/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.logging + helidon-examples-logging-log4j + 1.0.0-SNAPSHOT + Helidon Examples Logging Log4j + + + Example of logging and MDC using Log4j + + + + io.helidon.examples.logging.log4j.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.logging + helidon-logging-log4j + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-jul + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/logging/log4j/src/main/java/io/helidon/examples/logging/log4j/Main.java b/examples/logging/log4j/src/main/java/io/helidon/examples/logging/log4j/Main.java new file mode 100644 index 000000000..4ab382076 --- /dev/null +++ b/examples/logging/log4j/src/main/java/io/helidon/examples/logging/log4j/Main.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.logging.log4j; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; +import io.helidon.logging.common.HelidonMdc; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.appender.ConsoleAppender; +import org.apache.logging.log4j.core.config.Configurator; +import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory; + +/** + * Main class of the example, runnable from command line. + * There is a limitation of log4j in native image - we only have loggers that are + * initialized after we configure logging, which unfortunately excludes Helidon loggers. + * You would need to use JUL or slf4j to have Helidon logs combined with application logs. + */ +public final class Main { + private static System.Logger systemLogger; + private static Logger logger; + + private Main() { + } + + /** + * Starts the example. + * + * @param args not used + */ + public static void main(String[] args) { + // file based logging configuration does not work + // with native image! + configureLog4j(); + LogConfig.configureRuntime(); + // get logger after configuration + logger = LogManager.getLogger(Main.class); + systemLogger = System.getLogger(Main.class.getName()); + + // the Helidon context is used to propagate MDC across threads + // if running within Helidon WebServer, you do not need to runInContext, as that is already + // done by the webserver + Contexts.runInContext(Context.create(), Main::logging); + + WebServer server = WebServer.builder() + .routing(Main::routing) + .build() + .start(); + } + + private static void routing(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> { + HelidonMdc.set("name", String.valueOf(req.id())); + logger.info("Running in webserver, id:"); + res.send("Hello"); + }); + } + + private static void logging() { + HelidonMdc.set("name", "startup"); + logger.info("Starting up"); + systemLogger.log(System.Logger.Level.INFO, "Using System logger"); + + // now let's see propagation across executor service boundary, we can also use Log4j's ThreadContext + ThreadContext.put("name", "propagated"); + // wrap executor so it supports Helidon context, this is done for all built-in executors in Helidon + ExecutorService es = Contexts.wrap(Executors.newSingleThreadExecutor()); + + Future submit = es.submit(Main::log); + try { + submit.get(); + } catch (Exception e) { + e.printStackTrace(); + } + es.shutdown(); + } + + private static void log() { + logger.info("Running on another thread"); + } + + private static void configureLog4j() { + // configure log4j + final var builder = ConfigurationBuilderFactory.newConfigurationBuilder(); + builder.setConfigurationName("root"); + builder.setStatusLevel(Level.INFO); + final var appenderComponentBuilder = builder.newAppender("Stdout", "CONSOLE") + .addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT); + appenderComponentBuilder.add(builder.newLayout("PatternLayout") + .addAttribute("pattern", "%d{HH:mm:ss.SSS} %-5level [%t] %logger{36} - %msg " + + "\"%X{name}\"%n")); + builder.add(appenderComponentBuilder); + builder.add(builder.newRootLogger(Level.INFO) + .add(builder.newAppenderRef("Stdout"))); + Configurator.initialize(builder.build()); + } +} diff --git a/examples/logging/log4j/src/main/java/io/helidon/examples/logging/log4j/package-info.java b/examples/logging/log4j/src/main/java/io/helidon/examples/logging/log4j/package-info.java new file mode 100644 index 000000000..dfbef7be1 --- /dev/null +++ b/examples/logging/log4j/src/main/java/io/helidon/examples/logging/log4j/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Log4j example with Mapped diagnostics context (ThreadContext) in Helidon. + */ +package io.helidon.examples.logging.log4j; diff --git a/examples/logging/logback-aot/README.md b/examples/logging/logback-aot/README.md new file mode 100644 index 000000000..1ba544c0b --- /dev/null +++ b/examples/logging/logback-aot/README.md @@ -0,0 +1,60 @@ +Slf4j/Logback Example +--- + +This example shows how to use slf4j with MDC backed by Logback + using Helidon API. + +The example moves all Java Util Logging to slf4j and supports more advance configuration of logback. + +# AOT (native image) +To support native image, we need to use a different logback configuration at build time and at runtime. +To achieve this, we bundle `logback.xml` on classpath, and then have `logback-runtime.xml` with +configuration that requires started threads (which is not supported at build time). + +The implementation will re-configure logback (see method `setupLogging` in `Main.java). + +To see that configuration works as expected at runtime, change the log level of our package to `debug`. +Within 30 seconds the configuration should be reloaded, and next request will have two more debug messages. + +Expected output should be similar to the following (for both hotspot and native): +```text +15:40:44.240 [INFO ] [io.helidon.examples.logging.logback.aot.Main.logging:128] Starting up startup +15:40:44.241 [INFO ] [o.slf4j.jdk.platform.logging.SLF4JPlatformLogger.performLog:151] Using System logger startup +15:40:44.245 [INFO ] [io.helidon.examples.logging.logback.aot.Main.log:146] Running on another thread propagated +15:40:44.395 [INFO ] [o.slf4j.jdk.platform.logging.SLF4JPlatformLogger.performLog:151] Helidon 4.0.0-SNAPSHOT features: [Config, Encoding, Media, WebServer] +15:40:44.538 [INFO ] [o.slf4j.jdk.platform.logging.SLF4JPlatformLogger.performLog:151] Started all channels in 15 milliseconds. 647 milliseconds since JVM startup. Java 20.0.1+9-29 propagated +``` + +The output is also logged into `helidon.log`. + +# Running as jar + +Build this application: +```shell +mvn clean package +``` + +Run from command line: +```shell +java -jar target/helidon-examples-logging-slf4j-aot.jar +``` + +Execute endpoint: +```shell +curl -i http://localhost:8080 +``` + +# Running as native image +You must use GraalVM with native image installed as your JDK, +or you can specify an environment variable `GRAALVM_HOME` that points +to such an installation. + +Build this application: +```shell script +mvn clean package -Pnative-image +``` + +Run from command line: +```shell +./target/helidon-examples-logging-slf4j-aot +``` diff --git a/examples/logging/logback-aot/logback-runtime.xml b/examples/logging/logback-aot/logback-runtime.xml new file mode 100644 index 000000000..5d636ad19 --- /dev/null +++ b/examples/logging/logback-aot/logback-runtime.xml @@ -0,0 +1,60 @@ + + + + + + + helidon.log + true + false + + helidon.%d{yyyy-MM-dd}.gz + 10 + 5GB + + + + ${defaultPattern} + + + + + + 2048 + 15000 + true + true + + + + + ${defaultPattern} + + + + + + + + + + + + + + + diff --git a/examples/logging/logback-aot/pom.xml b/examples/logging/logback-aot/pom.xml new file mode 100644 index 000000000..453d9397f --- /dev/null +++ b/examples/logging/logback-aot/pom.xml @@ -0,0 +1,79 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.logging + helidon-examples-logging-slf4j-aot + 1.0.0-SNAPSHOT + Helidon Examples Logging Slf4j AOT + + + Example of logging and MDC using Slf4j ready for Ahead of time compilation + using GraalVM native image + + + + io.helidon.examples.logging.logback.aot.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.logging + helidon-logging-slf4j + + + org.slf4j + slf4j-api + + + org.slf4j + jul-to-slf4j + + + ch.qos.logback + logback-classic + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/logging/logback-aot/src/main/java/io/helidon/examples/logging/logback/aot/Main.java b/examples/logging/logback-aot/src/main/java/io/helidon/examples/logging/logback/aot/Main.java new file mode 100644 index 000000000..fb86d733c --- /dev/null +++ b/examples/logging/logback-aot/src/main/java/io/helidon/examples/logging/logback/aot/Main.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.logging.logback.aot; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; +import io.helidon.logging.common.HelidonMdc; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.core.joran.spi.JoranException; +import ch.qos.logback.core.util.StatusPrinter; +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +/** + * Main class of the example, runnable from command line. + */ +public final class Main { + private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); + private static final System.Logger SYSTEM_LOGGER = System.getLogger(Main.class.getName()); + + private Main() { + } + + /** + * Starts the example. + * + * @param args not used + */ + public static void main(String[] args) { + // use slf4j for JUL as well + setupLogging(); + + // the Helidon context is used to propagate MDC across threads + // if running within Helidon WebServer, you do not need to runInContext, as that is already + // done by the webserver + Contexts.runInContext(Context.create(), Main::logging); + + WebServer server = WebServer.builder() + .port(8080) + .routing(Main::routing) + .build() + .start(); + } + + private static void routing(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> { + HelidonMdc.set("name", String.valueOf(req.id())); + LOGGER.debug("Debug message to show runtime reloading works"); + LOGGER.info("Running in webserver, id:"); + res.send("Hello"); + LOGGER.debug("Response sent"); + }); + } + + private static void setupLogging() { + String location = System.getProperty("logback.configurationFile"); + location = (location == null) ? "logback-runtime.xml" : location; + // we cannot use anything that starts threads at build time, must re-configure here + resetLogging(location); + } + + private static void resetLogging(String location) { + ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); + if (loggerFactory instanceof LoggerContext) { + resetLogging(location, (LoggerContext) loggerFactory); + } else { + LOGGER.warn("Expecting a logback implementation, but got " + loggerFactory.getClass().getName()); + } + } + + private static void resetLogging(String location, LoggerContext loggerFactory) { + JoranConfigurator configurator = new JoranConfigurator(); + + configurator.setContext(loggerFactory); + loggerFactory.reset(); + + try { + configurator.doConfigure(location); + + Logger instance = LoggerFactory.getLogger(Main.class); + instance.info("Runtime logging configured from file \"{}\".", location); + StatusPrinter.print(loggerFactory); + } catch (JoranException e) { + LOGGER.warn("Failed to reload logging from " + location, e); + e.printStackTrace(); + } + } + + private static void logging() { + HelidonMdc.set("name", "startup"); + LOGGER.info("Starting up"); + SYSTEM_LOGGER.log(System.Logger.Level.INFO, "Using System logger"); + + // now let's see propagation across executor service boundary, we can also use Log4j's ThreadContext + MDC.put("name", "propagated"); + // wrap executor so it supports Helidon context, this is done for all built-in executors in Helidon + ExecutorService es = Contexts.wrap(Executors.newSingleThreadExecutor()); + + Future submit = es.submit(Main::log); + try { + submit.get(); + } catch (Exception e) { + e.printStackTrace(); + } + es.shutdown(); + } + + private static void log() { + LOGGER.info("Running on another thread"); + } +} diff --git a/examples/logging/logback-aot/src/main/java/io/helidon/examples/logging/logback/aot/package-info.java b/examples/logging/logback-aot/src/main/java/io/helidon/examples/logging/logback/aot/package-info.java new file mode 100644 index 000000000..6e718eafd --- /dev/null +++ b/examples/logging/logback-aot/src/main/java/io/helidon/examples/logging/logback/aot/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Slf4j example with Mapped diagnostics context (MDC) in Helidon ready for + * complex runtime configuration in native image. + */ +package io.helidon.examples.logging.logback.aot; diff --git a/examples/logging/logback-aot/src/main/resources/logback.xml b/examples/logging/logback-aot/src/main/resources/logback.xml new file mode 100644 index 000000000..6d50d18e2 --- /dev/null +++ b/examples/logging/logback-aot/src/main/resources/logback.xml @@ -0,0 +1,31 @@ + + + + + + + + %d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg %X{name}%n + + + + + + + + \ No newline at end of file diff --git a/examples/logging/pom.xml b/examples/logging/pom.xml new file mode 100644 index 000000000..32f3f188d --- /dev/null +++ b/examples/logging/pom.xml @@ -0,0 +1,45 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + io.helidon.examples.logging + helidon-examples-logging-project + 1.0.0-SNAPSHOT + Helidon Examples Logging + pom + + + Examples of Helidon Logging + + + + jul + log4j + slf4j + logback-aot + + diff --git a/examples/logging/slf4j/README.md b/examples/logging/slf4j/README.md new file mode 100644 index 000000000..7ef0fcd5c --- /dev/null +++ b/examples/logging/slf4j/README.md @@ -0,0 +1,45 @@ +Slf4j Example +--- + +This example shows how to use slf4j with MDC + using Helidon API. + +The example moves all Java Util Logging to slf4j + +The example can be built using GraalVM native image as well. + +Expected output should be similar to the following (for both hotspot and native): +```text +15:40:44.240 INFO [main] i.h.examples.logging.slf4j.Main - Starting up startup +15:40:44.241 INFO [main] i.h.examples.logging.slf4j.Main - Using System logger startup +15:40:44.245 INFO [pool-1-thread-1] i.h.examples.logging.slf4j.Main - Running on another thread propagated +15:40:44.395 INFO [features-thread] i.h.common.features.HelidonFeatures - Helidon 4.0.0-SNAPSHOT features: [Config, Encoding, Media, WebServer] +15:40:44.538 INFO [main] i.helidon.webserver.LoomServer - Started all channels in 15 milliseconds. 561 milliseconds since JVM startup. Java 20.0.1+9-29 propagated +``` + +# Running as jar + +Build this application: +```shell +mvn clean package +``` + +Run from command line: +```shell +java -jar target/helidon-examples-logging-slf4j.jar +``` + +# Running as native image +You must use GraalVM with native image installed as your JDK, +or you can specify an environment variable `GRAALVM_HOME` that points +to such an installation. + +Build this application: +```shell +mvn clean package -Pnative-image +``` + +Run from command line: +```shell +./target/helidon-examples-logging-slf4j +``` \ No newline at end of file diff --git a/examples/logging/slf4j/pom.xml b/examples/logging/slf4j/pom.xml new file mode 100644 index 000000000..7ce8af691 --- /dev/null +++ b/examples/logging/slf4j/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.logging + helidon-examples-logging-slf4j + 1.0.0-SNAPSHOT + Helidon Examples Logging Slf4j + + + Example of logging and MDC using Slf4j + + + + io.helidon.examples.logging.slf4j.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.logging + helidon-logging-slf4j + + + org.slf4j + slf4j-api + + + org.slf4j + jul-to-slf4j + + + ch.qos.logback + logback-classic + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/logging/slf4j/src/main/java/io/helidon/examples/logging/slf4j/Main.java b/examples/logging/slf4j/src/main/java/io/helidon/examples/logging/slf4j/Main.java new file mode 100644 index 000000000..35e59dcae --- /dev/null +++ b/examples/logging/slf4j/src/main/java/io/helidon/examples/logging/slf4j/Main.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.logging.slf4j; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; +import io.helidon.logging.common.HelidonMdc; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +/** + * Main class of the example, runnable from command line. + */ +public final class Main { + private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); + private static final System.Logger SYSTEM_LOGGER = System.getLogger(Main.class.getName()); + + private Main() { + } + + /** + * Starts the example. + * + * @param args not used + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + // the Helidon context is used to propagate MDC across threads + // if running within Helidon WebServer, you do not need to runInContext, as that is already + // done by the webserver + Contexts.runInContext(Context.create(), Main::logging); + + WebServer server = WebServer.builder() + .routing(Main::routing) + .build() + .start(); + } + + private static void routing(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> { + HelidonMdc.set("name", String.valueOf(req.id())); + LOGGER.info("Running in webserver, id:"); + res.send("Hello"); + }); + } + + private static void logging() { + HelidonMdc.set("name", "startup"); + LOGGER.info("Starting up"); + SYSTEM_LOGGER.log(System.Logger.Level.INFO, "Using System logger"); + + // now let's see propagation across executor service boundary, we can also use Log4j's ThreadContext + MDC.put("name", "propagated"); + // wrap executor so it supports Helidon context, this is done for all built-in executors in Helidon + ExecutorService es = Contexts.wrap(Executors.newSingleThreadExecutor()); + + Future submit = es.submit(Main::log); + try { + submit.get(); + } catch (Exception e) { + e.printStackTrace(); + } + es.shutdown(); + } + + private static void log() { + LOGGER.info("Running on another thread"); + } +} diff --git a/examples/logging/slf4j/src/main/java/io/helidon/examples/logging/slf4j/package-info.java b/examples/logging/slf4j/src/main/java/io/helidon/examples/logging/slf4j/package-info.java new file mode 100644 index 000000000..a0d60f40d --- /dev/null +++ b/examples/logging/slf4j/src/main/java/io/helidon/examples/logging/slf4j/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Slf4j example with Mapped diagnostics context (MDC) in Helidon. + */ +package io.helidon.examples.logging.slf4j; diff --git a/examples/logging/slf4j/src/main/resources/logback.xml b/examples/logging/slf4j/src/main/resources/logback.xml new file mode 100644 index 000000000..7a6f59f85 --- /dev/null +++ b/examples/logging/slf4j/src/main/resources/logback.xml @@ -0,0 +1,35 @@ + + + + + + true + + + + + + %d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg %X{name}%n + + + + + + + + \ No newline at end of file diff --git a/examples/logging/slf4j/src/main/resources/logging.properties b/examples/logging/slf4j/src/main/resources/logging.properties new file mode 100644 index 000000000..5e4967d7c --- /dev/null +++ b/examples/logging/slf4j/src/main/resources/logging.properties @@ -0,0 +1,18 @@ +# +# Copyright (c) 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# register SLF4JBridgeHandler as handler for the j.u.l. root logger +handlers=org.slf4j.bridge.SLF4JBridgeHandler diff --git a/examples/media/multipart/README.md b/examples/media/multipart/README.md new file mode 100644 index 000000000..eb7ee218b --- /dev/null +++ b/examples/media/multipart/README.md @@ -0,0 +1,23 @@ +# Helidon SE MultiPart Example + +This example demonstrates how to use `MultiPartSupport` with both the `WebServer` + and `WebClient` APIs. + +This project implements a simple file service web application that supports uploading +and downloading files. The unit test uses the `WebClient` API to test the endpoints. + +## Build + +```shell +mvn package +``` + +## Run + +First, start the server: + +```shell +java -jar target/helidon-examples-media-multipart.jar +``` + +Then open in your browser. diff --git a/examples/media/multipart/pom.xml b/examples/media/multipart/pom.xml new file mode 100644 index 000000000..bb65cc2bf --- /dev/null +++ b/examples/media/multipart/pom.xml @@ -0,0 +1,105 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.media + helidon-examples-media-multipart + 1.0.0-SNAPSHOT + Helidon Examples Media Support Multipart + + + Example of a form based file upload. + + + + io.helidon.examples.media.multipart.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.http.media + helidon-http-media-multipart + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.webserver + helidon-webserver-static-content + + + jakarta.json + jakarta.json-api + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java new file mode 100644 index 000000000..78d31f5e3 --- /dev/null +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.media.multipart; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Map; +import java.util.stream.Stream; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.http.ContentDisposition; +import io.helidon.http.Header; +import io.helidon.http.HeaderNames; +import io.helidon.http.HeaderValues; +import io.helidon.http.ServerResponseHeaders; +import io.helidon.http.media.multipart.MultiPart; +import io.helidon.http.media.multipart.ReadablePart; +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.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; + +import static io.helidon.http.Status.BAD_REQUEST_400; +import static io.helidon.http.Status.MOVED_PERMANENTLY_301; +import static io.helidon.http.Status.NOT_FOUND_404; + +/** + * File service. + */ +public final class FileService implements HttpService { + private static final Header UI_LOCATION = HeaderValues.createCached(HeaderNames.LOCATION, "/ui"); + private final JsonBuilderFactory jsonFactory; + private final Path storage; + + /** + * Create a new file upload service instance. + */ + FileService() { + jsonFactory = Json.createBuilderFactory(Map.of()); + storage = createStorage(); + System.out.println("Storage: " + storage); + } + + @Override + public void routing(HttpRules rules) { + rules.get("/", this::list) + .get("/{fname}", this::download) + .post("/", this::upload); + } + + private static Path createStorage() { + try { + return Files.createTempDirectory("fileupload"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private static Stream listFiles(Path storage) { + try (Stream walk = Files.walk(storage)) { + return walk.filter(Files::isRegularFile) + .map(storage::relativize) + .map(Path::toString) + .toList() + .stream(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private static OutputStream newOutputStream(Path storage, String fname) { + try { + return Files.newOutputStream(storage.resolve(fname), + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void list(ServerRequest req, ServerResponse res) { + JsonArrayBuilder arrayBuilder = jsonFactory.createArrayBuilder(); + listFiles(storage).forEach(arrayBuilder::add); + res.send(jsonFactory.createObjectBuilder().add("files", arrayBuilder).build()); + } + + private void download(ServerRequest req, ServerResponse res) { + Path filePath = storage.resolve(req.path().pathParameters().get("fname")); + if (!filePath.getParent().equals(storage)) { + res.status(BAD_REQUEST_400).send("Invalid file name"); + return; + } + if (!Files.exists(filePath)) { + res.status(NOT_FOUND_404).send(); + return; + } + if (!Files.isRegularFile(filePath)) { + res.status(BAD_REQUEST_400).send("Not a file"); + return; + } + ServerResponseHeaders headers = res.headers(); + headers.contentType(MediaTypes.APPLICATION_OCTET_STREAM); + headers.set(ContentDisposition.builder() + .filename(filePath.getFileName().toString()) + .build()); + res.send(filePath); + } + + private void upload(ServerRequest req, ServerResponse res) { + MultiPart mp = req.content().as(MultiPart.class); + + while (mp.hasNext()) { + ReadablePart part = mp.next(); + if ("file[]".equals(URLDecoder.decode(part.name(), StandardCharsets.UTF_8))) { + try (InputStream in = part.inputStream(); OutputStream out = newOutputStream(storage, part.fileName().get())) { + in.transferTo(out); + } catch (IOException e) { + throw new RuntimeException("Failed to write content", e); + } + } + } + + res.status(MOVED_PERMANENTLY_301) + .header(UI_LOCATION) + .send(); + } +} diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java new file mode 100644 index 000000000..e1a827e41 --- /dev/null +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.media.multipart; + +import io.helidon.http.Header; +import io.helidon.http.HeaderNames; +import io.helidon.http.HeaderValues; +import io.helidon.http.Status; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.staticcontent.StaticContentService; + +/** + * This application provides a simple file upload service with a UI to exercise multipart. + */ +public final class Main { + private static final Header UI_LOCATION = HeaderValues.createCached(HeaderNames.LOCATION, "/ui"); + + private Main() { + } + + /** + * Executes the example. + * + * @param args command line arguments, ignored + */ + public static void main(String[] args) { + WebServer server = WebServer.builder() + .routing(Main::routing) + .port(8080) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + } + + /** + * Updates the routing rules. + * + * @param rules routing rules + */ + static void routing(HttpRules rules) { + rules.any("/", (req, res) -> { + res.status(Status.MOVED_PERMANENTLY_301); + res.header(UI_LOCATION); + res.send(); + }) + .register("/ui", StaticContentService.builder("WEB") + .welcomeFileName("index.html") + .build()) + .register("/api", new FileService()); + } +} diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/package-info.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/package-info.java new file mode 100644 index 000000000..6479921e5 --- /dev/null +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon Examples Media MultiPart. + */ +package io.helidon.examples.media.multipart; diff --git a/examples/media/multipart/src/main/resources/WEB/index.html b/examples/media/multipart/src/main/resources/WEB/index.html new file mode 100644 index 000000000..d7acca73f --- /dev/null +++ b/examples/media/multipart/src/main/resources/WEB/index.html @@ -0,0 +1,66 @@ + + + + + + Helidon Examples Media Multipart + + + + + +

Uploaded files

+
+ +

Upload (buffered)

+
+ Select a file to upload: + + +
+ +

Upload (stream)

+
+ Select a file to upload: + + +
+ + + + diff --git a/examples/media/multipart/src/main/resources/logging.properties b/examples/media/multipart/src/main/resources/logging.properties new file mode 100644 index 000000000..ccdbe8a09 --- /dev/null +++ b/examples/media/multipart/src/main/resources/logging.properties @@ -0,0 +1,21 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.webserver.level=INFO diff --git a/examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java b/examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java new file mode 100644 index 000000000..0f3f2926c --- /dev/null +++ b/examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.media.multipart; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.http.HeaderNames; +import io.helidon.http.Status; +import io.helidon.http.media.multipart.WriteableMultiPart; +import io.helidon.http.media.multipart.WriteablePart; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests {@link FileService}. + */ +@TestMethodOrder(OrderAnnotation.class) +@ServerTest +public class FileServiceTest { + private final Http1Client client; + + FileServiceTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.routing(builder); + } + + @Test + @Order(1) + public void testUpload() throws IOException { + Path file = Files.writeString(Files.createTempFile(null, null), "bar\n"); + try (Http1ClientResponse response = client.post("/api") + .followRedirects(false) + .submit(WriteableMultiPart.builder() + .addPart(writeablePart("file[]", "foo.txt", file)) + .build())) { + assertThat(response.status(), is(Status.MOVED_PERMANENTLY_301)); + } + } + + @Test + @Order(2) + public void testStreamUpload() throws IOException { + Path file = Files.writeString(Files.createTempFile(null, null), "stream bar\n"); + Path file2 = Files.writeString(Files.createTempFile(null, null), "stream foo\n"); + try (Http1ClientResponse response = client.post("/api") + .queryParam("stream", "true") + .followRedirects(false) + .submit(WriteableMultiPart + .builder() + .addPart(writeablePart("file[]", "streamed-foo.txt", file)) + .addPart(writeablePart("otherPart", "streamed-foo2.txt", file2)) + .build())) { + assertThat(response.status(), is(Status.MOVED_PERMANENTLY_301)); + } + } + + @Test + @Order(3) + public void testList() { + try (Http1ClientResponse response = client.get("/api").request()) { + assertThat(response.status(), is(Status.OK_200)); + JsonObject json = response.as(JsonObject.class); + assertThat(json, Matchers.is(notNullValue())); + List files = json.getJsonArray("files").getValuesAs(v -> ((JsonString) v).getString()); + assertThat(files, hasItem("foo.txt")); + } + } + + @Test + @Order(4) + public void testDownload() { + try (Http1ClientResponse response = client.get("/api").path("foo.txt").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.headers().first(HeaderNames.CONTENT_DISPOSITION).orElse(null), + containsString("filename=\"foo.txt\"")); + byte[] bytes = response.as(byte[].class); + assertThat(new String(bytes, StandardCharsets.UTF_8), Matchers.is("bar\n")); + } + } + + private WriteablePart writeablePart(String partName, String fileName, Path filePath) throws IOException { + return WriteablePart.builder(partName) + .fileName(fileName) + .content(Files.readAllBytes(filePath)) + .contentType(MediaTypes.MULTIPART_FORM_DATA) + .build(); + } +} diff --git a/examples/media/pom.xml b/examples/media/pom.xml new file mode 100644 index 000000000..69e2cfbe6 --- /dev/null +++ b/examples/media/pom.xml @@ -0,0 +1,41 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + io.helidon.examples.media + helidon-examples-media-project + Helidon Examples Media Support + pom + + + Examples of Helidon Media usage + + + + multipart + + diff --git a/examples/messaging/README.md b/examples/messaging/README.md new file mode 100644 index 000000000..63201c10f --- /dev/null +++ b/examples/messaging/README.md @@ -0,0 +1,55 @@ +# Helidon Messaging Examples + +## Prerequisites +* Docker +* Java 21+ + +### Test Kafka server +To make examples easily runnable, +small, pocket size and pre-configured testing Kafka server Docker image is available. + +* To run it locally: `./kafkaRun.sh` + * Pre-configured topics: + * `messaging-test-topic-1` + * `messaging-test-topic-2` + * Stop it with `Ctrl+c` + +* Send messages manually with: `./kafkaProduce.sh [topic-name]` +* Consume messages manually with: `./kafkaConsume.sh [topic-name]` + +### Test JMS server +* Start ActiveMQ server locally: +```shell +docker run --name='activemq' --rm -p 61616:61616 -p 8161:8161 rmohr/activemq +``` + +### Test Oracle database +* Start ActiveMQ server locally: +```shell +cd ./docker/oracle-aq-18-xe +./buildAndRun.sh +``` + +For stopping Oracle database container use: +```shell +cd ./docker/oracle-aq-18-xe +./stopAndClean.sh +``` + +## Helidon SE Reactive Messaging with Kafka Example +For demonstration of Helidon SE Messaging with Kafka connector, +continue to [Kafka with WebSocket SE Example](kafka-websocket-se/README.md) + +## Helidon MP Reactive Messaging with Kafka Example +For demonstration of Helidon MP Messaging with Kafka connector, +continue to [Kafka with WebSocket MP Example](kafka-websocket-mp/README.md) + +## Helidon MP Reactive Messaging with JMS Example +For demonstration of Helidon MP Messaging with JMS connector, +continue to [JMS with WebSocket MP Example](jms-websocket-mp/README.md) + +## Helidon MP Reactive Messaging with Oracle AQ Example +For demonstration of Helidon MP Messaging with Oracle Advance Queueing connector, +continue to [Oracle AQ with WebSocket MP Example](oracle-aq-websocket-mp/README.md) + + diff --git a/examples/messaging/docker/kafka/Dockerfile.kafka b/examples/messaging/docker/kafka/Dockerfile.kafka new file mode 100644 index 000000000..761977c96 --- /dev/null +++ b/examples/messaging/docker/kafka/Dockerfile.kafka @@ -0,0 +1,51 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 + +ENV VERSION=2.7.0 +ENV SCALA_VERSION=2.13 + +RUN dnf update && dnf -y install wget jq nc + +RUN REL_PATH=kafka/${VERSION}/kafka_${SCALA_VERSION}-${VERSION}.tgz \ +&& BACKUP_ARCHIVE=https://archive.apache.org/dist/ \ +&& echo "Looking for closest mirror ..." \ +&& MIRROR=$(curl -s 'https://www.apache.org/dyn/closer.cgi?as_json=1' | jq -r '.http[0]') \ +&& echo "Checking if version ${VERSION} is available on the mirror: ${MIRROR} ..." \ +&& MIRROR_RESPONSE=$(curl -L --write-out '%{http_code}' --silent --output /dev/null ${MIRROR}kafka/${VERSION}) \ +&& if [ $MIRROR_RESPONSE -eq 200 ]; then BIN_URL=${MIRROR}${REL_PATH}; else BIN_URL=${BACKUP_ARCHIVE}${REL_PATH}; fi \ +&& if [ $MIRROR_RESPONSE -ne 200 ]; then echo "Version ${VERSION} not found on the mirror ${MIRROR}, defaulting to archive ${BACKUP_ARCHIVE}."; fi \ +&& wget -q -O kafka.tar.gz ${BIN_URL} \ +&& tar -xzf kafka.tar.gz -C /opt && rm kafka.tar.gz \ +&& mv /opt/kafka* /opt/kafka + +WORKDIR /opt/kafka + +COPY start_kafka.sh start_kafka.sh +COPY init_topics.sh init_topics.sh + +RUN chmod a+x ./*.sh + +RUN echo listener.security.protocol.map=INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT >> config/server.properties \ +&& echo advertised.listeners=INSIDE://localhost:9092,OUTSIDE://localhost:29092 >> config/server.properties \ +&& echo listeners=INSIDE://0.0.0.0:9092,OUTSIDE://0.0.0.0:29092 >> config/server.properties \ +&& echo inter.broker.listener.name=INSIDE >> config/server.properties + +# Expose Zookeeper and Kafka ports +EXPOSE 2181 9092 29092 + +CMD bash start_kafka.sh diff --git a/examples/messaging/docker/kafka/init_topics.sh b/examples/messaging/docker/kafka/init_topics.sh new file mode 100644 index 000000000..000ec3b53 --- /dev/null +++ b/examples/messaging/docker/kafka/init_topics.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Wait for Kafka to start and create test topics: +# topic messaging-test-topic-1 and topic messaging-test-topic-2 +# + +ZOOKEEPER_URL=localhost:2181 +KAFKA_TOPICS="/opt/kafka/bin/kafka-topics.sh --if-not-exists --zookeeper $ZOOKEEPER_URL" + +while sleep 2; do + brokers=$(echo dump | nc localhost 2181 | grep brokers | wc -l) + echo "Checking if Kafka is up: ${brokers}" + if [[ "$brokers" -gt "0" ]]; then + echo "KAFKA IS UP !!!" + + echo "Creating test topics" + bash $KAFKA_TOPICS \ + --create \ + --replication-factor 1 \ + --partitions 10 \ + --topic messaging-test-topic-1 + bash $KAFKA_TOPICS \ + --create \ + --replication-factor 1 \ + --partitions 10 \ + --topic messaging-test-topic-2 + bash $KAFKA_TOPICS \ + --create \ + --replication-factor 1 \ + --partitions 10 \ + --config compression.type=snappy \ + --topic messaging-test-topic-snappy-compressed + bash $KAFKA_TOPICS \ + --create \ + --replication-factor 1 \ + --partitions 10 \ + --config compression.type=lz4 \ + --topic messaging-test-topic-lz4-compressed + bash $KAFKA_TOPICS \ + --create \ + --replication-factor 1 \ + --partitions 10 \ + --config compression.type=zstd \ + --topic messaging-test-topic-zstd-compressed + bash $KAFKA_TOPICS \ + --create \ + --replication-factor 1 \ + --partitions 10 \ + --config compression.type=gzip \ + --topic messaging-test-topic-gzip-compressed + + echo + echo "Example topics created:" + echo " messaging-test-topic-1" + echo " messaging-test-topic-2" + echo " messaging-test-topic-snappy-compressed" + echo " messaging-test-topic-lz4-compressed" + echo " messaging-test-topic-zstd-compressed" + echo " messaging-test-topic-gzip-compressed" + echo + echo "================== Kafka is ready, stop it with Ctrl+C ==================" + exit 0 + fi +done diff --git a/examples/messaging/docker/kafka/start_kafka.sh b/examples/messaging/docker/kafka/start_kafka.sh new file mode 100644 index 000000000..875987b55 --- /dev/null +++ b/examples/messaging/docker/kafka/start_kafka.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Start Zookeeper, wait for it to come up and start Kafka. +# + +# Allow ruok +echo "4lw.commands.whitelist=*" >>/opt/kafka/config/zookeeper.properties + +# Start Zookeeper +/opt/kafka/bin/zookeeper-server-start.sh /opt/kafka/config/zookeeper.properties & + +while sleep 2; do + isOk=$(echo ruok | nc localhost 2181) + echo "Checking if Zookeeper is up: ${isOk}" + if [ "${isOk}" = "imok" ]; then + echo "ZOOKEEPER IS UP !!!" + break + fi +done + +# Create test topics when Kafka is ready +/opt/kafka/init_topics.sh & + +# Start Kafka +/opt/kafka/bin/kafka-server-start.sh /opt/kafka/config/server.properties +state=$? +if [ $state -ne 0 ]; then + echo "Kafka stopped." + exit $state +fi + +# Keep Kafka up till Ctrl+C +read ; diff --git a/examples/messaging/docker/oracle-aq-18-xe/Dockerfile b/examples/messaging/docker/oracle-aq-18-xe/Dockerfile new file mode 100644 index 000000000..ed27c31b7 --- /dev/null +++ b/examples/messaging/docker/oracle-aq-18-xe/Dockerfile @@ -0,0 +1,20 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +FROM oracle/database:18.4.0-xe as base + + +COPY init.sql /docker-entrypoint-initdb.d/setup/ \ No newline at end of file diff --git a/examples/messaging/docker/oracle-aq-18-xe/buildAndRun.sh b/examples/messaging/docker/oracle-aq-18-xe/buildAndRun.sh new file mode 100755 index 000000000..0d9d3334c --- /dev/null +++ b/examples/messaging/docker/oracle-aq-18-xe/buildAndRun.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +CURR_DIR=$(pwd) +TEMP_DIR=../../target +IMAGES_DIR=${TEMP_DIR}/ora-images +COMMIT="a69fe9b08ff147bb746d16af76cc5279ea5baf7a"; +IMAGES_ZIP_URL=https://github.com/oracle/docker-images/archive/${COMMIT:0:7}.zip +IMAGES_ZIP_DIR=docker-images-${COMMIT}/OracleDatabase/SingleInstance/dockerfiles +ORA_DB_VERSION=18.4.0 +BASE_IMAGE_NAME=oracle/database:${ORA_DB_VERSION}-xe +IMAGE_NAME=helidon/oracle-aq-example +CONTAINER_NAME=oracle-aq-example +ORACLE_PWD=frank + +printf "%-100s" "Checking if base image ${BASE_IMAGE_NAME} is available in local repository" +if [[ "$(docker images -q ${BASE_IMAGE_NAME} 2>/dev/null)" == "" ]]; then + printf "NOK\n" + + echo Base image ${BASE_IMAGE_NAME} not found. Building ... + + # cleanup + mkdir -p ${TEMP_DIR} + rm -rf ${IMAGES_DIR} + rm -f ${TEMP_DIR}/ora-images.zip + + # download official oracle docker images + curl -LJ -o ${TEMP_DIR}/ora-images.zip ${IMAGES_ZIP_URL} + # unzip only image for Oracle database 18.4.0 + unzip -qq ${TEMP_DIR}/ora-images.zip "${IMAGES_ZIP_DIR}/*" -d ${IMAGES_DIR} + mv ${IMAGES_DIR}/${IMAGES_ZIP_DIR}/${ORA_DB_VERSION} ${IMAGES_DIR}/ + mv ${IMAGES_DIR}/${IMAGES_ZIP_DIR}/buildContainerImage.sh ${IMAGES_DIR}/ + + # cleanup + rm -rf ${IMAGES_DIR}/docker-images-${COMMIT} + rm ${TEMP_DIR}/ora-images.zip + + # build base image + # can take long(15 minutes or so) + cd ${IMAGES_DIR} || exit + bash ./buildContainerImage.sh -v ${ORA_DB_VERSION} -x || exit + cd ${CURR_DIR} || exit +else + printf "OK\n" +fi + +printf "%-100s" "Checking if image ${IMAGE_NAME} is available in local repository" +if [[ "$(docker images -q ${IMAGE_NAME} 2>/dev/null)" == "" ]]; then + printf "NOK\n" + + echo Image ${IMAGE_NAME} not found. Building ... + docker build -t ${IMAGE_NAME} . || exit +else + printf "OK\n" +fi + +printf "%-100s" "Checking if container ${CONTAINER_NAME} is ready" +if [[ $(docker ps -a --filter "name=^/${CONTAINER_NAME}$" --format '{{.Names}}') != "${CONTAINER_NAME}" ]]; then + printf "NOK\n" + + echo "Container ${CONTAINER_NAME} not found. Running ..." + echo "!!! Be aware first time database initialization can take tens of minutes." + echo "!!! Follow docker logs -f ${CONTAINER_NAME} for 'DATABASE IS READY TO USE' message" + + docker run -d --name ${CONTAINER_NAME} \ + -p 1521:1521 \ + -p 5500:5500 \ + -e ORACLE_PWD=${ORACLE_PWD} \ + ${IMAGE_NAME} || exit +else + printf "OK\n" + printf "%-100s" "Checking if container ${CONTAINER_NAME} is started" + if [[ $(docker ps --filter "name=^/${CONTAINER_NAME}$" --format '{{.Names}}') != "${CONTAINER_NAME}" ]]; then + printf "NOK\n" + + echo "Container ${CONTAINER_NAME} not started. Starting ..." + docker start ${CONTAINER_NAME} || exit + else + printf "OK\n" + fi +fi + +echo "Container ${CONTAINER_NAME} with Oracle database ${ORA_DB_VERSION} XE populated with example AQ queues is either started or starting." +echo "For more info about the state of the database investigate logs:" +echo " docker logs -f ${CONTAINER_NAME}" +echo "Url: jdbc:oracle:thin:@localhost:1521:XE" +echo "user: frank" +echo "pass: frank" diff --git a/examples/messaging/docker/oracle-aq-18-xe/examples.sql b/examples/messaging/docker/oracle-aq-18-xe/examples.sql new file mode 100644 index 000000000..2e8dedc44 --- /dev/null +++ b/examples/messaging/docker/oracle-aq-18-xe/examples.sql @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +-- SEND MESSAGE AS RAW BYTES +DECLARE + id pls_integer; + enqueue_options DBMS_AQ.ENQUEUE_OPTIONS_T; + message_properties DBMS_AQ.MESSAGE_PROPERTIES_T; + message_handle RAW(16); + msg sys.aq$_jms_bytes_message; +BEGIN + msg := sys.aq$_jms_bytes_message.construct; + id := msg.clear_body(-1); + msg.write_bytes(id, UTL_RAW.CAST_TO_RAW('Hello raw bytes!')); + msg.flush(id); + DBMS_AQ.ENQUEUE( + queue_name => 'FRANK.EXAMPLE_QUEUE_BYTES', + enqueue_options => enqueue_options, + message_properties => message_properties, + payload => msg, + msgid => message_handle); + COMMIT; +END; + +-- SEND TEXT MESSAGE +DECLARE + enqueue_options DBMS_AQ.ENQUEUE_OPTIONS_T; + message_properties DBMS_AQ.MESSAGE_PROPERTIES_T; + message_handle RAW(16); + msg SYS.AQ$_JMS_TEXT_MESSAGE; +BEGIN + msg := SYS.AQ$_JMS_TEXT_MESSAGE.construct; + msg.set_text('Hello from PLSQL !'); + DBMS_AQ.ENQUEUE( + queue_name => 'FRANK.EXAMPLE_QUEUE_1', + enqueue_options => enqueue_options, + message_properties => message_properties, + payload => msg, + msgid => message_handle); + COMMIT; +END; + +-- SEND MAP MESSAGE +DECLARE + id pls_integer; + enqueue_options DBMS_AQ.ENQUEUE_OPTIONS_T; + message_properties DBMS_AQ.MESSAGE_PROPERTIES_T; + message_handle RAW(16); + msg SYS.AQ$_JMS_MAP_MESSAGE; +BEGIN + msg := SYS.AQ$_JMS_MAP_MESSAGE.construct; + id := msg.clear_body(-1); + msg.set_string(id, 'head', 'Hello'); + msg.set_bytes(id, 'body', UTL_RAW.CAST_TO_RAW('this is map')); + msg.set_string(id, 'tail', 'message!'); + msg.flush(id); + DBMS_AQ.ENQUEUE( + queue_name => 'FRANK.EXAMPLE_QUEUE_MAP', + enqueue_options => enqueue_options, + message_properties => message_properties, + payload => msg, + msgid => message_handle); + COMMIT; +END; \ No newline at end of file diff --git a/examples/messaging/docker/oracle-aq-18-xe/init.sql b/examples/messaging/docker/oracle-aq-18-xe/init.sql new file mode 100644 index 000000000..716c5e46d --- /dev/null +++ b/examples/messaging/docker/oracle-aq-18-xe/init.sql @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +alter session set "_ORACLE_SCRIPT"= true; +create user frank identified by frank; +grant dba to frank; + +grant execute on dbms_aq to frank; +grant execute on dbms_aqadm to frank; +grant execute on dbms_aqin to frank; + +CREATE OR REPLACE PROCEDURE create_queue(queueName IN VARCHAR2, qType IN VARCHAR2) IS +BEGIN + dbms_aqadm.create_queue_table('FRANK.'||queueName||'_TAB', qType); + dbms_aqadm.create_queue('FRANK.'||queueName,'FRANK.'||queueName||'_TAB'); + dbms_aqadm.start_queue('FRANK.'||queueName); +END; +/ + +-- Setup example AQ queues FRANK.EXAMPLE_QUEUE_1, FRANK.EXAMPLE_QUEUE_2, FRANK.EXAMPLE_QUEUE_3 +begin + CREATE_QUEUE('example_queue_1', 'SYS.AQ$_JMS_TEXT_MESSAGE'); + CREATE_QUEUE('example_queue_2', 'SYS.AQ$_JMS_TEXT_MESSAGE'); + CREATE_QUEUE('example_queue_3', 'SYS.AQ$_JMS_TEXT_MESSAGE'); + CREATE_QUEUE('example_queue_bytes', 'SYS.AQ$_JMS_BYTES_MESSAGE'); + CREATE_QUEUE('example_queue_map', 'SYS.AQ$_JMS_MAP_MESSAGE'); +end; +/ + +-- Setup example table +CREATE TABLE FRANK.MESSAGE_LOG ( + id NUMBER(15) PRIMARY KEY, + message VARCHAR2(255) NOT NULL, + insert_date DATE DEFAULT (sysdate)); +COMMENT ON TABLE FRANK.MESSAGE_LOG IS 'Manually logged messages'; + +CREATE SEQUENCE FRANK.MSG_LOG_SEQ START WITH 1; + +CREATE OR REPLACE TRIGGER MESSAGE_LOG_ID + BEFORE INSERT ON FRANK.MESSAGE_LOG + FOR EACH ROW + +BEGIN + SELECT FRANK.MSG_LOG_SEQ.NEXTVAL + INTO :new.id + FROM dual; +END; +/ diff --git a/examples/messaging/docker/oracle-aq-18-xe/stopAndClean.sh b/examples/messaging/docker/oracle-aq-18-xe/stopAndClean.sh new file mode 100755 index 000000000..4ba522541 --- /dev/null +++ b/examples/messaging/docker/oracle-aq-18-xe/stopAndClean.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +docker stop oracle-aq-example +docker container rm oracle-aq-example +docker image rm helidon/oracle-aq-example:latest \ No newline at end of file diff --git a/examples/messaging/jms-websocket-mp/README.md b/examples/messaging/jms-websocket-mp/README.md new file mode 100644 index 000000000..bb3aeaa9e --- /dev/null +++ b/examples/messaging/jms-websocket-mp/README.md @@ -0,0 +1,16 @@ +# Helidon Messaging with JMS Example + +## Prerequisites +* Java 21+ +* Docker +* [ActiveMQ server](../README.md) running on `localhost:61616` + +## Build & Run +```shell +#1. +mvn clean package +#2. + java -jar target/helidon-examples-jms-websocket-mp.jar +``` +3. Visit http://localhost:7001 + diff --git a/examples/messaging/jms-websocket-mp/pom.xml b/examples/messaging/jms-websocket-mp/pom.xml new file mode 100644 index 000000000..424b01ca3 --- /dev/null +++ b/examples/messaging/jms-websocket-mp/pom.xml @@ -0,0 +1,75 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.jms + helidon-examples-jms-websocket-mp + 1.0.0-SNAPSHOT + Helidon Examples Messaging JMS WebSocket SE + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.microprofile.messaging + helidon-microprofile-messaging + + + io.helidon.messaging.jms + helidon-messaging-jms + + + io.helidon.microprofile.websocket + helidon-microprofile-websocket + + + io.smallrye + jandex + runtime + true + + + org.apache.activemq + activemq-client + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/MsgProcessingBean.java b/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/MsgProcessingBean.java new file mode 100644 index 000000000..3c9134bfc --- /dev/null +++ b/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/MsgProcessingBean.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.mp; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.SubmissionPublisher; + +import io.helidon.common.reactive.Multi; +import io.helidon.messaging.connectors.jms.JmsMessage; + +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.eclipse.microprofile.reactive.streams.operators.ProcessorBuilder; +import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams; +import org.reactivestreams.FlowAdapters; +import org.reactivestreams.Publisher; + +/** + * Bean for message processing. + */ +@ApplicationScoped +public class MsgProcessingBean { + + private final SubmissionPublisher emitter = new SubmissionPublisher<>(); + private final SubmissionPublisher broadCaster = new SubmissionPublisher<>(); + + /** + * Create a publisher for the emitter. + * + * @return A Publisher from the emitter + */ + @Outgoing("multiplyVariants") + public Publisher preparePublisher() { + // Create new publisher for emitting to by this::process + return ReactiveStreams + .fromPublisher(FlowAdapters.toPublisher(Multi.create(emitter))) + .buildRs(); + } + + /** + * Returns a builder for a processor that maps a string into three variants. + * + * @return ProcessorBuilder + */ + @Incoming("multiplyVariants") + @Outgoing("toJms") + public ProcessorBuilder> multiply() { + // Multiply to 3 variants of same message + return ReactiveStreams.builder() + .flatMap(o -> + ReactiveStreams.of( + // upper case variant + o.toUpperCase(), + // repeat twice variant + o.repeat(2), + // reverse chars 'tnairav' + new StringBuilder(o).reverse().toString()) + ).map(Message::of); + } + + /** + * Broadcasts an event. + * + * @param msg Message to broadcast + * @return completed stage + */ + @Incoming("fromJms") + public CompletionStage broadcast(JmsMessage msg) { + // Broadcast to all subscribers + broadCaster.submit(msg.getPayload()); + return CompletableFuture.completedFuture(null); + } + + /** + * Same JMS session, different connector. + * + * @param msg Message to broadcast + * @return completed stage + */ + @Incoming("fromJmsSameSession") + public CompletionStage sameSession(JmsMessage msg) { + // Broadcast to all subscribers + broadCaster.submit(msg.getPayload()); + return CompletableFuture.completedFuture(null); + } + + /** + * Subscribe new Multi to broadcasting publisher. + * + * @return new Multi subscribed to broadcaster + */ + public Multi subscribeMulti() { + return Multi.create(broadCaster); + } + + /** + * Emit a message. + * + * @param msg message to emit + */ + public void process(final String msg) { + emitter.submit(msg); + } +} diff --git a/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/SendingResource.java b/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/SendingResource.java new file mode 100644 index 000000000..f68eaf9ca --- /dev/null +++ b/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/SendingResource.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.mp; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * Expose send method for publishing to messaging. + */ +@Path("rest/messages") +@RequestScoped +public class SendingResource { + private final MsgProcessingBean msgBean; + + /** + * Constructor injection of field values. + * + * @param msgBean Messaging example bean + */ + @Inject + public SendingResource(MsgProcessingBean msgBean) { + this.msgBean = msgBean; + } + + /** + * Send message through Messaging to JMS. + * + * @param msg message to process + */ + @Path("/send/{msg}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public void getSend(@PathParam("msg") String msg) { + msgBean.process(msg); + } +} diff --git a/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/WebSocketEndpoint.java b/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/WebSocketEndpoint.java new file mode 100644 index 000000000..ea760722b --- /dev/null +++ b/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/WebSocketEndpoint.java @@ -0,0 +1,95 @@ + +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.mp; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.reactive.Single; + +import jakarta.inject.Inject; +import jakarta.websocket.CloseReason; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +/** + * Register all WebSocket connection as subscribers + * of broadcasting {@link java.util.concurrent.SubmissionPublisher} + * in the {@link MsgProcessingBean}. + *

+ * When connection is closed, cancel subscription and remove reference. + */ +@ServerEndpoint("/ws/messages") +public class WebSocketEndpoint { + + private static final Logger LOGGER = Logger.getLogger(WebSocketEndpoint.class.getName()); + + private final Map> subscriberRegister = new HashMap<>(); + + @Inject + private MsgProcessingBean msgProcessingBean; + + /** + * On WebSocket session is opened. + * + * @param session web socket session + * @param endpointConfig endpoint config + */ + @OnOpen + public void onOpen(Session session, EndpointConfig endpointConfig) { + System.out.println("New WebSocket client connected with session " + session.getId()); + + Single single = msgProcessingBean.subscribeMulti() + // Watch for errors coming from upstream + .onError(throwable -> LOGGER.log(Level.SEVERE, "Upstream error!", throwable)) + // Send every item coming from upstream over web socket + .forEach(s -> sendTextMessage(session, s)); + + //Save forEach single promise for later cancellation + subscriberRegister.put(session.getId(), single); + } + + /** + * When WebSocket session is closed. + * + * @param session web socket session + * @param closeReason web socket close reason + */ + @OnClose + public void onClose(final Session session, final CloseReason closeReason) { + LOGGER.info("Closing session " + session.getId()); + // Properly unsubscribe from SubmissionPublisher + Optional.ofNullable(subscriberRegister.remove(session.getId())) + .ifPresent(Single::cancel); + } + + private void sendTextMessage(Session session, String msg) { + try { + session.getBasicRemote().sendText(msg); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Message sending over WebSocket failed", e); + } + } +} diff --git a/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java b/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java new file mode 100644 index 000000000..d2ddab5ba --- /dev/null +++ b/examples/messaging/jms-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java @@ -0,0 +1,21 @@ + +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Reactive Messaging JMS example. + */ +package io.helidon.examples.messaging.mp; diff --git a/examples/messaging/jms-websocket-mp/src/main/resources/META-INF/beans.xml b/examples/messaging/jms-websocket-mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..676e09a2d --- /dev/null +++ b/examples/messaging/jms-websocket-mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/messaging/jms-websocket-mp/src/main/resources/WEB/favicon.ico b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/favicon.ico new file mode 100644 index 000000000..d91659fdb Binary files /dev/null and b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/favicon.ico differ diff --git a/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/arrow-1.png b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/arrow-1.png new file mode 100644 index 000000000..bbba0aef8 Binary files /dev/null and b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/arrow-1.png differ diff --git a/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/arrow-2.png b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/arrow-2.png new file mode 100644 index 000000000..0b1096b07 Binary files /dev/null and b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/arrow-2.png differ diff --git a/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/cloud.png b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/cloud.png new file mode 100644 index 000000000..3e04833c0 Binary files /dev/null and b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/cloud.png differ diff --git a/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/frank.png b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/frank.png new file mode 100644 index 000000000..51a13d8db Binary files /dev/null and b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/img/frank.png differ diff --git a/examples/messaging/jms-websocket-mp/src/main/resources/WEB/index.html b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/index.html new file mode 100644 index 000000000..b35aed463 --- /dev/null +++ b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/index.html @@ -0,0 +1,127 @@ + + + + + + + + Helidon Reactive Messaging + + + + + + + + +

+
+
+ +
+
Send
+
+
+
+
+
REST call /rest/messages/send/{msg}
+
+
+
Messages received from JMS over websocket
+
+
+
+
+
+            
+        
+
+
+ + + + + \ No newline at end of file diff --git a/examples/messaging/jms-websocket-mp/src/main/resources/WEB/main.css b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/main.css new file mode 100644 index 000000000..8e4a7dcec --- /dev/null +++ b/examples/messaging/jms-websocket-mp/src/main/resources/WEB/main.css @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#root { + background-color: #36ABF2; + font-family: Roboto,sans-serif; + color: #fff; + position: absolute; + overflow-x: hidden; + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#root::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +#helidon { + width: 509px; + height: 273px; + position: relative; + left: -509px; + z-index: 4; + background: url('img/frank.png'); +} + +#rest-tip { + position: relative; + top: -80px; + left: 160px; +} + +#rest-tip-arrow { + width: 205px; + height: 304px; + z-index: 4; + top: -20px; + background: url('img/arrow-1.png'); +} +#rest-tip-label { + position: absolute; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; + left: -60px; +} + +#sse-tip { + position: absolute; + overflow: hidden; + display: flex; + width: auto; + height: auto; + top: 5%; + right: 10%; + z-index: 0; +} + +#sse-tip-arrow { + position: relative; + top: -30px; + width: 296px; + height: 262px; + z-index: 4; + background: url('img/arrow-2.png'); +} +#sse-tip-label { + position: relative; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; +} + +#producer { + float: left; + position: relative; + width: 300px; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 99; +} + +#msgBox { + position: absolute; + width: 300px; + top: 25%; + right: 3%; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 20; +} + +#input { + width: 210px; + height: 22px; + top: 58px; + left: 30px; + background-color: white; + border-radius: 10px; + border-style: solid; + border-color: white; + position: absolute; + z-index: 10; +} + +#inputCloud { + position: relative; + width: 310px; + height: 150px; + background: url('img/cloud.png'); +} + +#msg { + background-color: #D2EBFC; + color: #1A9BF4; + border-radius: 10px; + width: 300px; + height: 50px; + margin: 5px; + display: flex; + padding-left: 10px; + justify-content: center; + align-items: center; + z-index: 99; +} + +#submit { + font-weight: bold; + background-color: aqua; + color: #1A9BF4; + border-radius: 12px; + width: 100px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 5px; + cursor: pointer; +} + +#snippet { + position: absolute; + top: 15%; + left: 30%; + width: 40%; + z-index: 5; +} + +.hljs { + border-radius: 10px; + font-size: 12px; +} diff --git a/examples/messaging/jms-websocket-mp/src/main/resources/application.yaml b/examples/messaging/jms-websocket-mp/src/main/resources/application.yaml new file mode 100644 index 000000000..803e8862e --- /dev/null +++ b/examples/messaging/jms-websocket-mp/src/main/resources/application.yaml @@ -0,0 +1,49 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 7001 + host: 0.0.0.0 + static.classpath: + location: /WEB + welcome: index.html + +mp.messaging: + connector.helidon-jms: + jndi: + jms-factory: ConnectionFactory + env-properties: + java.naming.factory.initial: org.apache.activemq.jndi.ActiveMQInitialContextFactory + java.naming.provider.url: tcp://127.0.0.1:61616 + + outgoing: + toJms: + connector: helidon-jms + destination: messaging-queue-topic-2 + type: queue + + incoming: + fromJms: + connector: helidon-jms + destination: messaging-test-queue-1 + session-group-id: session-group-1 + type: queue + + fromJmsSameSession: + connector: helidon-jms + destination: messaging-queue-topic-2 + session-group-id: session-group-1 + type: queue diff --git a/examples/messaging/jms-websocket-mp/src/main/resources/logging.properties b/examples/messaging/jms-websocket-mp/src/main/resources/logging.properties new file mode 100644 index 000000000..361bf5f6c --- /dev/null +++ b/examples/messaging/jms-websocket-mp/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/messaging/jms-websocket-se/README.md b/examples/messaging/jms-websocket-se/README.md new file mode 100644 index 000000000..4d8eabba4 --- /dev/null +++ b/examples/messaging/jms-websocket-se/README.md @@ -0,0 +1,16 @@ +# Helidon Messaging with JMS Example + +## Prerequisites +* Java 21+ +* Docker +* [ActiveMQ server](../README.md) running on `localhost:61616` + +## Build & Run +```shell +#1. +mvn clean package +#2. +java -jar target/helidon-examples-jms-websocket-se.jar +``` +3. Visit http://localhost:7001 + diff --git a/examples/messaging/jms-websocket-se/pom.xml b/examples/messaging/jms-websocket-se/pom.xml new file mode 100644 index 000000000..80065a32e --- /dev/null +++ b/examples/messaging/jms-websocket-se/pom.xml @@ -0,0 +1,81 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.jms + helidon-examples-jms-websocket-se + 1.0.0-SNAPSHOT + Helidon Examples Messaging JMS WebSocket SE + + + io.helidon.examples.messaging.se.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-static-content + + + io.helidon.webserver + helidon-webserver-websocket + + + io.helidon.messaging + helidon-messaging + + + io.helidon.messaging.jms + helidon-messaging-jms + + + io.helidon.config + helidon-config-yaml + + + org.apache.activemq + activemq-client + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/Main.java b/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/Main.java new file mode 100644 index 000000000..2d46ed705 --- /dev/null +++ b/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/Main.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.se; + +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.LogManager; + +import io.helidon.config.Config; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.staticcontent.StaticContentService; +import io.helidon.webserver.websocket.WsRouting; + +/** + * The application main class. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + * @throws IOException if there are problems reading logging properties + */ + public static void main(final String[] args) throws IOException { + startServer(); + } + + /** + * Start the server. + * + * @return the created {@link WebServer} instance + * @throws IOException if there are problems reading logging properties + */ + static WebServer startServer() throws IOException { + // load logging configuration + setupLogging(); + + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + + SendingService sendingService = new SendingService(config); + + WebServer server = WebServer.builder() + .routing(routing -> routing + // register static content support (on "/") + .register(StaticContentService.builder("/WEB") + .welcomeFileName("index.html") + .build()) + // register rest endpoint for sending to Jms + .register("/rest/messages", sendingService)) + .addRouting(WsRouting.builder() + .endpoint("/ws/messages", new WebSocketEndpoint())) + .config(config.get("server")) + .build().start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + Runtime.getRuntime().addShutdownHook(new Thread(sendingService::shutdown)); + + // Server threads are not daemon. No need to block. Just react. + return server; + } + + /** + * Configure logging from logging.properties file. + */ + private static void setupLogging() throws IOException { + try (InputStream is = Main.class.getResourceAsStream("/logging.properties")) { + LogManager.getLogManager().readConfiguration(is); + } + } +} diff --git a/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/SendingService.java b/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/SendingService.java new file mode 100644 index 000000000..457f9c5f7 --- /dev/null +++ b/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/SendingService.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.se; + +import io.helidon.config.Config; +import io.helidon.messaging.Channel; +import io.helidon.messaging.Emitter; +import io.helidon.messaging.Messaging; +import io.helidon.messaging.connectors.jms.JmsConnector; +import io.helidon.messaging.connectors.jms.Type; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; + +import org.apache.activemq.jndi.ActiveMQInitialContextFactory; + +class SendingService implements HttpService { + + private final Emitter emitter; + private final Messaging messaging; + + SendingService(Config config) { + + String url = config.get("app.jms.url").asString().get(); + String destination = config.get("app.jms.destination").asString().get(); + + // Prepare channel for connecting processor -> jms connector with specific subscriber configuration, + // channel -> connector mapping is automatic when using JmsConnector.configBuilder() + Channel toJms = Channel.builder() + .subscriberConfig(JmsConnector.configBuilder() + .jndiInitialFactory(ActiveMQInitialContextFactory.class.getName()) + .jndiProviderUrl(url) + .type(Type.QUEUE) + .destination(destination) + .build()) + .build(); + + // Prepare channel for connecting emitter -> processor + Channel toProcessor = Channel.create(); + + // Prepare Jms connector, can be used by any channel + JmsConnector jmsConnector = JmsConnector.create(); + + // Prepare emitter for manual publishing to channel + emitter = Emitter.create(toProcessor); + + // Transforming to upper-case before sending to jms + messaging = Messaging.builder() + .emitter(emitter) + // Processor connect two channels together + .processor(toProcessor, toJms, String::toUpperCase) + .connector(jmsConnector) + .build() + .start(); + } + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + // Listen for GET /example/send/{msg} + // to send it through messaging to Jms + rules.get("/send/{msg}", (req, res) -> { + String msg = req.path().pathParameters().get("msg"); + System.out.println("Emitting: " + msg); + emitter.send(msg); + res.send(); + }); + } + + /** + * Gracefully terminate messaging. + */ + public void shutdown() { + messaging.stop(); + } +} diff --git a/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/WebSocketEndpoint.java b/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/WebSocketEndpoint.java new file mode 100644 index 000000000..4b9d0f6a1 --- /dev/null +++ b/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/WebSocketEndpoint.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.se; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.messaging.Channel; +import io.helidon.messaging.Messaging; +import io.helidon.messaging.connectors.jms.JmsConnector; +import io.helidon.messaging.connectors.jms.Type; +import io.helidon.websocket.WsListener; +import io.helidon.websocket.WsSession; + +import org.apache.activemq.jndi.ActiveMQInitialContextFactory; + +/** + * WebSocket endpoint. + */ +public class WebSocketEndpoint implements WsListener { + + private static final Logger LOGGER = Logger.getLogger(WebSocketEndpoint.class.getName()); + + private final Map messagingRegister = new HashMap<>(); + private final Config config = Config.create(); + + @Override + public void onOpen(WsSession session) { + System.out.println("Session " + session); + + String url = config.get("app.jms.url").asString().get(); + String destination = config.get("app.jms.destination").asString().get(); + + // Prepare channel for connecting jms connector with specific publisher configuration -> listener, + // channel -> connector mapping is automatic when using JmsConnector.configBuilder() + Channel fromJms = Channel.builder() + .name("from-jms") + .publisherConfig(JmsConnector.configBuilder() + .jndiInitialFactory(ActiveMQInitialContextFactory.class.getName()) + .jndiProviderUrl(url) + .type(Type.QUEUE) + .destination(destination) + .build()) + .build(); + + // Prepare Jms connector, can be used by any channel + JmsConnector jmsConnector = JmsConnector.create(); + + Messaging messaging = Messaging.builder() + .connector(jmsConnector) + .listener(fromJms, payload -> { + System.out.println("Jms says: " + payload); + session.send(payload, false); + }) + .build() + .start(); + + //Save the messaging instance for proper shutdown + // when websocket connection is terminated + messagingRegister.put(session, messaging); + } + + @Override + public void onClose(WsSession session, int status, String reason) { + LOGGER.info("Closing session " + session); + // Properly stop messaging when websocket connection is terminated + Optional.ofNullable(messagingRegister.remove(session)) + .ifPresent(Messaging::stop); + } +} diff --git a/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/package-info.java b/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/package-info.java new file mode 100644 index 000000000..0dca31628 --- /dev/null +++ b/examples/messaging/jms-websocket-se/src/main/java/io/helidon/examples/messaging/se/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon SE Reactive Messaging with Jms Example. + */ +package io.helidon.examples.messaging.se; diff --git a/examples/messaging/jms-websocket-se/src/main/resources/WEB/favicon.ico b/examples/messaging/jms-websocket-se/src/main/resources/WEB/favicon.ico new file mode 100644 index 000000000..d91659fdb Binary files /dev/null and b/examples/messaging/jms-websocket-se/src/main/resources/WEB/favicon.ico differ diff --git a/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/arrow-1.png b/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/arrow-1.png new file mode 100644 index 000000000..bbba0aef8 Binary files /dev/null and b/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/arrow-1.png differ diff --git a/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/arrow-2.png b/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/arrow-2.png new file mode 100644 index 000000000..0b1096b07 Binary files /dev/null and b/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/arrow-2.png differ diff --git a/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/cloud.png b/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/cloud.png new file mode 100644 index 000000000..3e04833c0 Binary files /dev/null and b/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/cloud.png differ diff --git a/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/frank.png b/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/frank.png new file mode 100644 index 000000000..51a13d8db Binary files /dev/null and b/examples/messaging/jms-websocket-se/src/main/resources/WEB/img/frank.png differ diff --git a/examples/messaging/jms-websocket-se/src/main/resources/WEB/index.html b/examples/messaging/jms-websocket-se/src/main/resources/WEB/index.html new file mode 100644 index 000000000..94e417ab6 --- /dev/null +++ b/examples/messaging/jms-websocket-se/src/main/resources/WEB/index.html @@ -0,0 +1,127 @@ + + + + + + + + Helidon Reactive Messaging + + + + + + + + +
+
+
+ +
+
Send
+
+
+
+
+
REST call /rest/messages/send/{msg}
+
+
+
Messages received from Jms over websocket
+
+
+
+
+
+            
+        
+
+
+ + + + + \ No newline at end of file diff --git a/examples/messaging/jms-websocket-se/src/main/resources/WEB/main.css b/examples/messaging/jms-websocket-se/src/main/resources/WEB/main.css new file mode 100644 index 000000000..8e4a7dcec --- /dev/null +++ b/examples/messaging/jms-websocket-se/src/main/resources/WEB/main.css @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#root { + background-color: #36ABF2; + font-family: Roboto,sans-serif; + color: #fff; + position: absolute; + overflow-x: hidden; + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#root::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +#helidon { + width: 509px; + height: 273px; + position: relative; + left: -509px; + z-index: 4; + background: url('img/frank.png'); +} + +#rest-tip { + position: relative; + top: -80px; + left: 160px; +} + +#rest-tip-arrow { + width: 205px; + height: 304px; + z-index: 4; + top: -20px; + background: url('img/arrow-1.png'); +} +#rest-tip-label { + position: absolute; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; + left: -60px; +} + +#sse-tip { + position: absolute; + overflow: hidden; + display: flex; + width: auto; + height: auto; + top: 5%; + right: 10%; + z-index: 0; +} + +#sse-tip-arrow { + position: relative; + top: -30px; + width: 296px; + height: 262px; + z-index: 4; + background: url('img/arrow-2.png'); +} +#sse-tip-label { + position: relative; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; +} + +#producer { + float: left; + position: relative; + width: 300px; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 99; +} + +#msgBox { + position: absolute; + width: 300px; + top: 25%; + right: 3%; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 20; +} + +#input { + width: 210px; + height: 22px; + top: 58px; + left: 30px; + background-color: white; + border-radius: 10px; + border-style: solid; + border-color: white; + position: absolute; + z-index: 10; +} + +#inputCloud { + position: relative; + width: 310px; + height: 150px; + background: url('img/cloud.png'); +} + +#msg { + background-color: #D2EBFC; + color: #1A9BF4; + border-radius: 10px; + width: 300px; + height: 50px; + margin: 5px; + display: flex; + padding-left: 10px; + justify-content: center; + align-items: center; + z-index: 99; +} + +#submit { + font-weight: bold; + background-color: aqua; + color: #1A9BF4; + border-radius: 12px; + width: 100px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 5px; + cursor: pointer; +} + +#snippet { + position: absolute; + top: 15%; + left: 30%; + width: 40%; + z-index: 5; +} + +.hljs { + border-radius: 10px; + font-size: 12px; +} diff --git a/examples/messaging/jms-websocket-se/src/main/resources/application.yaml b/examples/messaging/jms-websocket-se/src/main/resources/application.yaml new file mode 100644 index 000000000..38d4767c5 --- /dev/null +++ b/examples/messaging/jms-websocket-se/src/main/resources/application.yaml @@ -0,0 +1,28 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + jms: + url: tcp://127.0.0.1:61616 + destination: se-example-queue-1 + +server: + port: 7001 + host: 0.0.0.0 + static: + classpath: + location: /WEB + welcome: index.html diff --git a/examples/messaging/jms-websocket-se/src/main/resources/logging.properties b/examples/messaging/jms-websocket-se/src/main/resources/logging.properties new file mode 100644 index 000000000..361bf5f6c --- /dev/null +++ b/examples/messaging/jms-websocket-se/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/messaging/kafka-websocket-mp/README.md b/examples/messaging/kafka-websocket-mp/README.md new file mode 100644 index 000000000..75269ca5c --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/README.md @@ -0,0 +1,15 @@ +# Helidon MP Reactive Messaging with Kafka Example + +## Prerequisites +* Docker +* Java 21+ +* [Kafka bootstrap server](../README.md) running on `localhost:9092` + +## Build & Run +```shell +#1. +mvn clean package +#2. +java -jar target/kafka-websocket-mp.jar +``` +3. Visit http://localhost:7001 \ No newline at end of file diff --git a/examples/messaging/kafka-websocket-mp/pom.xml b/examples/messaging/kafka-websocket-mp/pom.xml new file mode 100644 index 000000000..e21075aa4 --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/pom.xml @@ -0,0 +1,80 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.messaging.mp + kafka-websocket-mp + 1.0.0-SNAPSHOT + Helidon Examples Messaging Kafka WebSocket MP + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.microprofile.messaging + helidon-microprofile-messaging + + + io.helidon.messaging.kafka + helidon-messaging-kafka + + + io.helidon.microprofile.websocket + helidon-microprofile-websocket + + + io.smallrye + jandex + runtime + true + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/MsgProcessingBean.java b/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/MsgProcessingBean.java new file mode 100644 index 000000000..6ed4d2c01 --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/MsgProcessingBean.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.mp; + +import java.util.concurrent.SubmissionPublisher; + +import io.helidon.common.reactive.Multi; + +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.eclipse.microprofile.reactive.streams.operators.ProcessorBuilder; +import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams; +import org.reactivestreams.FlowAdapters; +import org.reactivestreams.Publisher; + +/** + * Bean for message processing. + */ +@ApplicationScoped +public class MsgProcessingBean { + + private final SubmissionPublisher emitter = new SubmissionPublisher<>(); + private final SubmissionPublisher broadCaster = new SubmissionPublisher<>(); + + /** + * Create a publisher for the emitter. + * + * @return A Publisher from the emitter + */ + @Outgoing("multiplyVariants") + public Publisher preparePublisher() { + // Create new publisher for emitting to by this::process + return ReactiveStreams + .fromPublisher(FlowAdapters.toPublisher(Multi.create(emitter))) + .buildRs(); + } + + /** + * Returns a builder for a processor that maps a string into three variants. + * + * @return ProcessorBuilder + */ + @Incoming("multiplyVariants") + @Outgoing("toKafka") + public ProcessorBuilder> multiply() { + // Multiply to 3 variants of same message + return ReactiveStreams.builder() + .flatMap(o -> + ReactiveStreams.of( + // upper case variant + o.toUpperCase(), + // repeat twice variant + o.repeat(2), + // reverse chars 'tnairav' + new StringBuilder(o).reverse().toString()) + ).map(Message::of); + } + + /** + * Broadcasts an event. + * + * @param msg Message to broadcast + */ + @Incoming("fromKafka") + public void broadcast(String msg) { + // Broadcast to all subscribers + broadCaster.submit(msg); + } + + /** + * Subscribe new Multi to broadcasting publisher. + * + * @return new Multi subscribed to broadcaster + */ + public Multi subscribeMulti() { + return Multi.create(broadCaster); + } + + /** + * Emit a message. + * + * @param msg message to emit + */ + public void process(final String msg) { + emitter.submit(msg); + } +} diff --git a/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/SendingResource.java b/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/SendingResource.java new file mode 100644 index 000000000..78ec04ee7 --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/SendingResource.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.mp; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * Expose send method for publishing to messaging. + */ +@Path("rest/messages") +@RequestScoped +public class SendingResource { + private final MsgProcessingBean msgBean; + + /** + * Constructor injection of field values. + * + * @param msgBean Messaging example bean + */ + @Inject + public SendingResource(MsgProcessingBean msgBean) { + this.msgBean = msgBean; + } + + + /** + * Send message through Messaging to Kafka. + * + * @param msg message to process + */ + @Path("/send/{msg}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public void getSend(@PathParam("msg") String msg) { + msgBean.process(msg); + } +} diff --git a/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/WebSocketEndpoint.java b/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/WebSocketEndpoint.java new file mode 100644 index 000000000..ea760722b --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/WebSocketEndpoint.java @@ -0,0 +1,95 @@ + +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.mp; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.reactive.Single; + +import jakarta.inject.Inject; +import jakarta.websocket.CloseReason; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +/** + * Register all WebSocket connection as subscribers + * of broadcasting {@link java.util.concurrent.SubmissionPublisher} + * in the {@link MsgProcessingBean}. + *

+ * When connection is closed, cancel subscription and remove reference. + */ +@ServerEndpoint("/ws/messages") +public class WebSocketEndpoint { + + private static final Logger LOGGER = Logger.getLogger(WebSocketEndpoint.class.getName()); + + private final Map> subscriberRegister = new HashMap<>(); + + @Inject + private MsgProcessingBean msgProcessingBean; + + /** + * On WebSocket session is opened. + * + * @param session web socket session + * @param endpointConfig endpoint config + */ + @OnOpen + public void onOpen(Session session, EndpointConfig endpointConfig) { + System.out.println("New WebSocket client connected with session " + session.getId()); + + Single single = msgProcessingBean.subscribeMulti() + // Watch for errors coming from upstream + .onError(throwable -> LOGGER.log(Level.SEVERE, "Upstream error!", throwable)) + // Send every item coming from upstream over web socket + .forEach(s -> sendTextMessage(session, s)); + + //Save forEach single promise for later cancellation + subscriberRegister.put(session.getId(), single); + } + + /** + * When WebSocket session is closed. + * + * @param session web socket session + * @param closeReason web socket close reason + */ + @OnClose + public void onClose(final Session session, final CloseReason closeReason) { + LOGGER.info("Closing session " + session.getId()); + // Properly unsubscribe from SubmissionPublisher + Optional.ofNullable(subscriberRegister.remove(session.getId())) + .ifPresent(Single::cancel); + } + + private void sendTextMessage(Session session, String msg) { + try { + session.getBasicRemote().sendText(msg); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Message sending over WebSocket failed", e); + } + } +} diff --git a/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java b/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java new file mode 100644 index 000000000..30fb29b39 --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon MP Reactive Messaging with Kafka Example. + */ +package io.helidon.examples.messaging.mp; diff --git a/examples/messaging/kafka-websocket-mp/src/main/resources/META-INF/beans.xml b/examples/messaging/kafka-websocket-mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..676e09a2d --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/messaging/kafka-websocket-mp/src/main/resources/META-INF/microprofile-config.properties b/examples/messaging/kafka-websocket-mp/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..ff628b571 --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,36 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server.port=7001 +server.host=0.0.0.0 +server.static.classpath.location=/WEB +server.static.classpath.welcome=index.html + +# Configure channel fromKafka to ask Kafka connector for publisher +mp.messaging.incoming.fromKafka.connector=helidon-kafka +mp.messaging.incoming.fromKafka.enable.auto.commit=true +mp.messaging.incoming.fromKafka.group.id=websocket-mp-example-1 + +# Configure channel toKafka to ask Kafka connector for subscriber +mp.messaging.outgoing.toKafka.connector=helidon-kafka + +# Connector config properties are common to all channels +mp.messaging.connector.helidon-kafka.bootstrap.servers=localhost:9092 +mp.messaging.connector.helidon-kafka.topic=messaging-test-topic-1 +mp.messaging.connector.helidon-kafka.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer +mp.messaging.connector.helidon-kafka.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer +mp.messaging.connector.helidon-kafka.key.serializer=org.apache.kafka.common.serialization.StringSerializer +mp.messaging.connector.helidon-kafka.value.serializer=org.apache.kafka.common.serialization.StringSerializer diff --git a/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/favicon.ico b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/favicon.ico new file mode 100644 index 000000000..d91659fdb Binary files /dev/null and b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/favicon.ico differ diff --git a/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/arrow-1.png b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/arrow-1.png new file mode 100644 index 000000000..bbba0aef8 Binary files /dev/null and b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/arrow-1.png differ diff --git a/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/arrow-2.png b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/arrow-2.png new file mode 100644 index 000000000..0b1096b07 Binary files /dev/null and b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/arrow-2.png differ diff --git a/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/cloud.png b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/cloud.png new file mode 100644 index 000000000..3e04833c0 Binary files /dev/null and b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/cloud.png differ diff --git a/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/frank.png b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/frank.png new file mode 100644 index 000000000..51a13d8db Binary files /dev/null and b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/img/frank.png differ diff --git a/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/index.html b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/index.html new file mode 100644 index 000000000..de0d967fc --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/index.html @@ -0,0 +1,127 @@ + + + + + + + + Helidon Reactive Messaging + + + + + + + + +

+
+
+ +
+
Send
+
+
+
+
+
REST call /rest/messages/send/{msg}
+
+
+
Messages received from Kafka over websocket
+
+
+
+
+
+            
+        
+
+
+ + + + + \ No newline at end of file diff --git a/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/main.css b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/main.css new file mode 100644 index 000000000..8e4a7dcec --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/src/main/resources/WEB/main.css @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#root { + background-color: #36ABF2; + font-family: Roboto,sans-serif; + color: #fff; + position: absolute; + overflow-x: hidden; + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#root::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +#helidon { + width: 509px; + height: 273px; + position: relative; + left: -509px; + z-index: 4; + background: url('img/frank.png'); +} + +#rest-tip { + position: relative; + top: -80px; + left: 160px; +} + +#rest-tip-arrow { + width: 205px; + height: 304px; + z-index: 4; + top: -20px; + background: url('img/arrow-1.png'); +} +#rest-tip-label { + position: absolute; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; + left: -60px; +} + +#sse-tip { + position: absolute; + overflow: hidden; + display: flex; + width: auto; + height: auto; + top: 5%; + right: 10%; + z-index: 0; +} + +#sse-tip-arrow { + position: relative; + top: -30px; + width: 296px; + height: 262px; + z-index: 4; + background: url('img/arrow-2.png'); +} +#sse-tip-label { + position: relative; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; +} + +#producer { + float: left; + position: relative; + width: 300px; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 99; +} + +#msgBox { + position: absolute; + width: 300px; + top: 25%; + right: 3%; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 20; +} + +#input { + width: 210px; + height: 22px; + top: 58px; + left: 30px; + background-color: white; + border-radius: 10px; + border-style: solid; + border-color: white; + position: absolute; + z-index: 10; +} + +#inputCloud { + position: relative; + width: 310px; + height: 150px; + background: url('img/cloud.png'); +} + +#msg { + background-color: #D2EBFC; + color: #1A9BF4; + border-radius: 10px; + width: 300px; + height: 50px; + margin: 5px; + display: flex; + padding-left: 10px; + justify-content: center; + align-items: center; + z-index: 99; +} + +#submit { + font-weight: bold; + background-color: aqua; + color: #1A9BF4; + border-radius: 12px; + width: 100px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 5px; + cursor: pointer; +} + +#snippet { + position: absolute; + top: 15%; + left: 30%; + width: 40%; + z-index: 5; +} + +.hljs { + border-radius: 10px; + font-size: 12px; +} diff --git a/examples/messaging/kafka-websocket-mp/src/main/resources/logging.properties b/examples/messaging/kafka-websocket-mp/src/main/resources/logging.properties new file mode 100644 index 000000000..b5fc4d06e --- /dev/null +++ b/examples/messaging/kafka-websocket-mp/src/main/resources/logging.properties @@ -0,0 +1,32 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Kafka client has exhaustive logs +org.apache.kafka.clients.level=WARNING +org.apache.kafka.clients.consumer.ConsumerConfig.level=SEVERE +org.apache.kafka.clients.producer.ProducerConfig.level=SEVERE diff --git a/examples/messaging/kafka-websocket-se/README.md b/examples/messaging/kafka-websocket-se/README.md new file mode 100644 index 000000000..c3e397add --- /dev/null +++ b/examples/messaging/kafka-websocket-se/README.md @@ -0,0 +1,16 @@ +# Helidon Messaging with Kafka Examples + +## Prerequisites +* Java 21+ +* Docker +* [Kafka bootstrap server](../README.md) running on `localhost:9092` + +## Build & Run +```shell +#1. +mvn clean package +#2. +java -jar target/kafka-websocket-se.jar +``` +3. Visit http://localhost:7001 + diff --git a/examples/messaging/kafka-websocket-se/pom.xml b/examples/messaging/kafka-websocket-se/pom.xml new file mode 100644 index 000000000..b81278ec6 --- /dev/null +++ b/examples/messaging/kafka-websocket-se/pom.xml @@ -0,0 +1,77 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.messaging.se + kafka-websocket-se + 1.0.0-SNAPSHOT + Helidon Examples Messaging Kafka WebSocket MP + + + io.helidon.examples.messaging.se.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-static-content + + + io.helidon.webserver + helidon-webserver-websocket + + + io.helidon.messaging + helidon-messaging + + + io.helidon.messaging.kafka + helidon-messaging-kafka + + + io.helidon.config + helidon-config-yaml + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/Main.java b/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/Main.java new file mode 100644 index 000000000..cdc3441db --- /dev/null +++ b/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/Main.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.se; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.staticcontent.StaticContentService; +import io.helidon.webserver.websocket.WsRouting; + +/** + * The application main class. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(final String[] args) { + startServer(); + } + + /** + * Start the server. + * + * @return the created {@link WebServer} instance + */ + static WebServer startServer() { + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + + SendingService sendingService = new SendingService(config); + + WebServer server = + WebServer.builder() + .routing(r -> r + // register static content support (on "/") + .register(StaticContentService.builder("/WEB") + .welcomeFileName("index.html") + .build()) + // register rest endpoint for sending to Kafka + .register("/rest/messages", sendingService)) + // register WebSocket endpoint to push messages coming from Kafka to client + .addRouting(WsRouting.builder() + .endpoint("/ws/messages", new WebSocketEndpoint())) + .config(config.get("server")) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + Runtime.getRuntime().addShutdownHook(new Thread(sendingService::shutdown)); + + // Server threads are not daemon. No need to block. Just react. + return server; + } +} diff --git a/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/SendingService.java b/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/SendingService.java new file mode 100644 index 000000000..a7770b3e2 --- /dev/null +++ b/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/SendingService.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.se; + +import io.helidon.config.Config; +import io.helidon.messaging.Channel; +import io.helidon.messaging.Emitter; +import io.helidon.messaging.Messaging; +import io.helidon.messaging.connectors.kafka.KafkaConnector; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; + +import org.apache.kafka.common.serialization.StringSerializer; + +class SendingService implements HttpService { + + private final Emitter emitter; + private final Messaging messaging; + + SendingService(Config config) { + + String kafkaServer = config.get("app.kafka.bootstrap.servers").asString().get(); + String topic = config.get("app.kafka.topic").asString().get(); + String compression = config.get("app.kafka.compression").asString().orElse("none"); + + // Prepare channel for connecting processor -> kafka connector with specific subscriber configuration, + // channel -> connector mapping is automatic when using KafkaConnector.configBuilder() + Channel toKafka = Channel.builder() + .subscriberConfig(KafkaConnector.configBuilder() + .bootstrapServers(kafkaServer) + .topic(topic) + .compressionType(compression) + .keySerializer(StringSerializer.class) + .valueSerializer(StringSerializer.class) + .build()) + .build(); + + // Prepare channel for connecting emitter -> processor + Channel toProcessor = Channel.create(); + + // Prepare Kafka connector, can be used by any channel + KafkaConnector kafkaConnector = KafkaConnector.create(); + + // Prepare emitter for manual publishing to channel + emitter = Emitter.create(toProcessor); + + // Transforming to upper-case before sending to kafka + messaging = Messaging.builder() + .emitter(emitter) + // Processor connect two channels together + .processor(toProcessor, toKafka, String::toUpperCase) + .connector(kafkaConnector) + .build() + .start(); + } + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + // Listen for GET /example/send/{msg} + // to send it thru messaging to Kafka + rules.get("/send/{msg}", (req, res) -> { + String msg = req.path().pathParameters().get("msg"); + System.out.println("Emitting: " + msg); + emitter.send(msg); + res.send(); + }); + } + + /** + * Gracefully terminate messaging. + */ + public void shutdown() { + messaging.stop(); + } +} diff --git a/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/WebSocketEndpoint.java b/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/WebSocketEndpoint.java new file mode 100644 index 000000000..9e02c5f27 --- /dev/null +++ b/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/WebSocketEndpoint.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.se; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.messaging.Channel; +import io.helidon.messaging.Messaging; +import io.helidon.messaging.connectors.kafka.KafkaConfigBuilder; +import io.helidon.messaging.connectors.kafka.KafkaConnector; +import io.helidon.websocket.WsListener; +import io.helidon.websocket.WsSession; + +import org.apache.kafka.common.serialization.StringDeserializer; + +/** + * Web socket endpoint. + */ +public class WebSocketEndpoint implements WsListener { + + private static final Logger LOGGER = Logger.getLogger(WebSocketEndpoint.class.getName()); + + private final Map messagingRegister = new HashMap<>(); + private final Config config = Config.create(); + + @Override + public void onOpen(WsSession session) { + System.out.println("Session " + session); + + String kafkaServer = config.get("app.kafka.bootstrap.servers").asString().get(); + String topic = config.get("app.kafka.topic").asString().get(); + String compression = config.get("app.kafka.compression").asString().orElse("none"); + + // Prepare channel for connecting kafka connector with specific publisher configuration -> listener, + // channel -> connector mapping is automatic when using KafkaConnector.configBuilder() + Channel fromKafka = Channel.builder() + .name("from-kafka") + .publisherConfig(KafkaConnector.configBuilder() + .bootstrapServers(kafkaServer) + .groupId("example-group-" + session) + .topic(topic) + .autoOffsetReset(KafkaConfigBuilder.AutoOffsetReset.LATEST) + .enableAutoCommit(true) + .keyDeserializer(StringDeserializer.class) + .valueDeserializer(StringDeserializer.class) + .compressionType(compression) + .build() + ) + .build(); + + // Prepare Kafka connector, can be used by any channel + KafkaConnector kafkaConnector = KafkaConnector.create(); + + Messaging messaging = Messaging.builder() + .connector(kafkaConnector) + .listener(fromKafka, payload -> { + System.out.println("Kafka says: " + payload); + // Send message received from Kafka over websocket + session.send(payload, true); + }) + .build() + .start(); + + //Save the messaging instance for proper shutdown + // when websocket connection is terminated + messagingRegister.put(session, messaging); + } + + @Override + public void onClose(WsSession session, int status, String reason) { + LOGGER.info("Closing session " + session); + // Properly stop messaging when websocket connection is terminated + Optional.ofNullable(messagingRegister.remove(session)) + .ifPresent(Messaging::stop); + } +} diff --git a/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/package-info.java b/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/package-info.java new file mode 100644 index 000000000..c4771f973 --- /dev/null +++ b/examples/messaging/kafka-websocket-se/src/main/java/io/helidon/examples/messaging/se/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon SE Reactive Messaging with Kafka Example. + */ +package io.helidon.examples.messaging.se; diff --git a/examples/messaging/kafka-websocket-se/src/main/resources/WEB/favicon.ico b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/favicon.ico new file mode 100644 index 000000000..d91659fdb Binary files /dev/null and b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/favicon.ico differ diff --git a/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/arrow-1.png b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/arrow-1.png new file mode 100644 index 000000000..bbba0aef8 Binary files /dev/null and b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/arrow-1.png differ diff --git a/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/arrow-2.png b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/arrow-2.png new file mode 100644 index 000000000..0b1096b07 Binary files /dev/null and b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/arrow-2.png differ diff --git a/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/cloud.png b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/cloud.png new file mode 100644 index 000000000..3e04833c0 Binary files /dev/null and b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/cloud.png differ diff --git a/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/frank.png b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/frank.png new file mode 100644 index 000000000..51a13d8db Binary files /dev/null and b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/img/frank.png differ diff --git a/examples/messaging/kafka-websocket-se/src/main/resources/WEB/index.html b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/index.html new file mode 100644 index 000000000..de0d967fc --- /dev/null +++ b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/index.html @@ -0,0 +1,127 @@ + + + + + + + + Helidon Reactive Messaging + + + + + + + + +
+
+
+ +
+
Send
+
+
+
+
+
REST call /rest/messages/send/{msg}
+
+
+
Messages received from Kafka over websocket
+
+
+
+
+
+            
+        
+
+
+ + + + + \ No newline at end of file diff --git a/examples/messaging/kafka-websocket-se/src/main/resources/WEB/main.css b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/main.css new file mode 100644 index 000000000..8e4a7dcec --- /dev/null +++ b/examples/messaging/kafka-websocket-se/src/main/resources/WEB/main.css @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#root { + background-color: #36ABF2; + font-family: Roboto,sans-serif; + color: #fff; + position: absolute; + overflow-x: hidden; + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#root::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +#helidon { + width: 509px; + height: 273px; + position: relative; + left: -509px; + z-index: 4; + background: url('img/frank.png'); +} + +#rest-tip { + position: relative; + top: -80px; + left: 160px; +} + +#rest-tip-arrow { + width: 205px; + height: 304px; + z-index: 4; + top: -20px; + background: url('img/arrow-1.png'); +} +#rest-tip-label { + position: absolute; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; + left: -60px; +} + +#sse-tip { + position: absolute; + overflow: hidden; + display: flex; + width: auto; + height: auto; + top: 5%; + right: 10%; + z-index: 0; +} + +#sse-tip-arrow { + position: relative; + top: -30px; + width: 296px; + height: 262px; + z-index: 4; + background: url('img/arrow-2.png'); +} +#sse-tip-label { + position: relative; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; +} + +#producer { + float: left; + position: relative; + width: 300px; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 99; +} + +#msgBox { + position: absolute; + width: 300px; + top: 25%; + right: 3%; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 20; +} + +#input { + width: 210px; + height: 22px; + top: 58px; + left: 30px; + background-color: white; + border-radius: 10px; + border-style: solid; + border-color: white; + position: absolute; + z-index: 10; +} + +#inputCloud { + position: relative; + width: 310px; + height: 150px; + background: url('img/cloud.png'); +} + +#msg { + background-color: #D2EBFC; + color: #1A9BF4; + border-radius: 10px; + width: 300px; + height: 50px; + margin: 5px; + display: flex; + padding-left: 10px; + justify-content: center; + align-items: center; + z-index: 99; +} + +#submit { + font-weight: bold; + background-color: aqua; + color: #1A9BF4; + border-radius: 12px; + width: 100px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 5px; + cursor: pointer; +} + +#snippet { + position: absolute; + top: 15%; + left: 30%; + width: 40%; + z-index: 5; +} + +.hljs { + border-radius: 10px; + font-size: 12px; +} diff --git a/examples/messaging/kafka-websocket-se/src/main/resources/application.yaml b/examples/messaging/kafka-websocket-se/src/main/resources/application.yaml new file mode 100644 index 000000000..4738e7d15 --- /dev/null +++ b/examples/messaging/kafka-websocket-se/src/main/resources/application.yaml @@ -0,0 +1,32 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + kafka: + bootstrap.servers: localhost:9092 + compression: snappy +# compression: lz4 +# compression: zstd +# compression: gzip + topic: messaging-test-topic-${app.kafka.compression}-compressed + +server: + port: 7001 + host: 0.0.0.0 + static: + classpath: + location: /WEB + welcome: index.html diff --git a/examples/messaging/kafka-websocket-se/src/main/resources/logging.properties b/examples/messaging/kafka-websocket-se/src/main/resources/logging.properties new file mode 100644 index 000000000..361bf5f6c --- /dev/null +++ b/examples/messaging/kafka-websocket-se/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/messaging/kafkaConsume.sh b/examples/messaging/kafkaConsume.sh new file mode 100755 index 000000000..27ef1b236 --- /dev/null +++ b/examples/messaging/kafkaConsume.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +TOPIC="messaging-test-topic-1" +SERVER="localhost:9092" +CONTAINER="helidon_kafka" + +if [ -z "$1" ]; then + echo "No argument supplied, defaulting to topic ${TOPIC}" +else + TOPIC="$1" +fi + +docker exec -it ${CONTAINER} sh -c \ +"/opt/kafka/bin/kafka-console-consumer.sh --topic ${TOPIC} --bootstrap-server ${SERVER}" diff --git a/examples/messaging/kafkaProduce.sh b/examples/messaging/kafkaProduce.sh new file mode 100755 index 000000000..6da1e3e98 --- /dev/null +++ b/examples/messaging/kafkaProduce.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +TOPIC="messaging-test-topic-1" +SERVER="localhost:9092" +CONTAINER="helidon_kafka" + +if [ -z "$1" ]; then + echo "No argument supplied, defaulting to topic ${TOPIC}" +else + TOPIC="$1" +fi + +docker exec -it ${CONTAINER} sh -c \ +"/opt/kafka/bin/kafka-console-producer.sh --topic ${TOPIC} --bootstrap-server ${SERVER}" diff --git a/examples/messaging/kafkaRun.sh b/examples/messaging/kafkaRun.sh new file mode 100755 index 000000000..e40e3f4a2 --- /dev/null +++ b/examples/messaging/kafkaRun.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +if [[ "$(docker images -q helidon-test-kafka 2>/dev/null)" == "" ]]; then + # helidon:test-kafka not found, build it + docker build ./docker/kafka -t helidon-test-kafka -f ./docker/kafka/Dockerfile.kafka +fi + +if [ ! "$(docker ps -q -f name=helidon_kafka)" ]; then + if [ "$(docker ps -aq -f status=exited -f name=helidon_kafka)" ]; then + # Clean up exited container + docker rm helidon_kafka + fi + # Run test Kafka in new container, stop it by pressing Ctrl+C + docker run -it \ + --rm \ + --publish 2181:2181 \ + --publish 29092:9092 \ + --publish 9092:29092 \ + --name helidon_kafka \ + helidon-test-kafka +fi diff --git a/examples/messaging/oracle-aq-websocket-mp/README.md b/examples/messaging/oracle-aq-websocket-mp/README.md new file mode 100644 index 000000000..af31df6f5 --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/README.md @@ -0,0 +1,16 @@ +# Helidon Messaging with Oracle AQ Example + +## Prerequisites +* Java 21+ +* Docker +* [Oracle database](../README.md) running on `localhost:1521` + +## Build & Run +```shell +#1. + mvn clean package +#2. + java -jar target/aq-websocket-mp.jar +``` +3. Visit http://localhost:7001 + diff --git a/examples/messaging/oracle-aq-websocket-mp/pom.xml b/examples/messaging/oracle-aq-websocket-mp/pom.xml new file mode 100644 index 000000000..c4fe0f3d0 --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/pom.xml @@ -0,0 +1,76 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.messaging.aq + aq-websocket-mp + 1.0.0-SNAPSHOT + Helidon Examples Messaging Oracle AQ WebSocket MP + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.microprofile.messaging + helidon-microprofile-messaging + + + io.helidon.messaging.aq + helidon-messaging-aq + + + io.helidon.integrations.cdi + helidon-integrations-cdi-datasource-ucp + runtime + + + io.helidon.microprofile.websocket + helidon-microprofile-websocket + + + io.smallrye + jandex + runtime + true + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/MsgProcessingBean.java b/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/MsgProcessingBean.java new file mode 100644 index 000000000..ff56228f0 --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/MsgProcessingBean.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.mp; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.SubmissionPublisher; + +import io.helidon.common.reactive.BufferedEmittingPublisher; +import io.helidon.common.reactive.Multi; +import io.helidon.messaging.connectors.aq.AqMessage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.jms.JMSException; +import jakarta.jms.MapMessage; +import org.eclipse.microprofile.reactive.messaging.Acknowledgment; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams; +import org.reactivestreams.FlowAdapters; +import org.reactivestreams.Publisher; + +/** + * Bean for message processing. + */ +@ApplicationScoped +public class MsgProcessingBean { + + private final BufferedEmittingPublisher emitter = BufferedEmittingPublisher.create(); + private final SubmissionPublisher broadCaster = new SubmissionPublisher<>(); + + /** + * Create a publisher for the emitter. + * + * @return A Publisher from the emitter + */ + @Outgoing("to-queue-1") + public Publisher toFirstQueue() { + // Create new publisher for emitting to by this::process + return ReactiveStreams + .fromPublisher(FlowAdapters.toPublisher(emitter)) + .buildRs(); + } + + /** + * Example of resending message from one queue to another and logging the payload to DB in the process. + * + * @param msg received message + * @return message to be sent + */ + @Incoming("from-queue-1") + @Outgoing("to-queue-2") + //Leave commit by ack to outgoing connector + @Acknowledgment(Acknowledgment.Strategy.NONE) + public CompletionStage> betweenQueues(AqMessage msg) { + return CompletableFuture.supplyAsync(() -> { + try { + PreparedStatement statement = msg.getDbConnection() + .prepareStatement("INSERT INTO frank.message_log (message) VALUES (?)"); + statement.setString(1, msg.getPayload()); + statement.executeUpdate(); + } catch (SQLException e) { + //Gets caught by messaging engine and translated to onError signal + throw new RuntimeException("Error when saving message to log table.", e); + } + return msg; + }); + } + + /** + * Broadcasts an event. + * + * @param msg Message to broadcast + */ + @Incoming("from-queue-2") + public void fromSecondQueue(AqMessage msg) { + // Broadcast to all subscribers + broadCaster.submit(msg.getPayload()); + } + + /** + * Example of receiving a byte message. + * + * @param bytes received byte array + */ + @Incoming("from-byte-queue") + public void fromByteQueue(byte[] bytes) { + broadCaster.submit(new String(bytes)); + } + + /** + * Example of receiving a map message. + * + * @param msg received JMS MapMessage + * @throws JMSException when error arises during work with JMS message + */ + @Incoming("from-map-queue") + public void fromMapQueue(MapMessage msg) throws JMSException { + String head = msg.getString("head"); + byte[] body = msg.getBytes("body"); + String tail = msg.getString("tail"); + broadCaster.submit(String.join(" ", List.of(head, new String(body), tail))); + } + + Multi subscribeMulti() { + return Multi.create(broadCaster).log(); + } + + void process(final String msg) { + emitter.emit(msg); + } +} diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/SendingResource.java b/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/SendingResource.java new file mode 100644 index 000000000..f68eaf9ca --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/SendingResource.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.mp; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * Expose send method for publishing to messaging. + */ +@Path("rest/messages") +@RequestScoped +public class SendingResource { + private final MsgProcessingBean msgBean; + + /** + * Constructor injection of field values. + * + * @param msgBean Messaging example bean + */ + @Inject + public SendingResource(MsgProcessingBean msgBean) { + this.msgBean = msgBean; + } + + /** + * Send message through Messaging to JMS. + * + * @param msg message to process + */ + @Path("/send/{msg}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public void getSend(@PathParam("msg") String msg) { + msgBean.process(msg); + } +} diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/WebSocketEndpoint.java b/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/WebSocketEndpoint.java new file mode 100644 index 000000000..ea760722b --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/WebSocketEndpoint.java @@ -0,0 +1,95 @@ + +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.messaging.mp; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.reactive.Single; + +import jakarta.inject.Inject; +import jakarta.websocket.CloseReason; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +/** + * Register all WebSocket connection as subscribers + * of broadcasting {@link java.util.concurrent.SubmissionPublisher} + * in the {@link MsgProcessingBean}. + *

+ * When connection is closed, cancel subscription and remove reference. + */ +@ServerEndpoint("/ws/messages") +public class WebSocketEndpoint { + + private static final Logger LOGGER = Logger.getLogger(WebSocketEndpoint.class.getName()); + + private final Map> subscriberRegister = new HashMap<>(); + + @Inject + private MsgProcessingBean msgProcessingBean; + + /** + * On WebSocket session is opened. + * + * @param session web socket session + * @param endpointConfig endpoint config + */ + @OnOpen + public void onOpen(Session session, EndpointConfig endpointConfig) { + System.out.println("New WebSocket client connected with session " + session.getId()); + + Single single = msgProcessingBean.subscribeMulti() + // Watch for errors coming from upstream + .onError(throwable -> LOGGER.log(Level.SEVERE, "Upstream error!", throwable)) + // Send every item coming from upstream over web socket + .forEach(s -> sendTextMessage(session, s)); + + //Save forEach single promise for later cancellation + subscriberRegister.put(session.getId(), single); + } + + /** + * When WebSocket session is closed. + * + * @param session web socket session + * @param closeReason web socket close reason + */ + @OnClose + public void onClose(final Session session, final CloseReason closeReason) { + LOGGER.info("Closing session " + session.getId()); + // Properly unsubscribe from SubmissionPublisher + Optional.ofNullable(subscriberRegister.remove(session.getId())) + .ifPresent(Single::cancel); + } + + private void sendTextMessage(Session session, String msg) { + try { + session.getBasicRemote().sendText(msg); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Message sending over WebSocket failed", e); + } + } +} diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java b/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java new file mode 100644 index 000000000..d2ddab5ba --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java @@ -0,0 +1,21 @@ + +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Reactive Messaging JMS example. + */ +package io.helidon.examples.messaging.mp; diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/resources/META-INF/beans.xml b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..676e09a2d --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/resources/META-INF/microprofile-config.properties b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..79eb7b61d --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,52 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server.port=7001 +server.host=0.0.0.0 +server.static.classpath.location=/WEB +server.static.classpath.welcome=index.html + +javax.sql.DataSource.aq-test-ds.connectionFactoryClassName=oracle.jdbc.pool.OracleDataSource +javax.sql.DataSource.aq-test-ds.URL=jdbc:oracle:thin:@localhost:1521:XE +javax.sql.DataSource.aq-test-ds.user=frank +javax.sql.DataSource.aq-test-ds.password=frank + +mp.messaging.connector.helidon-aq.acknowledge-mode=CLIENT_ACKNOWLEDGE +mp.messaging.connector.helidon-aq.data-source=aq-test-ds + +mp.messaging.outgoing.to-queue-1.connector=helidon-aq +mp.messaging.outgoing.to-queue-1.destination=EXAMPLE_QUEUE_1 +mp.messaging.outgoing.to-queue-1.type=queue + +mp.messaging.incoming.from-queue-1.connector=helidon-aq +mp.messaging.incoming.from-queue-1.destination=EXAMPLE_QUEUE_1 +mp.messaging.incoming.from-queue-1.type=queue + +mp.messaging.outgoing.to-queue-2.connector=helidon-aq +mp.messaging.outgoing.to-queue-2.destination=EXAMPLE_QUEUE_2 +mp.messaging.outgoing.to-queue-2.type=queue + +mp.messaging.incoming.from-queue-2.connector=helidon-aq +mp.messaging.incoming.from-queue-2.destination=EXAMPLE_QUEUE_2 +mp.messaging.incoming.from-queue-2.type=queue + +mp.messaging.incoming.from-byte-queue.connector=helidon-aq +mp.messaging.incoming.from-byte-queue.destination=example_queue_bytes +mp.messaging.incoming.from-byte-queue.type=queue + +mp.messaging.incoming.from-map-queue.connector=helidon-aq +mp.messaging.incoming.from-map-queue.destination=example_queue_map +mp.messaging.incoming.from-map-queue.type=queue diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/favicon.ico b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/favicon.ico new file mode 100644 index 000000000..d91659fdb Binary files /dev/null and b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/favicon.ico differ diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/arrow-1.png b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/arrow-1.png new file mode 100644 index 000000000..bbba0aef8 Binary files /dev/null and b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/arrow-1.png differ diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/arrow-2.png b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/arrow-2.png new file mode 100644 index 000000000..0b1096b07 Binary files /dev/null and b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/arrow-2.png differ diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/cloud.png b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/cloud.png new file mode 100644 index 000000000..3e04833c0 Binary files /dev/null and b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/cloud.png differ diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/frank.png b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/frank.png new file mode 100644 index 000000000..51a13d8db Binary files /dev/null and b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/img/frank.png differ diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/index.html b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/index.html new file mode 100644 index 000000000..b35aed463 --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/index.html @@ -0,0 +1,127 @@ + + + + + + + + Helidon Reactive Messaging + + + + + + + + +

+
+
+ +
+
Send
+
+
+
+
+
REST call /rest/messages/send/{msg}
+
+
+
Messages received from JMS over websocket
+
+
+
+
+
+            
+        
+
+
+ + + + + \ No newline at end of file diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/main.css b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/main.css new file mode 100644 index 000000000..8e4a7dcec --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/WEB/main.css @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#root { + background-color: #36ABF2; + font-family: Roboto,sans-serif; + color: #fff; + position: absolute; + overflow-x: hidden; + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#root::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +#helidon { + width: 509px; + height: 273px; + position: relative; + left: -509px; + z-index: 4; + background: url('img/frank.png'); +} + +#rest-tip { + position: relative; + top: -80px; + left: 160px; +} + +#rest-tip-arrow { + width: 205px; + height: 304px; + z-index: 4; + top: -20px; + background: url('img/arrow-1.png'); +} +#rest-tip-label { + position: absolute; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; + left: -60px; +} + +#sse-tip { + position: absolute; + overflow: hidden; + display: flex; + width: auto; + height: auto; + top: 5%; + right: 10%; + z-index: 0; +} + +#sse-tip-arrow { + position: relative; + top: -30px; + width: 296px; + height: 262px; + z-index: 4; + background: url('img/arrow-2.png'); +} +#sse-tip-label { + position: relative; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; +} + +#producer { + float: left; + position: relative; + width: 300px; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 99; +} + +#msgBox { + position: absolute; + width: 300px; + top: 25%; + right: 3%; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 20; +} + +#input { + width: 210px; + height: 22px; + top: 58px; + left: 30px; + background-color: white; + border-radius: 10px; + border-style: solid; + border-color: white; + position: absolute; + z-index: 10; +} + +#inputCloud { + position: relative; + width: 310px; + height: 150px; + background: url('img/cloud.png'); +} + +#msg { + background-color: #D2EBFC; + color: #1A9BF4; + border-radius: 10px; + width: 300px; + height: 50px; + margin: 5px; + display: flex; + padding-left: 10px; + justify-content: center; + align-items: center; + z-index: 99; +} + +#submit { + font-weight: bold; + background-color: aqua; + color: #1A9BF4; + border-radius: 12px; + width: 100px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 5px; + cursor: pointer; +} + +#snippet { + position: absolute; + top: 15%; + left: 30%; + width: 40%; + z-index: 5; +} + +.hljs { + border-radius: 10px; + font-size: 12px; +} diff --git a/examples/messaging/oracle-aq-websocket-mp/src/main/resources/logging.properties b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/logging.properties new file mode 100644 index 000000000..361bf5f6c --- /dev/null +++ b/examples/messaging/oracle-aq-websocket-mp/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/messaging/pom.xml b/examples/messaging/pom.xml new file mode 100644 index 000000000..6019d3df0 --- /dev/null +++ b/examples/messaging/pom.xml @@ -0,0 +1,46 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + io.helidon.examples.messaging + helidon-examples-messaging-project + 1.0.0-SNAPSHOT + Helidon Examples Messaging + pom + + + Examples of Messaging usage + + + + kafka-websocket-mp + kafka-websocket-se + jms-websocket-mp + jms-websocket-se + oracle-aq-websocket-mp + weblogic-jms-mp + + diff --git a/examples/messaging/weblogic-jms-mp/README.md b/examples/messaging/weblogic-jms-mp/README.md new file mode 100644 index 000000000..610a417f1 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/README.md @@ -0,0 +1,28 @@ +# Helidon Messaging with Oracle Weblogic Example + +## Prerequisites +* JDK 21+ +* Maven +* Docker +* Account at https://container-registry.oracle.com/ with accepted Oracle Standard Terms and Restrictions for Weblogic. + +## Run Weblogic in docker +1. You will need to do a docker login to Oracle container registry with account which previously + accepted Oracle Standard Terms and Restrictions for Weblogic: + `docker login container-registry.oracle.com` +2. Run `bash buildAndRunWeblogic.sh` to build and run example Weblogic container. + * After example JMS resources are deployed, Weblogic console should be available at http://localhost:7001/console with `admin`/`Welcome1` +3. To obtain wlthint3client.jar necessary for connecting to Weblogic execute + `bash extractThinClientLib.sh`, file will be copied to `./weblogic` folder. + +## Build & Run +To run Helidon with thin client, flag `--add-opens=java.base/java.io=ALL-UNNAMED` is needed to +open java.base module to thin client internals. +```shell +#1. + mvn clean package +#2. + java --add-opens=java.base/java.io=ALL-UNNAMED -jar ./target/weblogic-jms-mp.jar +``` +3. Visit http://localhost:8080 and try to send and receive messages over Weblogic JMS queue. + diff --git a/examples/messaging/weblogic-jms-mp/buildAndRunWeblogic.sh b/examples/messaging/weblogic-jms-mp/buildAndRunWeblogic.sh new file mode 100644 index 000000000..5a113e80f --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/buildAndRunWeblogic.sh @@ -0,0 +1,53 @@ +#!/bin/bash -e +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +cd ./weblogic + +# Attempt Oracle container registry login. +# You need to accept the licence agreement for Weblogic Server at https://container-registry.oracle.com/ +# Search for weblogic and accept the Oracle Standard Terms and Restrictions +docker login container-registry.oracle.com + +docker build -t wls-admin . + +docker run --rm -d \ + -p 7001:7001 \ + -p 7002:7002 \ + --name wls-admin \ + --hostname wls-admin \ + wls-admin + +printf "Waiting for WLS to start ." +while true; +do + if docker logs wls-admin | grep -q "Server state changed to RUNNING"; then + break; + fi + printf "." + sleep 5 +done +printf " [READY]\n" + +echo Deploying example JMS queues +docker exec wls-admin \ +/bin/bash \ +/u01/oracle/wlserver/common/bin/wlst.sh \ +/u01/oracle/setupTestJMSQueue.py; + +echo Example JMS queues deployed! +echo Console avaiable at http://localhost:7001/console with admin/Welcome1 +echo 'Stop Weblogic server with "docker stop wls-admin"' \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/extractThinClientLib.sh b/examples/messaging/weblogic-jms-mp/extractThinClientLib.sh new file mode 100644 index 000000000..e61d95700 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/extractThinClientLib.sh @@ -0,0 +1,22 @@ +#!/bin/bash -e +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Copy wlthint3client.jar from docker container +docker cp wls-admin:/u01/oracle/wlserver/server/lib/wlthint3client.jar ./weblogic/wlthint3client.jar +# Copy DemoTrust.jks from docker container(needed if you want to try t3s protocol) +docker cp wls-admin:/u01/oracle/wlserver/server/lib/DemoTrust.jks ./weblogic/DemoTrust.jks \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/pom.xml b/examples/messaging/weblogic-jms-mp/pom.xml new file mode 100644 index 000000000..f5a71814f --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/pom.xml @@ -0,0 +1,82 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.messaging.wls + weblogic-jms-mp + 1.0.0-SNAPSHOT + Helidon Examples Messaging JMS WebLogic MP + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + + io.helidon.microprofile.messaging + helidon-microprofile-messaging + + + io.helidon.messaging.wls-jms + helidon-messaging-wls-jms + + + + org.glassfish.jersey.media + jersey-media-sse + + + + io.smallrye + jandex + runtime + true + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/FrankResource.java b/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/FrankResource.java new file mode 100644 index 000000000..05712bfe4 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/FrankResource.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.messaging.mp; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import io.helidon.messaging.connectors.jms.JmsMessage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.sse.Sse; +import jakarta.ws.rs.sse.SseBroadcaster; +import jakarta.ws.rs.sse.SseEventSink; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.glassfish.jersey.media.sse.OutboundEvent; + +/** + * SSE Jax-Rs resource for message publishing and consuming. + */ +@Path("/frank") +@ApplicationScoped +public class FrankResource { + + @Inject + @Channel("to-wls") + private Emitter emitter; + private SseBroadcaster sseBroadcaster; + + /** + * Consuming JMS messages from Weblogic and sending them to the client over SSE. + * + * @param msg dequeued message + * @return completion stage marking end of the processing + */ + @Incoming("from-wls") + public CompletionStage receive(JmsMessage msg) { + if (sseBroadcaster == null) { + System.out.println("No SSE client subscribed yet: " + msg.getPayload()); + return CompletableFuture.completedStage(null); + } + sseBroadcaster.broadcast(new OutboundEvent.Builder().data(msg.getPayload()).build()); + return CompletableFuture.completedStage(null); + } + + /** + * Send message to Weblogic JMS queue. + * + * @param msg message to be sent + */ + @POST + @Path("/send/{msg}") + public void send(@PathParam("msg") String msg) { + emitter.send(msg); + } + + /** + * Register SSE client to listen for messages coming from Weblogic JMS. + * + * @param eventSink client sink + * @param sse SSE context + */ + @GET + @Path("sse") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void listenToEvents(@Context SseEventSink eventSink, @Context Sse sse) { + if (sseBroadcaster == null) { + sseBroadcaster = sse.newBroadcaster(); + } + sseBroadcaster.register(eventSink); + } +} diff --git a/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java b/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java new file mode 100644 index 000000000..f4031710c --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon MP Reactive Messaging with Weblogic JMS. + */ +package io.helidon.examples.messaging.mp; diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/META-INF/beans.xml b/examples/messaging/weblogic-jms-mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..bc0dc880f --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,24 @@ + + + + diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/favicon.ico b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/favicon.ico new file mode 100644 index 000000000..d91659fdb Binary files /dev/null and b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/favicon.ico differ diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/arrow-1.png b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/arrow-1.png new file mode 100644 index 000000000..bbba0aef8 Binary files /dev/null and b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/arrow-1.png differ diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/arrow-2.png b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/arrow-2.png new file mode 100644 index 000000000..0b1096b07 Binary files /dev/null and b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/arrow-2.png differ diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/cloud.png b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/cloud.png new file mode 100644 index 000000000..3e04833c0 Binary files /dev/null and b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/cloud.png differ diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/frank.png b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/frank.png new file mode 100644 index 000000000..51a13d8db Binary files /dev/null and b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/frank.png differ diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/index.html b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/index.html new file mode 100644 index 000000000..9afd02c4a --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/index.html @@ -0,0 +1,101 @@ + + + + + + + Helidon Reactive Messaging + + + + + +
+
+
+ +
+
Send
+
+
+
+
+
REST call /frank/send/{msg}
+
+
+
SSE messages received
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/main.css b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/main.css new file mode 100644 index 000000000..4083b336b --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/main.css @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#root { + background-color: #36ABF2; + font-family: Roboto,sans-serif; + color: #fff; + position: absolute; + overflow-x: hidden; + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#root::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +#helidon { + width: 509px; + height: 273px; + position: relative; + left: -509px; + z-index: 4; + background: url('img/frank.png'); +} + +#rest-tip { + position: relative; + top: -80px; + left: 160px; +} + +#rest-tip-arrow { + width: 205px; + height: 304px; + z-index: 4; + top: -20px; + background: url('img/arrow-1.png'); +} +#rest-tip-label { + position: absolute; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; + left: -60px; +} + +#sse-tip { + position: absolute; + overflow: hidden; + display: flex; + width: auto; + height: auto; + top: 5%; + right: 10%; + z-index: 0; +} + +#sse-tip-arrow { + position: relative; + top: -30px; + width: 296px; + height: 262px; + z-index: 4; + background: url('img/arrow-2.png'); +} +#sse-tip-label { + position: relative; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; +} + +#producer { + float: left; + position: relative; + width: 300px; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 99; +} + +#msgBox { + position: absolute; + width: 300px; + top: 25%; + right: 3%; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 20; +} + +#input { + width: 210px; + height: 22px; + top: 58px; + left: 30px; + background-color: white; + border-radius: 10px; + border-style: solid; + border-color: white; + position: absolute; + z-index: 10; +} + +#inputCloud { + position: relative; + width: 310px; + height: 150px; + background: url('img/cloud.png'); +} + +#msg { + background-color: #D2EBFC; + color: #1A9BF4; + border-radius: 10px; + width: 300px; + height: 50px; + margin: 5px; + display: flex; + padding-left: 10px; + justify-content: center; + align-items: center; + z-index: 99; +} + +#submit { + font-weight: bold; + background-color: aqua; + color: #1A9BF4; + border-radius: 12px; + width: 100px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 5px; + cursor: pointer; +} + +#snippet { + position: absolute; + top: 15%; + left: 30%; + width: 40%; + z-index: 5; +} + +.hljs { + border-radius: 10px; + font-size: 12px; +} \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml b/examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml new file mode 100644 index 000000000..164b73ea9 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml @@ -0,0 +1,44 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + static: + classpath: + location: /WEB + welcome: index.html + +mp: + messaging: + connector: + helidon-weblogic-jms: + # JMS factory configured in Weblogic + jms-factory: jms/TestConnectionFactory + # Path to the WLS Thin T3 client jar(extract it from docker container with extractThinClientLib.sh) + thin-jar: weblogic/wlthint3client.jar + url: "t3://localhost:7001" + producer.unit-of-order: kec1 + incoming: + from-wls: + connector: helidon-weblogic-jms + # WebLogic CDI Syntax(CDI stands for Create Destination Identifier) + destination: ./TestJMSModule!TestQueue + outgoing: + to-wls: + connector: helidon-weblogic-jms + # JNDI identifier for the same queue + jndi.destination: jms/TestQueue \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties b/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties new file mode 100644 index 000000000..cc6aecf5c --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties @@ -0,0 +1,30 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.level=INFO diff --git a/examples/messaging/weblogic-jms-mp/weblogic/Dockerfile b/examples/messaging/weblogic-jms-mp/weblogic/Dockerfile new file mode 100644 index 000000000..d034fcc9b --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/Dockerfile @@ -0,0 +1,54 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# ORACLE DOCKERFILES PROJECT +# -------------------------- +# This docker file is customized, originaly taken from https://github.com/oracle/docker-images +# and extends the Oracle WebLogic image by creating a sample domain. +# +# Base image is available at https://container-registry.oracle.com/ +# +FROM container-registry.oracle.com/middleware/weblogic:14.1.1.0-dev-11 + +ENV ORACLE_HOME=/u01/oracle \ + USER_MEM_ARGS="-Djava.security.egd=file:/dev/./urandom" \ + SCRIPT_FILE=/u01/oracle/createAndStartEmptyDomain.sh \ + HEALTH_SCRIPT_FILE=/u01/oracle/get_healthcheck_url.sh \ + PATH=$PATH:${JAVA_HOME}/bin:/u01/oracle/oracle_common/common/bin:/u01/oracle/wlserver/common/bin + +ENV DOMAIN_NAME="${DOMAIN_NAME:-base_domain}" \ + ADMIN_LISTEN_PORT="${ADMIN_LISTEN_PORT:-7001}" \ + ADMIN_NAME="${ADMIN_NAME:-AdminServer}" \ + DEBUG_FLAG=true \ + PRODUCTION_MODE=dev \ + ADMINISTRATION_PORT_ENABLED="${ADMINISTRATION_PORT_ENABLED:-true}" \ + ADMINISTRATION_PORT="${ADMINISTRATION_PORT:-9002}" + +COPY container-scripts/createAndStartEmptyDomain.sh container-scripts/get_healthcheck_url.sh /u01/oracle/ +COPY container-scripts/create-wls-domain.py container-scripts/setupTestJMSQueue.py /u01/oracle/ +COPY properties/domain.properties /u01/oracle/properties/ + +USER root + +RUN chmod +xr $SCRIPT_FILE $HEALTH_SCRIPT_FILE && \ + chown oracle:root $SCRIPT_FILE /u01/oracle/create-wls-domain.py $HEALTH_SCRIPT_FILE + +USER oracle + +HEALTHCHECK --start-period=10s --timeout=30s --retries=3 CMD curl -k -s --fail `$HEALTH_SCRIPT_FILE` || exit 1 +WORKDIR ${ORACLE_HOME} + +CMD ["/u01/oracle/createAndStartEmptyDomain.sh"] \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/create-wls-domain.py b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/create-wls-domain.py new file mode 100644 index 000000000..9d74bff8e --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/create-wls-domain.py @@ -0,0 +1,104 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# WebLogic on Docker Default Domain +# +# Domain, as defined in DOMAIN_NAME, will be created in this script. Name defaults to 'base_domain'. +# +# Since : October, 2014 +# Author: monica.riccelli@oracle.com +# ============================================== +domain_name = os.environ.get("DOMAIN_NAME", "base_domain") +admin_name = os.environ.get("ADMIN_NAME", "AdminServer") +admin_listen_port = int(os.environ.get("ADMIN_LISTEN_PORT", "7001")) +domain_path = '/u01/oracle/user_projects/domains/%s' % domain_name +production_mode = os.environ.get("PRODUCTION_MODE", "prod") +administration_port_enabled = os.environ.get("ADMINISTRATION_PORT_ENABLED", "true") +administration_port = int(os.environ.get("ADMINISTRATION_PORT", "9002")) + +print('domain_name : [%s]' % domain_name); +print('admin_listen_port : [%s]' % admin_listen_port); +print('domain_path : [%s]' % domain_path); +print('production_mode : [%s]' % production_mode); +print('admin name : [%s]' % admin_name); +print('administration_port_enabled : [%s]' % administration_port_enabled); +print('administration_port : [%s]' % administration_port); + +# Open default domain template +# ============================ +readTemplate("/u01/oracle/wlserver/common/templates/wls/wls.jar") + +set('Name', domain_name) +setOption('DomainName', domain_name) + +# Set Administration Port +# ======================= +if administration_port_enabled != "false": + set('AdministrationPort', administration_port) + set('AdministrationPortEnabled', 'false') + +# Disable Admin Console +# -------------------- +# cmo.setConsoleEnabled(false) + +# Configure the Administration Server and SSL port. +# ================================================= +cd('/Servers/AdminServer') +set('Name', admin_name) +set('ListenAddress', '') +set('ListenPort', admin_listen_port) +if administration_port_enabled != "false": + create(admin_name, 'SSL') + cd('SSL/' + admin_name) + set('Enabled', 'True') + +# Define the user password for weblogic +# ===================================== +cd(('/Security/%s/User/weblogic') % domain_name) +cmo.setName(username) +cmo.setPassword(password) + +# Write the domain and close the domain template +# ============================================== +setOption('OverwriteDomain', 'true') +setOption('ServerStartMode',production_mode) + +# Create Node Manager +# =================== +#cd('/NMProperties') +#set('ListenAddress','') +#set('ListenPort',5556) +#set('CrashRecoveryEnabled', 'true') +#set('NativeVersionEnabled', 'true') +#set('StartScriptEnabled', 'false') +#set('SecureListener', 'false') +#set('LogLevel', 'FINEST') + +# Set the Node Manager user name and password +# =========================================== +#cd('/SecurityConfiguration/%s' % domain_name) +#set('NodeManagerUsername', username) +#set('NodeManagerPasswordEncrypted', password) + +# Write Domain +# ============ +writeDomain(domain_path) +closeTemplate() + +# Exit WLST +# ========= +exit() diff --git a/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/createAndStartEmptyDomain.sh b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/createAndStartEmptyDomain.sh new file mode 100644 index 000000000..6a3665be1 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/createAndStartEmptyDomain.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# If AdminServer.log does not exists, container is starting for 1st time +# So it should start NM and also associate with AdminServer +# Otherwise, only start NM (container restarted) +########### SIGTERM handler ############ +function _term() { + echo "Stopping container." + echo "SIGTERM received, shutting down the server!" + ${DOMAIN_HOME}/bin/stopWebLogic.sh +} + +########### SIGKILL handler ############ +function _kill() { + echo "SIGKILL received, shutting down the server!" + kill -9 $childPID +} + +# Set SIGTERM handler +trap _term SIGTERM + +# Set SIGKILL handler +trap _kill SIGKILL + +#Define DOMAIN_HOME +export DOMAIN_HOME=/u01/oracle/user_projects/domains/$DOMAIN_NAME +echo "Domain Home is: " $DOMAIN_HOME + +mkdir -p $ORACLE_HOME/properties +# Create Domain only if 1st execution +if [ ! -e ${DOMAIN_HOME}/servers/${ADMIN_NAME}/logs/${ADMIN_NAME}.log ]; then + echo "Create Domain" + PROPERTIES_FILE=/u01/oracle/properties/domain.properties + if [ ! -e "$PROPERTIES_FILE" ]; then + echo "A properties file with the username and password needs to be supplied." + exit + fi + + # Get Username + USER=`awk '{print $1}' $PROPERTIES_FILE | grep username | cut -d "=" -f2` + if [ -z "$USER" ]; then + echo "The domain username is blank. The Admin username must be set in the properties file." + exit + fi + # Get Password + PASS=`awk '{print $1}' $PROPERTIES_FILE | grep password | cut -d "=" -f2` + if [ -z "$PASS" ]; then + echo "The domain password is blank. The Admin password must be set in the properties file." + exit + fi + + # Create an empty domain + wlst.sh -skipWLSModuleScanning -loadProperties $PROPERTIES_FILE /u01/oracle/create-wls-domain.py + mkdir -p ${DOMAIN_HOME}/servers/${ADMIN_NAME}/security/ + chmod -R g+w ${DOMAIN_HOME} + echo "username=${USER}" >> $DOMAIN_HOME/servers/${ADMIN_NAME}/security/boot.properties + echo "password=${PASS}" >> $DOMAIN_HOME/servers/${ADMIN_NAME}/security/boot.properties + ${DOMAIN_HOME}/bin/setDomainEnv.sh + # Setup JMS examples +# wlst.sh -skipWLSModuleScanning -loadProperties $PROPERTIES_FILE /u01/oracle/setupTestJMSQueue.py +fi + +# Start Admin Server and tail the logs +${DOMAIN_HOME}/startWebLogic.sh +if [ -e ${DOMAIN_HOME}/servers/${ADMIN_NAME}/logs/${ADMIN_NAME}.log ]; then + echo "${DOMAIN_HOME}/servers/${ADMIN_NAME}/logs/${ADMIN_NAME}.log" +fi +touch ${DOMAIN_HOME}/servers/${ADMIN_NAME}/logs/${ADMIN_NAME}.log +tail -f ${DOMAIN_HOME}/servers/${ADMIN_NAME}/logs/${ADMIN_NAME}.log + +childPID=$! +wait $childPID diff --git a/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/get_healthcheck_url.sh b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/get_healthcheck_url.sh new file mode 100644 index 000000000..b67ac11cd --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/get_healthcheck_url.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +if [ "$ADMINISTRATION_PORT_ENABLED" = "true" ] ; then + echo "https://{localhost:$ADMINISTRATION_PORT}/weblogic/ready" ; +else + echo "http://{localhost:$ADMIN_LISTEN_PORT}/weblogic/ready" ; +fi diff --git a/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/setupTestJMSQueue.py b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/setupTestJMSQueue.py new file mode 100644 index 000000000..3548fe062 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/setupTestJMSQueue.py @@ -0,0 +1,115 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os.path +import sys + +System.setProperty("weblogic.security.SSL.ignoreHostnameVerification", "true") + +connect("admin","Welcome1","t3://localhost:7001") +adm_name=get('AdminServerName') +sub_deployment_name="TestJMSSubdeployment" +jms_module_name="TestJMSModule" +queue_name="TestQueue" +factory_name="TestConnectionFactory" +jms_server_name="TestJMSServer" + + +def createJMSServer(adm_name, jms_server_name): + cd('/JMSServers') + if (len(ls(returnMap='true')) == 0): + print 'No JMS Server found, creating ' + jms_server_name + cd('/') + cmo.createJMSServer(jms_server_name) + cd('/JMSServers/'+jms_server_name) + cmo.addTarget(getMBean("/Servers/" + adm_name)) + + +def createJMSModule(jms_module_name, adm_name, sub_deployment_name): + print "Creating JMS module " + jms_module_name + cd('/JMSServers') + jms_servers=ls(returnMap='true') + cd('/') + module = create(jms_module_name, "JMSSystemResource") + module.addTarget(getMBean("Servers/"+adm_name)) + cd('/SystemResources/'+jms_module_name) + module.createSubDeployment(sub_deployment_name) + cd('/SystemResources/'+jms_module_name+'/SubDeployments/'+sub_deployment_name) + + list=[] + for i in jms_servers: + list.append(ObjectName(str('com.bea:Name='+i+',Type=JMSServer'))) + set('Targets',jarray.array(list, ObjectName)) + +def getJMSModulePath(jms_module_name): + jms_module_path = "/JMSSystemResources/"+jms_module_name+"/JMSResource/"+jms_module_name + return jms_module_path + +def createJMSQueue(jms_module_name,jms_queue_name): + print "Creating JMS queue " + jms_queue_name + jms_module_path = getJMSModulePath(jms_module_name) + cd(jms_module_path) + cmo.createQueue(jms_queue_name) + cd(jms_module_path+'/Queues/'+jms_queue_name) + cmo.setJNDIName("jms/" + jms_queue_name) + cmo.setSubDeploymentName(sub_deployment_name) + +def createDistributedJMSQueue(jms_module_name,jms_queue_name): + print "Creating distributed JMS queue " + jms_queue_name + jms_module_path = getJMSModulePath(jms_module_name) + cd(jms_module_path) + cmo.createDistributedQueue(jms_queue_name) + cd(jms_module_path+'/DistributedQueues/'+jms_queue_name) + cmo.setJNDIName("jms/" + jms_queue_name) + +def addMemberQueue(udd_name,queue_name): + jms_module_path = getJMSModulePath(jms_module_name) + cd(jms_module_path+'/DistributedQueues/'+udd_name) + cmo.setLoadBalancingPolicy('Round-Robin') + cmo.createDistributedQueueMember(queue_name) + +def createJMSFactory(jms_module_name,jms_fact_name): + print "Creating JMS connection factory " + jms_fact_name + jms_module_path = getJMSModulePath(jms_module_name) + cd(jms_module_path) + cmo.createConnectionFactory(jms_fact_name) + cd(jms_module_path+'/ConnectionFactories/'+jms_fact_name) + cmo.setJNDIName("jms/" + jms_fact_name) + cmo.setSubDeploymentName(sub_deployment_name) + + + +edit() +startEdit() + +print "Server name: "+adm_name + +createJMSServer(adm_name,jms_server_name) +createJMSModule(jms_module_name,adm_name,sub_deployment_name) +createJMSFactory(jms_module_name,factory_name) +createJMSQueue(jms_module_name,queue_name) + +### Unified Distributed Destinations(UDD) example +createDistributedJMSQueue(jms_module_name,"udd_queue") +# Normally member queues would be in different sub-deployments +createJMSQueue(jms_module_name,"ms1@udd_queue") +createJMSQueue(jms_module_name,"ms2@udd_queue") +addMemberQueue("udd_queue", "ms1@udd_queue") +addMemberQueue("udd_queue", "ms2@udd_queue") + +save() +activate(block="true") +disconnect() \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/weblogic/properties/domain.properties b/examples/messaging/weblogic-jms-mp/weblogic/properties/domain.properties new file mode 100644 index 000000000..284210956 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/properties/domain.properties @@ -0,0 +1,35 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Env properties inherited from base image +DOMAIN_NAME=myDomain +ADMIN_LISTEN_PORT=7001 +ADMIN_NAME=myadmin +PRODUCTION_MODE=dev +DEBUG_FLAG=true +ADMINISTRATION_PORT_ENABLED=false +ADMINISTRATION_PORT=9002 +# Env properties for this image +ADMIN_HOST=AdminContainer +MANAGED_SERVER_PORT=8001 +MANAGED_SERVER_NAME_BASE=MS +CONFIGURED_MANAGED_SERVER_COUNT=2 +PRODUCTION_MODE_ENABLED=true +CLUSTER_NAME=cluster1 +CLUSTER_TYPE=DYNAMIC +DOMAIN_HOST_VOLUME=/Users/host/temp +username=admin +password=Welcome1 \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/weblogic/properties/domain_security.properties b/examples/messaging/weblogic-jms-mp/weblogic/properties/domain_security.properties new file mode 100644 index 000000000..c79491acd --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/properties/domain_security.properties @@ -0,0 +1,18 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +username=admin +password=Welcome1 \ No newline at end of file diff --git a/examples/metrics/exemplar/README.md b/examples/metrics/exemplar/README.md new file mode 100644 index 000000000..fd9bcf65e --- /dev/null +++ b/examples/metrics/exemplar/README.md @@ -0,0 +1,67 @@ +# Helidon Metrics Exemplar SE Example + +This project implements a simple Hello World REST service using Helidon SE and demonstrates the +optional metrics exemplar support. + +## Start Zipkin (optional) +If you do not start Zipkin, the example app will still function correctly but it will log a warning +when it cannot contact the Zipkin server to report the tracing spans. Even so, the metrics output +will contain valid exemplars. + +With Docker: +```shell +docker run --name zipkin -d -p 9411:9411 openzipkin/zipkin +``` + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-metrics-exemplar.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/greet +#output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#output: {"message":"Hola Jose!"} + +curl -X GET http://localhost:8080/greet +#output: {"message":"Hola World!"} +``` + +## Retrieve application metrics + +``` +# Prometheus format with exemplars + +curl -s -X GET http://localhost:8080/observe/metrics/application +# TYPE application_counterForPersonalizedGreetings_total counter +# HELP application_counterForPersonalizedGreetings_total +application_counterForPersonalizedGreetings_total 2 # {trace_id="78e61eed351f4c9d"} 1 1617812495.016000 +. . . +# TYPE application_timerForGets_mean_seconds gauge +application_timerForGets_mean_seconds 0.005772598385062112 # {trace_id="b22f13c37ba8b879"} 0.001563945 1617812578.687000 +# TYPE application_timerForGets_max_seconds gauge +application_timerForGets_max_seconds 0.028018165 # {trace_id="a1b127002725143c"} 0.028018165 1617812467.524000 +``` +The exemplars contain `trace_id` values tying them to specific samples. +Note that the exemplar for the counter refers to the most recent update to the counter. + +For the timer, the value for the `max` is exactly the same as the value for its exemplar, +because the `max` value has to come from at least one sample. +In contrast, Helidon calculates the `mean` value from possibly multiple samples. The exemplar for +`mean` is a sample with value as close as that of other samples to the mean. + +## Browse the Zipkin traces +If you started the Zipkin server, visit `http://localhost:9411` and click `Run Query` to see all +the spans your Helidon application reported to Zipkin. +You can compare the trace IDs in the Zipkin display to those in the metrics output. diff --git a/examples/metrics/exemplar/pom.xml b/examples/metrics/exemplar/pom.xml new file mode 100644 index 000000000..4ec0728d6 --- /dev/null +++ b/examples/metrics/exemplar/pom.xml @@ -0,0 +1,120 @@ + + + + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + 4.0.0 + + io.helidon.examples.metrics + helidon-examples-metrics-exemplar + 1.0.0-SNAPSHOT + + Helidon Examples Metrics Exemplar + + + io.helidon.examples.metrics.exemplar.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.webserver.observe + helidon-webserver-observe-tracing + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.tracing + helidon-tracing + + + io.helidon.metrics + helidon-metrics-trace-exemplar + + + io.helidon.tracing.providers + helidon-tracing-providers-zipkin + + + io.helidon.config + helidon-config-yaml + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/metrics/exemplar/src/main/java/io/helidon/examples/metrics/exemplar/GreetService.java b/examples/metrics/exemplar/src/main/java/io/helidon/examples/metrics/exemplar/GreetService.java new file mode 100644 index 000000000..cdade1691 --- /dev/null +++ b/examples/metrics/exemplar/src/main/java/io/helidon/examples/metrics/exemplar/GreetService.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.metrics.exemplar; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.Meter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.Timer; +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.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple service to greet you. Examples: + *

+ * Get default greeting message: + * curl -X GET http://localhost:8080/greet + *

+ * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + *

+ * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + *

+ * The message is returned as a JSON object + */ + +public class GreetService implements HttpService { + + /** + * The config value for the key {@code greeting}. + */ + private final AtomicReference greeting = new AtomicReference<>(); + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + static final String TIMER_FOR_GETS = "timerForGets"; + static final String COUNTER_FOR_PERSONALIZED_GREETINGS = "counterForPersonalizedGreetings"; + + private final Timer timerForGets; + private final Counter personalizedGreetingsCounter; + + GreetService() { + Config config = Config.global(); + greeting.set(config.get("app.greeting").asString().orElse("Ciao")); + + MeterRegistry meterRegistry = Metrics.globalRegistry(); + timerForGets = meterRegistry.getOrCreate(Timer.builder(TIMER_FOR_GETS) + .baseUnit(Meter.BaseUnits.NANOSECONDS)); + personalizedGreetingsCounter = meterRegistry.getOrCreate(Counter.builder(COUNTER_FOR_PERSONALIZED_GREETINGS)); + } + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.get("/", this::timeGet, this::getDefaultMessageHandler) + .get("/{name}", this::countPersonalized, 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().get("name"); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + String msg = String.format("%s %s!", greeting.get(), name); + + JsonObject returnObject = JSON.createObjectBuilder() + .add("message", msg) + .build(); + response.send(returnObject); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + + if (!jo.containsKey("greeting")) { + JsonObject jsonErrorObject = JSON.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting.set(jo.getString("greeting")); + response.status(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) { + JsonObject jsonObject = request.content().as(JsonObject.class); + updateGreetingFromJson(jsonObject, response); + } + + private void timeGet(ServerRequest request, ServerResponse response) { + timerForGets.record((Runnable) response::next); + } + + private void countPersonalized(ServerRequest request, ServerResponse response) { + personalizedGreetingsCounter.increment(); + response.next(); + } +} diff --git a/examples/metrics/exemplar/src/main/java/io/helidon/examples/metrics/exemplar/Main.java b/examples/metrics/exemplar/src/main/java/io/helidon/examples/metrics/exemplar/Main.java new file mode 100644 index 000000000..21c051477 --- /dev/null +++ b/examples/metrics/exemplar/src/main/java/io/helidon/examples/metrics/exemplar/Main.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.metrics.exemplar; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * The application main class. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build().start(); + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + /** + * Set up the server. + * + * @param server server builder + */ + static void setup(WebServerConfig.Builder server) { + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + Config.global(config); + + server.routing(Main::routing) + .config(config.get("server")); + + } + + /** + * Setup routing. + * + * @param routing routing builder + */ + static void routing(HttpRouting.Builder routing) { + routing.register("/greet", new GreetService()); + } +} diff --git a/examples/metrics/exemplar/src/main/java/io/helidon/examples/metrics/exemplar/package-info.java b/examples/metrics/exemplar/src/main/java/io/helidon/examples/metrics/exemplar/package-info.java new file mode 100644 index 000000000..2407f3454 --- /dev/null +++ b/examples/metrics/exemplar/src/main/java/io/helidon/examples/metrics/exemplar/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quickstart demo application + *

+ * Start with {@link io.helidon.examples.metrics.exemplar.Main} class. + *

+ * @see io.helidon.examples.metrics.exemplar.Main + */ +package io.helidon.examples.metrics.exemplar; diff --git a/examples/metrics/exemplar/src/main/resources/application.yaml b/examples/metrics/exemplar/src/main/resources/application.yaml new file mode 100644 index 000000000..051f67d85 --- /dev/null +++ b/examples/metrics/exemplar/src/main/resources/application.yaml @@ -0,0 +1,24 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + greeting: "Hello" + +server: + port: 8080 + host: 0.0.0.0 +tracing: + service: "hello-world" diff --git a/examples/metrics/exemplar/src/main/resources/logging.properties b/examples/metrics/exemplar/src/main/resources/logging.properties new file mode 100644 index 000000000..c916a0505 --- /dev/null +++ b/examples/metrics/exemplar/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/metrics/exemplar/src/test/java/io/helidon/examples/metrics/exemplar/MainTest.java b/examples/metrics/exemplar/src/test/java/io/helidon/examples/metrics/exemplar/MainTest.java new file mode 100644 index 000000000..faf0ee6d3 --- /dev/null +++ b/examples/metrics/exemplar/src/test/java/io/helidon/examples/metrics/exemplar/MainTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.metrics.exemplar; + +import java.io.LineNumberReader; +import java.io.StringReader; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.config.Config; +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 jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertLinesMatch; + +@ServerTest +public class MainTest { + + private static final JsonBuilderFactory JSON_BUILDER = Json.createBuilderFactory(Collections.emptyMap()); + private static final JsonObject TEST_JSON_OBJECT = JSON_BUILDER.createObjectBuilder() + .add("greeting", "Hola") + .build(); + + private final Http1Client client; + + public MainTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + Config.global(Config.create()); + server.routing(Main::routing); + } + + @Test + public void testHelloWorld() { + try (Http1ClientResponse response = client.get("/greet").request()) { + assertThat(response.as(JsonObject.class).getString("message"), is("Hello World!")); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(JsonObject.class).getString("message"), is("Hello Joe!")); + } + + try (Http1ClientResponse response = client.put("/greet/greeting").submit(TEST_JSON_OBJECT)) { + assertThat(response.status().code(), is(204)); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(JsonObject.class).getString("message"), is("Hola Joe!")); + } + + try (Http1ClientResponse response = client.get("/observe/metrics").request()) { + assertThat(response.status().code(), is(200)); + } + } + + @Disabled // because of intermittent pipeline failures; the assertLinesMatch exhausts the available lines + @Test + public void testMetrics() { + try (Http1ClientResponse response = client.get("/greet").request()) { + assertThat(response.as(String.class), containsString("Hello World!")); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(String.class), containsString("Hello Joe!")); + } + + try (Http1ClientResponse response = client.get("/observe/metrics/application") + .accept(MediaTypes.APPLICATION_OPENMETRICS_TEXT) + .request()) { + + String openMetricsOutput = response.as(String.class); + LineNumberReader reader = new LineNumberReader(new StringReader(openMetricsOutput)); + List returnedLines = reader.lines() + .collect(Collectors.toList()); + returnedLines.add("# extra line at end to make sure we do not deplete the actual lines"); + + List expected = List.of(">> skip to max >>", + "# TYPE " + GreetService.COUNTER_FOR_PERSONALIZED_GREETINGS + " counter", + "# HELP " + GreetService.COUNTER_FOR_PERSONALIZED_GREETINGS + ".*", + valueMatcher(GreetService.COUNTER_FOR_PERSONALIZED_GREETINGS,"", "total"), + ">> end of output >>"); + assertLinesMatch(expected, returnedLines, GreetService.TIMER_FOR_GETS + "_seconds_max TYPE and value"); + } + } + + private static String valueMatcher(String meterName, String unit, String statName) { + // counterForPersonalizedGreetings_total{scope="application"} 1.0 # {span_id="41d2a1755a2b8797",trace_id="41d2a1755a2b8797"} 1.0 1696888732.920 + return meterName + + (unit == null || unit.isBlank() ? "" : "_" + unit) + + "_" + statName + ".*? # .*?trace_id=\"[^\"]+\"\\} [\\d\\.]+ [\\d\\.]+"; + } + +} diff --git a/examples/metrics/filtering/mp/README.md b/examples/metrics/filtering/mp/README.md new file mode 100644 index 000000000..fb4fa520d --- /dev/null +++ b/examples/metrics/filtering/mp/README.md @@ -0,0 +1,47 @@ +# Helidon Metrics Filtering MP Example + +This project implements a simple Hello World REST service using Helidon SE and demonstrates the +optional metrics exemplar support. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-metrics-filtering-mp.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"message" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} + +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hola World!"} +``` + +## Retrieve application metrics + +```shell +# Prometheus format with exemplars + +curl -s -X GET http://localhost:8080/metrics/application +``` +``` +# TYPE application_counterForPersonalizedGreetings_total counter +# HELP application_counterForPersonalizedGreetings_total +application_counterForPersonalizedGreetings_total 2 # {trace_id="78e61eed351f4c9d"} 1 1617812495.016000 +. . . +# TYPE application_timerForGets_mean_seconds gauge +application_timerForGets_mean_seconds 0.005772598385062112 # {trace_id="b22f13c37ba8b879"} 0.001563945 1617812578.687000 +# TYPE application_timerForGets_max_seconds gauge +application_timerForGets_max_seconds 0.028018165 # {trace_id="a1b127002725143c"} 0.028018165 1617812467.524000 +``` diff --git a/examples/metrics/filtering/mp/pom.xml b/examples/metrics/filtering/mp/pom.xml new file mode 100644 index 000000000..9d3e82370 --- /dev/null +++ b/examples/metrics/filtering/mp/pom.xml @@ -0,0 +1,109 @@ + + + + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + 4.0.0 + + io.helidon.examples.metrics.filtering + helidon-examples-metrics-filtering-mp + 1.0.0-SNAPSHOT + Helidon Examples Metrics MP Filtering + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.microprofile.metrics + helidon-microprofile-metrics + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webclient + helidon-webclient + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + check-for-inflight-with-config-settings + + test + + + + true + + + + + + + + diff --git a/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/GreetResource.java b/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/GreetResource.java new file mode 100644 index 000000000..22b532b87 --- /dev/null +++ b/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/GreetResource.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.metrics.filtering.mp; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.metrics.annotation.Counted; +import org.eclipse.microprofile.metrics.annotation.Timed; + +/** + * A simple JAX-RS resource to greet you with filtered metrics support. + */ +@Path("/greet") +@RequestScoped +public class GreetResource { + + static final String TIMER_FOR_GETS = "timerForGets"; + static final String COUNTER_FOR_PERSONALIZED_GREETINGS = "counterForPersonalizedGreetings"; + + /** + * The greeting message provider. + */ + private final GreetingProvider greetingProvider; + + /** + * Using constructor injection to get a configuration property. + * By default this gets the value from META-INF/microprofile-config + * + * @param greetingConfig the configured greeting message + */ + @Inject + public GreetResource(GreetingProvider greetingConfig) { + this.greetingProvider = greetingConfig; + } + + /** + * Return a worldly greeting message. + * + * @return {@link jakarta.json.JsonObject} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Timed(name = TIMER_FOR_GETS, absolute = true) + public GreetingMessage getDefaultMessage() { + return createResponse("World"); + } + + /** + * Return a greeting message using the name that was provided. + * + * @param name the name to greet + * @return {@link jakarta.json.JsonObject} + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + @Timed(name = TIMER_FOR_GETS, absolute = true) + public GreetingMessage getMessage(@PathParam("name") String name) { + return createResponse(name); + } + + /** + * Set the greeting to use in future messages. + * + * @param message JSON containing the new greeting + * @return {@link Response} + */ + @Path("/greeting") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Counted(name = COUNTER_FOR_PERSONALIZED_GREETINGS, absolute = true) + public Response updateGreeting(GreetingMessage message) { + if (message.getMessage() == null) { + GreetingMessage entity = new GreetingMessage("No greeting provided"); + return Response.status(Response.Status.BAD_REQUEST).entity(entity).build(); + } + + greetingProvider.setMessage(message.getMessage()); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + private GreetingMessage createResponse(String who) { + String msg = String.format("%s %s!", greetingProvider.getMessage(), who); + + return new GreetingMessage(msg); + } +} diff --git a/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/GreetingMessage.java b/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/GreetingMessage.java new file mode 100644 index 000000000..23f8a6842 --- /dev/null +++ b/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/GreetingMessage.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.metrics.filtering.mp; + +/** + * POJO defining the greeting message content. + */ +@SuppressWarnings("unused") +public class GreetingMessage { + private String message; + + /** + * Create a new GreetingMessage instance. + */ + public GreetingMessage() { + } + + /** + * Create a new GreetingMessage instance. + * + * @param message message + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Gets the message value. + * + * @return message value + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message message value to set + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/GreetingProvider.java b/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/GreetingProvider.java new file mode 100644 index 000000000..84e9a7a67 --- /dev/null +++ b/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/GreetingProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.metrics.filtering.mp; + +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Provider for greeting message. + */ +@ApplicationScoped +public class GreetingProvider { + private final AtomicReference message = new AtomicReference<>(); + + /** + * Create a new greeting provider, reading the message from configuration. + * + * @param message greeting to use + */ + @Inject + public GreetingProvider(@ConfigProperty(name = "app.greeting") String message) { + this.message.set(message); + } + + String getMessage() { + return message.get(); + } + + void setMessage(String message) { + this.message.set(message); + } +} diff --git a/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/package-info.java b/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/package-info.java new file mode 100644 index 000000000..947e97fee --- /dev/null +++ b/examples/metrics/filtering/mp/src/main/java/io/helidon/examples/metrics/filtering/mp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon MicroProfile metrics filtering example. + */ +package io.helidon.examples.metrics.filtering.mp; diff --git a/examples/metrics/filtering/mp/src/main/resources/META-INF/beans.xml b/examples/metrics/filtering/mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e149ced7d --- /dev/null +++ b/examples/metrics/filtering/mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/metrics/filtering/mp/src/main/resources/META-INF/microprofile-config.properties b/examples/metrics/filtering/mp/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..8165ae06e --- /dev/null +++ b/examples/metrics/filtering/mp/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +app.greeting=Hello + +# Override configuration to use a random port for the unit tests +config_ordinal=1000 +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +metrics.scoping.scopes.0.name=application +metrics.scoping.scopes.0.filter.exclude = .*Gets diff --git a/examples/metrics/filtering/mp/src/main/resources/logging.properties b/examples/metrics/filtering/mp/src/main/resources/logging.properties new file mode 100644 index 000000000..d22eda763 --- /dev/null +++ b/examples/metrics/filtering/mp/src/main/resources/logging.properties @@ -0,0 +1,30 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.cors.level=INFO diff --git a/examples/metrics/filtering/mp/src/test/java/io/helidon/examples/metrics/filtering/mp/MainTest.java b/examples/metrics/filtering/mp/src/test/java/io/helidon/examples/metrics/filtering/mp/MainTest.java new file mode 100644 index 000000000..fc7189bfc --- /dev/null +++ b/examples/metrics/filtering/mp/src/test/java/io/helidon/examples/metrics/filtering/mp/MainTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.metrics.filtering.mp; + +import java.time.Duration; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.Timer; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@HelidonTest +public class MainTest { + + @Inject + private MetricRegistry appRegistry; + + @Test + void checkEnabledMetric() { + Counter personalizedGreetingsCounter = appRegistry.counter(GreetResource.COUNTER_FOR_PERSONALIZED_GREETINGS); + long before = personalizedGreetingsCounter.getCount(); + personalizedGreetingsCounter.inc(); + assertThat("Enabled counter value change", + personalizedGreetingsCounter.getCount() - before, is(1L)); + } + + @Test + void checkDisabledMetric() { + Timer getsTimer = appRegistry.timer(GreetResource.TIMER_FOR_GETS); + long before = getsTimer.getCount(); + getsTimer.update(Duration.ofSeconds(1)); + assertThat("Disabled timer value change", + getsTimer.getCount() - before, + is(0L)); + } +} diff --git a/examples/metrics/filtering/pom.xml b/examples/metrics/filtering/pom.xml new file mode 100644 index 000000000..62342195d --- /dev/null +++ b/examples/metrics/filtering/pom.xml @@ -0,0 +1,39 @@ + + + + + + helidon-examples-metrics-project + io.helidon.examples + 1.0.0-SNAPSHOT + + 4.0.0 + pom + + helidon-examples-metrics-filtering + Helidon Examples Metrics Filtering + 1.0.0-SNAPSHOT + + + se + mp + + diff --git a/examples/metrics/filtering/se/README.md b/examples/metrics/filtering/se/README.md new file mode 100644 index 000000000..bbbd3ebab --- /dev/null +++ b/examples/metrics/filtering/se/README.md @@ -0,0 +1,47 @@ +# Helidon Filtering Metrics SE Example + +This project implements a simple Hello World REST service using Helidon SE and demonstrates the +optional metrics exemplar support. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-metrics-se.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} + +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hola World!"} +``` + +## Retrieve application metrics + +```shell +# Prometheus format with exemplars + +curl -s -X GET http://localhost:8080/observe/metrics/application +``` +``` +# TYPE application_counterForPersonalizedGreetings_total counter +# HELP application_counterForPersonalizedGreetings_total +application_counterForPersonalizedGreetings_total 2 # {trace_id="78e61eed351f4c9d"} 1 1617812495.016000 +. . . +# TYPE application_timerForGets_mean_seconds gauge +application_timerForGets_mean_seconds 0.005772598385062112 # {trace_id="b22f13c37ba8b879"} 0.001563945 1617812578.687000 +# TYPE application_timerForGets_max_seconds gauge +application_timerForGets_max_seconds 0.028018165 # {trace_id="a1b127002725143c"} 0.028018165 1617812467.524000 +``` \ No newline at end of file diff --git a/examples/metrics/filtering/se/pom.xml b/examples/metrics/filtering/se/pom.xml new file mode 100644 index 000000000..2528852cb --- /dev/null +++ b/examples/metrics/filtering/se/pom.xml @@ -0,0 +1,117 @@ + + + + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + 4.0.0 + + io.helidon.examples.metrics.filtering + helidon-examples-metrics-se + 1.0.0-SNAPSHOT + Helidon Examples Metrics SE Filtering + + io.helidon.examples.metrics.filtering.se.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.metrics + helidon-metrics-api + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + check-for-inflight-with-config-settings + + test + + + + true + + + + + + + + diff --git a/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/GreetService.java b/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/GreetService.java new file mode 100644 index 000000000..7668e5956 --- /dev/null +++ b/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/GreetService.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.metrics.filtering.se; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Timer; +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.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple service to greet you. Examples: + *

+ * Get default greeting message: + * curl -X GET http://localhost:8080/greet + *

+ * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + *

+ * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + *

+ * The message is returned as a JSON object + */ + +public class GreetService implements HttpService { + + /** + * The config value for the key {@code greeting}. + */ + private final AtomicReference greeting = new AtomicReference<>(); + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + static final String TIMER_FOR_GETS = "timerForGets"; + static final String COUNTER_FOR_PERSONALIZED_GREETINGS = "counterForPersonalizedGreetings"; + + private final Timer timerForGets; + private final Counter personalizedGreetingsCounter; + + GreetService(MeterRegistry meterRegistry) { + Config config = Config.global(); + greeting.set(config.get("app.greeting").asString().orElse("Ciao")); + timerForGets = meterRegistry.getOrCreate(Timer.builder(TIMER_FOR_GETS)); + personalizedGreetingsCounter = meterRegistry.getOrCreate(Counter.builder(COUNTER_FOR_PERSONALIZED_GREETINGS)); + } + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.get("/", this::timeGet) + .get("/{name}", this::countPersonalized, 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().get("name"); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + String msg = String.format("%s %s!", greeting.get(), name); + + JsonObject returnObject = JSON.createObjectBuilder() + .add("message", msg) + .build(); + response.send(returnObject); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + + if (!jo.containsKey("greeting")) { + JsonObject jsonErrorObject = JSON.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting.set(jo.getString("greeting")); + response.status(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) { + JsonObject jsonObject = request.content().as(JsonObject.class); + updateGreetingFromJson(jsonObject, response); + } + + private void timeGet(ServerRequest request, ServerResponse response) { + timerForGets.record(() -> getDefaultMessageHandler(request, response)); + } + + private void countPersonalized(ServerRequest request, ServerResponse response) { + personalizedGreetingsCounter.increment(); + response.next(); + } +} diff --git a/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/Main.java b/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/Main.java new file mode 100644 index 000000000..5f8209503 --- /dev/null +++ b/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/Main.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.metrics.filtering.se; + +import java.util.regex.Pattern; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.metrics.api.Meter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.MetricsFactory; +import io.helidon.metrics.api.ScopeConfig; +import io.helidon.metrics.api.ScopingConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.metrics.MetricsObserver; + +/** + * The application main class. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(final String[] args) { + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build().start(); + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + /** + * Set up the server. + * + * @param server server builder + */ + static void setup(WebServerConfig.Builder server) { + + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + Config.global(config); + + // Programmatically (not through config), tell the metrics feature to ignore the "gets" timer. + // To do so, create the scope config, then add it to the metrics config that ultimately + // the metrics feature class will use. + ScopeConfig scopeConfig = ScopeConfig.builder() + .name(Meter.Scope.APPLICATION) + .exclude(Pattern.compile(GreetService.TIMER_FOR_GETS)) + .build(); + + MetricsConfig initialMetricsConfig = config.get(MetricsConfig.METRICS_CONFIG_KEY) + .map(MetricsConfig::create) + .orElseGet(MetricsConfig::create); + MetricsConfig.Builder metricsConfigBuilder = MetricsConfig.builder(initialMetricsConfig) + .scoping(ScopingConfig.builder() + .putScope(Meter.Scope.APPLICATION, scopeConfig)); + + MeterRegistry meterRegistry = MetricsFactory.getInstance(config).globalRegistry(metricsConfigBuilder.build()); + + MetricsObserver metrics = MetricsObserver.builder() + .meterRegistry(meterRegistry) + .metricsConfig(metricsConfigBuilder) + .build(); + + server.featuresDiscoverServices(false) + .addFeature(ObserveFeature.just(metrics)) + .config(config.get("server")) + .routing(r -> routing(r, meterRegistry)); + } + + /** + * Set up routing. + * + * @param routing routing builder + */ + static void routing(HttpRouting.Builder routing, MeterRegistry meterRegistry) { + GreetService greetService = new GreetService(meterRegistry); + + routing.register("/greet", greetService); + } +} diff --git a/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/package-info.java b/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/package-info.java new file mode 100644 index 000000000..2767bc4a8 --- /dev/null +++ b/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Helidon SE example of controlling metrics using filtering. + */ +package io.helidon.examples.metrics.filtering.se; diff --git a/examples/metrics/filtering/se/src/main/resources/application.yaml b/examples/metrics/filtering/se/src/main/resources/application.yaml new file mode 100644 index 000000000..39657caed --- /dev/null +++ b/examples/metrics/filtering/se/src/main/resources/application.yaml @@ -0,0 +1,25 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + greeting: "Hello" + +server: + port: 8080 + host: 0.0.0.0 + +tracing: + service: "hello-world" diff --git a/examples/metrics/filtering/se/src/main/resources/logging.properties b/examples/metrics/filtering/se/src/main/resources/logging.properties new file mode 100644 index 000000000..c916a0505 --- /dev/null +++ b/examples/metrics/filtering/se/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/metrics/filtering/se/src/test/java/io/helidon/examples/metrics/filtering/se/MainTest.java b/examples/metrics/filtering/se/src/test/java/io/helidon/examples/metrics/filtering/se/MainTest.java new file mode 100644 index 000000000..4a55a46de --- /dev/null +++ b/examples/metrics/filtering/se/src/test/java/io/helidon/examples/metrics/filtering/se/MainTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.metrics.filtering.se; + +import java.util.Collections; + +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 jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +@ServerTest +public class MainTest { + + private static final JsonBuilderFactory JSON_BUILDER = Json.createBuilderFactory(Collections.emptyMap()); + private static final JsonObject TEST_JSON_OBJECT = JSON_BUILDER.createObjectBuilder() + .add("greeting", "Hola") + .build(); + + private final Http1Client client; + + public MainTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + Main.setup(server); + } + + @Test + public void testHelloWorld() { + try (Http1ClientResponse response = client.get("/greet").request()) { + assertThat(response.as(JsonObject.class).getString("message"), CoreMatchers.is("Hello World!")); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(JsonObject.class).getString("message"), CoreMatchers.is("Hello Joe!")); + } + + try (Http1ClientResponse response = client.put("/greet/greeting").submit(TEST_JSON_OBJECT)) { + assertThat(response.status().code(), CoreMatchers.is(204)); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(JsonObject.class).getString("message"), CoreMatchers.is("Hola Joe!")); + } + + try (Http1ClientResponse response = client.get("/observe/metrics").request()) { + assertThat(response.status().code(), CoreMatchers.is(200)); + } + } + + @Test + public void testMetrics() { + try (Http1ClientResponse response = client.get("/greet").request()) { + assertThat(response.as(String.class), containsString("Hello World!")); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(String.class), containsString("Hello Joe!")); + } + + try (Http1ClientResponse response = client.get("/observe/metrics/application").request()) { + String openMetricsOutput = response.as(String.class); + assertThat("Metrics output", openMetricsOutput, not(containsString(GreetService.TIMER_FOR_GETS))); + assertThat("Metrics output", openMetricsOutput, containsString(GreetService.COUNTER_FOR_PERSONALIZED_GREETINGS)); + } + } +} diff --git a/examples/metrics/http-status-count-se/README.md b/examples/metrics/http-status-count-se/README.md new file mode 100644 index 000000000..68efb9c9c --- /dev/null +++ b/examples/metrics/http-status-count-se/README.md @@ -0,0 +1,101 @@ +# http-status-count-se + +This Helidon SE project illustrates a service which updates a family of counters based on the HTTP status returned in each response. + +The main source in this example is identical to that in the Helidon SE QuickStart application except in these ways: +* The `HttpStatusMetricService` class creates and updates the status metrics. +* The `Main` class has a two small enhancements: + * The `createRouting` method instantiates `HttpStatusMetricService` and sets up routing for it. + * The `startServer` method has an additional variant to simplify a new unit test. + +## Incorporating status metrics into your own application +Use this example for inspiration in writing your own service or just use the `HttpStatusMetricService` directly in your own application. + +1. Copy and paste the `HttpStatusMetricService` class into your application, adjusting the package declaration as needed. +2. Register routing for an instance of `HttpStatusMetricService`, as shown here: + ```java + Routing.Builder builder = Routing.builder() + ... + .register(HttpStatusMetricService.create() + ... + ``` + +## Build and run + + +```shell +mvn package +java -jar target/http-status-count-se.jar +``` + +## Exercise the application +```shell +curl -X GET http://localhost:8080/simple-greet +``` +```listing +{"message":"Hello World!"} +``` + +```shell +curl -X GET http://localhost:8080/greet +``` +```listing +{"message":"Hello World!"} +``` +```shell +curl -X GET http://localhost:8080/greet/Joe +``` +```listing +{"message":"Hello Joe!"} +``` +```shell +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +``` +```listing +{"message":"Hola Jose!"} +``` + +## Try metrics +```shell +# Prometheus Format +curl -s -X GET http://localhost:8080/observe/metrics/application +``` + +```listing +... +# TYPE application_httpStatus_total counter +# HELP application_httpStatus_total Counts the number of HTTP responses in each status category (1xx, 2xx, etc.) +application_httpStatus_total{range="1xx"} 0 +application_httpStatus_total{range="2xx"} 5 +application_httpStatus_total{range="3xx"} 0 +application_httpStatus_total{range="4xx"} 0 +application_httpStatus_total{range="5xx"} 0 +... +``` +# JSON Format + +```shell +curl -H "Accept: application/json" -X GET http://localhost:8080/observe/metrics +``` +```json +{ +... + "httpStatus;range=1xx": 0, + "httpStatus;range=2xx": 5, + "httpStatus;range=3xx": 0, + "httpStatus;range=4xx": 0, + "httpStatus;range=5xx": 0, +... +``` + +## Try health + +```shell +curl -s -X GET http://localhost:8080/observe/health +``` +```listing +{"outcome":"UP",... + +``` diff --git a/examples/metrics/http-status-count-se/pom.xml b/examples/metrics/http-status-count-se/pom.xml new file mode 100644 index 000000000..1571ad281 --- /dev/null +++ b/examples/metrics/http-status-count-se/pom.xml @@ -0,0 +1,112 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples + http-status-count-se + 1.0.0-SNAPSHOT + + Helidon Examples Metrics HTTP Status Counters + + + io.helidon.examples.se.httpstatuscount.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.config + helidon-config-yaml + + + io.helidon.metrics + helidon-metrics-api + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.health + helidon-health-checks + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/GreetService.java b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/GreetService.java new file mode 100644 index 000000000..4604ca362 --- /dev/null +++ b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/GreetService.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.se.httpstatuscount; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.config.Config; +import io.helidon.http.Status; +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.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple service to greet you. Examples: + *

+ * Get default greeting message: + * curl -X GET http://localhost:8080/greet + *

+ * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + *

+ * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + *

+ * The message is returned as a JSON object + */ + +public class GreetService implements HttpService { + + /** + * The config value for the key {@code greeting}. + */ + private final AtomicReference greeting = new AtomicReference<>(); + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + GreetService() { + Config config = Config.global(); + greeting.set(config.get("app.greeting").asString().orElse("Ciao")); + } + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.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().get("name"); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + String msg = String.format("%s %s!", greeting.get(), name); + + JsonObject returnObject = JSON.createObjectBuilder() + .add("message", msg) + .build(); + response.send(returnObject); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + if (!jo.containsKey("greeting")) { + JsonObject jsonErrorObject = JSON.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting.set(jo.getString("greeting")); + response.status(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) { + JsonObject jsonObject = request.content().as(JsonObject.class); + updateGreetingFromJson(jsonObject, response); + } +} diff --git a/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/HttpStatusMetricService.java b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/HttpStatusMetricService.java new file mode 100644 index 000000000..09ee79e51 --- /dev/null +++ b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/HttpStatusMetricService.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.se.httpstatuscount; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.Tag; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * 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 io.helidon.metrics.api.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 String COUNTER_DESCR = "Counts the number of HTTP responses in each status category (1xx, 2xx, etc.)"; + + private static final AtomicInteger IN_PROGRESS = new AtomicInteger(); + + private final Counter[] responseCounters = new Counter[6]; + + static HttpStatusMetricService create() { + return new HttpStatusMetricService(); + } + + private HttpStatusMetricService() { + MeterRegistry registry = Metrics.globalRegistry(); + + // Declare the counters and keep references to them. + for (int i = 1; i < responseCounters.length; i++) { + + responseCounters[i] = registry.getOrCreate(Counter.builder(STATUS_COUNTER_NAME) + .tags(Set.of(Tag.create(STATUS_TAG_NAME, i + "xx"))) + .description(COUNTER_DESCR)); + } + } + + @Override + public void routing(HttpRules rules) { + rules.any(this::updateRange); + } + + // for testing + static boolean isInProgress() { + return IN_PROGRESS.get() != 0; + } + + private void updateRange(ServerRequest request, ServerResponse response) { + IN_PROGRESS.incrementAndGet(); + response.whenSent(() -> logMetric(response)); + response.next(); + } + + private void logMetric(ServerResponse response) { + int range = response.status().code() / 100; + if (range > 0 && range < responseCounters.length) { + responseCounters[range].increment(); + } + IN_PROGRESS.decrementAndGet(); + } +} diff --git a/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/Main.java b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/Main.java new file mode 100644 index 000000000..582898a31 --- /dev/null +++ b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/Main.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.se.httpstatuscount; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * The application main class. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(final String[] args) { + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build().start(); + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + /** + * Set up the server. + * + * @param server server builder + */ + static void setup(WebServerConfig.Builder server) { + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + Config.global(config); + + server.routing(Main::routing) + .config(config.get("server")); + + } + + /** + * Set up routing. + * + * @param routing routing builder + */ + static void routing(HttpRouting.Builder routing) { + routing.register(HttpStatusMetricService.create()) // no endpoint, just metrics updates + .register("/simple-greet", new SimpleGreetService()) + .register("/greet", new GreetService()); + } +} diff --git a/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/SimpleGreetService.java b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/SimpleGreetService.java new file mode 100644 index 000000000..537250370 --- /dev/null +++ b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/SimpleGreetService.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.se.httpstatuscount; + +import java.util.Collections; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +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.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple service to greet you. Examples: + *

+ * Get default greeting message: + * curl -X GET http://localhost:8080/simple-greet + *

+ * The message is returned as a JSON object + */ +public class SimpleGreetService implements HttpService { + + private static final Logger LOGGER = Logger.getLogger(SimpleGreetService.class.getName()); + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + private final MeterRegistry registry = Metrics.globalRegistry(); + private final Counter accessCtr = registry.getOrCreate(Counter.builder("accessctr")); + + private final String greeting; + + SimpleGreetService() { + Config config = Config.global(); + greeting = config.get("app.greeting").asString().orElse("Ciao"); + } + + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.get("/", this::getDefaultMessageHandler); + rules.get("/greet-count", this::countAccess, this::getDefaultMessageHandler); + } + + /** + * Return a worldly greeting message. + * + * @param request the server request + * @param response the server response + */ + private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) { + String msg = String.format("%s %s!", greeting, "World"); + LOGGER.info("Greeting message is " + msg); + JsonObject returnObject = JSON.createObjectBuilder() + .add("message", msg) + .build(); + response.send(returnObject); + } + + + private void countAccess(ServerRequest request, ServerResponse response) { + accessCtr.increment(); + response.next(); + } +} diff --git a/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/package-info.java b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/package-info.java new file mode 100644 index 000000000..9baae4e62 --- /dev/null +++ b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * HTTP status count example. + */ +package io.helidon.examples.se.httpstatuscount; diff --git a/examples/metrics/http-status-count-se/src/main/resources/application.yaml b/examples/metrics/http-status-count-se/src/main/resources/application.yaml new file mode 100644 index 000000000..4f5711771 --- /dev/null +++ b/examples/metrics/http-status-count-se/src/main/resources/application.yaml @@ -0,0 +1,22 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + +app: + greeting: "Hello" diff --git a/examples/metrics/http-status-count-se/src/main/resources/logging.properties b/examples/metrics/http-status-count-se/src/main/resources/logging.properties new file mode 100644 index 000000000..bf3401a8c --- /dev/null +++ b/examples/metrics/http-status-count-se/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/MainTest.java b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/MainTest.java new file mode 100644 index 000000000..4c1a87659 --- /dev/null +++ b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/MainTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.se.httpstatuscount; + +import java.util.Collections; + +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 jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +@TestMethodOrder(MethodOrderer.MethodName.class) +@ServerTest +public class MainTest { + + private static final JsonBuilderFactory JSON_BUILDER = Json.createBuilderFactory(Collections.emptyMap()); + private static final JsonObject TEST_JSON_OBJECT = JSON_BUILDER.createObjectBuilder() + .add("greeting", "Hola") + .build(); + + private final Http1Client client; + + public MainTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + Main.setup(server); + } + + @Test + public void testMicroprofileMetrics() { + try (Http1ClientResponse response = client.get("/simple-greet/greet-count").request()) { + assertThat(response.as(String.class), containsString("Hello World!")); + } + + try (Http1ClientResponse response = client.get("/observe/metrics").request()) { + assertThat("Metrics output", response.as(String.class), containsString("accessctr_total")); + } + } + + @Test + public void testMetrics() { + try (Http1ClientResponse response = client.get("/observe/metrics").request()) { + assertThat(response.status().code(), is(200)); + } + } + + @Test + public void testHealth() { + try (Http1ClientResponse response = client.get("/observe/health").request()) { + assertThat(response.status().code(), is(204)); + } + } + + @Test + public void testSimpleGreet() { + try (Http1ClientResponse response = client.get("/simple-greet").request()) { + assertThat(response.as(JsonObject.class).getString("message"), is("Hello World!")); + } + } + + @Test + public void testGreetings() { + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(JsonObject.class).getString("message"), is("Hello Joe!")); + } + + try (Http1ClientResponse response = client.put("/greet/greeting").submit(TEST_JSON_OBJECT)) { + assertThat(response.status().code(), is(204)); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(JsonObject.class).getString("message"), is("Hola Joe!")); + } + } +} diff --git a/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusService.java b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusService.java new file mode 100644 index 000000000..c4fce31ac --- /dev/null +++ b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusService.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.se.httpstatuscount; + +import io.helidon.http.Status; +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().get("status"); + int status; + String msg; + try { + status = Integer.parseInt(statusText); + msg = "Successful conversion"; + } catch (NumberFormatException ex) { + status = Status.INTERNAL_SERVER_ERROR_500.code(); + msg = "Unsuccessful conversion"; + } + Status httpStatus = Status.create(status); + response.status(status); + if (httpStatus != Status.NO_CONTENT_204) { + response.send(msg); + } else { + response.send(); + } + } +} diff --git a/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusTest.java b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusTest.java new file mode 100644 index 000000000..39679ff4c --- /dev/null +++ b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.se.httpstatuscount; + +import java.util.Set; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.Tag; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.examples.se.httpstatuscount.HttpStatusMetricService.STATUS_COUNTER_NAME; +import static io.helidon.examples.se.httpstatuscount.HttpStatusMetricService.STATUS_TAG_NAME; +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 +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) { + Config.global(Config.create()); + server.routing(r -> { + Main.routing(r); + r.register("/status", new StatusService()); + }); + } + + @BeforeEach + void findStatusMetrics() { + MeterRegistry meterRegistry = Metrics.globalRegistry(); + for (int i = 1; i < STATUS_COUNTERS.length; i++) { + STATUS_COUNTERS[i] = meterRegistry.getOrCreate(Counter.builder(STATUS_COUNTER_NAME) + .tags(Set.of(Tag.create(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].count(); + } + 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].count(); + } + try (Http1ClientResponse response = client.get("/status/" + status.code()) + .accept(MediaTypes.APPLICATION_JSON) + .followRedirects(false) + .request()) { + assertThat("Response status", response.status().code(), is(status.code())); + checkCounters(status, before); + } + } + + @SuppressWarnings("BusyWait") + private void checkCounters(Status status, long[] before) throws InterruptedException { + // Make sure the server has updated the counter(s). + 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].count() - before[i], is(expectedDiff)); + } + } +} diff --git a/examples/metrics/http-status-count-se/src/test/resources/application.yaml b/examples/metrics/http-status-count-se/src/test/resources/application.yaml new file mode 100644 index 000000000..4f5711771 --- /dev/null +++ b/examples/metrics/http-status-count-se/src/test/resources/application.yaml @@ -0,0 +1,22 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + +app: + greeting: "Hello" diff --git a/examples/metrics/kpi/README.md b/examples/metrics/kpi/README.md new file mode 100644 index 000000000..6584578fb --- /dev/null +++ b/examples/metrics/kpi/README.md @@ -0,0 +1,125 @@ +# Helidon Metrics Key Performance Indicators SE Example + +This project implements a simple Hello World REST service using Helidon SE and demonstrates +support in Helidon for extended key performance indicator (KPI) metrics. + +Your application can set up KPI metrics either programmatically or using configuration. +The `Main` class of this example shows both techniques, checking the system property `useConfig` to +determine +which to use. +You would typically write any given application to use only one of the approaches. + +## Build and run + +```shell +mvn package +``` +To use programmatic set-up: +```shell +java -jar target/helidon-examples-metrics-kpi.jar +``` +To use configuration: +```shell +java -DuseConfig=true -jar target/helidon-examples-metrics-kpi.jar +```` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} + +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hola World!"} +``` + +## Retrieve vendor metrics with key performance indicators + +For brevity, the example output below shows only some of the KPI metrics. + +Note that, even though the `curl` commands above access `/greet` endpoints five times, +the +`load` and (not shown here) +`count` and +`meter` are `6` in the Prometheus output example and `7` in the JSON output example. +Further, the `inFlight` +current value is `1`. + +This is +because Helidon tallies +all +requests, even those +to Helidon-provided services such as `/metrics` and `/health`, in the KPI metrics. +The request +to retrieve the metrics is the one that is in flight, and it contributes to the KPI metrics just +as requests to application endpoints do. + +Further, the request to `/metrics` is still in progress when Helidon prepares the +output by getting the values of the KPI metrics at that moment. +If _that_ request turns out to be long- running, Helidon would discover so only +_after_ preparing the metrics output and completing the request. +The `longRunning` `Meter` +values in _that_ +response +could +not reflect the fact that Helidon would subsequently conclude that _that_ request was +long-running. + +## Prometheus format +```shell +curl -s -X GET http://localhost:8080/observe/metrics/vendor +``` +``` +... +# TYPE vendor_requests_inFlight_current concurrent gauge +# HELP vendor_requests_inFlight_current Measures the number of currently in-flight requests +vendor_requests_inFlight_current 1 +# TYPE vendor_requests_inFlight_min concurrent gauge +vendor_requests_inFlight_min 0 +# TYPE vendor_requests_inFlight_max concurrent gauge +vendor_requests_inFlight_max 1 +# TYPE vendor_requests_load_total counter +# HELP vendor_requests_load_total Measures the total number of in-flight requests and rates at which they occur +vendor_requests_load_total 6 +# TYPE vendor_requests_load_rate_per_second gauge +vendor_requests_load_rate_per_second 0.04932913209653636 +# TYPE vendor_requests_load_one_min_rate_per_second gauge +vendor_requests_load_one_min_rate_per_second 0.025499793037824785 +# TYPE vendor_requests_load_five_min_rate_per_second gauge +vendor_requests_load_five_min_rate_per_second 0.012963147773962286 +# TYPE vendor_requests_load_fifteen_min_rate_per_second gauge +vendor_requests_load_fifteen_min_rate_per_second 0.005104944851522425 +... +``` +## JSON output + + +```shell +curl -s -X GET -H "Accept: application/json" http://localhost:8080/observe/metrics/vendor +``` +``` +{ + ... + "requests.inFlight": { + "current": 1, + "max": 1, + "min": 0 + }, + "requests.load": { + "count": 7, + "meanRate": 0.01530869471741443, + "oneMinRate": 0.00016123154886115814, + "fiveMinRate": 0.005344110443653005, + "fifteenMinRate": 0.004286243527303867 + }, + ... +} +``` diff --git a/examples/metrics/kpi/pom.xml b/examples/metrics/kpi/pom.xml new file mode 100644 index 000000000..6912a76fa --- /dev/null +++ b/examples/metrics/kpi/pom.xml @@ -0,0 +1,119 @@ + + + + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + 4.0.0 + + io.helidon.examples.metrics + helidon-examples-metrics-kpi + 1.0.0-SNAPSHOT + Helidon Examples Metrics Key Performance Indicators + + + io.helidon.examples.metrics.kpi.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.metrics + helidon-metrics-trace-exemplar + + + io.helidon.config + helidon-config-yaml + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + check-for-inflight-with-config-settings + + test + + + + true + + + + + + + + diff --git a/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/GreetService.java b/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/GreetService.java new file mode 100644 index 000000000..5537ede76 --- /dev/null +++ b/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/GreetService.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.metrics.kpi; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.Meter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.Timer; +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.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple service to greet you. Examples: + *

+ * Get default greeting message: + * curl -X GET http://localhost:8080/greet + *

+ * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + *

+ * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + *

+ * The message is returned as a JSON object + */ + +public class GreetService implements HttpService { + + /** + * The config value for the key {@code greeting}. + */ + private final AtomicReference greeting = new AtomicReference<>(); + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + static final String TIMER_FOR_GETS = "timerForGets"; + static final String COUNTER_FOR_PERSONALIZED_GREETINGS = "counterForPersonalizedGreetings"; + + private final Timer timerForGets; + + private final Counter personalizedGreetingsCounter; + + GreetService() { + Config config = Config.global(); + greeting.set(config.get("app.greeting").asString().orElse("Ciao")); + MeterRegistry meterRegistry = Metrics.globalRegistry(); + timerForGets = meterRegistry.getOrCreate(Timer.builder(TIMER_FOR_GETS) + .baseUnit(Meter.BaseUnits.NANOSECONDS)); + personalizedGreetingsCounter = meterRegistry.getOrCreate(Counter.builder(COUNTER_FOR_PERSONALIZED_GREETINGS)); + } + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.get("/", this::timeGet, this::getDefaultMessageHandler) + .get("/{name}", this::countPersonalized, 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().get("name"); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + String msg = String.format("%s %s!", greeting.get(), name); + + JsonObject returnObject = JSON.createObjectBuilder() + .add("message", msg) + .build(); + response.send(returnObject); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + + if (!jo.containsKey("greeting")) { + JsonObject jsonErrorObject = JSON.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting.set(jo.getString("greeting")); + response.status(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) { + JsonObject jsonObject = request.content().as(JsonObject.class); + updateGreetingFromJson(jsonObject, response); + } + + private void timeGet(ServerRequest request, ServerResponse response) { + timerForGets.record((Runnable) response::next); + } + + private void countPersonalized(ServerRequest request, ServerResponse response) { + personalizedGreetingsCounter.increment(); + response.next(); + } +} diff --git a/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/Main.java b/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/Main.java new file mode 100644 index 000000000..253e1151b --- /dev/null +++ b/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/Main.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.metrics.kpi; + +import java.time.Duration; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.metrics.MetricsObserver; + +/** + * The application main class. + */ +public final class Main { + + static final String USE_CONFIG_PROPERTY_NAME = "useConfig"; + + static final boolean USE_CONFIG = Boolean.getBoolean(USE_CONFIG_PROPERTY_NAME); + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(final String[] args) { + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build().start(); + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + /** + * Set up the server. + * + * @param server server builder + */ + static void setup(WebServerConfig.Builder server) { + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + Config.global(config); + + /* + * For purposes of illustration, the key performance indicator settings for the + * MetricsSupport instance are set up according to a system property, so you can see, + * in one example, how to code each approach. Normally, you would choose one + * approach to use in an application. + */ + MetricsObserver metricsObserver = USE_CONFIG + ? metricsSupportWithConfig(config.get("metrics")) + : metricsSupportWithoutConfig(); + + + server.addFeature(ObserveFeature.just(metricsObserver)) + .routing(Main::routing) + .config(config.get("server")); + + } + + /** + * Setup routing. + * + * @param routing routing builder + */ + private static void routing(HttpRouting.Builder routing) { + + routing.register("/greet", new GreetService()); + } + + /** + * Creates a {@link MetricsObserver} instance using a "metrics" configuration node. + * + * @param metricsConfig {@link Config} node with key "metrics" if present; an empty node otherwise + * @return {@code MetricsSupport} object with metrics (including KPI) set up using the config node + */ + private static MetricsObserver metricsSupportWithConfig(Config metricsConfig) { + return MetricsObserver.create(metricsConfig); + } + + /** + * Creates a {@link MetricsObserver} instance explicitly turning on extended KPI metrics. + * + * @return {@code MetricsSupport} object with extended KPI metrics enabled + */ + private static MetricsObserver metricsSupportWithoutConfig() { + KeyPerformanceIndicatorMetricsConfig.Builder configBuilder = + KeyPerformanceIndicatorMetricsConfig.builder() + .extended(true) + .longRunningRequestThreshold(Duration.ofSeconds(2)); + return MetricsObserver.builder() + .metricsConfig(MetricsConfig.builder() + .keyPerformanceIndicatorMetricsConfig(configBuilder)) + .build(); + } +} diff --git a/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/package-info.java b/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/package-info.java new file mode 100644 index 000000000..d9fd1cf74 --- /dev/null +++ b/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quickstart demo application for key performance metrics. + *

+ * Start with {@link io.helidon.examples.metrics.kpi.Main} class. + *

+ * @see io.helidon.examples.metrics.kpi.Main + */ +package io.helidon.examples.metrics.kpi; diff --git a/examples/metrics/kpi/src/main/resources/application.yaml b/examples/metrics/kpi/src/main/resources/application.yaml new file mode 100644 index 000000000..08d322111 --- /dev/null +++ b/examples/metrics/kpi/src/main/resources/application.yaml @@ -0,0 +1,28 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + greeting: "Hello" + +server: + port: 8080 + host: 0.0.0.0 + +metrics: + key-performance-indicators: + extended: true + long-running: + threshold-ms: 2000 # two seconds diff --git a/examples/metrics/kpi/src/main/resources/logging.properties b/examples/metrics/kpi/src/main/resources/logging.properties new file mode 100644 index 000000000..c916a0505 --- /dev/null +++ b/examples/metrics/kpi/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/metrics/kpi/src/test/java/io/helidon/examples/metrics/kpi/MainTest.java b/examples/metrics/kpi/src/test/java/io/helidon/examples/metrics/kpi/MainTest.java new file mode 100644 index 000000000..e40b82f88 --- /dev/null +++ b/examples/metrics/kpi/src/test/java/io/helidon/examples/metrics/kpi/MainTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.metrics.kpi; + +import java.util.Collections; + +import io.helidon.metrics.api.Meter; +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 jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + +@ServerTest +public class MainTest { + + private static final String KPI_REGISTRY_TYPE = Meter.Scope.VENDOR; + private static final JsonBuilderFactory JSON_BUILDER = Json.createBuilderFactory(Collections.emptyMap()); + private static final JsonObject TEST_JSON_OBJECT = JSON_BUILDER.createObjectBuilder() + .add("greeting", "Hola") + .build(); + + private final Http1Client client; + + public MainTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + Main.setup(server); + } + + @Test + public void testHelloWorld() { + try (Http1ClientResponse response = client.get("/greet").request()) { + assertThat(response.as(JsonObject.class).getString("message"), is("Hello World!")); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(JsonObject.class).getString("message"), is("Hello Joe!")); + } + + try (Http1ClientResponse response = client.put("/greet/greeting").submit(TEST_JSON_OBJECT)) { + assertThat(response.status().code(), is(204)); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + assertThat(response.as(JsonObject.class).getString("message"), is("Hola Joe!")); + } + + try (Http1ClientResponse response = client.get("/observe/metrics").request()) { + assertThat(response.status().code(), is(200)); + } + } + + @Test + public void testMetrics() { + try (Http1ClientResponse response = client.get("/greet").request()) { + assertThat(response.as(String.class), containsString("Hello World!")); + } + + try (Http1ClientResponse response = client.get("/greet/Joe") + .request()) { + assertThat(response.as(String.class), containsString("Hello Joe!")); + } + + try (Http1ClientResponse response = client.get("/observe/metrics/" + KPI_REGISTRY_TYPE).request()) { + assertThat("Returned metrics output", response.as(String.class), + containsString("# TYPE " + "requests_inFlight")); + } + } +} diff --git a/examples/metrics/pom.xml b/examples/metrics/pom.xml new file mode 100644 index 000000000..002034b42 --- /dev/null +++ b/examples/metrics/pom.xml @@ -0,0 +1,42 @@ + + + + + + helidon-examples-project + io.helidon.examples + 1.0.0-SNAPSHOT + + 4.0.0 + pom + + helidon-examples-metrics-project + 1.0.0-SNAPSHOT + Helidon Examples Metrics + + + exemplar + kpi + filtering + http-status-count-se + + + diff --git a/examples/microprofile/README.md b/examples/microprofile/README.md new file mode 100644 index 000000000..f678273b5 --- /dev/null +++ b/examples/microprofile/README.md @@ -0,0 +1,3 @@ +# Helidon MP Examples + +This directory contains Helidon MP examples. diff --git a/examples/microprofile/bean-validation/README.md b/examples/microprofile/bean-validation/README.md new file mode 100644 index 000000000..0d6a80a35 --- /dev/null +++ b/examples/microprofile/bean-validation/README.md @@ -0,0 +1,39 @@ +# Helidon MP Bean Validation Example + +This example implements a simple Hello World REST service using MicroProfile demonstrating Bean Validation. + +## Usage + +To be able to use bean validation add the following dependency: + +```xml + + io.helidon.microprofile.bean-validation + helidon-microprofile-bean-validation + +``` + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-bean-validation.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/valid/test@test.com +#Output: {"Valid test@test.com!"} + +curl -X GET -I http://localhost:8080/valid/null + +``` +``` +HTTP/1.1 400 Bad Request +Content-Type: application/json +transfer-encoding: chunked +connection: keep-alive + + +``` \ No newline at end of file diff --git a/examples/microprofile/bean-validation/pom.xml b/examples/microprofile/bean-validation/pom.xml new file mode 100644 index 000000000..d01320dc4 --- /dev/null +++ b/examples/microprofile/bean-validation/pom.xml @@ -0,0 +1,95 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-bean-validation + 1.0.0-SNAPSHOT + Helidon Examples Bean Validation + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.microprofile.bean-validation + helidon-microprofile-bean-validation + + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.smallrye + jandex + runtime + true + + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/bean-validation/src/main/java/io/helidon/examples/microprofile/bean/validation/ValidEmailResource.java b/examples/microprofile/bean-validation/src/main/java/io/helidon/examples/microprofile/bean/validation/ValidEmailResource.java new file mode 100644 index 000000000..6bf98b01d --- /dev/null +++ b/examples/microprofile/bean-validation/src/main/java/io/helidon/examples/microprofile/bean/validation/ValidEmailResource.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.bean.validation; + +import java.util.Collections; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.validation.constraints.Email; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + + +/** + * A simple JAX-RS resource to validate email. + * Examples: + * + * Get valid response: + * curl -X GET http://localhost:8080/valid/e@mail.com + * + * Test failed response: + * curl -X GET http://localhost:8080/valid/email + */ +@Path("/valid") +@ApplicationScoped +public class ValidEmailResource { + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + /** + * Return a greeting message using the name that was provided. + * + * @param email the name to validate + * @return {@link JsonObject} + */ + @Path("/{email}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public JsonObject getMessage(@PathParam("email") @Email String email) { + return createResponse(email); + } + + + private JsonObject createResponse(String who) { + String msg = String.format("%s %s!", "Valid", who); + + return JSON.createObjectBuilder() + .add("message", msg) + .build(); + } +} diff --git a/examples/microprofile/bean-validation/src/main/java/io/helidon/examples/microprofile/bean/validation/package-info.java b/examples/microprofile/bean-validation/src/main/java/io/helidon/examples/microprofile/bean/validation/package-info.java new file mode 100644 index 000000000..6f3e6351d --- /dev/null +++ b/examples/microprofile/bean-validation/src/main/java/io/helidon/examples/microprofile/bean/validation/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Bean Validation example. + */ +package io.helidon.examples.microprofile.bean.validation; diff --git a/examples/microprofile/bean-validation/src/main/resources/META-INF/beans.xml b/examples/microprofile/bean-validation/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..292cd41a4 --- /dev/null +++ b/examples/microprofile/bean-validation/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/bean-validation/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/bean-validation/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..b399d005e --- /dev/null +++ b/examples/microprofile/bean-validation/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 diff --git a/examples/microprofile/bean-validation/src/main/resources/logging.properties b/examples/microprofile/bean-validation/src/main/resources/logging.properties new file mode 100644 index 000000000..ef323f89b --- /dev/null +++ b/examples/microprofile/bean-validation/src/main/resources/logging.properties @@ -0,0 +1,30 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Quiet Weld +org.jboss.level=WARNING diff --git a/examples/microprofile/bean-validation/src/test/java/io/helidon/tests/integration/bean/validation/TestValidationEndpoint.java b/examples/microprofile/bean-validation/src/test/java/io/helidon/tests/integration/bean/validation/TestValidationEndpoint.java new file mode 100644 index 000000000..83a6ecdff --- /dev/null +++ b/examples/microprofile/bean-validation/src/test/java/io/helidon/tests/integration/bean/validation/TestValidationEndpoint.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.tests.integration.bean.validation; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * When enabled, endpoints with bean validation should return response with BAD REQUEST status. + */ +@HelidonTest +public class TestValidationEndpoint { + + @Inject + private WebTarget webTarget; + + + /** + * This Endpoint should always fail with BAD REQUEST, as bean validation fails with Not Null. + */ + @Test + public void testValidation() { + + Response.StatusType statusInfo = webTarget + .path("/valid/email") + .request() + .get() + .getStatusInfo(); + + assertThat("Endpoint should return BAD REQUEST", statusInfo.getStatusCode(), is(Response.Status.BAD_REQUEST.getStatusCode())); + + } + + /** + * This test should always work, since no validation is performed. + */ + @Test + public void testNormalUsage(){ + + Response.StatusType statusInfo = webTarget + .path("/valid/e@mail.com") + .request() + .get() + .getStatusInfo(); + assertThat("Endpoint should return OK", statusInfo.getStatusCode(), is(Response.Status.OK.getStatusCode())); + + } +} diff --git a/examples/microprofile/cors/README.md b/examples/microprofile/cors/README.md new file mode 100644 index 000000000..575d01933 --- /dev/null +++ b/examples/microprofile/cors/README.md @@ -0,0 +1,130 @@ +# Helidon MP CORS Example + +This example shows a simple greeting application, similar to the one from the +Helidon MP QuickStart, enhanced with CORS support. + +Near the end of the `resources/logging.properties` file, a commented line would turn on `FINE` +logging that would reveal how the Helidon CORS support makes it decisions. To see that logging, +uncomment that line and then package and run the application. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-cors.jar +``` + +## Using the app endpoints as with the "classic" greeting app + +These normal greeting app endpoints work just as in the original greeting app: + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"message" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} +``` + +## Using CORS + +### Sending "simple" CORS requests + +The following requests illustrate the CORS protocol with the example app. + +By setting `Origin` and `Host` headers that do not indicate the same system we trigger CORS processing in the + server: + +```shell +# Follow the CORS protocol for GET +curl -i -X GET -H "Origin: http://foo.com" -H "Host: here.com" http://localhost:8080/greet +``` +``` + +HTTP/1.1 200 OK +Access-Control-Allow-Origin: * +Content-Type: application/json +Date: Thu, 30 Apr 2020 17:25:51 -0500 +Vary: Origin +connection: keep-alive +content-length: 27 + +{"greeting":"Hola World!"} +``` +Note the new headers `Access-Control-Allow-Origin` and `Vary` in the response. + +The same happens for a `GET` requesting a personalized greeting (by passing the name of the + person to be greeted): +```shell +curl -i -X GET -H "Origin: http://foo.com" -H "Host: here.com" http://localhost:8080/greet/Joe +#Output: {"greeting":"Hola Joe!"} +``` +Take a look at `GreetResource` and in particular the methods named `optionsForXXX` near the end of the class. +There is one for each different subpath that the resource's endpoints handle: no subpath, `/{name}`, and `/greeting`. The +`@CrossOrigin` annotation on each defines the CORS behavior for the corresponding path. +The `optionsForUpdatingGreeting` gives specific origins and the HTTP method (`PUT`) constraints for sharing that +resource. The other two `optionsForRetrievingXXXGreeting` methods use default parameters for the `@CrossOrigin` +annotation: allowing all origins, all methods, etc. + +With this in mind, we can see why the two earlier `GET` `curl` requests work. + +These are what CORS calls "simple" requests; the CORS protocol for these adds headers to the request and response that +would be exchanged between the client and server even without CORS. + +### "Non-simple" CORS requests + +The CORS protocol requires the client to send a _pre-flight_ request before sending a request +that changes state on the server, such as `PUT` or `DELETE` and to check the returned status +and headers to make sure the server is willing to accept the actual request. CORS refers to such `PUT` and `DELETE` +requests as "non-simple" ones. + +This command sends a pre-flight `OPTIONS` request to see if the server will accept a subsequent `PUT` request from the +specified origin to change the greeting: +```shell +curl -i -X OPTIONS \ + -H "Access-Control-Request-Method: PUT" \ + -H "Origin: http://foo.com" \ + -H "Host: here.com" \ + http://localhost:8080/greet/greeting +``` +``` +HTTP/1.1 200 OK +Access-Control-Allow-Methods: PUT +Access-Control-Allow-Origin: http://foo.com +Date: Thu, 30 Apr 2020 17:30:59 -0500 +transfer-encoding: chunked +connection: keep-alive +``` +The successful status and the returned `Access-Control-Allow-xxx` headers indicate that the + server accepted the pre-flight request. That means it is OK for us to send `PUT` request to perform the actual change + of greeting. (See below for how the server rejects a pre-flight request.) +```shell +curl -i -X PUT \ + -H "Origin: http://foo.com" \ + -H "Host: here.com" \ + -H "Access-Control-Allow-Methods: PUT" \ + -H "Access-Control-Allow-Origin: http://foo.com" \ + -H "Content-Type: application/json" \ + -d "{ \"message\" : \"Cheers\" }" \ + http://localhost:8080/greet/greeting +``` +``` +HTTP/1.1 204 No Content +Access-Control-Allow-Origin: http://foo.com +Date: Thu, 30 Apr 2020 17:32:55 -0500 +Vary: Origin +connection: keep-alive +``` +And we run one more `GET` to observe the change in the greeting: +```shell +curl -i -X GET -H "Origin: http://foo.com" -H "Host: here.com" http://localhost:8080/greet/Joe +#Output: {"greeting":"Cheers Joe!"} +``` +Note that the tests in the example `TestCORS` class follow these same steps. + + diff --git a/examples/microprofile/cors/pom.xml b/examples/microprofile/cors/pom.xml new file mode 100644 index 000000000..5a5534b41 --- /dev/null +++ b/examples/microprofile/cors/pom.xml @@ -0,0 +1,102 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-cors + 1.0.0-SNAPSHOT + Helidon Examples Microprofile CORS + + + Microprofile example showing CORS support + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.microprofile + helidon-microprofile-cors + + + org.glassfish.jersey.media + jersey-media-json-binding + runtime + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webclient + helidon-webclient + test + + + io.helidon.http.media + helidon-http-media-jsonb + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/GreetResource.java b/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/GreetResource.java new file mode 100644 index 000000000..e14b1a271 --- /dev/null +++ b/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/GreetResource.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.examples.cors; + +import io.helidon.microprofile.cors.CrossOrigin; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.OPTIONS; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; + +/** + * A simple JAX-RS resource to greet you with CORS support. + */ +@Path("/greet") +@RequestScoped +public class GreetResource { + + /** + * The greeting message provider. + */ + private final GreetingProvider greetingProvider; + + /** + * Using constructor injection to get a configuration property. + * By default this gets the value from META-INF/microprofile-config + * + * @param greetingConfig the configured greeting message + */ + @Inject + public GreetResource(GreetingProvider greetingConfig) { + this.greetingProvider = greetingConfig; + } + + /** + * Return a worldly greeting message. + * + * @return {@link GreetingMessage} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getDefaultMessage() { + return createResponse("World"); + } + + /** + * Return a greeting message using the name that was provided. + * + * @param name the name to greet + * @return {@link GreetingMessage} + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getMessage(@PathParam("name") String name) { + return createResponse(name); + } + + /** + * Set the greeting to use in future messages. + * + * @param message {@link GreetingMessage} containing the new greeting + * @return {@link Response} + */ + @Path("/greeting") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @RequestBody(name = "message", + required = true, + content = @Content(mediaType = "application/json", + schema = @Schema(type = SchemaType.OBJECT, requiredProperties = { "message" }))) + @APIResponses({ + @APIResponse(name = "normal", responseCode = "204", description = "Greeting updated"), + @APIResponse(name = "missing 'greeting'", responseCode = "400", + description = "JSON did not contain setting for 'greeting'")}) + public Response updateGreeting(GreetingMessage message) { + + if (message.getMessage() == null) { + GreetingMessage entity = new GreetingMessage("No greeting provided"); + return Response.status(Response.Status.BAD_REQUEST).entity(entity).build(); + } + + greetingProvider.setMessage(message.getMessage()); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + /** + * CORS set-up for updateGreeting. + */ + @OPTIONS + @Path("/greeting") + @CrossOrigin(value = {"http://foo.com", "http://there.com"}, + allowMethods = {HttpMethod.PUT}) + @APIResponses({ + @APIResponse(name = "normal", responseCode = "200", description = "Preflight request granted"), + @APIResponse(name = "bad preflight", responseCode = "403", + description = "Preflight request denied")}) + public void optionsForUpdatingGreeting() { + } + + /** + * CORS set-up for getDefaultMessage. + */ + @OPTIONS + @CrossOrigin() + public void optionsForRetrievingUnnamedGreeting() { + } + + /** + * CORS set-up for getMessage. + */ + @OPTIONS + @CrossOrigin() + @Path("/{name}") + public void optionsForRetrievingNamedGreeting() { + } + + private GreetingMessage createResponse(String who) { + String msg = String.format("%s %s!", greetingProvider.getMessage(), who); + return new GreetingMessage(msg); + } +} diff --git a/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/GreetingMessage.java b/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/GreetingMessage.java new file mode 100644 index 000000000..dc3298918 --- /dev/null +++ b/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/GreetingMessage.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.examples.cors; + +/** + * POJO defining the greeting message content. + */ +@SuppressWarnings("unused") +public class GreetingMessage { + private String message; + + /** + * Create a new GreetingMessage instance. + */ + public GreetingMessage() { + } + + /** + * Create a new GreetingMessage instance. + * + * @param message message + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Gets the message value. + * + * @return message value + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message message value to set + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/GreetingProvider.java b/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/GreetingProvider.java new file mode 100644 index 000000000..f4b2da656 --- /dev/null +++ b/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/GreetingProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.examples.cors; + +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Provider for greeting message. + */ +@ApplicationScoped +public class GreetingProvider { + private final AtomicReference message = new AtomicReference<>(); + + /** + * Create a new greeting provider, reading the message from configuration. + * + * @param message greeting to use + */ + @Inject + public GreetingProvider(@ConfigProperty(name = "app.greeting") String message) { + this.message.set(message); + } + + String getMessage() { + return message.get(); + } + + void setMessage(String message) { + this.message.set(message); + } +} diff --git a/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/package-info.java b/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/package-info.java new file mode 100644 index 000000000..0212fe72d --- /dev/null +++ b/examples/microprofile/cors/src/main/java/io/helidon/microprofile/examples/cors/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon MicroProfile CORS example. + */ +package io.helidon.microprofile.examples.cors; diff --git a/examples/microprofile/cors/src/main/resources/META-INF/beans.xml b/examples/microprofile/cors/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..676e09a2d --- /dev/null +++ b/examples/microprofile/cors/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/cors/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/cors/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..ae5243476 --- /dev/null +++ b/examples/microprofile/cors/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +app.greeting=Hello + +# Override configuration to use a random port for the unit tests +config_ordinal=1000 +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 diff --git a/examples/microprofile/cors/src/main/resources/logging.properties b/examples/microprofile/cors/src/main/resources/logging.properties new file mode 100644 index 000000000..7861f0bae --- /dev/null +++ b/examples/microprofile/cors/src/main/resources/logging.properties @@ -0,0 +1,30 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.cors.level=INFO diff --git a/examples/microprofile/cors/src/test/java/io/helidon/microprofile/examples/cors/TestCORS.java b/examples/microprofile/cors/src/test/java/io/helidon/microprofile/examples/cors/TestCORS.java new file mode 100644 index 000000000..d6d9769c5 --- /dev/null +++ b/examples/microprofile/cors/src/test/java/io/helidon/microprofile/examples/cors/TestCORS.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.examples.cors; + +import java.util.List; +import java.util.Optional; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.config.Config; +import io.helidon.http.HeaderNames; +import io.helidon.http.Headers; +import io.helidon.http.Method; +import io.helidon.http.media.jsonb.JsonbSupport; +import io.helidon.microprofile.server.Server; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientRequest; +import io.helidon.webclient.http1.Http1ClientResponse; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS; +import static io.helidon.cors.CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TestCORS { + + private static Http1Client client; + private static Server server; + + @BeforeAll + static void init() { + Config config = Config.create(); + Config serverConfig = config.get("server"); + Server.Builder serverBuilder = Server.builder(); + serverConfig.ifExists(serverBuilder::config); + server = serverBuilder + .port(-1) // override the port for testing + .build() + .start(); + client = Http1Client.builder() + .baseUri("http://localhost:" + server.port()) + .addMediaSupport(JsonbSupport.create(config)) + .build(); + } + + @AfterAll + static void cleanup() { + if (server != null) { + server.stop(); + } + } + + @Order(1) // Make sure this runs before the greeting message changes so responses are deterministic. + @Test + public void testHelloWorld() { + + Http1ClientResponse r = getResponse("/greet"); + + assertThat("HTTP response1", r.status().code(), is(200)); + assertThat("default message", fromPayload(r), is("Hello World!")); + + r = getResponse("/greet/Joe"); + assertThat("HTTP response2", r.status().code(), is(200)); + assertThat("Hello Joe message", fromPayload(r), is("Hello Joe!")); + + r = putResponse("/greet/greeting", "Hola"); + assertThat("HTTP response3", r.status().code(), is(204)); + + r = getResponse("/greet/Jose"); + assertThat("HTTP response4", r.status().code(), is(200)); + assertThat("Hola Jose message", fromPayload(r), is("Hola Jose!")); + + r = getResponse("/health"); + assertThat("HTTP response health", r.status().code(), is(200)); + + r = getResponse("/metrics"); + assertThat("HTTP response metrics", r.status().code(), is(200)); + } + + @Order(10) // Run after the non-CORS tests (so the greeting is Hola) but before the CORS test that changes the greeting again. + @Test + void testAnonymousGreetWithCors() { + Http1ClientRequest req = client.get() + .header(HeaderNames.ORIGIN, "http://foo.com") + .header(HeaderNames.HOST, "here.com"); + + Http1ClientResponse r = getResponse("/greet", req); + assertThat("HTTP response", r.status().code(), is(200)); + String payload = fromPayload(r); + assertThat("HTTP response payload", payload, is("Hola World!")); + Headers responseHeaders = r.headers(); + Optional allowOrigin = responseHeaders.value(HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN); + assertThat("Expected CORS header " + ACCESS_CONTROL_ALLOW_ORIGIN + " is present", + allowOrigin.isPresent(), is(true)); + assertThat("CORS header " + ACCESS_CONTROL_ALLOW_ORIGIN, allowOrigin.get(), is("*")); + } + + @Order(11) // Run after the non-CORS tests but before other CORS tests. + @Test + void testGreetingChangeWithCors() { + + // Send the pre-flight request and check the response. + + Http1ClientRequest req = client.method(Method.OPTIONS) + .header(HeaderNames.ORIGIN, "http://foo.com") + .header(HeaderNames.HOST, "here.com") + .header(HeaderNames.ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + List allowOrigins; + Headers responseHeaders; + Headers preflightResponseHeaders; + try (Http1ClientResponse res = req.path("/greet/greeting").request()) { + assertThat("pre-flight status", res.status().code(), is(200)); + preflightResponseHeaders = res.headers(); + } + + List allowMethods = preflightResponseHeaders.values(HeaderNames.ACCESS_CONTROL_ALLOW_METHODS); + assertThat("pre-flight response check for " + ACCESS_CONTROL_ALLOW_METHODS, allowMethods, is(not(empty()))); + assertThat("Header " + ACCESS_CONTROL_ALLOW_METHODS, allowMethods, contains("PUT")); + + allowOrigins = preflightResponseHeaders.values(HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN); + + assertThat("pre-flight response check for " + ACCESS_CONTROL_ALLOW_ORIGIN, allowOrigins, is(not(empty()))); + assertThat("Header " + ACCESS_CONTROL_ALLOW_ORIGIN, allowOrigins, contains("http://foo.com")); + + // Send the follow-up request. + + req = client.put() + .header(HeaderNames.ORIGIN, "http://foo.com") + .header(HeaderNames.HOST, "here.com"); + preflightResponseHeaders.forEach(req.headers()::add); + + try (Http1ClientResponse res = putResponse("/greet/greeting", "Cheers", req)) { + assertThat("HTTP response3", res.status().code(), is(204)); + responseHeaders = res.headers(); + } + + allowOrigins = responseHeaders.values(HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN); + assertThat("Expected CORS header " + ACCESS_CONTROL_ALLOW_ORIGIN, allowOrigins, is(not(empty()))); + assertThat("Header " + ACCESS_CONTROL_ALLOW_ORIGIN, allowOrigins, contains("http://foo.com")); + } + + @Order(12) // Run after CORS test changes greeting to Cheers. + @Test + void testNamedGreetWithCors() { + Http1ClientRequest req = client.get() + .header(HeaderNames.ORIGIN, "http://foo.com") + .header(HeaderNames.HOST, "here.com"); + + Http1ClientResponse r = getResponse("/greet/Maria", req); + assertThat("HTTP response", r.status().code(), is(200)); + assertThat(fromPayload(r), containsString("Cheers Maria")); + Headers responseHeaders = r.headers(); + Optional allowOrigin = responseHeaders.value(HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN); + assertThat("Expected CORS header " + ACCESS_CONTROL_ALLOW_ORIGIN + " presence check", allowOrigin.isPresent(), is(true)); + assertThat(allowOrigin.get(), is("*")); + } + + @Order(100) // After all other tests, so we can rely on deterministic greetings. + @Test + void testGreetingChangeWithCorsAndOtherOrigin() { + Http1ClientRequest req = client.put() + .header(HeaderNames.ORIGIN, "http://other.com") + .header(HeaderNames.HOST, "here.com"); + + try (Http1ClientResponse r = putResponse("/greet/greeting", "Ahoy", req)) { + // Result depends on whether we are using overrides or not. + boolean isOverriding = Config.create().get("cors").exists(); + assertThat("HTTP response3", r.status().code(), is(isOverriding ? 204 : 403)); + } + } + + + private static Http1ClientResponse getResponse(String path) { + return getResponse(path, client.get()); + } + + private static Http1ClientResponse getResponse(String path, Http1ClientRequest builder) { + return builder.accept(MediaTypes.APPLICATION_JSON) + .path(path) + .request(); + } + + private static String fromPayload(Http1ClientResponse response) { + GreetingMessage message = response + .entity() + .as(GreetingMessage.class); + return message.getMessage(); + } + + private static GreetingMessage toPayload(String message) { + return new GreetingMessage(message); + } + + private static Http1ClientResponse putResponse(String path, String message) { + return putResponse(path, message, client.put()); + } + + private static Http1ClientResponse putResponse(String path, String message, Http1ClientRequest builder) { + return builder.accept(MediaTypes.APPLICATION_JSON) + .path(path) + .submit(toPayload(message)); + } +} diff --git a/examples/microprofile/cors/src/test/resources/logging.properties b/examples/microprofile/cors/src/test/resources/logging.properties new file mode 100644 index 000000000..b3d32eef2 --- /dev/null +++ b/examples/microprofile/cors/src/test/resources/logging.properties @@ -0,0 +1,37 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.cors.level=FINE +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.microprofile.level=INFO +#io.helidon.common.level=INFO +#org.glassfish.jersey.level=INFO +#org.jboss.weld=INFO diff --git a/examples/microprofile/graphql/README.md b/examples/microprofile/graphql/README.md new file mode 100644 index 000000000..aed1a6d84 --- /dev/null +++ b/examples/microprofile/graphql/README.md @@ -0,0 +1,266 @@ +# Microprofile GraphQL Example + +This example creates a simple Task API using Helidon's implementation of the Microprofile GraphQL API Specification. + +See [here](https://github.com/eclipse/microprofile-graphql) for more information on the +Microprofile GraphQL Specification as well as the [Helidon documentation](https://helidon.io/docs/v2/#/mp/introduction/01_introduction) +for an introduction to using GraphQL in Helidon MP. + +## Running the example + +1. Build + +```shell +mvn clean install +``` + +2. Run the example + +```shell +java -jar target/helidon-examples-microprofile-graphql.jar +``` + +## Issuing GraphQL requests via REST + +Access the `/graphql` endpoint via `http://127.0.0.1:7001/graphql`: + +1. Display the generated GraphQL Schema + + ```shell + curl http://127.0.0.1:7001/graphql/schema.graphql + ``` + + This will produce the following: + + ```graphql + type Mutation { + "Create a task with the given description" + createTask(description: String!): Task + "Remove all completed tasks and return the tasks left" + deleteCompletedTasks: [Task] + "Delete a task and return the deleted task details" + deleteTask(id: String!): Task + "Update a task" + updateTask(completed: Boolean, description: String, id: String!): Task + } + + type Query { + "Return a given task" + findTask(id: String!): Task + "Query tasks and optionally specified only completed" + tasks(completed: Boolean): [Task] + } + + type Task { + completed: Boolean! + createdAt: BigInteger! + description: String + id: String + } + + "Custom: Built-in java.math.BigInteger" + scalar BigInteger + ``` + +1. Create a Task + + ```shell + curl -X POST http://127.0.0.1:7001/graphql -d '{"query":"mutation createTask { createTask(description: \"Task Description 1\") { id description createdAt completed }}"}' + ``` + + Response is a newly created task: + + ```json + {"data":{"createTask":{"id":"0d4a8d","description":"Task Description 1","createdAt":1605501774877,"completed":false}} + ``` + +## Accessing Metrics + +In [TaskApi.java](src/main/java/io/helidon/examples/graphql/basics/TaskApi.java), the [Microprofile Metrics](https://github.com/eclipse/microprofile-metrics) +annotation`@SimplyTimed` has been added to the class which will apply simple timing metrics to all methods. After +exercising the APIs, access the metrics endpoint at http://127.0.0.1:7001/metrics to see all metrics. +In the case below we have also appended `/application` to the URL to just retrieve the application metrics. + +> Note: `jq` has been used to format the JSON output. This can be downloaded from https://stedolan.github.io/jq/download/ or you can +> format the output with an alternate utility. + +```shell +curl -H 'Accept: application/json' http://127.0.0.1:7001/metrics/application | jq +``` +``` +{ + "io.helidon.examples.graphql.basics.TaskApi.TaskApi": { + "count": 1, + "elapsedTime": 0.000440414 + }, + "io.helidon.examples.graphql.basics.TaskApi.createTask": { + "count": 1, + "elapsedTime": 0.000112074 + }, + "io.helidon.examples.graphql.basics.TaskApi.deleteCompletedTasks": { + "count": 0, + "elapsedTime": 0 + }, + "io.helidon.examples.graphql.basics.TaskApi.deleteTask": { + "count": 0, + "elapsedTime": 0 + }, + "io.helidon.examples.graphql.basics.TaskApi.findTask": { + "count": 0, + "elapsedTime": 0 + }, + "io.helidon.examples.graphql.basics.TaskApi.getTasks": { + "count": 0, + "elapsedTime": 0 + }, + "io.helidon.examples.graphql.basics.TaskApi.updateTask": { + "count": 0, + "elapsedTime": 0 + } +} +``` + +## Incorporating the GraphiQL UI + +The [GraphiQL UI](https://github.com/graphql/graphiql), which provides a UI to execute GraphQL commands, is not included by default in Helidon's Microprofile GraphQL +implementation. You can follow the guide below to incorporate the UI into this example: + +1. Add the following contents to the sample `examples/microprofile/graphql/src/main/resources/web/index.html` file, which has been included below from [here](https://github.com/graphql/graphiql/blob/main/packages/graphiql/README.md) +for convenience. + + ```html + + + Simple GraphiQL Example + + + +
+ + + + + + + + + ``` + + > Note: If you copy the original file, change the URL in the line `fetch('https://my/graphql', {` to `http://127.0.0.1:7001/graphql` + +1. Build and run the example using the instructions above. + +1. Access the GraphiQL UI via the following URL: http://127.0.0.1:7001/ui. + +2. Copy the following commands into the editor on the left. + + ```graphql + # Fragment to allow shorcut to display all fields for a task + fragment task on Task { + id + description + createdAt + completed + } + + # Create a task + mutation createTask { + createTask(description: "Task Description 1") { + ...task + } + } + + # Find all the tasks + query findAllTasks { + tasks { + ...task + } + } + + # Find a task + query findTask { + findTask(id: "251474") { + ...task + } + } + + # Find completed Tasks + query findCompletedTasks { + tasks(completed: true) { + ...task + } + } + + # Find outstanding Tasks + query findOutstandingTasks { + tasks(completed: false) { + ...task + } + } + + mutation updateTask { + updateTask(id: "251474" description:"New Description") { + ...task + } + } + + mutation completeTask { + updateTask(id: "251474" completed:true) { + ...task + } + } + + # Delete a task + mutation deleteTask { + deleteTask(id: "1f6ae5") { + ...task + } + } + + # Delete completed + mutation deleteCompleted { + deleteCompletedTasks { + ...task + } + } + ``` + +3. Run individual commands by clicking on the `Play` button and choosing the query or mutation to run. + +4. Sample requests + + 1. Execute `createTask` + 2. Change the description and execute `createTask` + 3. Execute `findTask` to show the exception when a task does not exist + 3. Change the id and execute `findTask` to show your newly created task + 5. Execute `findAllTasks` to show the 2 tasks + 6. Change the id and execute `updateTask` to update the existing task + 7. Change the id and execute `completeTask` + 8. Execute `findAllTasks` to show the task completed + 9. Execute `findCompletedTasks` to show only completed tasks + 10. Execute `deleteCompleted` to delete completed task + 11. Execute `findCompletedTasks` to show no completed tasks + + diff --git a/examples/microprofile/graphql/pom.xml b/examples/microprofile/graphql/pom.xml new file mode 100644 index 000000000..23bca5321 --- /dev/null +++ b/examples/microprofile/graphql/pom.xml @@ -0,0 +1,68 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-graphql + 1.0.0-SNAPSHOT + Helidon Examples Microprofile GraphQL + + + Usage of GraphQL in Helidon MP + + + + + io.helidon.microprofile.graphql + helidon-microprofile-graphql-server + + + io.helidon.microprofile.bundles + helidon-microprofile + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/Task.java b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/Task.java new file mode 100644 index 000000000..6313b5a52 --- /dev/null +++ b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/Task.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.graphql.basics; + +import java.util.UUID; + +import io.helidon.common.Reflected; + +import org.eclipse.microprofile.graphql.NonNull; + +/** + * A data class representing a single To Do List task. + */ +@Reflected +public class Task { + + /** + * The creation time. + */ + private long createdAt; + + /** + * The completion status. + */ + private boolean completed; + + /** + * The task ID. + */ + @NonNull + private String id; + + /** + * The task description. + */ + @NonNull + private String description; + + /** + * Deserialization constructor. + */ + public Task() { + } + + /** + * Construct Task instance. + * + * @param description task description + */ + public Task(String description) { + this.id = UUID.randomUUID().toString().substring(0, 6); + this.createdAt = System.currentTimeMillis(); + this.description = description; + this.completed = false; + } + + /** + * Get the creation time. + * + * @return the creation time + */ + public long getCreatedAt() { + return createdAt; + } + + /** + * Get the task ID. + * + * @return the task ID + */ + public String getId() { + return id; + } + + /** + * Get the task description. + * + * @return the task description + */ + public String getDescription() { + return description; + } + + /** + * Set the task description. + * + * @param description the task description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Get the completion status. + * + * @return true if it is completed, false otherwise. + */ + public boolean isCompleted() { + return completed; + } + + /** + * Sets the completion status. + * + * @param completed the completion status + */ + public void setCompleted(boolean completed) { + this.completed = completed; + } + + @Override + public String toString() { + return "Task{" + + "id=" + id + + ", description=" + description + + ", completed=" + completed + + '}'; + } +} diff --git a/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskApi.java b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskApi.java new file mode 100644 index 000000000..7b36f89de --- /dev/null +++ b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskApi.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.graphql.basics; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.graphql.Description; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.NonNull; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.metrics.annotation.Timed; + +/** + * A CDI Bean that exposes a GraphQL API to query and mutate {@link Task}s. + */ +@GraphQLApi +@ApplicationScoped +@Timed +public class TaskApi { + + private static final String MESSAGE = "Unable to find task with id "; + + private Map tasks = new ConcurrentHashMap<>(); + + /** + * Create a {@link Task}. + * + * @param description task description + * @return the created {@link Task} + */ + @Mutation + @Description("Create a task with the given description") + public Task createTask(@Name("description") @NonNull String description) { + if (description == null) { + throw new IllegalArgumentException("Description must be provided"); + } + Task task = new Task(description); + tasks.put(task.getId(), task); + return task; + } + + /** + * Query {@link Task}s. + * + * @param completed optionally specify completion status + * @return a {@link Collection} of {@link Task}s + */ + @Query + @Description("Query tasks and optionally specify only completed") + public Collection getTasks(@Name("completed") Boolean completed) { + return tasks.values().stream() + .filter(task -> completed == null || task.isCompleted() == completed) + .collect(Collectors.toList()); + } + + /** + * Return a {@link Task}. + * + * @param id task id + * @return the {@link Task} with the given id + * @throws TaskNotFoundException if the task was not found + */ + @Query + @Description("Return a given task") + public Task findTask(@Name("id") @NonNull String id) throws TaskNotFoundException { + return Optional.ofNullable(tasks.get(id)) + .orElseThrow(() -> new TaskNotFoundException(MESSAGE + id)); + } + + /** + * Delete a {@link Task}. + * + * @param id task to delete + * @return the deleted {@link Task} + * @throws TaskNotFoundException if the task was not found + */ + @Mutation + @Description("Delete a task and return the deleted task details") + public Task deleteTask(@Name("id") @NonNull String id) throws TaskNotFoundException { + return Optional.ofNullable(tasks.remove(id)) + .orElseThrow(() -> new TaskNotFoundException(MESSAGE + id)); + } + + /** + * Remove all completed {@link Task}s. + * + * @return the {@link Task}s left + */ + @Mutation + @Description("Remove all completed tasks and return the tasks left") + public Collection deleteCompletedTasks() { + tasks.values().removeIf(Task::isCompleted); + return tasks.values(); + } + + /** + * Update a {@link Task}. + * + * @param id task to update + * @param description optional description + * @param completed optional completed + * @return the updated {@link Task} + * @throws TaskNotFoundException if the task was not found + */ + @Mutation + @Description("Update a task") + public Task updateTask(@Name("id") @NonNull String id, + @Name("description") String description, + @Name("completed") Boolean completed) throws TaskNotFoundException { + + try { + return tasks.compute(id, (k, v) -> { + Objects.requireNonNull(v); + + if (description != null) { + v.setDescription(description); + } + if (completed != null) { + v.setCompleted(completed); + } + return v; + }); + } catch (Exception e) { + throw new TaskNotFoundException(MESSAGE + id); + } + } +} diff --git a/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskNotFoundException.java b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskNotFoundException.java new file mode 100644 index 000000000..4e102c067 --- /dev/null +++ b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/TaskNotFoundException.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.graphql.basics; + +/** + * An exception indicating that a {@link Task} was not found. + */ +public class TaskNotFoundException extends Exception { + /** + * Create the exception. + * @param message reason for the exception. + */ + public TaskNotFoundException(String message) { + super(message); + } +} diff --git a/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/package-info.java b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/package-info.java new file mode 100644 index 000000000..2306b2b6a --- /dev/null +++ b/examples/microprofile/graphql/src/main/java/io/helidon/examples/graphql/basics/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A set of small usage examples. Start with {@link io.helidon.grpc.examples.basics.Main} class. + */ +package io.helidon.examples.graphql.basics; diff --git a/examples/microprofile/graphql/src/main/resources/META-INF/beans.xml b/examples/microprofile/graphql/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..676e09a2d --- /dev/null +++ b/examples/microprofile/graphql/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/graphql/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/graphql/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..d8c7a02ed --- /dev/null +++ b/examples/microprofile/graphql/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server.static.classpath.context=/ui +server.static.classpath.location=/web +graphql.cors=Access-Control-Allow-Origin +mp.graphql.exceptionsWhiteList=java.lang.IllegalArgumentException diff --git a/examples/microprofile/graphql/src/main/resources/logging.properties b/examples/microprofile/graphql/src/main/resources/logging.properties new file mode 100644 index 000000000..bb1a1ed67 --- /dev/null +++ b/examples/microprofile/graphql/src/main/resources/logging.properties @@ -0,0 +1,24 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +io.helidon.logging.jul.HelidonConsoleHandler.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#All log level details +.level=WARNING +io.helidon.level=INFO diff --git a/examples/microprofile/graphql/src/main/resources/web/index.html b/examples/microprofile/graphql/src/main/resources/web/index.html new file mode 100644 index 000000000..268ea84e9 --- /dev/null +++ b/examples/microprofile/graphql/src/main/resources/web/index.html @@ -0,0 +1,23 @@ + + + + + Sample + +

To enable GraphiQL UI please see the Microprofile GraphQL example README.md

+

+ \ No newline at end of file diff --git a/examples/microprofile/hello-world-explicit/README.md b/examples/microprofile/hello-world-explicit/README.md new file mode 100644 index 000000000..e93f7a689 --- /dev/null +++ b/examples/microprofile/hello-world-explicit/README.md @@ -0,0 +1,20 @@ +# Helidon MP Hello World Explicit Example + +This examples shows a simple application written using Helidon MP. +It is explicit because in this example you write the `main` class +and explicitly start the microprofile server. + +```shell +mvn package +java -jar target/helidon-examples-microprofile-hello-world-explicit.jar +``` + +Then try the endpoints: + +```shell +curl -X GET http://localhost:7001/helloworld +curl -X GET http://localhost:7001/helloworld/earth +``` + +By default the server will use a dynamic port, see the messages displayed +when the application starts. diff --git a/examples/microprofile/hello-world-explicit/pom.xml b/examples/microprofile/hello-world-explicit/pom.xml new file mode 100644 index 000000000..38613331a --- /dev/null +++ b/examples/microprofile/hello-world-explicit/pom.xml @@ -0,0 +1,76 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + helidon-examples-microprofile-hello-world-explicit + 1.0.0-SNAPSHOT + Helidon Examples Microprofile Explicit Hello World + + + Microprofile example with explicit bootstrapping (Server.create(Application.class).start()) + + + + io.helidon.microprofile.example.helloworld.explicit.Main + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.smallrye + jandex + runtime + true + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/hello-world-explicit/src/main/java/io/helidon/microprofile/example/helloworld/explicit/HelloWorldResource.java b/examples/microprofile/hello-world-explicit/src/main/java/io/helidon/microprofile/example/helloworld/explicit/HelloWorldResource.java new file mode 100644 index 000000000..e7db71747 --- /dev/null +++ b/examples/microprofile/hello-world-explicit/src/main/java/io/helidon/microprofile/example/helloworld/explicit/HelloWorldResource.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.helloworld.explicit; + +import java.net.URI; +import java.util.Collections; + +import io.helidon.config.Config; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Example resource. + */ +@Path("helloworld") +@RequestScoped +public class HelloWorldResource { + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + private final Config config; + private final String applicationName; + private final URI applicationUri; + private BeanManager beanManager; + + /** + * Constructor injection of field values. + * + * @param config configuration instance + * @param appName name of application from config (app.name) + * @param appUri URI of application from config (app.uri) + * @param beanManager bean manager (injected automatically by CDI) + */ + @Inject + public HelloWorldResource(Config config, + @ConfigProperty(name = "app.name") String appName, + @ConfigProperty(name = "app.uri") URI appUri, + BeanManager beanManager) { + this.config = config; + this.applicationName = appName; + this.applicationUri = appUri; + this.beanManager = beanManager; + } + + /** + * Hello world GET method. + * + * @return string with application name + */ + @GET + @Produces(MediaType.TEXT_PLAIN) + public String message() { + return "Hello World from application " + applicationName; + } + + /** + * Hello World GET method returning JSON. + * + * @param name name to add to response + * @return JSON with name and configured fields of this class + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public JsonObject getHello(@PathParam("name") String name) { + return JSON.createObjectBuilder() + .add("name", name) + .add("appName", applicationName) + .add("appUri", String.valueOf(applicationUri)) + .add("config", config.get("my.property").asString().get()) + .add("beanManager", beanManager.toString()) + .build(); + } +} diff --git a/examples/microprofile/hello-world-explicit/src/main/java/io/helidon/microprofile/example/helloworld/explicit/Main.java b/examples/microprofile/hello-world-explicit/src/main/java/io/helidon/microprofile/example/helloworld/explicit/Main.java new file mode 100644 index 000000000..c85566f5e --- /dev/null +++ b/examples/microprofile/hello-world-explicit/src/main/java/io/helidon/microprofile/example/helloworld/explicit/Main.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.helloworld.explicit; + +import io.helidon.microprofile.server.Server; + +/** + * Explicit example. + */ +public class Main { + private Main() { + } + + /** + * Starts server manually. + * + * @param args command line arguments (ignored) + */ + public static void main(String[] args) { + Server server = Server.builder() + .host("localhost") + // use port 7001 as per README.md + .port(7001) + .build(); + + server.start(); + + String endpoint = "http://" + server.host() + ":" + server.port(); + System.out.println("Started application on " + endpoint + "/helloworld"); + System.out.println("Metrics available on " + endpoint + "/metrics"); + System.out.println("Heatlh checks available on " + endpoint + "/health"); + + } +} diff --git a/examples/microprofile/hello-world-explicit/src/main/java/io/helidon/microprofile/example/helloworld/explicit/package-info.java b/examples/microprofile/hello-world-explicit/src/main/java/io/helidon/microprofile/example/helloworld/explicit/package-info.java new file mode 100644 index 000000000..b9685668a --- /dev/null +++ b/examples/microprofile/hello-world-explicit/src/main/java/io/helidon/microprofile/example/helloworld/explicit/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Explicit example (configures server and resources). + */ +package io.helidon.microprofile.example.helloworld.explicit; diff --git a/examples/microprofile/hello-world-explicit/src/main/resources/META-INF/beans.xml b/examples/microprofile/hello-world-explicit/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..ddb8316e3 --- /dev/null +++ b/examples/microprofile/hello-world-explicit/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/hello-world-explicit/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/hello-world-explicit/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..e91d81bb1 --- /dev/null +++ b/examples/microprofile/hello-world-explicit/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app.name=Hello World Application +app.uri=https://www.example.com + +my.property=propertyValue + +# Enable the optional MicroProfile Metrics REST.request metrics +metrics.rest-request.enabled=true diff --git a/examples/microprofile/hello-world-explicit/src/main/resources/logging.properties b/examples/microprofile/hello-world-explicit/src/main/resources/logging.properties new file mode 100644 index 000000000..fbf7de151 --- /dev/null +++ b/examples/microprofile/hello-world-explicit/src/main/resources/logging.properties @@ -0,0 +1,24 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +io.helidon.logging.jul.HelidonConsoleHandler.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#All log level details +.level=WARNING +io.helidon.level=INFO diff --git a/examples/microprofile/hello-world-implicit/README.md b/examples/microprofile/hello-world-implicit/README.md new file mode 100644 index 000000000..e8d812a97 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/README.md @@ -0,0 +1,20 @@ +# Helidon MP Hello World Implicit Example + +This examples shows a simple application written using Helidon MP. +It is implicit because in this example you don't write the +`main` class, instead you rely on the Microprofile Server main class. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-hello-world-implicit.jar +``` + +Then try the endpoints: + +```shell +curl -X GET http://localhost:7001/helloworld +curl -X GET http://localhost:7001/helloworld/earth +curl -X GET http://localhost:7001/another +``` diff --git a/examples/microprofile/hello-world-implicit/pom.xml b/examples/microprofile/hello-world-implicit/pom.xml new file mode 100644 index 000000000..5bd3664c7 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/pom.xml @@ -0,0 +1,93 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-hello-world-implicit + 1.0.0-SNAPSHOT + Helidon Examples Microprofile Implicit Hello World + + + Microprofile example with implicit bootstrapping (cdi.Main(new String[0]) + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.smallrye + jandex + runtime + true + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/AnotherResource.java b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/AnotherResource.java new file mode 100644 index 000000000..a51b959c8 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/AnotherResource.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.helloworld.implicit; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Resource showing all possible configuration injections. + */ +@Path("another") +@RequestScoped +public class AnotherResource { + @Inject + @ConfigProperty(name = "app.nonExistent", defaultValue = "145") + private int defaultValue; + + @Inject + @ConfigProperty(name = "app.nonExistent") + private Optional empty; + + @Inject + @ConfigProperty(name = "app.uri") + private Optional full; + + @Inject + @ConfigProperty(name = "app.someInt") + private Provider provider; + + @Inject + @ConfigProperty(name = "app.ints") + private List ints; + + @Inject + @ConfigProperty(name = "app.ints") + private Optional> optionalInts; + + @Inject + @ConfigProperty(name = "app.ints") + private Provider> providedInts; + + @Inject + @ConfigProperty(name = "app.ints") + private int[] intsArray; + + @Inject + @ConfigProperty(name = "app") + private Map detached; + + @Inject + private Config mpConfig; + + @Inject + private io.helidon.config.Config helidonConfig; + + /** + * Get method to validate that all injections worked. + * + * @return data from all fields of this class + */ + @GET + public String get() { + return toString(); + } + + @Override + public String toString() { + return "AnotherResource{" + + "defaultValue=" + defaultValue + + ", empty=" + empty + + ", full=" + full + + ", provider=" + provider + "(" + provider.get() + ")" + + ", ints=" + ints + + ", optionalInts=" + optionalInts + + ", providedInts=" + providedInts + "(" + providedInts.get() + ")" + + ", detached=" + detached + + ", microprofileConfig=" + mpConfig + + ", helidonConfig=" + helidonConfig + + ", intsArray=" + Arrays.toString(intsArray) + + '}'; + } +} diff --git a/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/HelloWorldResource.java b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/HelloWorldResource.java new file mode 100644 index 000000000..6adf7e365 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/HelloWorldResource.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.helloworld.implicit; + +import java.net.URI; +import java.util.Collections; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.microprofile.example.helloworld.implicit.cdi.LoggerQualifier; +import io.helidon.microprofile.example.helloworld.implicit.cdi.RequestId; +import io.helidon.microprofile.example.helloworld.implicit.cdi.ResourceProducer; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Resource for hello world example. + */ +@Path("helloworld") +@RequestScoped +public class HelloWorldResource { + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + private final Config config; + private final Logger logger; + private final int requestId; + private final String applicationName; + private final URI applicationUri; + private BeanManager beanManager; + + /** + * Using constructor injection for field values. + * + * @param config configuration instance + * @param logger logger (from {@link ResourceProducer} + * @param requestId requestId (from {@link ResourceProducer} + * @param appName name from configuration (app.name) + * @param appUri URI from configuration (app.uri) + * @param beanManager bean manager (injected automatically by CDI) + */ + @Inject + public HelloWorldResource(Config config, + @LoggerQualifier Logger logger, + @RequestId int requestId, + @ConfigProperty(name = "app.name") String appName, + @ConfigProperty(name = "app.uri") URI appUri, + BeanManager beanManager) { + this.config = config; + this.logger = logger; + this.requestId = requestId; + this.applicationName = appName; + this.applicationUri = appUri; + this.beanManager = beanManager; + } + + /** + * Get method for this resource, shows logger and request id. + * + * @return hello world + */ + @GET + @Produces(MediaType.TEXT_PLAIN) + public String message() { + return "Hello World: " + logger + ", request: " + requestId + ", appName: " + applicationName; + } + + /** + * Get method for this resource, returning JSON. + * + * @param name name to add to response + * @return JSON structure with injected fields + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public JsonObject getHello(@PathParam("name") String name) { + return JSON.createObjectBuilder() + .add("name", name) + .add("requestId", requestId) + .add("appName", applicationName) + .add("appUri", String.valueOf(applicationUri)) + .add("config", config.get("server.port").asInt().get()) + .add("beanManager", beanManager.toString()) + .add("logger", logger.getName()) + .build(); + } +} diff --git a/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/LoggerQualifier.java b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/LoggerQualifier.java new file mode 100644 index 000000000..a02380710 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/LoggerQualifier.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.helloworld.implicit.cdi; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.inject.Qualifier; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Use this qualifier to inject logger instances. + */ +@Qualifier +@Retention(RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) +public @interface LoggerQualifier { +} diff --git a/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/RequestId.java b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/RequestId.java new file mode 100644 index 000000000..de5b0ff4e --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/RequestId.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.helloworld.implicit.cdi; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.inject.Qualifier; + +/** + * Request id qualifier to inject increasing request id. + */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) +public @interface RequestId { +} diff --git a/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/RequestIdProducer.java b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/RequestIdProducer.java new file mode 100644 index 000000000..e4ccaf175 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/RequestIdProducer.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.helloworld.implicit.cdi; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Produce an ever increasing request id. + */ +@ApplicationScoped +public class RequestIdProducer { + +} diff --git a/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/ResourceProducer.java b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/ResourceProducer.java new file mode 100644 index 000000000..caa11f1f0 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/ResourceProducer.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.helloworld.implicit.cdi; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.InjectionPoint; + +/** + * Producer for various resources required by this example. + */ +@ApplicationScoped +public class ResourceProducer { + private static final AtomicInteger COUNTER = new AtomicInteger(); + + /** + * Each injection will increase the COUNTER. + * + * @return increased COUNTER value + */ + @Produces + @RequestId + public int produceRequestId() { + return COUNTER.incrementAndGet(); + } + + /** + * Create/get a logger instance for the class that the logger is being injected into. + * + * @param injectionPoint injection point + * @return a logger instance + */ + @Produces + @LoggerQualifier + public java.util.logging.Logger produceLogger(final InjectionPoint injectionPoint) { + return java.util.logging.Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName()); + } +} diff --git a/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/package-info.java b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/package-info.java new file mode 100644 index 000000000..2bd42114d --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/cdi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * CDI classes for example. + */ +package io.helidon.microprofile.example.helloworld.implicit.cdi; diff --git a/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/package-info.java b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/package-info.java new file mode 100644 index 000000000..06f8cd6f4 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/java/io/helidon/microprofile/example/helloworld/implicit/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Implicit HelloWorld example (starts server without configuration). + */ +package io.helidon.microprofile.example.helloworld.implicit; diff --git a/examples/microprofile/hello-world-implicit/src/main/resources/META-INF/beans.xml b/examples/microprofile/hello-world-implicit/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..ddb8316e3 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/hello-world-implicit/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/hello-world-implicit/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..1bea51d04 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app.name=Hello World Application +app.someInt=147 +app.uri=https://www.example.com +app.someInt=147 +app.ints=12,12,32,12,44 + +server.port=7001 + +# Enable the optional MicroProfile Metrics REST.request metrics +metrics.rest-request.enabled=true diff --git a/examples/microprofile/hello-world-implicit/src/main/resources/logging.properties b/examples/microprofile/hello-world-implicit/src/main/resources/logging.properties new file mode 100644 index 000000000..5a5140e2e --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO +io.helidon.microprofile.config.level=FINEST diff --git a/examples/microprofile/hello-world-implicit/src/test/java/io/helidon/microprofile/example/helloworld/implicit/ImplicitHelloWorldTest.java b/examples/microprofile/hello-world-implicit/src/test/java/io/helidon/microprofile/example/helloworld/implicit/ImplicitHelloWorldTest.java new file mode 100644 index 000000000..11d0bd279 --- /dev/null +++ b/examples/microprofile/hello-world-implicit/src/test/java/io/helidon/microprofile/example/helloworld/implicit/ImplicitHelloWorldTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.helloworld.implicit; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.WebTarget; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertAll; + +/** + * Unit test for {@link HelloWorldResource}. + */ +@HelidonTest +class ImplicitHelloWorldTest { + private final WebTarget target; + + @Inject + ImplicitHelloWorldTest(WebTarget target) { + this.target = target; + } + @Test + void testJsonResource() { + JsonObject jsonObject = target + .path("/helloworld/unit") + .request() + .get(JsonObject.class); + + assertAll("JSON fields must match expected injection values", + () -> assertThat("Name from request", jsonObject.getString("name"), is("unit")), + () -> assertThat("Request id from CDI provider", jsonObject.getInt("requestId"), is(1)), + () -> assertThat("App name from config", jsonObject.getString("appName"), is("Hello World Application")), + () -> assertThat("Logger name", jsonObject.getString("logger"), is(HelloWorldResource.class.getName())) + ); + + } +} diff --git a/examples/microprofile/http-status-count-mp/README.md b/examples/microprofile/http-status-count-mp/README.md new file mode 100644 index 000000000..36fa4b6b0 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/README.md @@ -0,0 +1,76 @@ +# http-status-count-mp + +This Helidon MP project illustrates a filter which updates a family of counters based on the HTTP status returned in each response. + +The addition of the single filter class `HttpStatusMetricFilter` is the only difference from the Helidon MP QuickStart project. + +## Incorporating status metrics into your own application +Use this example for inspiration in writing your own filter or just use the filter directly in your own application by copying and pasting the `HttpStatusMetricFilter` class into your application, adjusting the package declaration as needed. Helidon MP discovers and uses your filter automatically. + +## Build and run + +```shell +mvn package +java -jar target/http-status-count-mp.jar +``` + +## Exercise the application +```shell +curl -X GET http://localhost:8080/simple-greet +``` +```listing +{"message":"Hello World!"} +``` + +```shell +curl -X GET http://localhost:8080/greet +``` +```listing +{"message":"Hello World!"} +``` +```shell +curl -X GET http://localhost:8080/greet/Joe +``` +```listing +{"message":"Hello Joe!"} +``` +```shell +curl -X PUT -H "Content-Type: application/json" -d '{"message" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +``` +```listing +{"message":"Hola Jose!"} +``` + +## Try metrics +```shell +# Prometheus Format +curl -s -X GET http://localhost:8080/metrics/application +``` + +```listing +... +# TYPE application_httpStatus_total counter +# HELP application_httpStatus_total Counts the number of HTTP responses in each status category (1xx, 2xx, etc.) +application_httpStatus_total{range="1xx"} 0 +application_httpStatus_total{range="2xx"} 5 +application_httpStatus_total{range="3xx"} 0 +application_httpStatus_total{range="4xx"} 0 +application_httpStatus_total{range="5xx"} 0 +... +``` +# JSON Format + +```shell +curl -H "Accept: application/json" -X GET http://localhost:8080/metrics +``` +```json +{ +... + "httpStatus;range=1xx": 0, + "httpStatus;range=2xx": 5, + "httpStatus;range=3xx": 0, + "httpStatus;range=4xx": 0, + "httpStatus;range=5xx": 0, +... diff --git a/examples/microprofile/http-status-count-mp/app.yaml b/examples/microprofile/http-status-count-mp/app.yaml new file mode 100644 index 000000000..9ebcde77e --- /dev/null +++ b/examples/microprofile/http-status-count-mp/app.yaml @@ -0,0 +1,32 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +kind: Service +apiVersion: v1 +metadata: + name: http-status-count-mp + labels: + app: http-status-count-mp +spec: + type: NodePort + selector: + app: http-status-count-mp + ports: + - port: 8080 + targetPort: 8080 + name: http +--- + diff --git a/examples/microprofile/http-status-count-mp/pom.xml b/examples/microprofile/http-status-count-mp/pom.xml new file mode 100644 index 000000000..aa34fc7bf --- /dev/null +++ b/examples/microprofile/http-status-count-mp/pom.xml @@ -0,0 +1,119 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples + http-status-count-mp + 1.0.0-SNAPSHOT + + Helidon Examples Metrics HTTP Status Counters + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + com.fasterxml.jackson.core + jackson-databind + + + org.eclipse.microprofile.metrics + microprofile-metrics-api + + + io.helidon.microprofile.metrics + helidon-microprofile-metrics + + + io.helidon.microprofile.health + helidon-microprofile-health + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.smallrye + jandex + runtime + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetResource.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetResource.java new file mode 100644 index 000000000..ca461e13b --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetResource.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.mp.httpstatuscount; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; + +/** + * A simple JAX-RS resource to greet you. Examples: + * + * Get default greeting message: + * curl -X GET http://localhost:8080/greet + * + * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + * + * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + * + * The message is returned as a JSON object. + */ +@Path("/greet") +@RequestScoped +public class GreetResource { + + /** + * The greeting message provider. + */ + private final GreetingProvider greetingProvider; + + /** + * Using constructor injection to get a configuration property. + * By default this gets the value from META-INF/microprofile-config + * + * @param greetingConfig the configured greeting message + */ + @Inject + public GreetResource(GreetingProvider greetingConfig) { + this.greetingProvider = greetingConfig; + } + + /** + * Return a worldly greeting message. + * + * @return {@link GreetingMessage} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getDefaultMessage() { + return createResponse("World"); + } + + /** + * Return a greeting message using the name that was provided. + * + * @param name the name to greet + * @return {@link GreetingMessage} + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getMessage(@PathParam("name") String name) { + return createResponse(name); + } + + /** + * Set the greeting to use in future messages. + * + * @param message the new greeting + * @return {@link Response} + */ + @Path("/greeting") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @RequestBody(name = "greeting", + required = true, + content = @Content(mediaType = "application/json", + schema = @Schema(type = SchemaType.OBJECT, requiredProperties = { "greeting" }))) + @APIResponses({ + @APIResponse(name = "normal", responseCode = "204", description = "Greeting updated"), + @APIResponse(name = "missing 'greeting'", responseCode = "400", + description = "JSON did not contain setting for 'greeting'")}) + public Response updateGreeting(GreetingMessage message) { + + if (message.getMessage() == null) { + GreetingMessage error = new GreetingMessage("No greeting provided"); + return Response.status(Response.Status.BAD_REQUEST).entity(error).build(); + } + + greetingProvider.setMessage(message.getMessage()); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + private GreetingMessage createResponse(String who) { + String msg = String.format("%s %s!", greetingProvider.getMessage(), who); + + return new GreetingMessage(msg); + } +} diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetingMessage.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetingMessage.java new file mode 100644 index 000000000..b5bae67ad --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetingMessage.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.mp.httpstatuscount; + +/** + * Greeting message. + */ +public class GreetingMessage { + private String message; + + /** + * Create a new GreetingMessage instance. + */ + public GreetingMessage() { + } + + /** + * Create a new GreetingMessage instance. + * + * @param message message + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Gets the message value. + * + * @return message value + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message message value to set + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetingProvider.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetingProvider.java new file mode 100644 index 000000000..f1823e8bc --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetingProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.mp.httpstatuscount; + +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Provider for greeting message. + */ +@ApplicationScoped +public class GreetingProvider { + private final AtomicReference message = new AtomicReference<>(); + + /** + * Create a new greeting provider, reading the message from configuration. + * + * @param message greeting to use + */ + @Inject + public GreetingProvider(@ConfigProperty(name = "app.greeting") String message) { + this.message.set(message); + } + + String getMessage() { + return message.get(); + } + + void setMessage(String message) { + this.message.set(message); + } +} diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/HttpStatusMetricFilter.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/HttpStatusMetricFilter.java new file mode 100644 index 000000000..f4491d7c6 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/HttpStatusMetricFilter.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.mp.httpstatuscount; + +import java.io.IOException; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.ws.rs.ConstrainedTo; +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; +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; + +/** + * REST service filter to update a family of counters based on the HTTP status of each response. + *

+ * The filter 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. + *

+ */ +@ConstrainedTo(RuntimeType.SERVER) +@Provider +public class HttpStatusMetricFilter implements ContainerResponseFilter { + + static final String STATUS_COUNTER_NAME = "httpStatus"; + static final String STATUS_TAG_NAME = "range"; + + @Inject + private MetricRegistry metricRegistry; + + private final Counter[] responseCounters = new Counter[6]; + + @PostConstruct + private void init() { + 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] = metricRegistry.counter(metadata, new Tag(STATUS_TAG_NAME, i + "xx")); + } + } + + @Override + public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) + throws IOException { + updateCountForStatus(containerResponseContext.getStatus()); + } + + private void updateCountForStatus(int statusCode) { + int range = statusCode / 100; + if (range > 0 && range < responseCounters.length) { + responseCounters[range].inc(); + } + } +} diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/SimpleGreetResource.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/SimpleGreetResource.java new file mode 100644 index 000000000..a11d4cba9 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/SimpleGreetResource.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.mp.httpstatuscount; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.annotation.Counted; +import org.eclipse.microprofile.metrics.annotation.Timed; + +/** + * A simple JAX-RS resource to greet you. Examples: + * + * Get default greeting message: + * curl -X GET http://localhost:8080/simple-greet + * + * The message is returned as a JSON object. + */ +@Path("/simple-greet") +public class SimpleGreetResource { + + private static final String PERSONALIZED_GETS_COUNTER_NAME = "personalizedGets"; + private static final String PERSONALIZED_GETS_COUNTER_DESCRIPTION = "Counts personalized GET operations"; + private static final String GETS_TIMER_NAME = "allGets"; + private static final String GETS_TIMER_DESCRIPTION = "Tracks all GET operations"; + private final String message; + + /** + * Creates a new instance using the configured default greeting. + * @param message initial greeting message + */ + @Inject + public SimpleGreetResource(@ConfigProperty(name = "app.greeting") String message) { + this.message = message; + } + + /** + * Return a worldly greeting message. + * + * @return {@link GreetingMessage} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getDefaultMessage() { + String msg = String.format("%s %s!", message, "World"); + GreetingMessage message = new GreetingMessage(); + message.setMessage(msg); + return message; + } + + /** + * Returns a personalized greeting. + * + * @param name name with which to personalize the greeting + * @return personalized greeting message + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + @Counted(name = PERSONALIZED_GETS_COUNTER_NAME, + absolute = true, + description = PERSONALIZED_GETS_COUNTER_DESCRIPTION) + @Timed(name = GETS_TIMER_NAME, + description = GETS_TIMER_DESCRIPTION, + unit = MetricUnits.SECONDS, + absolute = true) + public String getMessage(@PathParam("name") String name) { + return String.format("Hello %s", name); + } + +} diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/package-info.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/package-info.java new file mode 100644 index 000000000..01f61f642 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * HTTP status count example. + */ +package io.helidon.examples.mp.httpstatuscount; diff --git a/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/beans.xml b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..f0ce85167 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..b141d1ed2 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +# Change the following to true to enable the optional MicroProfile Metrics REST.request metrics +metrics.rest-request.enabled=false + +# Application properties. This is the default greeting +app.greeting=Hello + + diff --git a/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/native-image/reflect-config.json b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1 @@ +[] diff --git a/examples/microprofile/http-status-count-mp/src/main/resources/application.yaml b/examples/microprofile/http-status-count-mp/src/main/resources/application.yaml new file mode 100644 index 000000000..4379253ad --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/resources/application.yaml @@ -0,0 +1,16 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +server.port: 8080 diff --git a/examples/microprofile/http-status-count-mp/src/main/resources/logging.properties b/examples/microprofile/http-status-count-mp/src/main/resources/logging.properties new file mode 100644 index 000000000..769cf9733 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/main/resources/logging.properties @@ -0,0 +1,36 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Quiet Weld +org.jboss.level=WARNING + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/MainTest.java b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/MainTest.java new file mode 100644 index 000000000..526ec9102 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/MainTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.mp.httpstatuscount; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@TestMethodOrder(MethodOrderer.MethodName.class) +public class MainTest { + + @Inject + private MetricRegistry registry; + + @Inject + private WebTarget target; + + + @Test + public void testMicroprofileMetrics() { + String message = target.path("simple-greet/Joe") + .request() + .get(String.class); + + assertThat(message, is("Hello Joe")); + Counter counter = registry.counter("personalizedGets"); + double before = counter.getCount(); + + message = target.path("simple-greet/Eric") + .request() + .get(String.class); + + assertThat(message, is("Hello Eric")); + double after = counter.getCount(); + assertThat("Difference in personalized greeting counter between successive calls", after - before, is(1d)); + } + + @Test + public void testMetrics() throws Exception { + Response response = target + .path("metrics") + .request() + .get(); + assertThat(response.getStatus(), is(200)); + } + + @Test + public void testHealth() throws Exception { + Response response = target + .path("health") + .request() + .get(); + assertThat(response.getStatus(), is(200)); + } + + @Test + public void testGreet() throws Exception { + GreetingMessage message = target + .path("simple-greet") + .request() + .get(GreetingMessage.class); + assertThat(message.getMessage(), is("Hello World!")); + } + + @Test + public void testGreetings() throws Exception { + GreetingMessage jsonMessage = target + .path("greet/Joe") + .request() + .get(GreetingMessage.class); + assertThat(jsonMessage.getMessage(), is("Hello Joe!")); + + try (Response r = target + .path("greet/greeting") + .request() + .put(Entity.entity("{\"message\" : \"Hola\"}", MediaType.APPLICATION_JSON))) { + assertThat(r.getStatus(), is(204)); + } + + jsonMessage = target + .path("greet/Jose") + .request() + .get(GreetingMessage.class); + assertThat(jsonMessage.getMessage(), is("Hola Jose!")); + } + +} diff --git a/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusResource.java b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusResource.java new file mode 100644 index 000000000..5e17de2a1 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusResource.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.mp.httpstatuscount; + +import io.helidon.http.Status; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * Test-only resource 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. + */ +@RequestScoped +@Path("/status") +public class StatusResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/{status}") + public Response reportStatus(@PathParam("status") String statusText) { + int status; + String msg; + try { + status = Integer.parseInt(statusText); + msg = "Successful conversion"; + } catch (NumberFormatException ex) { + status = Status.INTERNAL_SERVER_ERROR_500.code(); + msg = "Unsuccessful conversion"; + } + return status == 204 ? Response.status(204).build() : Response.status(status).entity(msg).build(); + } +} diff --git a/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusTest.java b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusTest.java new file mode 100644 index 000000000..1c64f2855 --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.mp.httpstatuscount; + +import io.helidon.http.Status; +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +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.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@AddBean(StatusResource.class) +public class StatusTest { + + @Inject + private WebTarget webTarget; + + @Inject + private MetricRegistry metricRegistry; + + private final Counter[] STATUS_COUNTERS = new Counter[6]; + + @BeforeEach + void findStatusMetrics() { + for (int i = 1; i < STATUS_COUNTERS.length; i++) { + STATUS_COUNTERS[i] = metricRegistry.counter(new MetricID(HttpStatusMetricFilter.STATUS_COUNTER_NAME, + new Tag(HttpStatusMetricFilter.STATUS_TAG_NAME, i + "xx"))); + } + } + + @Test + void checkStatusMetrics() { + // intermediate responses are not "full" responses and since JDK 20 they are not returned by the client at all + // checkAfterStatus(171); + checkAfterStatus(200); + checkAfterStatus(201); + checkAfterStatus(204); + checkAfterStatus(301); + checkAfterStatus(401); + checkAfterStatus(404); + } + + @Test + void checkStatusAfterGreet() { + long[] before = new long[6]; + for (int i = 1; i < 6; i++) { + before[i] = STATUS_COUNTERS[i].getCount(); + } + Response response = webTarget.path("/greet") + .request(MediaType.APPLICATION_JSON) + .get(); + assertThat("Status of /greet", response.getStatus(), is(Status.OK_200.code())); + checkCounters(response.getStatus(), before); + } + + void checkAfterStatus(int status) { + String path = "/status/" + status; + long[] before = new long[6]; + for (int i = 1; i < 6; i++) { + before[i] = STATUS_COUNTERS[i].getCount(); + } + Response response = webTarget.path(path) + .request(MediaType.TEXT_PLAIN_TYPE) + .get(); + assertThat("Response status", response.getStatus(), is(status)); + checkCounters(status, before); + } + + private void checkCounters(int status, long[] before) { + int family = status / 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/examples/microprofile/http-status-count-mp/src/test/resources/application.yaml b/examples/microprofile/http-status-count-mp/src/test/resources/application.yaml new file mode 100644 index 000000000..9d37ce31c --- /dev/null +++ b/examples/microprofile/http-status-count-mp/src/test/resources/application.yaml @@ -0,0 +1,18 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +security: + enabled: false \ No newline at end of file diff --git a/examples/microprofile/idcs/README.md b/examples/microprofile/idcs/README.md new file mode 100644 index 000000000..2ac3ecbac --- /dev/null +++ b/examples/microprofile/idcs/README.md @@ -0,0 +1,54 @@ +# Helidon MP IDCS + +This example demonstrates integration with IDCS (Oracle identity service, integrated with Open ID Connect provider) where JAX-RS application resources are protected by IDCS. + +## Contents + +This project contains two samples, one (IdcsApplication.java) and a second (ReactiveService.java). It also contains a static resource. When configured the example exposes multiple HTTP endpoints. + +### IDCS Configuration + +[This documentation](https://docs.oracle.com/en/cloud/paas/identity-cloud/uaids/oracle-identity-cloud-service.html#GUID-BC4769EE-258A-4B53-AED5-6BA9888C8275) describes basics of IDCS as well as how you can get IDCS instance. + +1. [Log in to the IDCS console](https://docs.oracle.com/en/cloud/paas/identity-cloud/uaids/how-access-oracle-identity-cloud-service.html) and create a new application of type "confidential app" +2. Within **Resources** + 1. Create two resources called `first_scope` and `second_scope` + 2. Primary Audience = `http://localhost:7987/"` (ensure there is a trailing /) +3. Within **Client Configuration** + 1. Register a client + 2. Allowed Grant Types = Client Credentials,JWT Assertion, Refresh Token, Authorization Code + 3. Check "Allow non-HTTPS URLs" + 4. Set Redirect URL to `http://localhost:7987/oidc/redirect` + 5. Client Type = Confidential + 6. Add all Scopes defined in the resources section + 7. Set allowed operations to `Introspect` + 8. Set Post Logout Redirect URL to `http://localhost:7987/loggedout` + +Ensure you save and *activate* the application + +### Application Configuration + +Edit application.yaml based on your IDCS Configuration + +1. idcs-uri : Base URL of your idcs instance, usually something like https://idcs-.identity.oraclecloud.com +2. idcs-client-id : This is obtained from your IDCS application in the IDCS console +3. idcs-client-secret : This is obtained from your IDCS application in the IDCS console +4. frontend-uri : This is the base URL of your application when run, e.g. `http://localhost:7987` +5. proxy-host : Your proxy server if needed +6. scope-audience : This is the scope audience which MUST match the primary audience in the IDCS resource, recommendation is not to have a trailing slash (/) + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-security-idcs.jar +``` + +Try the endpoints: + +| Endpoint | Description | +|:--------------------|:-----------------------------------------------------------------------| +| `rest/login` | Login | +| `rest/scopes` | Full security with scopes and roles (see IdcsResource.java) | +| `rest/service` | Protected service (see application.yaml - security.web-server) | +| `web/resource.html` | Protected static resource (see application.yaml - security.web-server) | diff --git a/examples/microprofile/idcs/pom.xml b/examples/microprofile/idcs/pom.xml new file mode 100644 index 000000000..374f6ae29 --- /dev/null +++ b/examples/microprofile/idcs/pom.xml @@ -0,0 +1,85 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-security-idcs + 1.0.0-SNAPSHOT + Helidon Examples Microprofile IDCS Security + + + Microprofile example with IDCS integration (through Open ID Connect) + + + + io.helidon.examples.microprofile.security.idcs.IdcsMain + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.microprofile + helidon-microprofile-oidc + + + io.helidon.security.providers + helidon-security-providers-idcs-mapper + + + io.smallrye + jandex + runtime + true + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/IdcsApplication.java b/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/IdcsApplication.java new file mode 100644 index 000000000..a3c5d3ff6 --- /dev/null +++ b/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/IdcsApplication.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.security.idcs; + +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * Example JAX-RS application with resources protected by IDCS. + */ +@ApplicationScoped +@ApplicationPath("/rest") +public class IdcsApplication extends Application { + @Override + public Set> getClasses() { + return Set.of(IdcsResource.class); + } +} diff --git a/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/IdcsMain.java b/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/IdcsMain.java new file mode 100644 index 000000000..840747d5f --- /dev/null +++ b/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/IdcsMain.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.security.idcs; + +import io.helidon.microprofile.server.Server; + +/** + * IDCS example. + */ +public final class IdcsMain { + private IdcsMain() { + } + + /** + * Start the server and use the application picked up by CDI. + * + * @param args command line arguments, ignored + */ + public static void main(String[] args) { + Server.create().start(); + + System.out.println("Endpoints:"); + System.out.println("Login"); + System.out.println(" http://localhost:7987/rest/login"); + System.out.println("Full security with scopes and roles (see IdcsResource.java)"); + System.out.println(" http://localhost:7987/rest/scopes"); + System.out.println("A protected service (see application.yaml - security.web-server)"); + System.out.println(" http://localhost:7987/service"); + System.out.println("A protected static resource (see application.yaml - security.web-server"); + System.out.println(" http://localhost:7987/web/resource.html"); + } +} diff --git a/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/IdcsResource.java b/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/IdcsResource.java new file mode 100644 index 000000000..4fa504be5 --- /dev/null +++ b/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/IdcsResource.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.security.idcs; + +import io.helidon.security.SecurityContext; +import io.helidon.security.abac.role.RoleValidator; +import io.helidon.security.abac.scope.ScopeValidator; +import io.helidon.security.annotations.Authenticated; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; + +/** + * JAX-RS resource. + */ +@Path("/") +public class IdcsResource { + /** + * A protected resource (authentication required). + * + * @param context security context that will contain user's subject once login is completed + * @return user's subject as a string + */ + @GET + @Path("/login") + @Authenticated + public String login(@Context SecurityContext context) { + return context.user().toString(); + } + + /** + * A login flow example to use from a HTML frontend. A login button would call this method with a + * query parameter with redirect to the first page. + * + * @param context security context that will contain user's subject once login is completed + * @param redirectTo target URI (relative) to redirect to + * @return redirect response + */ + @GET + @Path("/login2") + @Authenticated + public Response login(@Context SecurityContext context, @QueryParam("target") String redirectTo) { + return Response + .status(Response.Status.TEMPORARY_REDIRECT) + .header("Location", redirectTo) + .build(); + } + + /** + * Authenticated and authorized endpoint that requires two scopes (these must be configure in your IDCS application). + * + * @param context security context + * @return current user's subject as a string, should now contain the scopes required + */ + @GET + @Path("/scopes") + @Authenticated + // Scopes defined in IDCS in my scope audience (see application.yaml) + @ScopeValidator.Scope("first_scope") + @ScopeValidator.Scope("second_scope") + // A group defined in my IDCS domain + @RoleValidator.Roles("my_admins") + public String scopes(@Context SecurityContext context) { + return context.user().toString(); + } +} diff --git a/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/ServiceImpl.java b/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/ServiceImpl.java new file mode 100644 index 000000000..86ac5d21e --- /dev/null +++ b/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/ServiceImpl.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.security.idcs; + +import io.helidon.microprofile.server.RoutingPath; +import io.helidon.security.Principal; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; +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.enterprise.context.ApplicationScoped; + +/** + * Service implementation. + */ +@ApplicationScoped +@RoutingPath("/service") +public class ServiceImpl implements HttpService { + + @Override + public void routing(HttpRules rules) { + rules.get(this::hello); + } + + private void hello(ServerRequest req, ServerResponse res) { + String username = req.context() + .get(SecurityContext.class) + .flatMap(SecurityContext::user) + .map(Subject::principal) + .map(Principal::getName) + .orElse("not authenticated"); + + res.send("Hello from service, you are " + username); + } +} diff --git a/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/package-info.java b/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/package-info.java new file mode 100644 index 000000000..1e5f284bb --- /dev/null +++ b/examples/microprofile/idcs/src/main/java/io/helidon/examples/microprofile/security/idcs/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of integration with IDCS (through Open ID Connect). + */ +package io.helidon.examples.microprofile.security.idcs; diff --git a/examples/microprofile/idcs/src/main/resources/META-INF/beans.xml b/examples/microprofile/idcs/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..ddb8316e3 --- /dev/null +++ b/examples/microprofile/idcs/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/idcs/src/main/resources/WEB/resource.html b/examples/microprofile/idcs/src/main/resources/WEB/resource.html new file mode 100644 index 000000000..63a766a46 --- /dev/null +++ b/examples/microprofile/idcs/src/main/resources/WEB/resource.html @@ -0,0 +1,32 @@ + + + + + +Hello, this is a static resource loaded from classpath. +

+ The configuration (microprofile config, accessed from application.yaml): +


+        server.static.classpath.location=/WEB
+        server.static.classpath.context=/web
+    
+

+ + + diff --git a/examples/microprofile/idcs/src/main/resources/application.yaml b/examples/microprofile/idcs/src/main/resources/application.yaml new file mode 100644 index 000000000..44f508484 --- /dev/null +++ b/examples/microprofile/idcs/src/main/resources/application.yaml @@ -0,0 +1,63 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Security requires Helidon config + +server: + port: 7987 + # location on classpath (e.g. src/main/resources/WEB in maven) + static.classpath: + location: "/WEB" + # this is optional, defaults to "/" + context: "/web" +security: + config.require-encryption: false + properties: + # This is a nice way to be able to override this with local properties or env-vars + idcs-uri: "https://tenant-id.identity.oracle.com" + idcs-client-id: "client-id" + idcs-client-secret: "changeit" + # Used as a base for redirects back to us + frontend-uri: "http://localhost:7987" + proxy-host: "if you need proxy" + providers: + - abac: + # Adds ABAC Provider - it does not require any configuration + - oidc: + client-id: "${security.properties.idcs-client-id}" + client-secret: "${security.properties.idcs-client-secret}" + identity-uri: "${security.properties.idcs-uri}" + # A prefix used for custom scopes + scope-audience: "http://localhost:7987/test-application" + proxy-host: "${security.properties.proxy-host}" + frontend-uri: "${security.properties.frontend-uri}" + # We want to redirect to login page (and token can be received either through cookie or header) + redirect: true + - idcs-role-mapper: + multitenant: false + oidc-config: + # we must repeat IDCS configuration, as in this case + # IDCS serves both as open ID connect authenticator and + # as a role mapper. Using minimal configuration here + client-id: "${security.properties.idcs-client-id}" + client-secret: "${security.properties.idcs-client-secret}" + identity-uri: "${security.properties.idcs-uri}" + web-server: + paths: + - path: "/web[/{*}]" + authenticate: true + - path: "/service[/{*}]" + authenticate: true diff --git a/examples/microprofile/idcs/src/main/resources/logging.properties b/examples/microprofile/idcs/src/main/resources/logging.properties new file mode 100644 index 000000000..3500f8af3 --- /dev/null +++ b/examples/microprofile/idcs/src/main/resources/logging.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO +AUDIT.level=FINEST +io.helidon.security.providers.oidc.level=FINEST +io.helidon.microprofile.config.level=FINEST diff --git a/examples/microprofile/lra/README.md b/examples/microprofile/lra/README.md new file mode 100644 index 000000000..41e38cb43 --- /dev/null +++ b/examples/microprofile/lra/README.md @@ -0,0 +1,50 @@ +# Helidon LRA Example + +## Build and run + +```shell +mvn package +java -jar ./target/helidon-examples-microprofile-lra.jar +``` + +### Coordinator +Narayana like coordinator is expected to be running on the port `8070`, url can be changed in application.yaml +```yaml +mp.lra.coordinator.url: http://localhost:8070/lra-coordinator +``` + +#### Build and run LRA coordinator +> :warning: **Experimental feature**: Helidon LRA coordinator is an experimental tool, running it in production is not advised + +```shell +docker build -t helidon/lra-coordinator https://github.com/oracle/helidon.git#:lra/coordinator/server +docker run --rm --name lra-coordinator --network="host" helidon/lra-coordinator +``` + +### Test LRA resource +Then call for completed transaction: +```shell +curl -X PUT -d 'lra rocks' http://localhost:7001/example/start-example +``` +And observe processing success in the output followed by complete called by LRA coordinator: +``` +Data lra rocks processed 🏭 +LRA id: f120a842-88da-429b-82d9-7274ee9ce8f6 completed 🎉 +``` + +For compensated transaction: +```shell +curl -X PUT -d BOOM http://localhost:7001/example/start-example +``` +Observe exception in the output followed by compensation called by LRA coordinator: +``` +java.lang.RuntimeException: BOOM 💥 + at io.helidon.microprofile.example.lra.LRAExampleResource.startExample(LRAExampleResource.java:56) +... +LRA id: 3629421b-b2a4-4fc4-a2f0-941cbf3fa8ad compensated 🚒 +``` + +Or compensated transaction timeout: +```shell +curl -X PUT -d TIMEOUT http://localhost:7001/example/start-example +``` \ No newline at end of file diff --git a/examples/microprofile/lra/pom.xml b/examples/microprofile/lra/pom.xml new file mode 100644 index 000000000..9e5c8da0e --- /dev/null +++ b/examples/microprofile/lra/pom.xml @@ -0,0 +1,85 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-lra + 1.0.0-SNAPSHOT + Helidon Examples Microprofile LRA + + + Microprofile Long Running Actions Example + + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.microprofile.config + helidon-microprofile-config + + + io.helidon.microprofile.lra + helidon-microprofile-lra + + + io.helidon.lra + helidon-lra-coordinator-narayana-client + + + io.smallrye + jandex + runtime + true + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/lra/src/main/java/io/helidon/microprofile/example/lra/LRAExampleResource.java b/examples/microprofile/lra/src/main/java/io/helidon/microprofile/example/lra/LRAExampleResource.java new file mode 100644 index 000000000..ec460b3bb --- /dev/null +++ b/examples/microprofile/lra/src/main/java/io/helidon/microprofile/example/lra/LRAExampleResource.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.lra; + +import java.net.URI; +import java.time.temporal.ChronoUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.lra.LRAResponse; +import org.eclipse.microprofile.lra.annotation.Compensate; +import org.eclipse.microprofile.lra.annotation.Complete; +import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; + +import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; + +/** + * Example resource with LRA. + */ +@Path("/example") +@ApplicationScoped +public class LRAExampleResource { + + private static final Logger LOGGER = Logger.getLogger(LRAExampleResource.class.getName()); + + /** + * Starts a new long-running action. + * + * @param lraId id of this action + * @param data entity + * @return empty response + * + * @throws InterruptedException this method is sleeping on thread, so it can throw interrupted exception + */ + @PUT + @LRA(value = LRA.Type.REQUIRES_NEW, timeLimit = 500, timeUnit = ChronoUnit.MILLIS) + @Path("start-example") + public Response startExample(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId, + String data) throws InterruptedException { + if (data.contains("BOOM")) { + throw new RuntimeException("BOOM 💥"); + } + + if (data.contains("TIMEOUT")) { + Thread.sleep(2000); + } + + LOGGER.info("Data " + data + " processed 🏭"); + return Response.ok().build(); + } + + /** + * Completes the long-running action. + * + * @param lraId id of this action + * @return completed response + */ + @PUT + @Complete + @Path("complete-example") + public Response completeExample(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) { + LOGGER.log(Level.INFO, "LRA id: {0} completed 🎉", lraId); + return LRAResponse.completed(); + } + + /** + * Compensation for long-running action. + * + * @param lraId id of action to compensate + * @return compensated response + */ + @PUT + @Compensate + @Path("compensate-example") + public Response compensateExample(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) { + LOGGER.log(Level.SEVERE, "LRA id: {0} compensated 🚒", lraId); + return LRAResponse.compensated(); + } + +} diff --git a/examples/microprofile/lra/src/main/java/io/helidon/microprofile/example/lra/package-info.java b/examples/microprofile/lra/src/main/java/io/helidon/microprofile/example/lra/package-info.java new file mode 100644 index 000000000..ce6cbc3d6 --- /dev/null +++ b/examples/microprofile/lra/src/main/java/io/helidon/microprofile/example/lra/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon MicroProfile LRA Example. + */ +package io.helidon.microprofile.example.lra; + diff --git a/examples/microprofile/lra/src/main/resources/META-INF/beans.xml b/examples/microprofile/lra/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e149ced7d --- /dev/null +++ b/examples/microprofile/lra/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/lra/src/main/resources/application.yaml b/examples/microprofile/lra/src/main/resources/application.yaml new file mode 100644 index 000000000..4d0150f6a --- /dev/null +++ b/examples/microprofile/lra/src/main/resources/application.yaml @@ -0,0 +1,19 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +server.port: 7001 + +mp.lra: + coordinator.url: http://localhost:8070/lra-coordinator diff --git a/examples/microprofile/lra/src/main/resources/logging.properties b/examples/microprofile/lra/src/main/resources/logging.properties new file mode 100644 index 000000000..1dae6118e --- /dev/null +++ b/examples/microprofile/lra/src/main/resources/logging.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO diff --git a/examples/microprofile/messaging-sse/README.md b/examples/microprofile/messaging-sse/README.md new file mode 100644 index 000000000..11f1719b2 --- /dev/null +++ b/examples/microprofile/messaging-sse/README.md @@ -0,0 +1,17 @@ +# Helidon Reactive Messaging Example + + Example showing + * [Microprofile Reactive Messaging](https://github.com/eclipse/microprofile-reactive-messaging) + with [Microprofile Reactive Stream Operators](https://github.com/eclipse/microprofile-reactive-streams-operators) + connected to [Server-Sent Events](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/sse.html). + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-messaging-sse.jar +``` + +Then try in the browser: + +http://localhost:7001 \ No newline at end of file diff --git a/examples/microprofile/messaging-sse/pom.xml b/examples/microprofile/messaging-sse/pom.xml new file mode 100644 index 000000000..b9ed8a169 --- /dev/null +++ b/examples/microprofile/messaging-sse/pom.xml @@ -0,0 +1,104 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-messaging-sse + 1.0.0-SNAPSHOT + Helidon Examples Microprofile Messaging with SSE + + + Microprofile Reactive Messaging with Server-Sent Events example + + + + + io.helidon.microprofile.messaging + helidon-microprofile-messaging + + + io.helidon.microprofile.bundles + helidon-microprofile + + + org.glassfish.jersey.media + jersey-media-sse + + + io.smallrye + jandex + runtime + true + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + snippet + + resources + + + + + src/main/java/io/helidon/microprofile/example/messaging/sse + + MsgProcessingBean.java + + + + ${project.build.directory}/classes/WEB + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/messaging-sse/src/main/java/io/helidon/microprofile/example/messaging/sse/MessagingExampleResource.java b/examples/microprofile/messaging-sse/src/main/java/io/helidon/microprofile/example/messaging/sse/MessagingExampleResource.java new file mode 100644 index 000000000..6a61c123a --- /dev/null +++ b/examples/microprofile/messaging-sse/src/main/java/io/helidon/microprofile/example/messaging/sse/MessagingExampleResource.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.messaging.sse; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.sse.Sse; +import jakarta.ws.rs.sse.SseEventSink; + +/** + * Example resource with SSE. + */ +@Path("example") +@RequestScoped +public class MessagingExampleResource { + private final MsgProcessingBean msgBean; + + /** + * Constructor injection of field values. + * + * @param msgBean Messaging example bean + */ + @Inject + public MessagingExampleResource(MsgProcessingBean msgBean) { + this.msgBean = msgBean; + } + + + /** + * Process send. + * @param msg message to process + */ + @Path("/send/{msg}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public void getSend(@PathParam("msg") String msg) { + msgBean.process(msg); + } + + /** + * Consume event. + * + * @param eventSink sink + * @param sse event + */ + @GET + @Path("sse") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void listenToEvents(@Context SseEventSink eventSink, @Context Sse sse) { + msgBean.addSink(eventSink, sse); + } +} diff --git a/examples/microprofile/messaging-sse/src/main/java/io/helidon/microprofile/example/messaging/sse/MsgProcessingBean.java b/examples/microprofile/messaging-sse/src/main/java/io/helidon/microprofile/example/messaging/sse/MsgProcessingBean.java new file mode 100644 index 000000000..189b86f4b --- /dev/null +++ b/examples/microprofile/messaging-sse/src/main/java/io/helidon/microprofile/example/messaging/sse/MsgProcessingBean.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.messaging.sse; + +import java.util.concurrent.SubmissionPublisher; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.sse.OutboundSseEvent; +import jakarta.ws.rs.sse.Sse; +import jakarta.ws.rs.sse.SseBroadcaster; +import jakarta.ws.rs.sse.SseEventSink; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.eclipse.microprofile.reactive.streams.operators.ProcessorBuilder; +import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams; +import org.glassfish.jersey.media.sse.OutboundEvent; +import org.reactivestreams.FlowAdapters; +import org.reactivestreams.Publisher; + +/** + * Bean for message processing. + */ +@ApplicationScoped +public class MsgProcessingBean { + private final SubmissionPublisher emitter = new SubmissionPublisher<>(); + private SseBroadcaster sseBroadcaster; + + /** + * Create a publisher for the emitter. + * + * @return A Publisher from the emitter + */ + @Outgoing("multiplyVariants") + public Publisher preparePublisher() { + // Create new publisher for emitting to by this::process + return ReactiveStreams + .fromPublisher(FlowAdapters.toPublisher(emitter)) + .buildRs(); + } + + /** + * Returns a builder for a processor that maps a string into three variants. + * + * @return ProcessorBuilder + */ + @Incoming("multiplyVariants") + @Outgoing("wrapSseEvent") + public ProcessorBuilder multiply() { + // Multiply to 3 variants of same message + return ReactiveStreams.builder() + .flatMap(o -> + ReactiveStreams.of( + // upper case variant + o.toUpperCase(), + // repeat twice variant + o.repeat(2), + // reverse chars 'tnairav' + new StringBuilder(o).reverse().toString()) + ); + } + + /** + * Maps a message to an sse event. + * + * @param msg to wrap + * @return an outbound SSE event + */ + @Incoming("wrapSseEvent") + @Outgoing("broadcast") + public OutboundSseEvent wrapSseEvent(String msg) { + // Map every message to sse event + return new OutboundEvent.Builder().data(msg).build(); + } + + /** + * Broadcasts an event. + * + * @param sseEvent Event to broadcast + */ + @Incoming("broadcast") + public void broadcast(OutboundSseEvent sseEvent) { + // Broadcast to all sse sinks + this.sseBroadcaster.broadcast(sseEvent); + } + + /** + * Consumes events. + * + * @param eventSink event sink + * @param sse event + */ + public void addSink(final SseEventSink eventSink, final Sse sse) { + if (this.sseBroadcaster == null) { + this.sseBroadcaster = sse.newBroadcaster(); + } + this.sseBroadcaster.register(eventSink); + } + + /** + * Emit a message. + * + * @param msg message to emit + */ + public void process(final String msg) { + emitter.submit(msg); + } +} diff --git a/examples/microprofile/messaging-sse/src/main/java/io/helidon/microprofile/example/messaging/sse/package-info.java b/examples/microprofile/messaging-sse/src/main/java/io/helidon/microprofile/example/messaging/sse/package-info.java new file mode 100644 index 000000000..77fbcf451 --- /dev/null +++ b/examples/microprofile/messaging-sse/src/main/java/io/helidon/microprofile/example/messaging/sse/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon MicroProfile Messaging Example. + */ +package io.helidon.microprofile.example.messaging.sse; + diff --git a/examples/microprofile/messaging-sse/src/main/resources/META-INF/beans.xml b/examples/microprofile/messaging-sse/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..676e09a2d --- /dev/null +++ b/examples/microprofile/messaging-sse/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/messaging-sse/src/main/resources/WEB/favicon.ico b/examples/microprofile/messaging-sse/src/main/resources/WEB/favicon.ico new file mode 100644 index 000000000..d91659fdb Binary files /dev/null and b/examples/microprofile/messaging-sse/src/main/resources/WEB/favicon.ico differ diff --git a/examples/microprofile/messaging-sse/src/main/resources/WEB/img/arrow-1.png b/examples/microprofile/messaging-sse/src/main/resources/WEB/img/arrow-1.png new file mode 100644 index 000000000..bbba0aef8 Binary files /dev/null and b/examples/microprofile/messaging-sse/src/main/resources/WEB/img/arrow-1.png differ diff --git a/examples/microprofile/messaging-sse/src/main/resources/WEB/img/arrow-2.png b/examples/microprofile/messaging-sse/src/main/resources/WEB/img/arrow-2.png new file mode 100644 index 000000000..0b1096b07 Binary files /dev/null and b/examples/microprofile/messaging-sse/src/main/resources/WEB/img/arrow-2.png differ diff --git a/examples/microprofile/messaging-sse/src/main/resources/WEB/img/cloud.png b/examples/microprofile/messaging-sse/src/main/resources/WEB/img/cloud.png new file mode 100644 index 000000000..3e04833c0 Binary files /dev/null and b/examples/microprofile/messaging-sse/src/main/resources/WEB/img/cloud.png differ diff --git a/examples/microprofile/messaging-sse/src/main/resources/WEB/img/frank.png b/examples/microprofile/messaging-sse/src/main/resources/WEB/img/frank.png new file mode 100644 index 000000000..51a13d8db Binary files /dev/null and b/examples/microprofile/messaging-sse/src/main/resources/WEB/img/frank.png differ diff --git a/examples/microprofile/messaging-sse/src/main/resources/WEB/index.html b/examples/microprofile/messaging-sse/src/main/resources/WEB/index.html new file mode 100644 index 000000000..232bb4b4e --- /dev/null +++ b/examples/microprofile/messaging-sse/src/main/resources/WEB/index.html @@ -0,0 +1,120 @@ + + + + + + + Helidon Reactive Messaging + + + + + + + + +
+
+
+ +
+
Send
+
+
+
+
+
REST call /example/send/{msg}
+
+
+
SSE messages received
+
+
+
+
+
+            
+        
+
+
+ + + + + \ No newline at end of file diff --git a/examples/microprofile/messaging-sse/src/main/resources/WEB/main.css b/examples/microprofile/messaging-sse/src/main/resources/WEB/main.css new file mode 100644 index 000000000..5fdb48791 --- /dev/null +++ b/examples/microprofile/messaging-sse/src/main/resources/WEB/main.css @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#root { + background-color: #36ABF2; + font-family: Roboto,sans-serif; + color: #fff; + position: absolute; + overflow-x: hidden; + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#root::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +#helidon { + width: 509px; + height: 273px; + position: relative; + left: -509px; + z-index: 4; + background: url('img/frank.png'); +} + +#rest-tip { + position: relative; + top: -80px; + left: 160px; +} + +#rest-tip-arrow { + width: 205px; + height: 304px; + z-index: 4; + top: -20px; + background: url('img/arrow-1.png'); +} +#rest-tip-label { + position: absolute; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; + left: -60px; +} + +#sse-tip { + position: absolute; + overflow: hidden; + display: flex; + width: auto; + height: auto; + top: 5%; + right: 10%; + z-index: 0; +} + +#sse-tip-arrow { + position: relative; + top: -30px; + width: 296px; + height: 262px; + z-index: 4; + background: url('img/arrow-2.png'); +} +#sse-tip-label { + position: relative; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; +} + +#producer { + float: left; + position: relative; + width: 300px; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 99; +} + +#msgBox { + position: absolute; + width: 300px; + top: 25%; + right: 3%; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 20; +} + +#input { + width: 210px; + height: 22px; + top: 58px; + left: 30px; + background-color: white; + border-radius: 10px; + border-style: solid; + border-color: white; + position: absolute; + z-index: 10; +} + +#inputCloud { + position: relative; + width: 310px; + height: 150px; + background: url('img/cloud.png'); +} + +#msg { + background-color: #D2EBFC; + color: #1A9BF4; + border-radius: 10px; + width: 300px; + height: 50px; + margin: 5px; + display: flex; + padding-left: 10px; + justify-content: center; + align-items: center; + z-index: 99; +} + +#submit { + font-weight: bold; + background-color: aqua; + color: #1A9BF4; + border-radius: 12px; + width: 100px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 5px; + cursor: pointer; +} + +#snippet { + position: absolute; + top: 15%; + left: 30%; + width: 40%; + z-index: 5; +} + +.hljs { + border-radius: 10px; + font-size: 12px; +} \ No newline at end of file diff --git a/examples/microprofile/messaging-sse/src/main/resources/application.yaml b/examples/microprofile/messaging-sse/src/main/resources/application.yaml new file mode 100644 index 000000000..729532a22 --- /dev/null +++ b/examples/microprofile/messaging-sse/src/main/resources/application.yaml @@ -0,0 +1,18 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +server.port: 7001 +server.static.classpath.location: /WEB +server.static.classpath.welcome: index.html diff --git a/examples/microprofile/messaging-sse/src/main/resources/logging.properties b/examples/microprofile/messaging-sse/src/main/resources/logging.properties new file mode 100644 index 000000000..eb9b339b7 --- /dev/null +++ b/examples/microprofile/messaging-sse/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO +io.helidon.microprofile.config.level=FINEST diff --git a/examples/microprofile/multipart/README.md b/examples/microprofile/multipart/README.md new file mode 100644 index 000000000..58aea2ba6 --- /dev/null +++ b/examples/microprofile/multipart/README.md @@ -0,0 +1,22 @@ +# MicroProfile MultiPart Example + +This example demonstrates how to use the Jersey `MultiPartFeature` with Helidon. + +This project implements a simple file service web application that supports uploading + and downloading files. The unit test uses the JAXRS client API to test the endpoints. + +## Build + +```shell +mvn package +``` + +## Run + +First, start the server: + +```shell +java -jar target/helidon-examples-microprofile-multipart.jar +``` + +Then open in your browser. diff --git a/examples/microprofile/multipart/pom.xml b/examples/microprofile/multipart/pom.xml new file mode 100644 index 000000000..4212b1cf5 --- /dev/null +++ b/examples/microprofile/multipart/pom.xml @@ -0,0 +1,94 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-multipart + 1.0.0-SNAPSHOT + Helidon Examples Microprofile Multipart + + + Example of a form based file upload with Helidon MP. + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + org.glassfish.jersey.media + jersey-media-multipart + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileService.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileService.java new file mode 100644 index 000000000..8dc01b403 --- /dev/null +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileService.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.multipart; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; +import org.glassfish.jersey.media.multipart.BodyPart; +import org.glassfish.jersey.media.multipart.BodyPartEntity; +import org.glassfish.jersey.media.multipart.MultiPart; + +/** + * File service. + */ +@Path("/api") +@ApplicationScoped +public class FileService { + + private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of()); + + private final FileStorage storage; + + @Inject + FileService(FileStorage storage) { + this.storage = storage; + } + + /** + * Upload a file to the storage. + * @param multiPart multipart entity + * @return Response + * @throws IOException if an IO error occurs + */ + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response upload(MultiPart multiPart) throws IOException { + for (BodyPart part : multiPart.getBodyParts()) { + if ("file[]".equals(part.getContentDisposition().getParameters().get("name"))) { + Files.copy(part.getEntityAs(BodyPartEntity.class).getInputStream(), + storage.create(part.getContentDisposition().getFileName()), + StandardCopyOption.REPLACE_EXISTING); + } + } + return Response.seeOther(URI.create("ui")).build(); + } + + /** + * Download a file from the storage. + * @param fname file name of the file to download + * @return Response + */ + @GET + @Path("{fname}") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + public Response download(@PathParam("fname") String fname) { + return Response.ok() + .header("Content-Disposition", "attachment; filename=\"" + fname + "\"") + .entity((StreamingOutput) output -> Files.copy(storage.lookup(fname), output)) + .build(); + } + + /** + * List the files in the storage. + * @return JsonObject + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public JsonObject list() { + JsonArrayBuilder arrayBuilder = JSON_FACTORY.createArrayBuilder(); + storage.listFiles().forEach(arrayBuilder::add); + return JSON_FACTORY.createObjectBuilder().add("files", arrayBuilder).build(); + } +} diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java new file mode 100644 index 000000000..ec08d4755 --- /dev/null +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.NotFoundException; + +/** + * Simple bean to managed a directory based storage. + */ +@ApplicationScoped +public class FileStorage { + + private final Path storageDir; + + /** + * Create a new instance. + */ + public FileStorage() { + try { + storageDir = Files.createTempDirectory("fileupload"); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Get the storage directory. + * @return directory + */ + public Path storageDir() { + return storageDir; + } + + /** + * Get the names of the files in the storage directory. + * @return Stream of file names + */ + public Stream listFiles() { + try { + return Files.walk(storageDir) + .filter(Files::isRegularFile) + .map(storageDir::relativize) + .map(java.nio.file.Path::toString); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Create a new file in the storage. + * @param fname file name + * @return file + * @throws BadRequestException if the resolved file is not contained in the storage directory + */ + public Path create(String fname) { + Path file = storageDir.resolve(fname); + if (!file.getParent().equals(storageDir)) { + throw new BadRequestException("Invalid file name"); + } + return file; + } + + /** + * Lookup an existing file in the storage. + * @param fname file name + * @return file + * @throws NotFoundException If the resolved file does not exist + * @throws BadRequestException if the resolved file is not contained in the storage directory + */ + public Path lookup(String fname) { + Path file = storageDir.resolve(fname); + if (!file.getParent().equals(storageDir)) { + throw new BadRequestException("Invalid file name"); + } + if (!Files.exists(file)) { + throw new NotFoundException(); + } + if (!Files.isRegularFile(file)) { + throw new BadRequestException("Not a file"); + } + return file; + } +} diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java new file mode 100644 index 000000000..1cb9769bd --- /dev/null +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.multipart; + +import jakarta.ws.rs.core.Feature; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.ext.Provider; +import org.glassfish.jersey.media.multipart.MultiPartFeature; + +/** + * {@link MultiPartFeature} is not auto-discovered. This {@link Feature} is discovered with {@link @Provider} + * and registers {@link MultiPartFeature} manually. + */ +@Provider +public class MultiPartFeatureProvider implements Feature { + + @Override + public boolean configure(FeatureContext context) { + return new MultiPartFeature().configure(context); + } +} diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/package-info.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/package-info.java new file mode 100644 index 000000000..f9d33dba2 --- /dev/null +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon Examples Microprofile Multipart. + */ +package io.helidon.examples.microprofile.multipart; diff --git a/examples/microprofile/multipart/src/main/resources/META-INF/beans.xml b/examples/microprofile/multipart/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..36a1bf7fa --- /dev/null +++ b/examples/microprofile/multipart/src/main/resources/META-INF/beans.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/examples/microprofile/multipart/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/multipart/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..5a2768c80 --- /dev/null +++ b/examples/microprofile/multipart/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +server.static.classpath.location=/WEB +server.static.classpath.welcome=index.html +server.static.classpath.context=/ui diff --git a/examples/microprofile/multipart/src/main/resources/WEB/index.html b/examples/microprofile/multipart/src/main/resources/WEB/index.html new file mode 100644 index 000000000..f8d2b5b2e --- /dev/null +++ b/examples/microprofile/multipart/src/main/resources/WEB/index.html @@ -0,0 +1,59 @@ + + + + + + Helidon Examples Microprofile Multipart + + + + + +

Uploaded files

+
+ +

Upload

+
+ Select a file to upload: + + +
+ + + + \ No newline at end of file diff --git a/examples/microprofile/multipart/src/main/resources/logging.properties b/examples/microprofile/multipart/src/main/resources/logging.properties new file mode 100644 index 000000000..c5299aba0 --- /dev/null +++ b/examples/microprofile/multipart/src/main/resources/logging.properties @@ -0,0 +1,36 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.microprofile.level=INFO +#io.helidon.common.level=INFO +#org.glassfish.jersey.level=INFO +#org.jboss.weld=INFO diff --git a/examples/microprofile/multipart/src/test/java/io/helidon/examples/microprofile/multipart/FileServiceTest.java b/examples/microprofile/multipart/src/test/java/io/helidon/examples/microprofile/multipart/FileServiceTest.java new file mode 100644 index 000000000..d6294f4b3 --- /dev/null +++ b/examples/microprofile/multipart/src/test/java/io/helidon/examples/microprofile/multipart/FileServiceTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.multipart; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import io.helidon.microprofile.server.JaxRsCdiExtension; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.DisableDiscovery; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; +import org.glassfish.jersey.media.multipart.MultiPart; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.io.TempDir; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests {@link FileService}. + */ +@HelidonTest +@DisableDiscovery +@AddExtension(ServerCdiExtension.class) +@AddExtension(JaxRsCdiExtension.class) +@AddExtension(CdiComponentProvider.class) +@AddBean(FileService.class) +@AddBean(FileStorage.class) +@AddBean(MultiPartFeatureProvider.class) +@TestMethodOrder(OrderAnnotation.class) +public class FileServiceTest { + + @Test + @Order(1) + public void testUpload(WebTarget target, @TempDir Path tempDirectory) throws IOException { + File file = Files.write(tempDirectory.resolve("foo.txt"), "bar\n".getBytes(StandardCharsets.UTF_8)).toFile(); + MultiPart multipart = new MultiPart() + .bodyPart(new FileDataBodyPart("file[]", file, MediaType.APPLICATION_OCTET_STREAM_TYPE)); + try (Response response = target + .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) + .register(MultiPartFeature.class) + .path("/api") + .request() + .post(Entity.entity(multipart, MediaType.MULTIPART_FORM_DATA_TYPE))) { + assertThat(response.getStatus(), is(303)); + } + } + + @Test + @Order(2) + public void testList(WebTarget target) { + try (Response response = target + .path("/api") + .request(MediaType.APPLICATION_JSON_TYPE) + .get()) { + assertThat(response.getStatus(), is(200)); + JsonObject json = response.readEntity(JsonObject.class); + assertThat(json, is(notNullValue())); + List files = json.getJsonArray("files").getValuesAs(v -> ((JsonString) v).getString()); + assertThat(files, hasItem("foo.txt")); + } + } + + @Test + @Order(3) + public void testDownload(WebTarget target) throws IOException { + try (Response response = target + .register(MultiPartFeature.class) + .path("/api/foo.txt") + .request(MediaType.APPLICATION_OCTET_STREAM_TYPE) + .get()) { + assertThat(response.getStatus(), is(200)); + assertThat(response.getHeaderString("Content-Disposition"), containsString("filename=\"foo.txt\"")); + InputStream inputStream = response.readEntity(InputStream.class); + assertThat(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8), is("bar\n")); + } + } +} \ No newline at end of file diff --git a/examples/microprofile/multiport/README.md b/examples/microprofile/multiport/README.md new file mode 100644 index 000000000..4ec6b8ae9 --- /dev/null +++ b/examples/microprofile/multiport/README.md @@ -0,0 +1,39 @@ +# Helidon MicroProfile Multiple Port Example + +It is common when deploying a microservice to run your service on +multiple ports so that you can control the visibility of your +service's endpoints. For example you might want to use three ports: + +- 8080: public REST endpoints of application +- 8081: private REST endpoints of application +- 8082: admin endpoints for health, metrics, etc. + +This lets you expose only the public endpoints via your +ingress controller or load balancer. + +This example shows a Helidon JAX-RS application running on three ports +as described above. + +The ports are configured in `application.yaml` by using named sockets. + +Two Applications are defined, each associated with a different socket +using @RoutingName. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-multiport.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/hello + +curl -X GET http://localhost:8081/private/hello + +curl -X GET http://localhost:8082/health + +curl -X GET http://localhost:8082/metrics +``` diff --git a/examples/microprofile/multiport/pom.xml b/examples/microprofile/multiport/pom.xml new file mode 100644 index 000000000..4e03ba70b --- /dev/null +++ b/examples/microprofile/multiport/pom.xml @@ -0,0 +1,108 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-multiport + 1.0.0-SNAPSHOT + Helidon Examples Microprofile Multiple Ports + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.config + helidon-config-yaml + + + io.helidon.microprofile.metrics + helidon-microprofile-metrics + + + io.helidon.microprofile.health + helidon-microprofile-health + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-params + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + + diff --git a/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PrivateApplication.java b/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PrivateApplication.java new file mode 100644 index 000000000..946bd9a5b --- /dev/null +++ b/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PrivateApplication.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.multiport; + +import java.util.Set; + +import io.helidon.microprofile.server.RoutingName; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.core.Application; + +/** + * Application to expose private resource. + */ +@ApplicationScoped +@RoutingName("private") +public class PrivateApplication extends Application { + @Override + public Set> getClasses() { + return Set.of(PrivateResource.class); + } +} diff --git a/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PrivateResource.java b/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PrivateResource.java new file mode 100644 index 000000000..e9192d828 --- /dev/null +++ b/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PrivateResource.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.multiport; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +/** + * Simple resource. + */ +@RequestScoped +@Path("/private") +public class PrivateResource { + + /** + * Return a private worldly greeting message. + * + * @return {@link String} + */ + @Path("/hello") + @GET + public String helloWorld() { + return "Private Hello World!!"; + } +} diff --git a/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PublicApplication.java b/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PublicApplication.java new file mode 100644 index 000000000..863429605 --- /dev/null +++ b/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PublicApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.multiport; + +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.core.Application; + +/** + * Application to expose public resource. + */ +@ApplicationScoped +public class PublicApplication extends Application { + @Override + public Set> getClasses() { + return Set.of(PublicResource.class); + } +} diff --git a/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PublicResource.java b/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PublicResource.java new file mode 100644 index 000000000..516e5120c --- /dev/null +++ b/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/PublicResource.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.multiport; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +/** + * Simple resource. + */ +@RequestScoped +@Path("/") +public class PublicResource { + + /** + * Return a worldly greeting message. + * + * @return {@link String} + */ + @Path("/hello") + @GET + public String helloWorld() { + return "Public Hello World!!"; + } +} diff --git a/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/package-info.java b/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/package-info.java new file mode 100644 index 000000000..0454201b6 --- /dev/null +++ b/examples/microprofile/multiport/src/main/java/io/helidon/examples/microprofile/multiport/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Application that exposes multiple ports. + */ +package io.helidon.examples.microprofile.multiport; diff --git a/examples/microprofile/multiport/src/main/resources/META-INF/beans.xml b/examples/microprofile/multiport/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e149ced7d --- /dev/null +++ b/examples/microprofile/multiport/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/multiport/src/main/resources/application.yaml b/examples/microprofile/multiport/src/main/resources/application.yaml new file mode 100644 index 000000000..a3bb865fb --- /dev/null +++ b/examples/microprofile/multiport/src/main/resources/application.yaml @@ -0,0 +1,29 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +server: + port: 8080 + host: "localhost" + sockets: + - name: "private" + port: 8081 + - name: "admin" + port: 8082 + # bind address is optional, if not defined, server host will be used) + bind-address: "localhost" + features: + observe: + # Metrics and health run on admin port + sockets: "admin" diff --git a/examples/microprofile/multiport/src/main/resources/logging.properties b/examples/microprofile/multiport/src/main/resources/logging.properties new file mode 100644 index 000000000..bf5ca3c71 --- /dev/null +++ b/examples/microprofile/multiport/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.level=INFO +io.helidon.faulttolerance.level=INFO + + diff --git a/examples/microprofile/multiport/src/test/java/io/helidon/examples/microprofile/multiport/MainTest.java b/examples/microprofile/multiport/src/test/java/io/helidon/examples/microprofile/multiport/MainTest.java new file mode 100644 index 000000000..1517b299f --- /dev/null +++ b/examples/microprofile/multiport/src/test/java/io/helidon/examples/microprofile/multiport/MainTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.multiport; + +import java.util.List; +import java.util.stream.Stream; + +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Unit test for MP multiport example. + */ +@HelidonTest +@AddConfig(key = "server.sockets.0.port", value = "0") // Force ports to be dynamically allocated +@AddConfig(key = "server.sockets.1.port", value = "0") +class MainTest { + // Port names + private static final String ADMIN_PORT = "admin"; + private static final String PRIVATE_PORT = "private"; + private static final String PUBLIC_PORT = "default"; + + private static final String BASE_URL = "http://localhost:"; + + // Used for other (private, admin) ports + private static Client client; + + // Used for the default (public) port which HelidonTest will configure for us + @Inject + private WebTarget publicWebTarget; + + // Needed to get values of dynamically allocated admin and private ports + @Inject + private ServerCdiExtension serverCdiExtension; + + static Stream initParams() { + final String PUBLIC_PATH = "/hello"; + final String PRIVATE_PATH = "/private/hello"; + final String HEALTH_PATH = "/health"; + final String METRICS_PATH = "/metrics"; + + return List.of( + new Params(PUBLIC_PORT, PUBLIC_PATH, Status.OK), + new Params(PUBLIC_PORT, PRIVATE_PATH, Status.NOT_FOUND), + new Params(PUBLIC_PORT, HEALTH_PATH, Status.NOT_FOUND), + new Params(PUBLIC_PORT, METRICS_PATH, Status.NOT_FOUND), + + new Params(PRIVATE_PORT, PUBLIC_PATH, Status.NOT_FOUND), + new Params(PRIVATE_PORT, PRIVATE_PATH, Status.OK), + new Params(PRIVATE_PORT, HEALTH_PATH, Status.NOT_FOUND), + new Params(PRIVATE_PORT, METRICS_PATH, Status.NOT_FOUND), + + new Params(ADMIN_PORT, PUBLIC_PATH, Status.NOT_FOUND), + new Params(ADMIN_PORT, PRIVATE_PATH, Status.NOT_FOUND), + new Params(ADMIN_PORT, HEALTH_PATH, Status.OK), + new Params(ADMIN_PORT, METRICS_PATH, Status.OK) + ).stream(); + } + + @BeforeAll + static void initClass() { + client = ClientBuilder.newClient(); + } + + @AfterAll + static void destroyClass() { + client.close();; + } + + @MethodSource("initParams") + @ParameterizedTest + public void testPortAccess(Params params) { + WebTarget webTarget; + + if (params.portName.equals(PUBLIC_PORT)) { + webTarget = publicWebTarget; + } else { + webTarget = client.target(BASE_URL + serverCdiExtension.port(params.portName)); + } + + try (Response response = webTarget.path(params.path) + .request() + .get()) { + assertThat(webTarget.getUri() + " returned incorrect HTTP status", + response.getStatusInfo().toEnum(), is(params.httpStatus)); + } + } + + @Test + void testEndpoints() { + // Probe PUBLIC port + try (Response response = publicWebTarget.path("/hello") + .request() + .get()) { + assertThat("default port should be serving public resource", + response.getStatusInfo().toEnum(), is(Response.Status.OK)); + assertThat("default port should return public data", + response.readEntity(String.class), is("Public Hello World!!")); + } + + // Probe PRIVATE port + int privatePort = serverCdiExtension.port(PRIVATE_PORT); + WebTarget baseTarget = client.target(BASE_URL + privatePort); + try (Response response = baseTarget.path("/private/hello") + .request() + .get()) { + assertThat("port " + privatePort + " should be serving private resource", + response.getStatusInfo().toEnum(), is(Response.Status.OK)); + assertThat("port " + privatePort + " should return private data", + response.readEntity(String.class), is("Private Hello World!!")); + } + } + + private static class Params { + String portName; + String path; + Response.Status httpStatus; + + private Params(String portName, String path, Response.Status httpStatus) { + this.portName = portName; + this.path = path; + this.httpStatus = httpStatus; + } + + @Override + public String toString() { + return portName + ":" + path + " should return " + httpStatus; + } + } +} diff --git a/examples/microprofile/oci-tls-certificates/README.md b/examples/microprofile/oci-tls-certificates/README.md new file mode 100644 index 000000000..856840195 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/README.md @@ -0,0 +1,114 @@ +# Helidon TLS and OCI Certificate Service Integration Example + +This module contains an example usage of the [OciCertificatesTlsManager](../../../integrations/oci/tls-certificates) provider that offers lifecycle and rotation of certificates to be used with Helidon. + +1. [Prerequisites](#prerequisites) +2. [Setting up OCI](#setting-up-oci) + 1. [Configuration](#configuration) + 2. [Prepare CA(Certification Authority)](#prepare-cacertification-authority) + 3. [Prepare keys and certificates](#prepare-keys-and-certificates) +3. [Configuration](#configuration) +4. [Rotating mTLS certificates](#rotating-mtls-certificates) +5. [Build and run example](#build-and-run-example) + 1. [Build & Run](#build--run) + 2. [Test with WebClient](#test-with-webclient) + 3. [Test with cURL](#test-with-curl) + +## Prerequisites +- OCI Tenancy with Vault [KMS](https://www.oracle.com/security/cloud-security/key-management) and [Certificate service](https://www.oracle.com/security/cloud-security/ssl-tls-certificates) availability. +- [OCI CLI](https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/cliinstall.htm#Quickstart) 3.2.1 or later with properly configured `~/.oci/config` to access your tenancy +- OpenSSL (on deb based distros `apt install openssl`) +- [Keytool](https://docs.oracle.com/en/java/javase/17/docs/specs/man/keytool.html) (comes with JDK installation) + +## Setting up OCI + +### Prepare CA(Certification Authority) +Follow [OCI documentation](https://docs.oracle.com/en-us/iaas/Content/certificates/managing-certificate-authorities.htm): +0. Signup or use an OCI tenancy (see above links). +1. Create group `CertificateAuthorityAdmins` and add your user in it. +2. Create dynamic group `CertificateAuthority-DG` with single rule `resource.type='certificateauthority'` +3. Create policy `CertificateAuthority-PL` with following statements: + ``` + Allow dynamic-group CertificateAuthority-DG to use keys in tenancy + Allow dynamic-group CertificateAuthority-DG to manage objects in tenancy + Allow group CertificateAuthorityAdmins to manage certificate-authority-family in tenancy + Allow group CertificateAuthorityAdmins to read keys in tenancy + Allow group CertificateAuthorityAdmins to use key-delegate in tenancy + Allow group CertificateAuthorityAdmins to read buckets in tenancy + Allow group CertificateAuthorityAdmins to read vaults in tenancy + ``` +4. Create policy `Vaults-PL` with following statements: + ``` + Allow group CertificateAuthorityAdmins to manage vaults in tenancy + Allow group CertificateAuthorityAdmins to manage keys in tenancy + Allow group CertificateAuthorityAdmins to manage secret-family in tenancy + ``` +5. Create or reuse an OCI Vault and notice there are cryptographic and management endpoints in the vault general info, we will need them later. +6. Create new key in the vault with following properties: + - Name: `mySuperCAKey` + - Protection Mode: **HSM** (requirement for CA keys, those can't be downloaded) + - Algorithm: **RSA** +7. Create CA: + 1. In OCI menu select `Identity & Security>Certificates>Certificate Authorities` + 2. Select button `Create Certificate Authority` + 3. Choose `Root Certificate Authority` and choose the name, for example `MySuperCA` + 4. Enter CN(Common Name), for example `my.super.authority` + 5. Select your vault and the key `mySuperCAKey` + 6. Select max validity for signed certs(or leave the default 90 days) + 7. Check `Skip Revocation` to keep it simple + 8. Select `Create Certificate Authority` button on the summary page + 9. Notice OCID of the newly created CA, we will need it later + +### Configuration +Following env variables to be configured in [config.sh](etc/unsupported-cert-tools/config.sh) +for both [rotating](#rotating-mtls-certificates) certificates and [running](#build--run) the examples. + +- **COMPARTMENT_OCID** - OCID of compartment the services are in +- **VAULT_CRYPTO_ENDPOINT** - Each OCI Vault has public crypto and management endpoints, we need to specify crypto endpoint of the vault we are rotating the private keys in (example expects both client and server to store private key in the same vault) +- **VAULT_MANAGEMENT_ENDPOINT** - crypto endpoint of the vault we are rotating the private keys in +- **CA_OCID** - OCID of the CA authority we have created in [Prepare CA](#prepare-cacertification-authority) step + +Following env variables are generated automatically by [create-keys.sh](etc/unsupported-cert-tools/create-keys.sh) or needs to be configured manually for [rotate-keys.sh](etc/unsupported-cert-tools/rotate-keys.sh) in [generated-config.sh](etc/unsupported-cert-tools/generated-config.sh) +- **SERVER_CERT_OCID** - OCID of the server certificate(not the specific version!) +- **SERVER_KEY_OCID** - OCID of the server private key in vault(not the specific version!) + +Optional: +- **CLIENT_CERT_OCID** - OCID of the client certificate(not the specific version!) +- **CLIENT_KEY_OCID** - OCID of the client private key in vault(not the specific version!) + +### Prepare keys and certificates +Make sure you are in the directory [./etc/unsupported-cert-tools/](etc/unsupported-cert-tools/). +```shell +cd etc/unsupported-cert-tools/ +bash create-keys.sh +``` + +## Rotating mTLS certificates +Make sure you are in the directory [./etc/unsupported-cert-tools/](etc/unsupported-cert-tools/). +```shell +cd etc/unsupported-cert-tools +bash rotate-keys.sh +``` +⚠️ Keep in mind that rotation creates new [versions](https://docs.oracle.com/en-us/iaas/Content/certificates/rotation-states.htm), OCIDs of the keys and certificates stays the same, and you don't need to change your configuration. + +## Build and run example + +Update the [pom.xml](../pom.xml) to define the system properties for the configuration as mentioned above. + +### Build & Run + +```shell +mvn clean package +``` + +Run mTLS secured web server: +```shell +source ./etc/unsupported-cert-tools/config.sh && \ +source ./etc/unsupported-cert-tools/generated-config.sh && \ +java -jar ./target/helidon-examples-microprofile-oci-tls-certificates.jar +``` + +### Test with cURL +```shell +curl --key key-pair.pem --cert cert-chain.cer --cacert ca.cer -v https://localhost:8443 +``` diff --git a/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/config.sh b/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/config.sh new file mode 100644 index 000000000..c05c86176 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/config.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# The caller should update these accordingly +export COMPARTMENT_OCID=ocid1.tenancy.oc1.. +export VAULT_CRYPTO_ENDPOINT=https://TODO.oraclecloud.com +export VAULT_MANAGEMENT_ENDPOINT=https://TODO.oraclecloud.com +export CA_OCID=TODO +export DISPLAY_NAME_PREFIX="tls-test-1" diff --git a/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/create-keys.sh b/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/create-keys.sh new file mode 100644 index 000000000..f2d44567f --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/create-keys.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -e + +source ./config.sh +source ./utils.sh + +# Cleanup +rm -rf ./server ./client +mkdir -p server client + +CDIR=$(pwd) + +# Rotate server cert and key +cd ${CDIR}/server +genCertAndCSR server +NEW_SERVER_CERT_OCID=$(uploadNewCert server $DISPLAY_NAME_PREFIX) +prepareKeyToUpload server +NEW_SERVER_KEY_OCID=$(createKeyInVault server $DISPLAY_NAME_PREFIX) + +# Rotate client cert and key +cd ${CDIR}/client +genCertAndCSR client +NEW_CLIENT_CERT_OCID=$(uploadNewCert client $DISPLAY_NAME_PREFIX) +prepareKeyToUpload client +NEW_CLIENT_KEY_OCID=$(createKeyInVault client $DISPLAY_NAME_PREFIX) + +echo "======= ALL done! =======" +echo "Newly created OCI resources:" +echo "Server certificate OCID: $NEW_SERVER_CERT_OCID" +echo "Server private key OCID: $NEW_SERVER_KEY_OCID" +echo "Client certificate OCID: $NEW_CLIENT_CERT_OCID" +echo "Client private key OCID: $NEW_CLIENT_KEY_OCID" +echo "Saving to gen-config.sh" +tee ${CDIR}/generated-config.sh << EOF +#!/bin/bash +## Content of this file gets rewritten by create-keys.sh +export SERVER_CERT_OCID=$NEW_SERVER_CERT_OCID +export SERVER_KEY_OCID=$NEW_SERVER_KEY_OCID + +export CLIENT_CERT_OCID=$NEW_CLIENT_CERT_OCID +export CLIENT_KEY_OCID=$NEW_CLIENT_KEY_OCID +EOF diff --git a/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/generated-config.sh b/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/generated-config.sh new file mode 100644 index 000000000..42e7d90b2 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/generated-config.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +## Content of this file gets rewritten by create-keys.sh +export SERVER_CERT_OCID=ocid1.certificate.oc1. +export SERVER_KEY_OCID=ocid1.key.oc1. + +export CLIENT_CERT_OCID=ocid1.certificate.oc1. +export CLIENT_KEY_OCID=ocid1.key.oc1. diff --git a/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/rotate-keys.sh b/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/rotate-keys.sh new file mode 100644 index 000000000..cf5bf43a2 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/rotate-keys.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -e + +source ./config.sh +source ./generated-config.sh +source ./utils.sh + +# Cleanup +rm -rf ./server ./client +mkdir -p server client + +CDIR=$(pwd) + +# Rotate server cert and key +cd ${CDIR}/server +genCertAndCSR server +rotateCert server $SERVER_CERT_OCID +prepareKeyToUpload server +rotateKeyInVault server $SERVER_KEY_OCID + +# Rotate client cert and key +cd ${CDIR}/client +genCertAndCSR client +rotateCert client $CLIENT_CERT_OCID +prepareKeyToUpload client +rotateKeyInVault client $CLIENT_KEY_OCID + +echo "ALL done!" diff --git a/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/utils.sh b/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/utils.sh new file mode 100644 index 000000000..684f93045 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/etc/unsupported-cert-tools/utils.sh @@ -0,0 +1,159 @@ +#!/bin/bash -e + +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -e + +PRIVATE_KEY_AS_PEM=private-key.pem +VAULT_PUBLIC_WRAPPING_KEY_PATH=vaultWrappingPub.key +PRIVATE_KEY_AS_DER=uploadedKey.der +TEMPORARY_AES_KEY_PATH=tmpAES.key +WRAPPED_TEMPORARY_AES_KEY_FILE=wrappedTmpAES.key +WRAPPED_TARGET_KEY_FILE=wrappedUploadedKey.key +WRAPPED_KEY_MATERIAL_FILE=readyToUpload.der + +prepareKeyToUpload() { + KEYSTORE_FILE=${1}.jks + + # Obtain OCI wrapping key + oci kms management wrapping-key get \ + --query 'data."public-key"' \ + --raw-output \ + --endpoint ${VAULT_MANAGEMENT_ENDPOINT} \ + >$VAULT_PUBLIC_WRAPPING_KEY_PATH + + # Extract server/client private key + openssl pkcs12 -in "$KEYSTORE_FILE" \ + -nocerts \ + -passin pass:changeit -passout pass:changeit \ + -out $PRIVATE_KEY_AS_PEM + + ## Upload server/client private key to vault + # Generate a temporary AES key + openssl rand -out $TEMPORARY_AES_KEY_PATH 32 + + # Wrap the temporary AES key with the public wrapping key using RSA-OAEP with SHA-256: + openssl pkeyutl -encrypt -in $TEMPORARY_AES_KEY_PATH \ + -inkey $VAULT_PUBLIC_WRAPPING_KEY_PATH \ + -pubin -out $WRAPPED_TEMPORARY_AES_KEY_FILE \ + -pkeyopt rsa_padding_mode:oaep \ + -pkeyopt rsa_oaep_md:sha256 + + # Generate hexadecimal of the temporary AES key material: + TEMPORARY_AES_KEY_HEXDUMP=$(hexdump -v -e '/1 "%02x"' <${TEMPORARY_AES_KEY_PATH}) + + # If the RSA private key you want to import is in PEM format, convert it to DER: + openssl pkcs8 -topk8 -nocrypt \ + -inform PEM -outform DER \ + -passin pass:changeit -passout pass:changeit \ + -in $PRIVATE_KEY_AS_PEM -out $PRIVATE_KEY_AS_DER + + # Wrap RSA private key with the temporary AES key: + openssl enc -id-aes256-wrap-pad -iv A65959A6 -K "${TEMPORARY_AES_KEY_HEXDUMP}" -in $PRIVATE_KEY_AS_DER -out $WRAPPED_TARGET_KEY_FILE + + # Create the wrapped key material by concatenating both wrapped keys: + cat $WRAPPED_TEMPORARY_AES_KEY_FILE $WRAPPED_TARGET_KEY_FILE >$WRAPPED_KEY_MATERIAL_FILE + +# linux +# KEY_MATERIAL_AS_BASE64=$(base64 -w 0 readyToUpload.der) +# macOS + KEY_MATERIAL_AS_BASE64=$(base64 -i readyToUpload.der) + + JSON_KEY_MATERIAL="{\"keyMaterial\": \"$KEY_MATERIAL_AS_BASE64\",\"wrappingAlgorithm\": \"RSA_OAEP_AES_SHA256\"}" + + echo $JSON_KEY_MATERIAL >key-material.json +} + +createKeyInVault() { + TYPE=$1 + KEY_NAME=${2} + + export NEW_KEY_OCID=$(oci kms management key import \ + --compartment-id ${COMPARTMENT_OCID} \ + --display-name ${KEY_NAME}-${TYPE} \ + --key-shape '{"algorithm": "RSA", "length": 256}' \ + --protection-mode SOFTWARE \ + --endpoint ${VAULT_MANAGEMENT_ENDPOINT} \ + --wrapped-import-key file://key-material.json \ + --query 'data.id' \ + --raw-output) + + echo "$NEW_KEY_OCID" +} + +rotateKeyInVault() { + TYPE=$1 + KEY_OCID=${2} + + oci kms management key-version import \ + --key-id $KEY_OCID \ + --endpoint ${VAULT_MANAGEMENT_ENDPOINT} \ + --wrapped-import-key file://key-material.json +} + +genCertAndCSR() { + TYPE=$1 + + # Get CA cert + oci certificates certificate-authority-bundle get --query 'data."certificate-pem"' \ + --raw-output \ + --certificate-authority-id ${CA_OCID} \ + >ca.pem + + # Generating new server key store + keytool -genkeypair -keyalg RSA -keysize 2048 \ + -alias ${TYPE} \ + -dname "CN=localhost" \ + -validity 60 \ + -keystore ${TYPE}.jks \ + -storepass password -keypass password \ + -deststoretype pkcs12 + + # Create CSR + keytool -certreq -keystore "${TYPE}.jks" \ + -alias ${TYPE} \ + -keypass password \ + -storepass password \ + -validity 60 \ + -keyalg rsa \ + -file ${TYPE}.csr +} + +uploadNewCert() { + TYPE=$1 + CERT_NAME=$2 + ## Create server/client certificate in OCI + export NEW_CERT_OCID=$(oci certs-mgmt certificate create-certificate-managed-externally-issued-by-internal-ca \ + --compartment-id ${COMPARTMENT_OCID} \ + --issuer-certificate-authority-id ${CA_OCID} \ + --name ${CERT_NAME}-${TYPE} \ + --csr-pem "$(cat ${TYPE}.csr)" \ + --query 'data.id' \ + --raw-output) + + echo "$NEW_CERT_OCID" +} + +rotateCert() { + TYPE=$1 + CERT_OCID=$2 + + ## Renew server certificate in OCI + oci certs-mgmt certificate update-certificate-managed-externally \ + --certificate-id "${CERT_OCID}" \ + --csr-pem "$(cat ${TYPE}.csr)" +} diff --git a/examples/microprofile/oci-tls-certificates/pom.xml b/examples/microprofile/oci-tls-certificates/pom.xml new file mode 100644 index 000000000..164bc58e4 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/pom.xml @@ -0,0 +1,108 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + + io.helidon.examples.microprofile + helidon-examples-microprofile-oci-tls-certificates + 1.0.0-SNAPSHOT + Helidon Microprofile Examples TLS Manager and OCI Certificates Service Integration + + + Microprofile example that configures TLS via a Manager and OCI Certificates Service integration with cert rotation + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.integrations.oci + helidon-integrations-oci-tls-certificates + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey3 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + maven-surefire-plugin + + + + + + + + + ${project.build.testOutputDirectory}/logging.properties + jdk + + + + + + + diff --git a/examples/microprofile/oci-tls-certificates/src/main/java/io/helidon/examples/microprofile/oci/tls/certificates/GreetResource.java b/examples/microprofile/oci-tls-certificates/src/main/java/io/helidon/examples/microprofile/oci/tls/certificates/GreetResource.java new file mode 100644 index 000000000..8d0ea5999 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/src/main/java/io/helidon/examples/microprofile/oci/tls/certificates/GreetResource.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.oci.tls.certificates; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * A simple JAX-RS resource to greet you. + */ +@Path("/") +@RequestScoped +public class GreetResource { + + /** + * Return a greeting message. + * + * @return greeting + */ + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getDefaultMessage() { + return "Hello user!"; + } + +} diff --git a/examples/microprofile/oci-tls-certificates/src/main/java/io/helidon/examples/microprofile/oci/tls/certificates/Main.java b/examples/microprofile/oci-tls-certificates/src/main/java/io/helidon/examples/microprofile/oci/tls/certificates/Main.java new file mode 100644 index 000000000..58b775560 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/src/main/java/io/helidon/examples/microprofile/oci/tls/certificates/Main.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.oci.tls.certificates; + +import io.helidon.microprofile.server.Server; + +/** + * Starts the server. + */ +public final class Main { + + private Main() { + } + + /** + * Main method. + * + * @param args args + */ + public static void main(final String[] args) { + startServer(); + } + + static Server startServer() { + return Server.create().start(); + } + +} diff --git a/examples/microprofile/oci-tls-certificates/src/main/java/io/helidon/examples/microprofile/oci/tls/certificates/package-info.java b/examples/microprofile/oci-tls-certificates/src/main/java/io/helidon/examples/microprofile/oci/tls/certificates/package-info.java new file mode 100644 index 000000000..3d3112bb9 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/src/main/java/io/helidon/examples/microprofile/oci/tls/certificates/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon Microprofile Examples OCI TLS Manager. + */ +package io.helidon.examples.microprofile.oci.tls.certificates; diff --git a/examples/microprofile/oci-tls-certificates/src/main/resources/META-INF/beans.xml b/examples/microprofile/oci-tls-certificates/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..5a4035959 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/oci-tls-certificates/src/main/resources/application.yaml b/examples/microprofile/oci-tls-certificates/src/main/resources/application.yaml new file mode 100644 index 000000000..1229eb7cf --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/src/main/resources/application.yaml @@ -0,0 +1,37 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + sockets: + - name: "secured" + port: 8443 + tls: + # for server-side auth this should be set to false (or removed altogether) + trust-all: true + manager: + oci-certificates-tls-manager: + # Download mTls context every 30 seconds + schedule: "0/30 * * * * ? *" + # Each OCI Vault has public crypto and management endpoints + vault-crypto-endpoint: ${VAULT_CRYPTO_ENDPOINT} + # Certification Authority in OCI we have signed rotated certificates with + ca-ocid: ${CA_OCID} + cert-ocid: ${SERVER_CERT_OCID} + key-ocid: ${SERVER_KEY_OCID} + # note that this will eventually come from the OCI Vault Config Source - https://github.com/helidon-io/helidon/issues/4238 + key-password: password diff --git a/examples/microprofile/oci-tls-certificates/src/main/resources/logging.properties b/examples/microprofile/oci-tls-certificates/src/main/resources/logging.properties new file mode 100644 index 000000000..7b97a7bce --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/src/main/resources/logging.properties @@ -0,0 +1,30 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.common.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO + +# OCI SDK logging +com.oracle.bmc.level=WARNING diff --git a/examples/microprofile/oci-tls-certificates/src/main/resources/oci.yaml b/examples/microprofile/oci-tls-certificates/src/main/resources/oci.yaml new file mode 100644 index 000000000..431734a29 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/src/main/resources/oci.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +auth-strategies: ["instance-principals", "config-file", "resource-principal", "config"] diff --git a/examples/microprofile/oci-tls-certificates/src/test/java/io/helidon/examples/microprofile/oci/tls/certificates/SimpleUsageTest.java b/examples/microprofile/oci-tls-certificates/src/test/java/io/helidon/examples/microprofile/oci/tls/certificates/SimpleUsageTest.java new file mode 100644 index 000000000..10282aca9 --- /dev/null +++ b/examples/microprofile/oci-tls-certificates/src/test/java/io/helidon/examples/microprofile/oci/tls/certificates/SimpleUsageTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.oci.tls.certificates; + +import java.net.URI; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import io.helidon.microprofile.server.Server; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Start the server that will download and watch OCI's Certificates service for dynamic updates. + */ +class SimpleUsageTest { + + private static Client client; + static { + try { + SSLContext sslcontext = SSLContext.getInstance("TLS"); + sslcontext.init(null, new TrustManager[]{new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] arg0, String arg1) {} + public void checkServerTrusted(X509Certificate[] arg0, String arg1) {} + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + }}, new java.security.SecureRandom()); + + client = ClientBuilder.newBuilder() + .sslContext(sslcontext) + .build(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + // see pom.xml + public void testIt() { + assumeTrue(System.getProperty("VAULT_CRYPTO_ENDPOINT") != null, + "be sure to set required system properties"); + assumeTrue(System.getProperty("CA_OCID") != null, + "be sure to set required system properties"); + assumeTrue(System.getProperty("SERVER_CERT_OCID") != null, + "be sure to set required system properties"); + assumeTrue(System.getProperty("SERVER_KEY_OCID") != null, + "be sure to set required system properties"); + + Server server = Main.startServer(); + try { + URI restUri = URI.create("https://localhost:" + server.port() + "/"); + try (Response res = client.target(restUri).request().get()) { + assertThat(res.getStatus(), is(200)); + assertThat(res.readEntity(String.class), is("Hello user!")); + } + } finally { + server.stop(); + } + } + +} diff --git a/examples/microprofile/oidc/README.md b/examples/microprofile/oidc/README.md new file mode 100644 index 000000000..81ff0e7a7 --- /dev/null +++ b/examples/microprofile/oidc/README.md @@ -0,0 +1,27 @@ +# Security integration with OIDC + +This example demonstrates integration with OIDC (Open ID Connect) providers. + +## Contents + +MP example that integrates with an OIDC provider. + +To configure this example, you need to replace the following: +1. src/main/resources/application.yaml - set security.properties.oidc-* to your tenant and application configuration + +## Running the Example + +Run the "OidcMain" class for file configuration based example. + +## Local configuration + +The example is already set up to read +`${user.home}/helidon/conf/examples.yaml` to override defaults configured +in `application.yaml`. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-security-oidc-login.jar +``` diff --git a/examples/microprofile/oidc/pom.xml b/examples/microprofile/oidc/pom.xml new file mode 100644 index 000000000..6a7ecb326 --- /dev/null +++ b/examples/microprofile/oidc/pom.xml @@ -0,0 +1,80 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-security-oidc-login + 1.0.0-SNAPSHOT + Helidon Examples Microprofile OIDC Security + + + Microprofile example with (any) OIDC provider integration. + + + + io.helidon.examples.microprofile.security.oidc.OidcMain + + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.microprofile + helidon-microprofile-oidc + + + io.smallrye + jandex + runtime + true + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/oidc/src/main/java/io/helidon/examples/microprofile/security/oidc/OidcMain.java b/examples/microprofile/oidc/src/main/java/io/helidon/examples/microprofile/security/oidc/OidcMain.java new file mode 100644 index 000000000..9bef83e39 --- /dev/null +++ b/examples/microprofile/oidc/src/main/java/io/helidon/examples/microprofile/security/oidc/OidcMain.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.security.oidc; + +import io.helidon.config.Config; +import io.helidon.microprofile.server.Server; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * Main class for MP. + */ +public final class OidcMain { + private OidcMain() { + } + + /** + * Start the application. + * @param args ignored. + */ + public static void main(String[] args) { + Server server = Server.builder() + .config(buildConfig()) + .build() + .start(); + + System.out.println("http://localhost:" + server.port() + "/test"); + } + + private static Config buildConfig() { + return Config.builder() + .sources( + // you can use this file to override the defaults that are built-in + file(System.getProperty("user.home") + "/helidon/conf/examples.yaml").optional(), + // in jar file (see src/main/resources/application.yaml) + classpath("application.yaml")) + .build(); + } +} diff --git a/examples/microprofile/oidc/src/main/java/io/helidon/examples/microprofile/security/oidc/OidcResource.java b/examples/microprofile/oidc/src/main/java/io/helidon/examples/microprofile/security/oidc/OidcResource.java new file mode 100644 index 000000000..1044f2538 --- /dev/null +++ b/examples/microprofile/oidc/src/main/java/io/helidon/examples/microprofile/security/oidc/OidcResource.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.security.oidc; + +import io.helidon.security.SecurityContext; +import io.helidon.security.annotations.Authenticated; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; + +/** + * A simple JAX-RS resource with a single GET method. + */ +@Path("/test") +public class OidcResource { + + /** + * Hello world using security context. + * @param securityContext context as established during login + * @return a string with current username + */ + @Authenticated + @GET + public String getIt(@Context SecurityContext securityContext) { + return "Hello " + securityContext.userName(); + } + +} diff --git a/examples/microprofile/oidc/src/main/java/io/helidon/examples/microprofile/security/oidc/package-info.java b/examples/microprofile/oidc/src/main/java/io/helidon/examples/microprofile/security/oidc/package-info.java new file mode 100644 index 000000000..f1846431e --- /dev/null +++ b/examples/microprofile/oidc/src/main/java/io/helidon/examples/microprofile/security/oidc/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * An OIDC (Open ID Connect) example. + */ +package io.helidon.examples.microprofile.security.oidc; diff --git a/examples/microprofile/oidc/src/main/resources/META-INF/beans.xml b/examples/microprofile/oidc/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..015e445f3 --- /dev/null +++ b/examples/microprofile/oidc/src/main/resources/META-INF/beans.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/examples/microprofile/oidc/src/main/resources/application.yaml b/examples/microprofile/oidc/src/main/resources/application.yaml new file mode 100644 index 000000000..c3def999a --- /dev/null +++ b/examples/microprofile/oidc/src/main/resources/application.yaml @@ -0,0 +1,54 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 7987 +security: + config.require-encryption: false + properties: + # this should be defined by the identity server + oidc-identity-uri: "https://tenant.some-server.com/oauth2/default" + # when you create a new client in identity server configuration, you should get a client id and a client secret + oidc-client-id: "some client id" + oidc-client-secret: "changeit" + # issuer of the tokens - identity server specific (maybe even configurable) + oidc-issuer: "https://tenant.some-server.com/oauth2/default" + # audience of the tokens - identity server specific (usually configurable) + oidc-audience: "configured audience" + # The frontend URI defines the possible load balancer address + # The redirect URI used is ${frontend-uri}${redirect-uri} + # Webserver is by default listening on /oidc/redirect - this can be modified by `redirect-uri` option in oidc configuration + frontend-uri: "http://localhost:7987" + server-type: "@default" + providers: + - abac: + # Adds ABAC Provider - it does not require any configuration + - oidc: + # use a custom name, so it does not clash with other examples + cookie-name: "OIDC_EXAMPLE_COOKIE" + # support for "Authorization" header with bearer token + header-use: true + # the default redirect-uri, where the webserver listens on redirects from identity server + redirect-uri: "/oidc/redirect" + issuer: "${security.properties.oidc-issuer}" + audience: "${security.properties.oidc-audience}" + client-id: "${security.properties.oidc-client-id}" + client-secret: "${security.properties.oidc-client-secret}" + identity-uri: "${security.properties.oidc-identity-uri}" + frontend-uri: "${security.properties.frontend-uri}" + server-type: "${security.properties.server-type}" + # We want to redirect to login page (and token can be received either through cookie or header) + redirect: true \ No newline at end of file diff --git a/examples/microprofile/oidc/src/main/resources/logging.properties b/examples/microprofile/oidc/src/main/resources/logging.properties new file mode 100644 index 000000000..e925738a6 --- /dev/null +++ b/examples/microprofile/oidc/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n + +.level = INFO +io.helidon.config.level = WARNING +# io.helidon.security.providers.oidc.level = FINEST +AUDIT.level=FINEST +# to see detailed information about failed validations of tokens +io.helidon.security.providers.oidc.level=FINEST diff --git a/examples/microprofile/openapi/README.md b/examples/microprofile/openapi/README.md new file mode 100644 index 000000000..3141be919 --- /dev/null +++ b/examples/microprofile/openapi/README.md @@ -0,0 +1,33 @@ +# Helidon MP OpenAPI Example + +This example shows a simple greeting application, similar to the one from the +Helidon MP QuickStart, enhanced with OpenAPI support. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-openapi.jar +``` + +Try the endpoints: + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"message" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} + +curl -X GET http://localhost:8080/openapi +#Output: [lengthy OpenAPI document] +``` +The output describes not only then endpoints from `GreetResource` but +also one contributed by the `SimpleAPIModelReader`. + + diff --git a/examples/microprofile/openapi/pom.xml b/examples/microprofile/openapi/pom.xml new file mode 100644 index 000000000..77403a81b --- /dev/null +++ b/examples/microprofile/openapi/pom.xml @@ -0,0 +1,98 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-openapi + 1.0.0-SNAPSHOT + Helidon Examples Microprofile OpenAPI + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.microprofile.openapi + helidon-microprofile-openapi + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.glassfish.jersey.media + jersey-media-json-binding + runtime + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetResource.java b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetResource.java new file mode 100644 index 000000000..f40c6fd6e --- /dev/null +++ b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetResource.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.examples.openapi; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; + +/** + * A simple JAX-RS resource with OpenAPI annotations to greet you. Examples: + * + * Get default greeting message: + * curl -X GET http://localhost:8080/greet + * + * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + * + * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + * + * Get OpenAPI document for the endpoints + * curl -X GET http://localhost:8080/openapi + * + * Note that the output will include not only the annotated endpoints from this + * class but also an endpoint added by the + * {@link io.helidon.microprofile.examples.openapi.internal.SimpleAPIModelReader}. + * + * The message is returned as a JSON object. + */ +@Path("/greet") +@RequestScoped +public class GreetResource { + + /** + * The greeting message provider. + */ + private final GreetingProvider greetingProvider; + + /** + * Using constructor injection to get a configuration property. + * By default this gets the value from META-INF/microprofile-config + * + * @param greetingConfig the configured greeting message + */ + @Inject + public GreetResource(GreetingProvider greetingConfig) { + this.greetingProvider = greetingConfig; + } + + /** + * Return a worldly greeting message. + * + * @return {@link GreetingMessage} + */ + @GET + @Operation(summary = "Returns a generic greeting", + description = "Greets the user generically") + @APIResponse(description = "Simple JSON containing the greeting", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = GreetingMessage.class))) + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getDefaultMessage() { + return createResponse("World"); + } + + /** + * Return a greeting message using the name that was provided. + * + * @param name the name to greet + * @return {@link GreetingMessage} + */ + @Path("/{name}") + @GET + @Operation(summary = "Returns a personalized greeting") + @APIResponse(description = "Simple JSON containing the greeting", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = GreetingMessage.class))) + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getMessage(@PathParam("name") String name) { + return createResponse(name); + } + + /** + * Set the greeting to use in future messages. + * + * @param message JSON containing the new greeting + * @return {@link Response} + */ + @Path("/greeting") + @PUT + @Operation(summary = "Set the greeting prefix", + description = "Permits the client to set the prefix part of the greeting (\"Hello\")") + @RequestBody( + name = "greeting", + description = "Conveys the new greeting prefix to use in building greetings", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = GreetingMessage.class), + examples = @ExampleObject( + name = "greeting", + summary = "Example greeting message to update", + value = "{\"greeting\": \"New greeting message\"}"))) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response updateGreeting(GreetingMessage message) { + if (message.getMessage() == null) { + GreetingMessage entity = new GreetingMessage("No greeting provided"); + return Response.status(Response.Status.BAD_REQUEST).entity(entity).build(); + } + + greetingProvider.setMessage(message.getMessage()); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + private GreetingMessage createResponse(String who) { + String msg = String.format("%s %s!", greetingProvider.getMessage(), who); + + return new GreetingMessage(msg); + } +} diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingMessage.java b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingMessage.java new file mode 100644 index 000000000..752e9b132 --- /dev/null +++ b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingMessage.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.examples.openapi; + +/** + * POJO defining the greeting message content. + */ +@SuppressWarnings("unused") +public class GreetingMessage { + private String message; + + /** + * Create a new GreetingMessage instance. + */ + public GreetingMessage() { + } + + /** + * Create a new GreetingMessage instance. + * + * @param message message + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Gets the message value. + * + * @return message value + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message message value to set + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingProvider.java b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingProvider.java new file mode 100644 index 000000000..515214ac6 --- /dev/null +++ b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/GreetingProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.examples.openapi; + +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Provider for greeting message. + */ +@ApplicationScoped +public class GreetingProvider { + private final AtomicReference message = new AtomicReference<>(); + + /** + * Create a new greeting provider, reading the message from configuration. + * + * @param message greeting to use + */ + @Inject + public GreetingProvider(@ConfigProperty(name = "app.greeting") String message) { + this.message.set(message); + } + + String getMessage() { + return message.get(); + } + + void setMessage(String message) { + this.message.set(message); + } +} diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIFilter.java b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIFilter.java new file mode 100644 index 000000000..60e872b80 --- /dev/null +++ b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIFilter.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.examples.openapi.internal; + +import java.util.Map; + +import org.eclipse.microprofile.openapi.OASFilter; +import org.eclipse.microprofile.openapi.models.Operation; +import org.eclipse.microprofile.openapi.models.PathItem; + +/** + * Example OpenAPI filter which hides a single endpoint from the OpenAPI document. + */ +public class SimpleAPIFilter implements OASFilter { + + @Override + public PathItem filterPathItem(PathItem pathItem) { + for (Map.Entry methodOp + : pathItem.getOperations().entrySet()) { + if (SimpleAPIModelReader.DOOMED_OPERATION_ID + .equals(methodOp.getValue().getOperationId())) { + return null; + } + } + return OASFilter.super.filterPathItem(pathItem); + } +} diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIModelReader.java b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIModelReader.java new file mode 100644 index 000000000..6d60bfcd8 --- /dev/null +++ b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/SimpleAPIModelReader.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.examples.openapi.internal; + +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.OASModelReader; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.eclipse.microprofile.openapi.models.PathItem; +import org.eclipse.microprofile.openapi.models.Paths; + +/** + * Defines two paths using the OpenAPI model reader mechanism, one that should + * be suppressed by the filter class and one that should appear in the published + * OpenAPI document. + */ +public class SimpleAPIModelReader implements OASModelReader { + + /** + * Path for the example endpoint added by this model reader that should be visible. + */ + public static final String MODEL_READER_PATH = "/test/newpath"; + + /** + * Path for an endpoint that the filter should hide. + */ + public static final String DOOMED_PATH = "/test/doomed"; + + /** + * ID for an endpoint that the filter should hide. + */ + public static final String DOOMED_OPERATION_ID = "doomedPath"; + + /** + * Summary text for the endpoint. + */ + public static final String SUMMARY = "A sample test endpoint from ModelReader"; + + @Override + public OpenAPI buildModel() { + /* + * Add two path items, one of which we expect to be removed by + * the filter and a very simple one that will appear in the + * published OpenAPI document. + */ + PathItem newPathItem = OASFactory.createPathItem() + .GET(OASFactory.createOperation() + .operationId("newPath") + .summary(SUMMARY)); + PathItem doomedPathItem = OASFactory.createPathItem() + .GET(OASFactory.createOperation() + .operationId(DOOMED_OPERATION_ID) + .summary("This should become invisible")); + OpenAPI openAPI = OASFactory.createOpenAPI(); + Paths paths = OASFactory.createPaths() + .addPathItem(MODEL_READER_PATH, newPathItem) + .addPathItem(DOOMED_PATH, doomedPathItem); + openAPI.paths(paths); + + return openAPI; + } +} diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/package-info.java b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/package-info.java new file mode 100644 index 000000000..e794f83fc --- /dev/null +++ b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/internal/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Internal classes supporting Helidon MP OpenAPI. + */ +package io.helidon.microprofile.examples.openapi.internal; diff --git a/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/package-info.java b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/package-info.java new file mode 100644 index 000000000..2d75af1aa --- /dev/null +++ b/examples/microprofile/openapi/src/main/java/io/helidon/microprofile/examples/openapi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon MicroProfile OpenAPI example. + */ +package io.helidon.microprofile.examples.openapi; diff --git a/examples/microprofile/openapi/src/main/resources/META-INF/beans.xml b/examples/microprofile/openapi/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..a0938bff7 --- /dev/null +++ b/examples/microprofile/openapi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/openapi/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/openapi/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..393a125ef --- /dev/null +++ b/examples/microprofile/openapi/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Application properties. This is the default greeting +app.greeting=Hello + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +mp.openapi.filter=io.helidon.microprofile.examples.openapi.internal.SimpleAPIFilter +mp.openapi.model.reader=io.helidon.microprofile.examples.openapi.internal.SimpleAPIModelReader diff --git a/examples/microprofile/openapi/src/main/resources/logging.properties b/examples/microprofile/openapi/src/main/resources/logging.properties new file mode 100644 index 000000000..936978537 --- /dev/null +++ b/examples/microprofile/openapi/src/main/resources/logging.properties @@ -0,0 +1,36 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.microprofile.level=INFO +#io.helidon.common.level=INFO +#org.glassfish.jersey.level=INFO +#org.jboss.weld=INFO diff --git a/examples/microprofile/openapi/src/test/java/io/helidon/microprofile/examples/openapi/MainTest.java b/examples/microprofile/openapi/src/test/java/io/helidon/microprofile/examples/openapi/MainTest.java new file mode 100644 index 000000000..402f4572b --- /dev/null +++ b/examples/microprofile/openapi/src/test/java/io/helidon/microprofile/examples/openapi/MainTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.examples.openapi; + +import io.helidon.microprofile.examples.openapi.internal.SimpleAPIModelReader; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonPointer; +import jakarta.json.JsonString; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +class MainTest { + + private final WebTarget target; + + @Inject + MainTest(WebTarget target) { + this.target = target; + } + + @Test + void testHelloWorld() { + GreetingMessage message = target.path("/greet") + .request() + .get(GreetingMessage.class); + assertThat("default message", message.getMessage(), + is("Hello World!")); + + message = target.path("/greet/Joe") + .request() + .get(GreetingMessage.class); + assertThat("hello Joe message", message.getMessage(), + is("Hello Joe!")); + + try (Response r = target.path("/greet/greeting") + .request() + .put(Entity.entity("{\"message\" : \"Hola\"}", MediaType.APPLICATION_JSON))) { + assertThat("PUT status code", r.getStatus(), is(204)); + } + + message = target.path("/greet/Jose") + .request() + .get(GreetingMessage.class); + assertThat("hola Jose message", message.getMessage(), + is("Hola Jose!")); + } + + @Test + public void testOpenAPI() { + JsonObject jsonObject = target.path("/openapi") + .request(MediaType.APPLICATION_JSON) + .get(JsonObject.class); + JsonObject paths = jsonObject.get("paths").asJsonObject(); + + JsonPointer jp = Json.createPointer("/" + escape(SimpleAPIModelReader.MODEL_READER_PATH) + "/get/summary"); + JsonString js = (JsonString) jp.getValue(paths); + assertThat("/test/newpath GET summary did not match", js.getString(), is(SimpleAPIModelReader.SUMMARY)); + + jp = Json.createPointer("/" + escape(SimpleAPIModelReader.DOOMED_PATH)); + assertThat("/test/doomed should not appear but does", jp.containsValue(paths), is(false)); + + jp = Json.createPointer("/" + escape("/greet") + "/get/summary"); + js = (JsonString) jp.getValue(paths); + assertThat("/greet GET summary did not match", js.getString(), is("Returns a generic greeting")); + } + + private static String escape(String path) { + return path.replace("/", "~1"); + } +} diff --git a/examples/microprofile/openapi/src/test/resources/META-INF/microprofile-config.properties b/examples/microprofile/openapi/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..0502dc34c --- /dev/null +++ b/examples/microprofile/openapi/src/test/resources/META-INF/microprofile-config.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Override configuration in main source branch, so we do not use 8080 port for tests +config_ordinal=1000 +# Microprofile server properties +server.port=-1 +server.host=0.0.0.0 diff --git a/examples/microprofile/pom.xml b/examples/microprofile/pom.xml new file mode 100644 index 000000000..a4ce7bcab --- /dev/null +++ b/examples/microprofile/pom.xml @@ -0,0 +1,56 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + pom + io.helidon.examples.microprofile + helidon-examples-microprofile-project + 1.0.0-SNAPSHOT + Helidon Examples Microprofile + + + graphql + hello-world-implicit + hello-world-explicit + static-content + security + idcs + multipart + oidc + openapi + websocket + messaging-sse + cors + tls + oci-tls-certificates + multiport + bean-validation + http-status-count-mp + lra + telemetry + + diff --git a/examples/microprofile/security/README.md b/examples/microprofile/security/README.md new file mode 100644 index 000000000..7636f44b9 --- /dev/null +++ b/examples/microprofile/security/README.md @@ -0,0 +1,28 @@ + +# Helidon MP Multiple Applications with Security + +Example MicroProfile application. This has two JAX-RS applications +sharing a common resource accessed through different context roots. + +The resource has multiple endpoints, protected with different +levels of security. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-mp1_1-security.jar +``` + +## Endpoints + +Open either of the following in a browser. Once the page loads +click on the links to try and load endpoints restricted to +admin roles or user roles. See the example's `application.yaml` +for the list of user, passwords and roles. + +|Endpoint |Description | +|:-----------|:----------------| +|`static/helloworld`|Public page with no security| +|`other/helloworld`|Same page from second application| + diff --git a/examples/microprofile/security/pom.xml b/examples/microprofile/security/pom.xml new file mode 100644 index 000000000..3de012061 --- /dev/null +++ b/examples/microprofile/security/pom.xml @@ -0,0 +1,87 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-mp1_1-security + 1.0.0-SNAPSHOT + Helidon Examples Microprofile MP 1.1 Security + + + Microprofile 1.1 example with security + + + + io.helidon.microprofile.example.security.Main + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/HelloWorldResource.java b/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/HelloWorldResource.java new file mode 100644 index 000000000..da44393da --- /dev/null +++ b/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/HelloWorldResource.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.security; + +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.annotations.Authenticated; +import io.helidon.security.annotations.Authorized; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * A dynamic resource that shows a link to the static resource. + */ +@Path("/helloworld") +@RequestScoped +public class HelloWorldResource { + @Inject + private Security security; + @Inject + private SecurityContext securityContext; + + @Inject + @ConfigProperty(name = "server.static.classpath.context") + private String context; + + /** + * Public page (does not require authentication). + * If there is pre-emptive basic auth, it will run within a user context. + * + * @return web page with links to other resources + */ + @GET + @Produces(MediaType.TEXT_HTML) + @Authenticated(optional = true) + public String getPublic() { + return "Hello World. This is a public page with no security " + + "Allowed for admin only
" + + "Allowed for user only
" + + "" + context + "/resource.html allowed for a logged in user
" + + "you are logged in as: " + securityContext.user() + + ""; + } + + /** + * Page restricted to users in "admin" role. + * + * @param securityContext Helidon security context + * @return web page with links to other resources + */ + @GET + @Path("/admin") + @Produces(MediaType.TEXT_HTML) + @Authenticated + @Authorized + @RolesAllowed("admin") + public String getAdmin(@Context SecurityContext securityContext) { + return "Hello World. You may want to check " + + "" + context + "/resource.html
" + + "you are logged in as: " + securityContext.user() + + ""; + } + + /** + * Page restricted to users in "user" role. + * + * @param securityContext Helidon security context + * @return web page with links to other resources + */ + @GET + @Path("/user") + @Produces(MediaType.TEXT_HTML) + @Authenticated + @Authorized + @RolesAllowed("user") + public String getUser(@Context SecurityContext securityContext) { + return "Hello World. You may want to check " + + "" + context + "/resource.html
" + + "you are logged in as: " + securityContext.user() + + ""; + } +} diff --git a/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/Main.java b/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/Main.java new file mode 100644 index 000000000..fef790e97 --- /dev/null +++ b/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/Main.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.security; + +import java.util.concurrent.TimeUnit; + +import io.helidon.microprofile.server.Server; + +/** + * Main class to start the application. + * See resources/META-INF/microprofile-config.properties. + */ +public final class Main { + private Main() { + } + + /** + * Run this example. + * + * @param args command line arguments (ignored) + */ + public static void main(String[] args) { + long now = System.nanoTime(); + + Server server = Server.create(StaticContentApp.class, OtherApp.class) + .start(); + + now = System.nanoTime() - now; + System.out.println("Start server: " + TimeUnit.MILLISECONDS.convert(now, TimeUnit.NANOSECONDS)); + System.out.println("Endpoint available at http://localhost:" + server.port() + "/static/helloworld"); + System.out.println("Alternative endpoint (second application) available at http://localhost:" + server + .port() + "/other/helloworld"); + } +} diff --git a/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/OtherApp.java b/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/OtherApp.java new file mode 100644 index 000000000..f5ceef388 --- /dev/null +++ b/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/OtherApp.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.security; + +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * An example of two applications in a single MP Server. + * This is the "other" application - serving the same resource on a different context. + */ +@ApplicationScoped +@ApplicationPath("/other") +public class OtherApp extends Application { + @Override + public Set> getClasses() { + return Set.of( + HelloWorldResource.class + ); + } +} diff --git a/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/StaticContentApp.java b/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/StaticContentApp.java new file mode 100644 index 000000000..2999db470 --- /dev/null +++ b/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/StaticContentApp.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.security; + +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * Example JAX-RS application with static content. + */ +@ApplicationScoped +@ApplicationPath("/static") +public class StaticContentApp extends Application { + @Override + public Set> getClasses() { + return Set.of( + HelloWorldResource.class + ); + } +} diff --git a/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/package-info.java b/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/package-info.java new file mode 100644 index 000000000..69070ced9 --- /dev/null +++ b/examples/microprofile/security/src/main/java/io/helidon/microprofile/example/security/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Static content example. + */ +package io.helidon.microprofile.example.security; diff --git a/examples/microprofile/security/src/main/resources/META-INF/beans.xml b/examples/microprofile/security/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..ddb8316e3 --- /dev/null +++ b/examples/microprofile/security/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/security/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/security/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..c70bb0238 --- /dev/null +++ b/examples/microprofile/security/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# web server configuration +# Use a random free port +server.port=0 + +# location on classpath (e.g. src/main/resources/WEB in maven) +server.static.classpath.location=/WEB +# this is optional, defaults to "/" +server.static.classpath.context=/static-cp +# server.static.path.location=/content +# server.static.path.context=/static-file diff --git a/examples/microprofile/security/src/main/resources/WEB/resource.html b/examples/microprofile/security/src/main/resources/WEB/resource.html new file mode 100644 index 000000000..0f51fd1ab --- /dev/null +++ b/examples/microprofile/security/src/main/resources/WEB/resource.html @@ -0,0 +1,32 @@ + + + + + +Hello, this is a static resource loaded from classpath. +

+ The configuration (microprofile config): +


+        server.static.classpath.location=/WEB
+        server.static.classpath.context=/static-cp
+    
+

+ + + diff --git a/examples/microprofile/security/src/main/resources/application.yaml b/examples/microprofile/security/src/main/resources/application.yaml new file mode 100644 index 000000000..2ea4cce61 --- /dev/null +++ b/examples/microprofile/security/src/main/resources/application.yaml @@ -0,0 +1,40 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# As security uses a tree structure with object arrays, it is easier to define in yaml or JSON +# META-INF/microprofile-config.properties is still used for basic server configuration + + +# security for jersey is based on annotations +# security for webserver is configured here (static content) +security: + providers: + - abac: + - http-basic-auth: + realm: "helidon" + users: + - login: "jack" + password: "changeit" + roles: ["user", "admin"] + - login: "jill" + password: "changeit" + roles: ["user"] + - login: "john" + password: "changeit" + web-server: + paths: + - path: "/static-cp[/{*}]" + authenticate: true diff --git a/examples/microprofile/security/src/main/resources/logging.properties b/examples/microprofile/security/src/main/resources/logging.properties new file mode 100644 index 000000000..5a5140e2e --- /dev/null +++ b/examples/microprofile/security/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO +io.helidon.microprofile.config.level=FINEST diff --git a/examples/microprofile/static-content/README.md b/examples/microprofile/static-content/README.md new file mode 100644 index 000000000..083ac4103 --- /dev/null +++ b/examples/microprofile/static-content/README.md @@ -0,0 +1,20 @@ +# Helidon MP with Static Content + +This example has a simple Hello World rest enpoint, plus +static content that is loaded from the application's classpath. +The configuration for the static content is in the +`microprofile-config.properties` file. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-mp1_1-static-content.jar +``` + +## Endpoints + +|Endpoint |Description | +|:-----------|:----------------| +|`helloworld`|Rest enpoint providing a link to the static content| +|`resource.html`|The static content| diff --git a/examples/microprofile/static-content/pom.xml b/examples/microprofile/static-content/pom.xml new file mode 100644 index 000000000..ba8e6739f --- /dev/null +++ b/examples/microprofile/static-content/pom.xml @@ -0,0 +1,122 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-mp1_1-static-content + 1.0.0-SNAPSHOT + Helidon Examples Microprofile MP 1.1 Static Content + + + Microprofile 1.1 example with static content + + + + io.helidon.microprofile.example.staticc.Main + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*$* + io/helidon/microprofile/example/staticc/StaticContentTest.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + Run integration tests + integration-test + + integration-test + + + + io/helidon/microprofile/example/staticc/StaticContentTest.java + + + + + Verify integration tests + verify + + verify + + + + + + + diff --git a/examples/microprofile/static-content/src/main/java/io/helidon/microprofile/example/staticc/HelloWorldResource.java b/examples/microprofile/static-content/src/main/java/io/helidon/microprofile/example/staticc/HelloWorldResource.java new file mode 100644 index 000000000..9f0199170 --- /dev/null +++ b/examples/microprofile/static-content/src/main/java/io/helidon/microprofile/example/staticc/HelloWorldResource.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.staticc; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * A dynamic resource that shows a link to the static resource. + */ +@Path("/helloworld") +@RequestScoped +public class HelloWorldResource { + @Inject + @ConfigProperty(name = "server.static.classpath.context", defaultValue = "${EMPTY}") + private String context; + + @GET + @Produces(MediaType.TEXT_HTML) + public String getIt() { + return "Hello World. You may want to check " + + "" + context + "/resource.html" + + ""; + } +} diff --git a/examples/microprofile/static-content/src/main/java/io/helidon/microprofile/example/staticc/Main.java b/examples/microprofile/static-content/src/main/java/io/helidon/microprofile/example/staticc/Main.java new file mode 100644 index 000000000..b71a58d98 --- /dev/null +++ b/examples/microprofile/static-content/src/main/java/io/helidon/microprofile/example/staticc/Main.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.staticc; + +import java.util.concurrent.TimeUnit; + +import io.helidon.microprofile.server.Server; + +/** + * Main class to start the application. + * See resources/META-INF/microprofile-config.properties. + */ +public class Main { + private static int port; + + private Main() { + } + + /** + * Run this example. + * + * @param args command line arguments (ignored) + */ + public static void main(String[] args) { + long now = System.nanoTime(); + + // everything is configured through application.yaml + Server server = Server.create(); + + now = System.nanoTime() - now; + System.out.println("Create server: " + TimeUnit.MILLISECONDS.convert(now, TimeUnit.NANOSECONDS)); + now = System.nanoTime(); + + server.start(); + + port = server.port(); + + now = System.nanoTime() - now; + System.out.println("Start server: " + TimeUnit.MILLISECONDS.convert(now, TimeUnit.NANOSECONDS)); + System.out.println("Endpoint available at http://localhost:" + port + "/helloworld"); + } + + public static int getPort() { + return port; + } +} diff --git a/examples/microprofile/static-content/src/main/java/io/helidon/microprofile/example/staticc/package-info.java b/examples/microprofile/static-content/src/main/java/io/helidon/microprofile/example/staticc/package-info.java new file mode 100644 index 000000000..afde4fe4d --- /dev/null +++ b/examples/microprofile/static-content/src/main/java/io/helidon/microprofile/example/staticc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Static content example. + */ +package io.helidon.microprofile.example.staticc; diff --git a/examples/microprofile/static-content/src/main/java/module-info.java.txt b/examples/microprofile/static-content/src/main/java/module-info.java.txt new file mode 100644 index 000000000..1118a9c48 --- /dev/null +++ b/examples/microprofile/static-content/src/main/java/module-info.java.txt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Microprofile 1.1. static content example. + */ + // TODO disabled for now, as automatic modules don't work with e-api of jboss (number is not allowed): + /* + java.lang.module.FindException: Unable to derive module descriptor for /Users/tomas/.m2/repository/org/jboss/spec/javax/el/jboss-el-api_3.0_spec/1.0.7.Final/jboss-el-api_3.0_spec-1.0.7.Final.jar + Caused by: java.lang.IllegalArgumentException: jboss.el.api.3.0.spec: Invalid module name: '3' is not a Java identifier + */ +module io.helidon.microprofile.example.staticc { + requires jakarta.cdi; + requires jakarta.inject; + requires jakarta.ws.rs; + requires microprofile.config.api; + requires io.helidon.microprofile.server; +} diff --git a/examples/microprofile/static-content/src/main/resources/META-INF/beans.xml b/examples/microprofile/static-content/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..ddb8316e3 --- /dev/null +++ b/examples/microprofile/static-content/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/static-content/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/static-content/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..213ea0f7d --- /dev/null +++ b/examples/microprofile/static-content/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# web server configuration +# Use a random free port +server.port=0 + +# location on classpath (e.g. src/main/resources/WEB in maven) +server.static.classpath.location=/WEB +server.static.classpath.welcome=resource.html +# this is optional, defaults to "/" +# server.static.classpath.context=/static-cp +# server.static.path=/content +# server.static.path.context=/static-file diff --git a/examples/microprofile/static-content/src/main/resources/WEB/resource.html b/examples/microprofile/static-content/src/main/resources/WEB/resource.html new file mode 100644 index 000000000..ed5c6a966 --- /dev/null +++ b/examples/microprofile/static-content/src/main/resources/WEB/resource.html @@ -0,0 +1,33 @@ + + + + + +Hello, this is a static resource loaded from classpath. +

+ The configuration (microprofile config): +


+        server.static.classpath.location=/WEB
+    
+ +

+ Dynamic page (a hello world) can be found here: /helloworld + + + diff --git a/examples/microprofile/static-content/src/main/resources/logging.properties b/examples/microprofile/static-content/src/main/resources/logging.properties new file mode 100644 index 000000000..72268667b --- /dev/null +++ b/examples/microprofile/static-content/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#All attributes details +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL] %4$s %3$s : %5$s%6$s%n +#All log level details +.level=INFO +# Microprofile config +# io.helidon.microprofile.config.level = FINEST +# Microprofile server startup +io.helidon.microprofile.startup.level=FINEST +org.glassfish.jersey.internal.Errors.level=SEVERE diff --git a/examples/microprofile/static-content/src/test/java/io/helidon/microprofile/example/staticc/StaticContentTest.java b/examples/microprofile/static-content/src/test/java/io/helidon/microprofile/example/staticc/StaticContentTest.java new file mode 100644 index 000000000..22deb15b3 --- /dev/null +++ b/examples/microprofile/static-content/src/test/java/io/helidon/microprofile/example/staticc/StaticContentTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.staticc; + +import java.io.IOException; + +import io.helidon.http.Status; + +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertAll; + +/** + * Unit test for {@link HelloWorldResource}. + */ +class StaticContentTest { + @BeforeAll + static void initClass() throws IOException { + Main.main(new String[0]); + } + + @AfterAll + static void destroyClass() { + CDI current = CDI.current(); + ((SeContainer) current).close(); + } + + @Test + void testDynamicResource() { + String response = ClientBuilder.newClient() + .target("http://localhost:" + Main.getPort() + "/helloworld") + .request() + .get(String.class); + + assertAll("Response must be HTML and contain a ref to static resource", + () -> assertThat(response, containsString("/resource.html")), + () -> assertThat(response, containsString("Hello World")) + ); + } + + @Test + void testWelcomePage() { + try (Response response = ClientBuilder.newClient() + .target("http://localhost:" + Main.getPort()) + .request() + .accept(MediaType.TEXT_HTML_TYPE) + .get()) { + assertThat("Status should be 200", response.getStatus(), is(Status.OK_200.code())); + + String str = response.readEntity(String.class); + + assertAll( + () -> assertThat(response.getMediaType(), is(MediaType.TEXT_HTML_TYPE)), + () -> assertThat(str, containsString("server.static.classpath.location=/WEB")) + ); + } + } + + @Test + void testStaticResource() { + try (Response response = ClientBuilder.newClient() + .target("http://localhost:" + Main.getPort() + "/resource.html") + .request() + .accept(MediaType.TEXT_HTML_TYPE) + .get()) { + assertThat("Status should be 200", response.getStatus(), is(Status.OK_200.code())); + + String str = response.readEntity(String.class); + + assertAll( + () -> assertThat(response.getMediaType(), is(MediaType.TEXT_HTML_TYPE)), + () -> assertThat(str, containsString("server.static.classpath.location=/WEB")) + ); + } + } +} diff --git a/examples/microprofile/static-content/src/test/resources/logging.properties b/examples/microprofile/static-content/src/test/resources/logging.properties new file mode 100644 index 000000000..f9194cdd4 --- /dev/null +++ b/examples/microprofile/static-content/src/test/resources/logging.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +.level=INFO +org.jboss.weld.level=INFO diff --git a/examples/microprofile/telemetry/README.md b/examples/microprofile/telemetry/README.md new file mode 100644 index 000000000..621c9fa9e --- /dev/null +++ b/examples/microprofile/telemetry/README.md @@ -0,0 +1,66 @@ +# Helidon MicroProfile Telemetry Example + +This example implements demonstrates usage of MP Telemetry Tracing. + +## Build and run + +```shell +mvn package +java -jar greeting/target/helidon-examples-microprofile-telemetry-greeting.jar +``` + +Run Jaeger tracer. If you prefer to use Docker, run in terminal: +```shell +docker run -d --name jaeger \ + -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ + -e COLLECTOR_OTLP_ENABLED=true \ + -p 6831:6831/udp \ + -p 6832:6832/udp \ + -p 5778:5778 \ + -p 16686:16686 \ + -p 4317:4317 \ + -p 4318:4318 \ + -p 14250:14250 \ + -p 14268:14268 \ + -p 14269:14269 \ + -p 9411:9411 \ + jaegertracing/all-in-one:1.50 +``` + +If you have Jaeger all-in-one installed, use this command: + +```shell +jaeger-all-in-one --collector.zipkin.host-port=9411 --collector.otlp.enabled=true +``` + +Run the Secondary service: + +```shell +mvn package +java -jar secondary/target/helidon-examples-microprofile-telemetry-secondary.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/greet +#Output: "Hello World!" + +curl -X GET http://localhost:8080/greet/span +#Output: {"Span":"PropagatedSpan{ImmutableSpanContext{traceId=00000000000000000000000000000000, spanId=0000000000000000, traceFlags=00, traceState=ArrayBasedTraceState{entries=[]}, remote=false, valid=false}}"} + +curl -X GET http://localhost:8080/greet/custom +``` +``` +#Output: +{ + "Custom Span": "SdkSpan{traceId=bea7da56d1fe82400af8ec0a8adb370d, spanId=57647ead5dc32ae7, parentSpanContext=ImmutableSpanContext{traceId=bea7da56d1fe82400af8ec0a8adb370d, spanId=0ca670f1e3330ea5, traceFlags=01, traceState=ArrayBasedTraceState{entries=[]}, remote=false, valid=true}, name=custom, kind=INTERNAL, attributes=AttributesMap{data={attribute=value}, capacity=128, totalAddedValues=1}, status=ImmutableStatusData{statusCode=UNSET, description=}, totalRecordedEvents=0, totalRecordedLinks=0, startEpochNanos=1683724682576003542, endEpochNanos=1683724682576006000}" +} +``` +```shell +curl -X GET http://localhost:8080/greet/outbound +#Output: Secondary + +``` + +Proceed Jaeger UI http://localhost:16686. In the top-down menu select "greeting-service" and click "Find traces". Tracing information should become available. \ No newline at end of file diff --git a/examples/microprofile/telemetry/greeting/pom.xml b/examples/microprofile/telemetry/greeting/pom.xml new file mode 100644 index 000000000..d0a8621e9 --- /dev/null +++ b/examples/microprofile/telemetry/greeting/pom.xml @@ -0,0 +1,77 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-telemetry-greeting + 1.0.0-SNAPSHOT + Helidon Examples MicroProfile Telemetry Greeting + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.opentelemetry + opentelemetry-exporter-jaeger + + + io.helidon.microprofile.telemetry + helidon-microprofile-telemetry + + + io.smallrye + jandex + runtime + true + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/GreetResource.java b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/GreetResource.java new file mode 100644 index 000000000..09c583d25 --- /dev/null +++ b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/GreetResource.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.telemetry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import org.glassfish.jersey.server.Uri; + +/** + * A simple JAX-RS resource to greet you. Examples: + * + * Get default greeting message: + * curl -X GET http://localhost:8080/greet + * + * Get Span information: + * curl -X GET http://localhost:8080/greet/span + * + * Call secondary service: + * curl -X GET http://localhost:8080/greet/outbound + * + * Explore traces in Jaeger UI. + */ +@Path("/greet") +public class GreetResource { + + private Span span; + + private Tracer tracer; + + @Uri("http://localhost:8081/secondary") + private WebTarget target; + + @Inject + GreetResource(Span span, Tracer tracer) { + this.span = span; + this.tracer = tracer; + } + + /** + * Return a worldly greeting message. + * + * @return {@link String} + */ + @GET + @WithSpan("default") + public String getDefaultMessage() { + return "Hello World"; + } + + /** + * Create an internal custom span and return its description. + * @return {@link GreetingMessage} + */ + @GET + @Path("custom") + @Produces(MediaType.APPLICATION_JSON) + @WithSpan + public GreetingMessage useCustomSpan() { + Span span = tracer.spanBuilder("custom") + .setSpanKind(SpanKind.INTERNAL) + .setAttribute("attribute", "value") + .startSpan(); + span.end(); + + return new GreetingMessage("Custom Span" + span); + } + + /** + * Get Span info. + * + * @return {@link GreetingMessage} + */ + @GET + @Path("span") + @Produces(MediaType.APPLICATION_JSON) + @WithSpan + public GreetingMessage getSpanInfo() { + return new GreetingMessage("Span " + span.toString()); + } + + /** + * Call the secondary service running on port 8081. + * + * @return String from the secondary service. + */ + @GET + @Path("/outbound") + @WithSpan("outbound") + public String outbound() { + return target.request().accept(MediaType.TEXT_PLAIN).get(String.class); + } + +} diff --git a/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/GreetingMessage.java b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/GreetingMessage.java new file mode 100644 index 000000000..ce9d05f07 --- /dev/null +++ b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/GreetingMessage.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.microprofile.telemetry; + +/** + * POJO defining the greeting message content. + */ +@SuppressWarnings("unused") +public class GreetingMessage { + private String message; + + /** + * Create a new GreetingMessage instance. + */ + public GreetingMessage() { + } + + /** + * Create a new GreetingMessage instance. + * + * @param message message + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Gets the message value. + * + * @return message value + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message message value to set + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/MixedTelemetryGreetResource.java b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/MixedTelemetryGreetResource.java new file mode 100644 index 000000000..c06711452 --- /dev/null +++ b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/MixedTelemetryGreetResource.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.telemetry; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * A simple JAX-RS resource to use `io.opentelemetry` and `io.helidon.api` in mixed mode. Examples: + * + * Get mixed traces with Global tracer: + * curl -X GET http://localhost:8080/mixed + * + * Get mixed traces with an injected Helidon tracer: + * curl -X GET http://localhost:8080/mixed/injected + * + * Explore traces in Jaeger UI. + */ +@Path("/mixed") +public class MixedTelemetryGreetResource { + + private io.helidon.tracing.Tracer helidonTracerInjected; + + @Inject + MixedTelemetryGreetResource(io.helidon.tracing.Tracer helidonTracerInjected) { + this.helidonTracerInjected = helidonTracerInjected; + } + + + /** + * Create a helidon mixed span using Helidon Global tracer. + * @return {@link GreetingMessage} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @WithSpan("mixed_parent") + public GreetingMessage mixedSpan() { + + io.helidon.tracing.Tracer helidonTracer = io.helidon.tracing.Tracer.global(); + io.helidon.tracing.Span mixedSpan = helidonTracer.spanBuilder("mixed_inner") + .kind(io.helidon.tracing.Span.Kind.SERVER) + .tag("attribute", "value") + .start(); + mixedSpan.end(); + + return new GreetingMessage("Mixed Span"); + } + + /** + * Create a helidon mixed span using injected Helidon Tracer. + * @return {@link GreetingMessage} + */ + @GET + @Path("injected") + @Produces(MediaType.APPLICATION_JSON) + @WithSpan("mixed_parent_injected") + public GreetingMessage mixedSpanInjected() { + io.helidon.tracing.Span mixedSpan = helidonTracerInjected.spanBuilder("mixed_injected_inner") + .kind(io.helidon.tracing.Span.Kind.SERVER) + .tag("attribute", "value") + .start(); + mixedSpan.end(); + + return new GreetingMessage("Mixed Span Injected"); + } +} diff --git a/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/package-info.java b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/package-info.java new file mode 100644 index 000000000..28b47fdf4 --- /dev/null +++ b/examples/microprofile/telemetry/greeting/src/main/java/io/helidon/examples/microprofile/telemetry/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * MicroProfile Telemetry example. + */ +package io.helidon.examples.microprofile.telemetry; diff --git a/examples/microprofile/telemetry/greeting/src/main/resources/META-INF/beans.xml b/examples/microprofile/telemetry/greeting/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..5a4035959 --- /dev/null +++ b/examples/microprofile/telemetry/greeting/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/telemetry/greeting/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/telemetry/greeting/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..1153d3a52 --- /dev/null +++ b/examples/microprofile/telemetry/greeting/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,29 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +# Enable the optional MicroProfile Metrics REST.request metrics +metrics.rest-request.enabled=true + +#OpenTelemtry +otel.sdk.disabled=false +otel.traces.exporter=jaeger +otel.service.name=greeting-service + +#telemetry.span.full.url=true diff --git a/examples/microprofile/telemetry/greeting/src/main/resources/logging.properties b/examples/microprofile/telemetry/greeting/src/main/resources/logging.properties new file mode 100644 index 000000000..95fe0b0f7 --- /dev/null +++ b/examples/microprofile/telemetry/greeting/src/main/resources/logging.properties @@ -0,0 +1,39 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Quiet Weld +org.jboss.level=WARNING + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.microprofile.level=INFO +#io.helidon.common.level=INFO +#org.glassfish.jersey.level=INFO +#org.jboss.weld=INFO diff --git a/examples/microprofile/telemetry/pom.xml b/examples/microprofile/telemetry/pom.xml new file mode 100644 index 000000000..5b25d62f7 --- /dev/null +++ b/examples/microprofile/telemetry/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + io.helidon.examples.microprofile + helidon-examples-microprofile-project + 1.0.0-SNAPSHOT + + io.helidon.examples.telemetry + helidon-examples-microprofile-telemetry + 1.0.0-SNAPSHOT + Helidon Examples MicroProfile Telemetry + pom + + + greeting + secondary + + diff --git a/examples/microprofile/telemetry/secondary/pom.xml b/examples/microprofile/telemetry/secondary/pom.xml new file mode 100644 index 000000000..cda57d645 --- /dev/null +++ b/examples/microprofile/telemetry/secondary/pom.xml @@ -0,0 +1,92 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-telemetry-secondary + 1.0.0-SNAPSHOT + Helidon Examples MicroProfile Telemetry Secondary service + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.microprofile.telemetry + helidon-microprofile-telemetry + + + io.opentelemetry + opentelemetry-exporter-jaeger + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/telemetry/secondary/src/main/java/io/helidon/examples/microprofile/telemetry/SecondaryResource.java b/examples/microprofile/telemetry/secondary/src/main/java/io/helidon/examples/microprofile/telemetry/SecondaryResource.java new file mode 100644 index 000000000..496025daa --- /dev/null +++ b/examples/microprofile/telemetry/secondary/src/main/java/io/helidon/examples/microprofile/telemetry/SecondaryResource.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.microprofile.telemetry; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +/** + * A simple JAX-RS resource used by Telemetry Example. + */ +@Path("/secondary") +public class SecondaryResource { + + /** + * Return a secondary message. + * + * @return {@link String} + */ + @GET + @WithSpan + public String getSecondaryMessage() { + return "Secondary"; + } + +} diff --git a/examples/microprofile/telemetry/secondary/src/main/java/io/helidon/examples/microprofile/telemetry/package-info.java b/examples/microprofile/telemetry/secondary/src/main/java/io/helidon/examples/microprofile/telemetry/package-info.java new file mode 100644 index 000000000..28b47fdf4 --- /dev/null +++ b/examples/microprofile/telemetry/secondary/src/main/java/io/helidon/examples/microprofile/telemetry/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * MicroProfile Telemetry example. + */ +package io.helidon.examples.microprofile.telemetry; diff --git a/examples/microprofile/telemetry/secondary/src/main/resources/META-INF/beans.xml b/examples/microprofile/telemetry/secondary/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..5a4035959 --- /dev/null +++ b/examples/microprofile/telemetry/secondary/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/telemetry/secondary/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/telemetry/secondary/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..3f84e1bbe --- /dev/null +++ b/examples/microprofile/telemetry/secondary/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Microprofile server properties +server.port=8081 +server.host=0.0.0.0 + +# Enable the optional MicroProfile Metrics REST.request metrics +metrics.rest-request.enabled=true + +#OpenTelemtry +otel.sdk.disabled=false +otel.traces.exporter=jaeger +otel.service.name=secondary-service + diff --git a/examples/microprofile/telemetry/secondary/src/main/resources/logging.properties b/examples/microprofile/telemetry/secondary/src/main/resources/logging.properties new file mode 100644 index 000000000..95fe0b0f7 --- /dev/null +++ b/examples/microprofile/telemetry/secondary/src/main/resources/logging.properties @@ -0,0 +1,39 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Quiet Weld +org.jboss.level=WARNING + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.microprofile.level=INFO +#io.helidon.common.level=INFO +#org.glassfish.jersey.level=INFO +#org.jboss.weld=INFO diff --git a/examples/microprofile/tls/README.md b/examples/microprofile/tls/README.md new file mode 100644 index 000000000..31bceaacb --- /dev/null +++ b/examples/microprofile/tls/README.md @@ -0,0 +1,16 @@ +# Helidon MP TLS Example + +This examples shows how to configure server TLS using Helidon MP. + +Note: This example uses self-signed server certificate! + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-tls.jar +``` +## Exercise the application +```shell +curl -k -X GET https://localhost:8080 +``` diff --git a/examples/microprofile/tls/pom.xml b/examples/microprofile/tls/pom.xml new file mode 100644 index 000000000..297e74794 --- /dev/null +++ b/examples/microprofile/tls/pom.xml @@ -0,0 +1,88 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + + io.helidon.examples.microprofile + helidon-examples-microprofile-tls + 1.0.0-SNAPSHOT + Helidon Examples Microprofile TLS + + + Microprofile example that configures TLS + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + + diff --git a/examples/microprofile/tls/src/main/java/io/helidon/microprofile/example/tls/GreetResource.java b/examples/microprofile/tls/src/main/java/io/helidon/microprofile/example/tls/GreetResource.java new file mode 100644 index 000000000..45a10b224 --- /dev/null +++ b/examples/microprofile/tls/src/main/java/io/helidon/microprofile/example/tls/GreetResource.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.tls; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * A simple JAX-RS resource to greet you. Examples: + * + * Get default greeting message: + * curl -X GET https://localhost:8080 + * + * The message is returned as a plain text. + */ +@Path("/") +@RequestScoped +public class GreetResource { + + /** + * Return a greeting message. + * + * @return {@link String} + */ + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getDefaultMessage() { + return "Hello user!"; + } + +} diff --git a/examples/microprofile/tls/src/main/java/io/helidon/microprofile/example/tls/Main.java b/examples/microprofile/tls/src/main/java/io/helidon/microprofile/example/tls/Main.java new file mode 100644 index 000000000..404b570ce --- /dev/null +++ b/examples/microprofile/tls/src/main/java/io/helidon/microprofile/example/tls/Main.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.example.tls; + +import io.helidon.microprofile.server.Server; + +/** + * Starts the server. + */ +public class Main { + + private Main() { + } + + /** + * Main method. + * + * @param args args + */ + public static void main(String[] args) { + startServer(); + } + + static Server startServer() { + return Server.create().start(); + } + +} diff --git a/examples/microprofile/tls/src/main/java/io/helidon/microprofile/example/tls/package-info.java b/examples/microprofile/tls/src/main/java/io/helidon/microprofile/example/tls/package-info.java new file mode 100644 index 000000000..7670fbf61 --- /dev/null +++ b/examples/microprofile/tls/src/main/java/io/helidon/microprofile/example/tls/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quickstart MicroProfile example. + */ +package io.helidon.microprofile.example.tls; diff --git a/examples/microprofile/tls/src/main/resources/META-INF/beans.xml b/examples/microprofile/tls/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..676e09a2d --- /dev/null +++ b/examples/microprofile/tls/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/tls/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/tls/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..2c3978a78 --- /dev/null +++ b/examples/microprofile/tls/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +#Truststore setup +server.tls.trust.keystore.resource.resource-path=server.p12 +server.tls.trust.keystore.passphrase=changeit +server.tls.trust.keystore.trust-store=true + +#Keystore with private key and server certificate +server.tls.private-key.keystore.resource.resource-path=server.p12 +server.tls.private-key.keystore.passphrase=changeit diff --git a/examples/microprofile/tls/src/main/resources/logging.properties b/examples/microprofile/tls/src/main/resources/logging.properties new file mode 100644 index 000000000..24a6b0743 --- /dev/null +++ b/examples/microprofile/tls/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + diff --git a/examples/microprofile/tls/src/main/resources/server.p12 b/examples/microprofile/tls/src/main/resources/server.p12 new file mode 100644 index 000000000..d2599833a Binary files /dev/null and b/examples/microprofile/tls/src/main/resources/server.p12 differ diff --git a/examples/microprofile/tls/src/test/java/io/helidon/microprofile/example/tls/TlsTest.java b/examples/microprofile/tls/src/test/java/io/helidon/microprofile/example/tls/TlsTest.java new file mode 100644 index 000000000..67710f29d --- /dev/null +++ b/examples/microprofile/tls/src/test/java/io/helidon/microprofile/example/tls/TlsTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.example.tls; + +import java.net.URI; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import io.helidon.microprofile.server.Server; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Test of the example. + */ +public class TlsTest { + + private static Client client; + static { + + try { + SSLContext sslcontext = SSLContext.getInstance("TLS"); + sslcontext.init(null, new TrustManager[]{new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} + public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + }}, new java.security.SecureRandom()); + + client = ClientBuilder.newBuilder() + .sslContext(sslcontext) + .build(); + } catch (Exception e) { + e.printStackTrace(); + } + } + private static Server server; + + @BeforeAll + static void initClass() { + server = Main.startServer(); + server.start(); + } + + @AfterAll + static void destroyClass() { + server.stop(); + } + + @Test + public void testTls() { + URI restUri = URI.create("https://localhost:" + server.port() + "/"); + try (Response res = client.target(restUri).request().get()) { + assertThat(res.getStatus(), is(200)); + assertThat(res.readEntity(String.class), is("Hello user!")); + } + } + +} diff --git a/examples/microprofile/tls/src/test/resources/META-INF/microprofile-config.properties b/examples/microprofile/tls/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..97306b54c --- /dev/null +++ b/examples/microprofile/tls/src/test/resources/META-INF/microprofile-config.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Microprofile server properties +server.port=0 + +config_ordinal=500 diff --git a/examples/microprofile/websocket/README.md b/examples/microprofile/websocket/README.md new file mode 100644 index 000000000..60300ddd9 --- /dev/null +++ b/examples/microprofile/websocket/README.md @@ -0,0 +1,12 @@ +# Helidon MP WebSocket Example + +This examples shows a simple application written using Helidon MP +that combines REST resources and WebSocket endpoints. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-websocket.jar +``` +[Open In browser](http://localhost:7001/web/index.html) diff --git a/examples/microprofile/websocket/pom.xml b/examples/microprofile/websocket/pom.xml new file mode 100644 index 000000000..492b2c159 --- /dev/null +++ b/examples/microprofile/websocket/pom.xml @@ -0,0 +1,97 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.microprofile + helidon-examples-microprofile-websocket + 1.0.0-SNAPSHOT + Helidon Examples Microprofile WebSocket + + + Microprofile example that uses websockets + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.microprofile.websocket + helidon-microprofile-websocket + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.smallrye + jandex + runtime + true + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageBoardEndpoint.java b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageBoardEndpoint.java new file mode 100644 index 000000000..0b1ad4993 --- /dev/null +++ b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageBoardEndpoint.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.websocket; + +import java.io.IOException; +import java.util.logging.Logger; + +import jakarta.inject.Inject; +import jakarta.websocket.Encoder; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +/** + * Class MessageBoardEndpoint. + */ +@ServerEndpoint( + value = "/websocket", + encoders = { MessageBoardEndpoint.UppercaseEncoder.class } +) +public class MessageBoardEndpoint { + private static final Logger LOGGER = Logger.getLogger(MessageBoardEndpoint.class.getName()); + + @Inject + private MessageQueue messageQueue; + + /** + * OnOpen call. + * + * @param session The websocket session. + * @throws IOException If error occurs. + */ + @OnOpen + public void onOpen(Session session) throws IOException { + LOGGER.info("OnOpen called"); + } + + /** + * OnMessage call. + * + * @param session The websocket session. + * @param message The message received. + * @throws Exception If error occurs. + */ + @OnMessage + public void onMessage(Session session, String message) throws Exception { + LOGGER.info("OnMessage called '" + message + "'"); + + // Send all messages in the queue + if (message.equals("SEND")) { + while (!messageQueue.isEmpty()) { + session.getBasicRemote().sendObject(messageQueue.pop()); + } + } + } + + /** + * OnError call. + * + * @param t The throwable. + */ + @OnError + public void onError(Throwable t) { + LOGGER.info("OnError called"); + } + + /** + * OnError call. + * + * @param session The websocket session. + */ + @OnClose + public void onClose(Session session) { + LOGGER.info("OnClose called"); + } + + /** + * Uppercase encoder. + */ + public static class UppercaseEncoder implements Encoder.Text { + + @Override + public String encode(String s) { + LOGGER.info("UppercaseEncoder encode called"); + return s.toUpperCase(); + } + + @Override + public void init(EndpointConfig config) { + } + + @Override + public void destroy() { + } + } +} diff --git a/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueue.java b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueue.java new file mode 100644 index 000000000..0119265a8 --- /dev/null +++ b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueue.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.websocket; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Class MessageQueue. + */ +@ApplicationScoped +public class MessageQueue { + + private Queue queue = new ConcurrentLinkedQueue<>(); + + /** + * Push string on stack. + * + * @param s String to push. + */ + public void push(String s) { + queue.add(s); + } + + /** + * Pop string from stack. + * + * @return The string or {@code null}. + */ + public String pop() { + return queue.poll(); + } + + /** + * Check if stack is empty. + * + * @return Outcome of test. + */ + public boolean isEmpty() { + return queue.isEmpty(); + } + + /** + * Peek at stack without changing it. + * + * @return String peeked or {@code null}. + */ + public String peek() { + return queue.peek(); + } +} diff --git a/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueueResource.java b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueueResource.java new file mode 100644 index 000000000..ae21b6900 --- /dev/null +++ b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/MessageQueueResource.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.websocket; + +import java.util.logging.Logger; + +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +/** + * Class MessageQueueResource. + */ +@Path("rest") +public class MessageQueueResource { + + private static final Logger LOGGER = Logger.getLogger(MessageQueueResource.class.getName()); + + @Inject + private MessageQueue messageQueue; + + /** + * Resource to push string into queue. + * + * @param s The string. + */ + @POST + @Consumes("text/plain") + public void push(String s) { + LOGGER.info("push called '" + s + "'"); + messageQueue.push(s); + } +} diff --git a/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/package-info.java b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/package-info.java new file mode 100644 index 000000000..4d53b5452 --- /dev/null +++ b/examples/microprofile/websocket/src/main/java/io/helidon/microprofile/example/websocket/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon WebSocket example. + */ +package io.helidon.microprofile.example.websocket; diff --git a/examples/microprofile/websocket/src/main/resources/META-INF/beans.xml b/examples/microprofile/websocket/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..676e09a2d --- /dev/null +++ b/examples/microprofile/websocket/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/websocket/src/main/resources/WEB/index.html b/examples/microprofile/websocket/src/main/resources/WEB/index.html new file mode 100644 index 000000000..070b78117 --- /dev/null +++ b/examples/microprofile/websocket/src/main/resources/WEB/index.html @@ -0,0 +1,81 @@ + + + + + + + + + + + +

+
+

+

+ +

+

+ +

+

+

History

+
+
+ + \ No newline at end of file diff --git a/examples/microprofile/websocket/src/main/resources/application.yaml b/examples/microprofile/websocket/src/main/resources/application.yaml new file mode 100644 index 000000000..87bbd41da --- /dev/null +++ b/examples/microprofile/websocket/src/main/resources/application.yaml @@ -0,0 +1,22 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 7001 + # location on classpath (e.g. src/main/resources/WEB in maven) + static.classpath: + location: "/WEB" + context: "/web" \ No newline at end of file diff --git a/examples/microprofile/websocket/src/main/resources/logging.properties b/examples/microprofile/websocket/src/main/resources/logging.properties new file mode 100644 index 000000000..62a490106 --- /dev/null +++ b/examples/microprofile/websocket/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO +io.helidon.microprofile.server.level=INFO diff --git a/examples/microprofile/websocket/src/test/java/io/helidon/microprofile/example/websocket/MessageBoardTest.java b/examples/microprofile/websocket/src/test/java/io/helidon/microprofile/example/websocket/MessageBoardTest.java new file mode 100644 index 000000000..af2b693b7 --- /dev/null +++ b/examples/microprofile/websocket/src/test/java/io/helidon/microprofile/example/websocket/MessageBoardTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.example.websocket; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.CloseReason; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.Session; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.glassfish.tyrus.client.ClientManager; +import org.glassfish.tyrus.container.jdk.client.JdkClientContainer; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Class MessageBoardTest. + */ +@HelidonTest +class MessageBoardTest { + private static final Logger LOGGER = Logger.getLogger(MessageBoardTest.class.getName()); + private static final String[] MESSAGES = { "Whisky", "Tango", "Foxtrot" }; + + private final WebTarget webTarget; + private final ServerCdiExtension server; + private final ClientManager websocketClient = ClientManager.createClient(JdkClientContainer.class.getName()); + + @Inject + MessageBoardTest(WebTarget webTarget, ServerCdiExtension server) { + this.webTarget = webTarget; + this.server = server; + } + + @Test + public void testBoard() throws IOException, DeploymentException, InterruptedException { + // Post messages using REST resource + for (String message : MESSAGES) { + try (Response res = webTarget.path("/rest").request().post(Entity.text(message))) { + assertThat(res.getStatus(), is(204)); + LOGGER.info("Posting message '" + message + "'"); + } + } + + // Now connect to message board using WS and them back + URI websocketUri = URI.create("ws://localhost:" + server.port() + "/websocket"); + CountDownLatch messageLatch = new CountDownLatch(MESSAGES.length); + ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build(); + + websocketClient.connectToServer(new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig EndpointConfig) { + try { + // Set message handler to receive messages + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(String message) { + LOGGER.info("Client OnMessage called '" + message + "'"); + messageLatch.countDown(); + if (messageLatch.getCount() == 0) { + try { + session.close(); + } catch (IOException e) { + fail("Unexpected exception " + e); + } + } + } + }); + + // Send an initial message to start receiving + session.getBasicRemote().sendText("SEND"); + } catch (IOException e) { + fail("Unexpected exception " + e); + } + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + LOGGER.info("Client OnClose called '" + closeReason + "'"); + } + + @Override + public void onError(Session session, Throwable thr) { + LOGGER.info("Client OnError called '" + thr + "'"); + + } + }, config, websocketUri); + + // Wait until all messages are received + assertThat("Message latch should have counted down to 0", + messageLatch.await(1000, TimeUnit.SECONDS), + is(true)); + } +} diff --git a/examples/openapi-tools/README.md b/examples/openapi-tools/README.md new file mode 100644 index 000000000..c043e0cc5 --- /dev/null +++ b/examples/openapi-tools/README.md @@ -0,0 +1,3 @@ +# OpenApi Tools Examples + +This directory contains OpenApi Tools examples for Helidon SE/MP clients and servers. diff --git a/examples/openapi-tools/pom.xml b/examples/openapi-tools/pom.xml new file mode 100644 index 000000000..105b2f215 --- /dev/null +++ b/examples/openapi-tools/pom.xml @@ -0,0 +1,39 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + pom + io.helidon.examples.openapi.tools + helidon-examples-openapi-tools-project + Helidon Examples OpenApi Tools + + + + + + + diff --git a/examples/openapi-tools/quickstart.yaml b/examples/openapi-tools/quickstart.yaml new file mode 100644 index 000000000..75bdc18fa --- /dev/null +++ b/examples/openapi-tools/quickstart.yaml @@ -0,0 +1,96 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +openapi: 3.0.0 +servers: + - url: 'http://localhost:8080' +info: + description: >- + This is a sample for Helidon Quickstart project. + version: 1.0.0 + title: OpenAPI Helidon Quickstart + license: + name: Apache-2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: message +paths: + /greet: + get: + tags: + - message + summary: Return a worldly greeting message. + operationId: getDefaultMessage + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + /greet/greeting: + put: + tags: + - message + summary: Set the greeting to use in future messages. + operationId: updateGreeting + responses: + '200': + description: successful operation + '400': + description: No greeting provided + requestBody: + $ref: '#/components/requestBodies/Message' + '/greet/{name}': + get: + tags: + - message + summary: Return a greeting message using the name that was provided. + operationId: getMessage + parameters: + - name: name + in: path + description: the name to greet + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Message' +components: + requestBodies: + Message: + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + description: Message for the user + required: true + schemas: + Message: + description: An message for the user + type: object + properties: + message: + type: string + format: int64 + greeting: + type: string + format: int64 diff --git a/examples/openapi/README.md b/examples/openapi/README.md new file mode 100644 index 000000000..370bf8f1f --- /dev/null +++ b/examples/openapi/README.md @@ -0,0 +1,36 @@ + +# Helidon SE OpenAPI Example + +This example shows a simple greeting application, similar to the one from the +Helidon SE QuickStart, enhanced with OpenAPI support. + +Most of the OpenAPI document in this example comes from a static file packaged +with the application. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-openapi.jar +``` + +Try the endpoints: + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} + +curl -X GET http://localhost:8080/openapi +#Output: [lengthy OpenAPI document] +``` + +The output describes not only then endpoints in `GreetService` as described in +the static file but also an endpoint contributed by the `SimpleAPIModelReader`. diff --git a/examples/openapi/pom.xml b/examples/openapi/pom.xml new file mode 100644 index 000000000..ffe3c4e64 --- /dev/null +++ b/examples/openapi/pom.xml @@ -0,0 +1,120 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples + helidon-examples-openapi + 1.0.0-SNAPSHOT + Helidon Examples OpenAPI + + + Basic illustration of OpenAPI support in Helidon SE + + + + io.helidon.examples.openapi.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.openapi + helidon-openapi + + + io.helidon.config + helidon-config-yaml + + + io.helidon.health + helidon-health-checks + + + jakarta.json + jakarta.json-api + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.webclient + helidon-webclient + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/openapi/src/main/java/io/helidon/examples/openapi/GreetService.java b/examples/openapi/src/main/java/io/helidon/examples/openapi/GreetService.java new file mode 100644 index 000000000..549f8f694 --- /dev/null +++ b/examples/openapi/src/main/java/io/helidon/examples/openapi/GreetService.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.openapi; + +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.http.Status; +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.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple service to greet you. Examples: + *

+ * Get default greeting message: + * curl -X GET http://localhost:8080/greet + *

+ * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + *

+ * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + *

+ * The message is returned as a JSON object + */ +public class GreetService implements HttpService { + + /** + * The config value for the key {@code greeting}. + */ + private String greeting; + + private static final JsonBuilderFactory JSON_BF = Json.createBuilderFactory(Map.of()); + + GreetService() { + Config config = Config.global(); + this.greeting = config.get("app.greeting").asString().orElse("Ciao"); + } + + /** + * A service registers itself by updating the routine rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.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().get("name"); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + GreetingMessage msg = new GreetingMessage(String.format("%s %s!", greeting, name)); + response.send(msg.forRest()); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + if (!jo.containsKey(GreetingMessage.JSON_LABEL)) { + JsonObject jsonErrorObject = JSON_BF.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting = GreetingMessage.fromRest(jo).getMessage(); + response.status(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) { + JsonObject jo = request.content().as(JsonObject.class); + updateGreetingFromJson(jo, response); + } + +} diff --git a/examples/openapi/src/main/java/io/helidon/examples/openapi/GreetingMessage.java b/examples/openapi/src/main/java/io/helidon/examples/openapi/GreetingMessage.java new file mode 100644 index 000000000..00f94e13e --- /dev/null +++ b/examples/openapi/src/main/java/io/helidon/examples/openapi/GreetingMessage.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.openapi; + +import java.util.Collections; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; + +/** + * POJO for the greeting message exchanged between the server and the client. + */ +public class GreetingMessage { + + /** + * Label for tagging a {@code GreetingMessage} instance in JSON. + */ + public static final String JSON_LABEL = "greeting"; + + private static final JsonBuilderFactory JSON_BF = Json.createBuilderFactory(Collections.emptyMap()); + + private String message; + + /** + * Create a new greeting with the specified message content. + * + * @param message the message to store in the greeting + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Returns the message value. + * + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message value to be set + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Converts a JSON object (typically read from the request payload) + * into a {@code GreetingMessage}. + * + * @param jsonObject the {@link JsonObject} to convert. + * @return {@code GreetingMessage} set according to the provided object + */ + public static GreetingMessage fromRest(JsonObject jsonObject) { + return new GreetingMessage(jsonObject.getString(JSON_LABEL)); + } + + /** + * Prepares a {@link JsonObject} corresponding to this instance. + * + * @return {@code JsonObject} representing this {@code GreetingMessage} instance + */ + public JsonObject forRest() { + JsonObjectBuilder builder = JSON_BF.createObjectBuilder(); + return builder.add(JSON_LABEL, message) + .build(); + } +} diff --git a/examples/openapi/src/main/java/io/helidon/examples/openapi/Main.java b/examples/openapi/src/main/java/io/helidon/examples/openapi/Main.java new file mode 100644 index 000000000..5c25f4e75 --- /dev/null +++ b/examples/openapi/src/main/java/io/helidon/examples/openapi/Main.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.openapi; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * Simple Hello World rest application. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(final String[] args) { + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build().start(); + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + /** + * Set up the server. + * + * @param server server builder + */ + static void setup(WebServerConfig.Builder server) { + + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + Config.global(config); + + server.config(config.get("server")) + .routing(Main::routing); + } + + /** + * Set up routing. + * + * @param routing routing builder + */ + static void routing(HttpRouting.Builder routing) { + routing.register("/greet", new GreetService()); + } + +} diff --git a/examples/openapi/src/main/java/io/helidon/examples/openapi/package-info.java b/examples/openapi/src/main/java/io/helidon/examples/openapi/package-info.java new file mode 100644 index 000000000..fd83d08b0 --- /dev/null +++ b/examples/openapi/src/main/java/io/helidon/examples/openapi/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example application showing support for OpenAPI in Helidon SE + *

+ * Start with {@link io.helidon.examples.openapi.Main} class. + * + * @see io.helidon.examples.openapi.Main + */ +package io.helidon.examples.openapi; diff --git a/examples/openapi/src/main/resources/META-INF/openapi.yml b/examples/openapi/src/main/resources/META-INF/openapi.yml new file mode 100644 index 000000000..b365fa4f1 --- /dev/null +++ b/examples/openapi/src/main/resources/META-INF/openapi.yml @@ -0,0 +1,77 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +--- +openapi: 3.0.0 +info: + title: Helidon SE Quickstart Example + description: A very simple application to reply with friendly greetings + version: 1.0.0 + +servers: + - url: http://localhost:8080 + description: Local test server + +paths: + /greet: + get: + summary: Returns a generic greeting + description: Greets the user generically + responses: + default: + description: Simple JSON containing the greeting + content: + application/json: + schema: + $ref: '#/components/schemas/GreetingMessage' + /greet/greeting: + put: + summary: Set the greeting prefix + description: Permits the client to set the prefix part of the greeting ("Hello") + requestBody: + description: Conveys the new greeting prefix to use in building greetings + content: + application/json: + schema: + $ref: '#/components/schemas/GreetingMessage' + examples: + greeting: + summary: Example greeting message to update + value: { "greeting": "Hola" } + responses: + "204": + description: Updated + /greet/{name}: + get: + summary: Returns a personalized greeting + parameters: + - name: name + in: path + required: true + schema: + type: string + responses: + default: + description: Simple JSON containing the greeting + content: + application/json: + schema: + $ref: '#/components/schemas/GreetingMessage' +components: + schemas: + GreetingMessage: + properties: + message: + type: string diff --git a/examples/openapi/src/main/resources/application.yaml b/examples/openapi/src/main/resources/application.yaml new file mode 100644 index 000000000..0f784dd50 --- /dev/null +++ b/examples/openapi/src/main/resources/application.yaml @@ -0,0 +1,22 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + greeting: "Hello" + +server: + port: 8080 + host: 0.0.0.0 diff --git a/examples/openapi/src/main/resources/logging.properties b/examples/openapi/src/main/resources/logging.properties new file mode 100644 index 000000000..6f4d5ad5f --- /dev/null +++ b/examples/openapi/src/main/resources/logging.properties @@ -0,0 +1,34 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO +io.helidon.openapi.OpenAPISupport.level=FINER diff --git a/examples/openapi/src/test/java/io/helidon/examples/openapi/MainTest.java b/examples/openapi/src/test/java/io/helidon/examples/openapi/MainTest.java new file mode 100644 index 000000000..7310ad75c --- /dev/null +++ b/examples/openapi/src/test/java/io/helidon/examples/openapi/MainTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.openapi; + +import java.util.Map; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonPointer; +import jakarta.json.JsonString; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +public class MainTest { + + private static final JsonBuilderFactory JSON_BF = Json.createBuilderFactory(Map.of()); + private static final JsonObject TEST_JSON_OBJECT = JSON_BF.createObjectBuilder() + .add("greeting", "Hola") + .build(); + + private final Http1Client client; + + public MainTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + Main.setup(server); + } + + @Test + public void testHelloWorld() { + try (Http1ClientResponse response = client.get("/greet").request()) { + JsonObject jsonObject = response.as(JsonObject.class); + assertThat(jsonObject.getString("greeting"), is("Hello World!")); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + JsonObject jsonObject = response.as(JsonObject.class); + assertThat(jsonObject.getString("greeting"), is("Hello Joe!")); + } + + try (Http1ClientResponse response = client.put("/greet/greeting").submit(TEST_JSON_OBJECT)) { + assertThat(response.status().code(), is(204)); + } + + try (Http1ClientResponse response = client.get("/greet/Joe").request()) { + JsonObject jsonObject = response.as(JsonObject.class); + assertThat(jsonObject.getString("greeting"), is("Hola Joe!")); + } + + try (Http1ClientResponse response = client.get("/observe/health").request()) { + assertThat(response.status(), is(Status.NO_CONTENT_204)); + } + + try (Http1ClientResponse response = client.get("/observe/metrics").request()) { + assertThat(response.status().code(), is(200)); + } + } + + @Test + public void testOpenAPI() { + JsonObject jsonObject = client.get("/openapi") + .accept(MediaTypes.APPLICATION_JSON) + .requestEntity(JsonObject.class); + JsonObject paths = jsonObject.getJsonObject("paths"); + + JsonPointer jp = Json.createPointer("/" + escape("/greet/greeting") + "/put/summary"); + JsonString js = (JsonString) jp.getValue(paths); + assertThat("/greet/greeting.put.summary not as expected", js.getString(), is("Set the greeting prefix")); + } + + private static String escape(String path) { + return path.replace("/", "~1"); + } + +} diff --git a/examples/pom.xml b/examples/pom.xml index ac341349b..e2336a439 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -27,6 +27,7 @@ io.helidon.examples helidon-examples-project + 1.0.0-SNAPSHOT Helidon Examples pom @@ -40,30 +41,27 @@ - + webserver diff --git a/examples/quickstarts/README.md b/examples/quickstarts/README.md new file mode 100644 index 000000000..83f2f7d6e --- /dev/null +++ b/examples/quickstarts/README.md @@ -0,0 +1,8 @@ +# Helidon Examples Quickstart + +These are the examples used by the Helidon Getting Started guide, and +the quickstart Maven archetypes. All examples implement the same +simple greeting service, one using Helidon MP and one using Helidon SE. + +The `archetypes` directory contains scripts for building the Maven +archetypes from these examples. diff --git a/examples/quickstarts/helidon-quickstart-mp/.dockerignore b/examples/quickstarts/helidon-quickstart-mp/.dockerignore new file mode 100644 index 000000000..c8b241f22 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/.dockerignore @@ -0,0 +1 @@ +target/* \ No newline at end of file diff --git a/examples/quickstarts/helidon-quickstart-mp/.gitignore b/examples/quickstarts/helidon-quickstart-mp/.gitignore new file mode 100644 index 000000000..594f3abf6 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/.gitignore @@ -0,0 +1,16 @@ +hs_err_pid* +target/ +.DS_Store +.idea/ +*.iws +*.ipr +*.iml +atlassian-ide-plugin.xml +nbactions.xml +nb-configuration.xml +.settings +.settings/ +.project +.classpath +*.swp +*~ diff --git a/examples/quickstarts/helidon-quickstart-mp/Dockerfile b/examples/quickstarts/helidon-quickstart-mp/Dockerfile new file mode 100644 index 000000000..0ed90957e --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/Dockerfile @@ -0,0 +1,53 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -DskipTests +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-quickstart-mp.jar ./ +COPY --from=build /helidon/target/libs ./libs + +CMD ["java", "-jar", "helidon-quickstart-mp.jar"] + +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-quickstart-mp/Dockerfile.jlink b/examples/quickstarts/helidon-quickstart-mp/Dockerfile.jlink new file mode 100644 index 000000000..ad408f617 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/Dockerfile.jlink @@ -0,0 +1,49 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build to create the custom Java Runtime Image +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -Pjlink-image -DskipTests +RUN echo "done!" + +# 2nd stage, build the final image with the JRI built in the 1st stage + +FROM debian:stretch-slim +WORKDIR /helidon +COPY --from=build /helidon/target/helidon-quickstart-mp-jri ./ +ENTRYPOINT ["/bin/bash", "/helidon/bin/start"] +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-quickstart-mp/Dockerfile.native b/examples/quickstarts/helidon-quickstart-mp/Dockerfile.native new file mode 100644 index 000000000..2f56f1a78 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/Dockerfile.native @@ -0,0 +1,54 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM ghcr.io/graalvm/graalvm-community:21.0.0-ol9 as build + +WORKDIR /usr/share + +# Install maven +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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Pnative-image -Dnative.image.skip -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -Pnative-image -Dnative.image.buildStatic -DskipTests + +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM scratch +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-quickstart-mp . + +ENTRYPOINT ["./helidon-quickstart-mp"] + +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-quickstart-mp/README.md b/examples/quickstarts/helidon-quickstart-mp/README.md new file mode 100644 index 000000000..c5a0e6782 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/README.md @@ -0,0 +1,162 @@ +# Helidon Quickstart MP Example + +This example implements a simple Hello World REST service using MicroProfile. + +## Build and run + +```shell +mvn package +java -jar target/helidon-quickstart-mp.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"message" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} +``` + +## Try health and metrics + +```shell +curl -s -X GET http://localhost:8080/health +#Output: {"outcome":"UP",... + + +# Prometheus Format +curl -s -X GET http://localhost:8080/metrics +# TYPE base:gc_g1_young_generation_count gauge + +# JSON Format +curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics +#Output: {"base":... +``` + +## Build the Docker Image + +```shell +docker build -t helidon-quickstart-mp . +``` + +## Start the application with Docker + +```shell +docker run --rm -p 8080:8080 helidon-quickstart-mp:latest +``` + +Exercise the application as described above + +## Deploy the application to Kubernetes + +```shell +kubectl cluster-info # Verify which cluster +kubectl get pods # Verify connectivity to cluster +kubectl create -f app.yaml # Deploy application +kubectl get service helidon-quickstart-mp # Verify deployed service +``` + +## Build a native image with GraalVM + +GraalVM allows you to compile your programs ahead-of-time into a native + executable. See https://www.graalvm.org/docs/reference-manual/aot-compilation/ + for more information. + +You can build a native executable in 2 different ways: +* With a local installation of GraalVM +* Using Docker + +### Local build + +Download Graal VM at https://www.graalvm.org/downloads. We recommend +version `23.1.0` or later. + +```shell +# Setup the environment +export GRAALVM_HOME=/path +# build the native executable +mvn package -Pnative-image +``` + +You can also put the Graal VM `bin` directory in your PATH, or pass + `-DgraalVMHome=/path` to the Maven command. + +See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-native-image + for more information. + +Start the application: + +```shell +./target/helidon-quickstart-mp +``` + +### Multi-stage Docker build + +Build the "native" Docker Image + +```shell +docker build -t helidon-quickstart-mp-native -f Dockerfile.native . +``` + +Start the application: + +```shell +docker run --rm -p 8080:8080 helidon-quickstart-mp-native:latest +``` + + +## Build a Java Runtime Image using jlink + +You can build a custom Java Runtime Image (JRI) containing the application jars and the JDK modules +on which they depend. This image also: + +* Enables Class Data Sharing by default to reduce startup time. +* Contains a customized `start` script to simplify CDS usage and support debug and test modes. + +You can build a custom JRI in two different ways: +* Local +* Using Docker + + +### Local build + +```shell +# build the JRI +mvn package -Pjlink-image +``` + +See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-jlink-image + for more information. + +Start the application: + +```shell +./target/helidon-quickstart-mp-jri/bin/start +``` + +### Multi-stage Docker build + +Build the JRI as a Docker Image + +```shell +docker build -t helidon-quickstart-mp-jri -f Dockerfile.jlink . +``` + +Start the application: + +```shell +docker run --rm -p 8080:8080 helidon-quickstart-mp-jri:latest +``` + +See the start script help: + +```shell +docker run --rm helidon-quickstart-mp-jri:latest --help +``` diff --git a/examples/quickstarts/helidon-quickstart-mp/app.yaml b/examples/quickstarts/helidon-quickstart-mp/app.yaml new file mode 100644 index 000000000..32184d6c1 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/app.yaml @@ -0,0 +1,57 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +kind: Service +apiVersion: v1 +metadata: + name: helidon-quickstart-mp + labels: + app: helidon-quickstart-mp +spec: + type: NodePort + selector: + app: helidon-quickstart-mp + ports: + - port: 8080 + targetPort: 8080 + name: http +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helidon-quickstart-mp + labels: + app: helidon-quickstart-mp + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: helidon-quickstart-mp + version: v1 + template: + metadata: + labels: + app: helidon-quickstart-mp + version: v1 + spec: + containers: + - name: helidon-quickstart-mp + image: helidon-quickstart-mp + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 diff --git a/examples/quickstarts/helidon-quickstart-mp/build.gradle b/examples/quickstarts/helidon-quickstart-mp/build.gradle new file mode 100644 index 000000000..ea6f662eb --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/build.gradle @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'java' + id 'org.kordamp.gradle.jandex' version '0.6.0' + id 'application' +} + +group = 'io.helidon.examples' +version = '1.0-SNAPSHOT' + +description = """helidon-quickstart-mp""" + +sourceCompatibility = 21 +targetCompatibility = 21 +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +ext { + helidonversion = '4.1.0-SNAPSHOT' + mainClass='io.helidon.microprofile.cdi.Main' +} + +repositories { + mavenCentral() + mavenLocal() + gradlePluginPortal() +} + +dependencies { + // import Helidon BOM + implementation enforcedPlatform("io.helidon:helidon-dependencies:${project.helidonversion}") + implementation 'io.helidon.microprofile.bundles:helidon-microprofile' + implementation 'org.glassfish.jersey.media:jersey-media-json-binding' + + runtimeOnly 'io.smallrye:jandex' + runtimeOnly 'jakarta.activation:jakarta.activation-api' + + testImplementation 'io.helidon.microprofile.testing:helidon-microprofile-testing-junit5' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.hamcrest:hamcrest-all' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} + +test { + useJUnitPlatform() +} + +// define a custom task to copy all dependencies in the runtime classpath +// into build/libs/libs +// uses built-in Copy +task copyLibs(type: Copy) { + from configurations.runtimeClasspath + into 'build/libs/libs' +} + +// add it as a dependency of built-in task 'assemble' +copyLibs.dependsOn jar +assemble.dependsOn copyLibs + +// default jar configuration +// set the main classpath +// add each jar under build/libs/libs into the classpath +jar { + archiveFileName = "${project.name}.jar" + manifest { + attributes ('Main-Class': "${project.mainClass}" , + 'Class-Path': configurations.runtimeClasspath.files.collect { "libs/$it.name" }.join(' ') + ) + } +} + +application { + mainClass = "${project.mainClass}" +} + +// This is a work-around for running unit tests. +// Gradle places resource files under ${buildDir}/resources. In order for +// beans.xml to get picked up by CDI it must be co-located with the classes. +// So we move it before running tests. +// In either case it ends up AOK in the final jar artifact +task moveBeansXML { + doLast { + ant.move file: "${buildDir}/resources/main/META-INF/beans.xml", + todir: "${buildDir}/classes/java/main/META-INF" + } +} +compileTestJava.dependsOn jandex +jar.dependsOn jandex +test.dependsOn moveBeansXML +run.dependsOn moveBeansXML diff --git a/examples/quickstarts/helidon-quickstart-mp/pom.xml b/examples/quickstarts/helidon-quickstart-mp/pom.xml new file mode 100644 index 000000000..b9f5d8a48 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/pom.xml @@ -0,0 +1,110 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.quickstarts + helidon-quickstart-mp + 1.0.0-SNAPSHOT + Helidon Examples Quickstart MP + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.microprofile.openapi + helidon-microprofile-openapi + + + io.helidon.microprofile.health + helidon-microprofile-health + + + io.helidon.microprofile.metrics + helidon-microprofile-metrics + + + io.helidon.logging + helidon-logging-jul + runtime + + + jakarta.json.bind + jakarta.json.bind-api + + + org.glassfish.jersey.media + jersey-media-json-binding + runtime + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/quickstarts/helidon-quickstart-mp/settings.gradle b/examples/quickstarts/helidon-quickstart-mp/settings.gradle new file mode 100644 index 000000000..82acd1f10 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/settings.gradle @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +rootProject.name = 'helidon-quickstart-mp' diff --git a/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetResource.java b/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetResource.java new file mode 100644 index 000000000..b4bb3b81a --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetResource.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.quickstart.mp; + +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; + +/** + * A simple JAX-RS resource to greet you. Examples: + * + * Get default greeting message: + * curl -X GET http://localhost:8080/greet + * + * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + * + * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + * + * The message is returned as a JSON object. + */ +@Path("/greet") +public class GreetResource { + + /** + * The greeting message provider. + */ + private final GreetingProvider greetingProvider; + + /** + * Using constructor injection to get a configuration property. + * By default this gets the value from META-INF/microprofile-config + * + * @param greetingConfig the configured greeting message + */ + @Inject + public GreetResource(GreetingProvider greetingConfig) { + this.greetingProvider = greetingConfig; + } + + /** + * Return a worldly greeting message. + * + * @return {@link GreetingMessage} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getDefaultMessage() { + return createResponse("World"); + } + + /** + * Return a greeting message using the name that was provided. + * + * @param name the name to greet + * @return {@link GreetingMessage} + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getMessage(@PathParam("name") String name) { + return createResponse(name); + } + + /** + * Set the greeting to use in future messages. + * + * @param message JSON containing the new greeting + * @return {@link Response} + */ + @Path("/greeting") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @RequestBody(name = "greeting", + required = true, + content = @Content(mediaType = "application/json", + schema = @Schema(type = SchemaType.OBJECT, requiredProperties = { "greeting" }))) + @APIResponses({ + @APIResponse(name = "normal", responseCode = "204", description = "Greeting updated"), + @APIResponse(name = "missing 'greeting'", responseCode = "400", + description = "JSON did not contain setting for 'greeting'")}) + public Response updateGreeting(GreetingMessage message) { + if (message.getMessage() == null) { + GreetingMessage entity = new GreetingMessage("No greeting provided"); + return Response.status(Response.Status.BAD_REQUEST).entity(entity).build(); + } + + greetingProvider.setMessage(message.getMessage()); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + private GreetingMessage createResponse(String who) { + String msg = String.format("%s %s!", greetingProvider.getMessage(), who); + + return new GreetingMessage(msg); + } +} diff --git a/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingMessage.java b/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingMessage.java new file mode 100644 index 000000000..b3c8a56f9 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingMessage.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.quickstart.mp; + +/** + * POJO defining the greeting message content. + */ +@SuppressWarnings("unused") +public class GreetingMessage { + private String message; + + /** + * Create a new GreetingMessage instance. + */ + public GreetingMessage() { + } + + /** + * Create a new GreetingMessage instance. + * + * @param message message + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Gets the message value. + * + * @return message value + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message message value to set + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingProvider.java b/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingProvider.java new file mode 100644 index 000000000..a2215a053 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.quickstart.mp; + +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Provider for greeting message. + */ +@ApplicationScoped +public class GreetingProvider { + private final AtomicReference message = new AtomicReference<>(); + + /** + * Create a new greeting provider, reading the message from configuration. + * + * @param message greeting to use + */ + @Inject + public GreetingProvider(@ConfigProperty(name = "app.greeting") String message) { + this.message.set(message); + } + + String getMessage() { + return message.get(); + } + + void setMessage(String message) { + this.message.set(message); + } +} diff --git a/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/package-info.java b/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/package-info.java new file mode 100644 index 000000000..9d19e0a05 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quickstart MicroProfile example. + */ +package io.helidon.examples.quickstart.mp; diff --git a/examples/quickstarts/helidon-quickstart-mp/src/main/resources/META-INF/beans.xml b/examples/quickstarts/helidon-quickstart-mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..ddb8316e3 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/quickstarts/helidon-quickstart-mp/src/main/resources/META-INF/microprofile-config.properties b/examples/quickstarts/helidon-quickstart-mp/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..c071c0a41 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Application properties. This is the default greeting +app.greeting=Hello + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +# Enable the optional MicroProfile Metrics REST.request metrics +metrics.rest-request.enabled=true diff --git a/examples/quickstarts/helidon-quickstart-mp/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-quickstart-mp/native-image.properties b/examples/quickstarts/helidon-quickstart-mp/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-quickstart-mp/native-image.properties new file mode 100644 index 000000000..72e99d2af --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-quickstart-mp/native-image.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +Args=--initialize-at-build-time=io.helidon.examples diff --git a/examples/quickstarts/helidon-quickstart-mp/src/main/resources/logging.properties b/examples/quickstarts/helidon-quickstart-mp/src/main/resources/logging.properties new file mode 100644 index 000000000..d19bc389d --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/src/main/resources/logging.properties @@ -0,0 +1,39 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Quiet Weld +org.jboss.level=WARNING + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.microprofile.level=INFO +#io.helidon.common.level=INFO +#org.glassfish.jersey.level=INFO +#org.jboss.weld=INFO diff --git a/examples/quickstarts/helidon-quickstart-mp/src/test/java/io/helidon/examples/quickstart/mp/MainTest.java b/examples/quickstarts/helidon-quickstart-mp/src/test/java/io/helidon/examples/quickstart/mp/MainTest.java new file mode 100644 index 000000000..99f5a52ba --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/src/test/java/io/helidon/examples/quickstart/mp/MainTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.quickstart.mp; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +class MainTest { + private final WebTarget target; + + @Inject + MainTest(WebTarget target) { + this.target = target; + } + + @Test + void testHelloWorld() { + + GreetingMessage message = target.path("/greet") + .request() + .get(GreetingMessage.class); + assertThat("default message", message.getMessage(), + is("Hello World!")); + + message = target.path("/greet/Joe") + .request() + .get(GreetingMessage.class); + assertThat("hello Joe message", message.getMessage(), + is("Hello Joe!")); + + try (Response r = target.path("/greet/greeting") + .request() + .put(Entity.entity("{\"message\" : \"Hola\"}", MediaType.APPLICATION_JSON))) { + assertThat("PUT status code", r.getStatus(), is(204)); + } + + message = target.path("/greet/Jose") + .request() + .get(GreetingMessage.class); + assertThat("hola Jose message", message.getMessage(), + is("Hola Jose!")); + } + @Test + void testMetrics() { + try (Response r = target.path("/metrics") + .request() + .get()) { + assertThat("GET metrics status code", r.getStatus(), is(200)); + } + } + + @Test + void testHealth() { + try (Response r = target.path("/health") + .request() + .get()) { + assertThat("GET health status code", r.getStatus(), is(200)); + } + } +} diff --git a/examples/quickstarts/helidon-quickstart-mp/src/test/resources/META-INF/microprofile-config.properties b/examples/quickstarts/helidon-quickstart-mp/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..db419ce7e --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-mp/src/test/resources/META-INF/microprofile-config.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Override configuration to use a random port for the unit tests +config_ordinal=1000 +# Microprofile server properties +server.port=-1 +server.host=0.0.0.0 diff --git a/examples/quickstarts/helidon-quickstart-se/.dockerignore b/examples/quickstarts/helidon-quickstart-se/.dockerignore new file mode 100644 index 000000000..c8b241f22 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/.dockerignore @@ -0,0 +1 @@ +target/* \ No newline at end of file diff --git a/examples/quickstarts/helidon-quickstart-se/.gitignore b/examples/quickstarts/helidon-quickstart-se/.gitignore new file mode 100644 index 000000000..241e8042d --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/.gitignore @@ -0,0 +1,16 @@ +hs_err_pid* +target/ +.DS_Store +.idea/ +*.iws +*.ipr +*.iml +atlassian-ide-plugin.xml +nbactions.xml +nb-configuration.xml +.settings +.settings/ +.project +.classpath +*.swp +*~ \ No newline at end of file diff --git a/examples/quickstarts/helidon-quickstart-se/Dockerfile b/examples/quickstarts/helidon-quickstart-se/Dockerfile new file mode 100644 index 000000000..b83351760 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/Dockerfile @@ -0,0 +1,54 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -DskipTests + +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-quickstart-se.jar ./ +COPY --from=build /helidon/target/libs ./libs + +CMD ["java", "-jar", "helidon-quickstart-se.jar"] + +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-quickstart-se/Dockerfile.jlink b/examples/quickstarts/helidon-quickstart-se/Dockerfile.jlink new file mode 100644 index 000000000..7b4c37eaf --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/Dockerfile.jlink @@ -0,0 +1,49 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build to create the custom Java Runtime Image +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -Pjlink-image -DskipTests +RUN echo "done!" + +# 2nd stage, build the final image with the JRI built in the 1st stage + +FROM debian:stretch-slim +WORKDIR /helidon +COPY --from=build /helidon/target/helidon-quickstart-se-jri ./ +ENTRYPOINT ["/bin/bash", "/helidon/bin/start"] +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-quickstart-se/Dockerfile.native b/examples/quickstarts/helidon-quickstart-se/Dockerfile.native new file mode 100644 index 000000000..24bbfb8d6 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/Dockerfile.native @@ -0,0 +1,54 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM ghcr.io/graalvm/graalvm-community:21.0.0-ol9 as build + +WORKDIR /usr/share + +# Install maven +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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Pnative-image -Dnative.image.skip -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -Pnative-image -Dnative.image.buildStatic -DskipTests + +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM scratch +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-quickstart-se . + +ENTRYPOINT ["./helidon-quickstart-se"] + +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-quickstart-se/README.md b/examples/quickstarts/helidon-quickstart-se/README.md new file mode 100644 index 000000000..bbb38bcc9 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/README.md @@ -0,0 +1,161 @@ +# Helidon Quickstart SE Example + +This project implements a simple Hello World REST service using Helidon SE. + +## Build and run + +```shell +mvn package +java -jar target/helidon-quickstart-se.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} +``` + +## Try health and metrics + +```shell +curl -s -X GET http://localhost:8080/observe/health +#Output: {"outcome":"UP",... + +# Prometheus Format +curl -s -X GET http://localhost:8080/observe/metrics +# TYPE base:gc_g1_young_generation_count gauge + +# JSON Format +curl -H 'Accept: application/json' -X GET http://localhost:8080/observe/metrics +#Output: {"base":... + +``` + +## Build the Docker Image + +```shell +docker build -t helidon-quickstart-se . +``` + +## Start the application with Docker + +```shell +docker run --rm -p 8080:8080 helidon-quickstart-se:latest +``` + +Exercise the application as described above + +## Deploy the application to Kubernetes + +```shell +kubectl cluster-info # Verify which cluster +kubectl get pods # Verify connectivity to cluster +kubectl create -f app.yaml # Deply application +kubectl get service helidon-quickstart-se # Get service info +``` + +## Build a native image with GraalVM + +GraalVM allows you to compile your programs ahead-of-time into a native + executable. See https://www.graalvm.org/docs/reference-manual/aot-compilation/ + for more information. + +You can build a native executable in 2 different ways: +* With a local installation of GraalVM +* Using Docker + +### Local build + +Download Graal VM at https://www.graalvm.org/downloads. We recommend +version `23.1.0` or later. + +```shell +# Setup the environment +export GRAALVM_HOME=/path +# build the native executable +mvn package -Pnative-image +``` + +You can also put the Graal VM `bin` directory in your PATH, or pass + `-DgraalVMHome=/path` to the Maven command. + +See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-native-image + for more information. + +Start the application: + +```shell +./target/helidon-quickstart-se +``` + +### Multi-stage Docker build + +Build the "native" Docker Image + +```shell +docker build -t helidon-quickstart-se-native -f Dockerfile.native . +``` + +Start the application: + +```shell +docker run --rm -p 8080:8080 helidon-quickstart-se-native:latest +``` + +## Build a Java Runtime Image using jlink + +You can build a custom Java Runtime Image (JRI) containing the application jars and the JDK modules +on which they depend. This image also: + +* Enables Class Data Sharing by default to reduce startup time. +* Contains a customized `start` script to simplify CDS usage and support debug and test modes. + +You can build a custom JRI in two different ways: +* Local +* Using Docker + + +### Local build + +```shell +# build the JRI +mvn package -Pjlink-image +``` + +See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-jlink-image + for more information. + +Start the application: + +```shell +./target/helidon-quickstart-se-jri/bin/start +``` + +### Multi-stage Docker build + +Build the JRI as a Docker Image + +```shell +docker build -t helidon-quickstart-se-jri -f Dockerfile.jlink . +``` + +Start the application: + +```shell +docker run --rm -p 8080:8080 helidon-quickstart-se-jri:latest +``` + +See the start script help: + +```shell +docker run --rm helidon-quickstart-se-jri:latest --help +``` \ No newline at end of file diff --git a/examples/quickstarts/helidon-quickstart-se/app.yaml b/examples/quickstarts/helidon-quickstart-se/app.yaml new file mode 100644 index 000000000..8a314633c --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/app.yaml @@ -0,0 +1,57 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +kind: Service +apiVersion: v1 +metadata: + name: helidon-quickstart-se + labels: + app: helidon-quickstart-se +spec: + type: NodePort + selector: + app: helidon-quickstart-se + ports: + - port: 8080 + targetPort: 8080 + name: http +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helidon-quickstart-se + labels: + app: helidon-quickstart-se + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: helidon-quickstart-se + version: v1 + template: + metadata: + labels: + app: helidon-quickstart-se + version: v1 + spec: + containers: + - name: helidon-quickstart-se + image: helidon-quickstart-se + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 diff --git a/examples/quickstarts/helidon-quickstart-se/build.gradle b/examples/quickstarts/helidon-quickstart-se/build.gradle new file mode 100644 index 000000000..5d1f00951 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/build.gradle @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'java' + id 'application' +} + +group = 'io.helidon.examples' +version = '1.0-SNAPSHOT' + +description = """helidon-quickstart-se""" + +sourceCompatibility = 21 +targetCompatibility = 21 +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +ext { + helidonversion = '4.1.0-SNAPSHOT' + mainClass='io.helidon.examples.quickstart.se.Main' +} + +test { + useJUnitPlatform() +} + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + // import Helidon BOM + implementation enforcedPlatform("io.helidon:helidon-dependencies:${project.helidonversion}") + implementation 'io.helidon.webserver:helidon-webserver' + implementation 'io.helidon.http.media:helidon-http-media-jsonp' + implementation 'io.helidon.webserver.observe:helidon-webserver-observe-health' + implementation 'io.helidon.webserver.observe:helidon-webserver-observe-metrics' + implementation 'io.helidon.config:helidon-config-yaml' + implementation 'io.helidon.health:helidon-health-checks' + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'io.helidon.webserver.testing.junit5:helidon-webserver-testing-junit5' + testImplementation 'io.helidon.webclient:helidon-webclient' + testImplementation 'org.hamcrest:hamcrest-all' +} + +// define a custom task to copy all dependencies in the runtime classpath +// into build/libs/libs +// uses built-in Copy +task copyLibs(type: Copy) { + from configurations.runtimeClasspath + into 'build/libs/libs' +} + +// add it as a dependency of built-in task 'assemble' +copyLibs.dependsOn jar +assemble.dependsOn copyLibs + +// default jar configuration +// set the main classpath +// add each jar under build/libs/libs into the classpath +jar { + archiveFileName = "${project.name}.jar" + manifest { + attributes ('Main-Class': "${project.mainClass}", + 'Class-Path': configurations.runtimeClasspath.files.collect { "libs/$it.name" }.join(' ') + ) + } +} + +application { + mainClass = "${project.mainClass}" +} + diff --git a/examples/quickstarts/helidon-quickstart-se/pom.xml b/examples/quickstarts/helidon-quickstart-se/pom.xml new file mode 100644 index 000000000..e2d95cee6 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/pom.xml @@ -0,0 +1,110 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.quickstarts + helidon-quickstart-se + 1.0.0-SNAPSHOT + Helidon Examples Quickstart SE + + + io.helidon.examples.quickstart.se.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + + + io.helidon.metrics + helidon-metrics-system-meters + + + io.helidon.openapi + helidon-openapi + + + io.helidon.config + helidon-config-yaml + + + io.helidon.health + helidon-health-checks + + + jakarta.json + jakarta.json-api + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.webclient + helidon-webclient + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/quickstarts/helidon-quickstart-se/settings.gradle b/examples/quickstarts/helidon-quickstart-se/settings.gradle new file mode 100644 index 000000000..ef7a8abd7 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/settings.gradle @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +rootProject.name = 'helidon-quickstart-se' diff --git a/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java new file mode 100644 index 000000000..225fd686f --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.quickstart.se; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.config.Config; +import io.helidon.http.Status; +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.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * 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 JSON object. + */ +public class GreetService implements HttpService { + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + /** + * The config value for the key {@code greeting}. + */ + private final AtomicReference greeting = new AtomicReference<>(); + + GreetService() { + this(Config.global().get("app")); + } + + GreetService(Config appConfig) { + greeting.set(appConfig.get("greeting").asString().orElse("Ciao")); + } + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.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().get("name"); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + String msg = String.format("%s %s!", greeting.get(), name); + + JsonObject returnObject = JSON.createObjectBuilder() + .add("message", msg) + .build(); + response.send(returnObject); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + if (!jo.containsKey("greeting")) { + JsonObject jsonErrorObject = JSON.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting.set(jo.getString("greeting")); + response.status(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) { + updateGreetingFromJson(request.content().as(JsonObject.class), response); + } +} diff --git a/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java new file mode 100644 index 000000000..0e8a7b4dc --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.quickstart.se; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +/** + * The application main class. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + // load logging configuration + LogConfig.configureRuntime(); + + // initialize global config from default configuration + Config config = Config.create(); + Config.global(config); + + WebServer server = WebServer.builder() + .config(config.get("server")) + .routing(Main::routing) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + /** + * Updates HTTP Routing and registers observe providers. + */ + static void routing(HttpRouting.Builder routing) { + routing.register("/greet", new GreetService()); + } +} diff --git a/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/package-info.java b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/package-info.java new file mode 100644 index 000000000..d282464f9 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quickstart demo application + *

+ * Start with {@link io.helidon.examples.quickstart.se.Main} class. + * + * @see io.helidon.examples.quickstart.se.Main + */ +package io.helidon.examples.quickstart.se; diff --git a/examples/quickstarts/helidon-quickstart-se/src/main/resources/application.yaml b/examples/quickstarts/helidon-quickstart-se/src/main/resources/application.yaml new file mode 100644 index 000000000..2e7c572f9 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/src/main/resources/application.yaml @@ -0,0 +1,31 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + greeting: "Hello" + +server: + port: 8080 + host: 0.0.0.0 + # default behavior to discover Server features (observability, openapi, metrics) + # features-discovers-services: true + # all of the below is discovered automatically + # features: + # observe: + # observers: + # metrics: + # health: + # enabled: false diff --git a/examples/quickstarts/helidon-quickstart-se/src/main/resources/logging.properties b/examples/quickstarts/helidon-quickstart-se/src/main/resources/logging.properties new file mode 100644 index 000000000..717a25c4a --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/src/main/resources/logging.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.level=INFO diff --git a/examples/quickstarts/helidon-quickstart-se/src/test/java/io/helidon/examples/quickstart/se/MainTest.java b/examples/quickstarts/helidon-quickstart-se/src/test/java/io/helidon/examples/quickstart/se/MainTest.java new file mode 100644 index 000000000..07ba4e941 --- /dev/null +++ b/examples/quickstarts/helidon-quickstart-se/src/test/java/io/helidon/examples/quickstart/se/MainTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.quickstart.se; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import jakarta.json.JsonObject; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class MainTest { + + private final Http1Client client; + + protected MainTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.routing(builder); + } + + @Test + void testRootRoute() { + try (Http1ClientResponse response = client.get("/greet").request()) { + assertThat(response.status(), is(Status.OK_200)); + JsonObject json = response.as(JsonObject.class); + assertThat(json.getString("message"), is("Hello World!")); + } + } + + @Test + void testHealthObserver() { + try (Http1ClientResponse response = client.get("/observe/health").request()) { + assertThat(response.status(), is(Status.NO_CONTENT_204)); + } + } + + @Test + void testDeadlockHealthCheck() { + try (Http1ClientResponse response = client.get("/observe/health/live/deadlock").request()) { + assertThat(response.status(), is(Status.NO_CONTENT_204)); + } + } + + @Test + void testMetricsObserver() { + try (Http1ClientResponse response = client.get("/observe/metrics").request()) { + assertThat(response.status(), is(Status.OK_200)); + } + } +} diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/.dockerignore b/examples/quickstarts/helidon-standalone-quickstart-mp/.dockerignore new file mode 100644 index 000000000..c8b241f22 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/.dockerignore @@ -0,0 +1 @@ +target/* \ No newline at end of file diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/.gitignore b/examples/quickstarts/helidon-standalone-quickstart-mp/.gitignore new file mode 100644 index 000000000..594f3abf6 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/.gitignore @@ -0,0 +1,16 @@ +hs_err_pid* +target/ +.DS_Store +.idea/ +*.iws +*.ipr +*.iml +atlassian-ide-plugin.xml +nbactions.xml +nb-configuration.xml +.settings +.settings/ +.project +.classpath +*.swp +*~ diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/Dockerfile b/examples/quickstarts/helidon-standalone-quickstart-mp/Dockerfile new file mode 100644 index 000000000..5a1cb9c90 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/Dockerfile @@ -0,0 +1,53 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -DskipTests +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-standalone-quickstart-mp.jar ./ +COPY --from=build /helidon/target/libs ./libs + +CMD ["java", "-jar", "helidon-standalone-quickstart-mp.jar"] + +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/Dockerfile.jlink b/examples/quickstarts/helidon-standalone-quickstart-mp/Dockerfile.jlink new file mode 100644 index 000000000..a33effbdb --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/Dockerfile.jlink @@ -0,0 +1,49 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build to create the custom Java Runtime Image +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -Pjlink-image -DskipTests +RUN echo "done!" + +# 2nd stage, build the final image with the JRI built in the 1st stage + +FROM debian:stretch-slim +WORKDIR /helidon +COPY --from=build /helidon/target/helidon-standalone-quickstart-mp-jri ./ +ENTRYPOINT ["/bin/bash", "/helidon/bin/start"] +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/Dockerfile.native b/examples/quickstarts/helidon-standalone-quickstart-mp/Dockerfile.native new file mode 100644 index 000000000..46dc5ef4c --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/Dockerfile.native @@ -0,0 +1,54 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM ghcr.io/graalvm/graalvm-community:21.0.0-ol9 as build + +WORKDIR /usr/share + +# Install maven +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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Pnative-image -Dnative.image.skip -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -Pnative-image -Dnative.image.buildStatic -DskipTests + +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM scratch +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-standalone-quickstart-mp . + +ENTRYPOINT ["./helidon-standalone-quickstart-mp"] + +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/README.md b/examples/quickstarts/helidon-standalone-quickstart-mp/README.md new file mode 100644 index 000000000..302974648 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/README.md @@ -0,0 +1,160 @@ +# Helidon Examples Standalone Quickstart MP + +This example implements a simple Hello World REST service using MicroProfile + with a standalone Maven pom. + +## Build and run + +```shell +mvn package +java -jar target/helidon-standalone-quickstart-mp.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"message" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} +``` + +## Try health and metrics + +```shell +curl -s -X GET http://localhost:8080/health +#Output: {"outcome":"UP",... + +# Prometheus Format +curl -s -X GET http://localhost:8080/metrics +# TYPE base:gc_g1_young_generation_count gauge + +# JSON Format +curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics +#Output: {"base":... +``` + +## Build the Docker Image + +```shell +docker build -t helidon-standalone-quickstart-mp . +``` + +## Start the application with Docker + +```shell +docker run --rm -p 8080:8080 helidon-standalone-quickstart-mp:latest +``` + +Exercise the application as described above + +## Deploy the application to Kubernetes + +```shell +kubectl cluster-info # Verify which cluster +kubectl get pods # Verify connectivity to cluster +kubectl create -f app.yaml # Deploy application +kubectl get service helidon-standalone-quickstart-mp # Verify deployed service +``` + +## Build a native image with GraalVM + +GraalVM allows you to compile your programs ahead-of-time into a native + executable. See https://www.graalvm.org/docs/reference-manual/aot-compilation/ + for more information. + +You can build a native executable in 2 different ways: +* With a local installation of GraalVM +* Using Docker + +### Local build + +Download Graal VM at https://www.graalvm.org/downloads. + +```shell +# Setup the environment +export GRAALVM_HOME=/path +# build the native executable +mvn package -Pnative-image +``` + +You can also put the Graal VM `bin` directory in your PATH, or pass + `-DgraalVMHome=/path` to the Maven command. + +See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-native-image + for more information. + +Start the application: + +```shell +./target/helidon-quickstart-mp +``` + +### Multi-stage Docker build + +Build the "native" Docker Image + +```shell +docker build -t helidon-standalone-quickstart-mp-native -f Dockerfile.native . +``` + +Start the application: + +```shell +docker run --rm -p 8080:8080 helidon-standalone-quickstart-mp-native:latest +``` + +## Build a Java Runtime Image using jlink + +You can build a custom Java Runtime Image (JRI) containing the application jars and the JDK modules +on which they depend. This image also: + +* Enables Class Data Sharing by default to reduce startup time. +* Contains a customized `start` script to simplify CDS usage and support debug and test modes. + +You can build a custom JRI in two different ways: +* Local +* Using Docker + + +### Local build + +```shell +# build the JRI +mvn package -Pjlink-image +``` + +See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-jlink-image + for more information. + +Start the application: + +```shell +./target/helidon-standalone-quickstart-mp-jri/bin/start +``` + +### Multi-stage Docker build + +Build the JRI as a Docker Image + +```shell +docker build -t helidon-standalone-quickstart-mp-jri -f Dockerfile.jlink . +``` + +Start the application: + +```shell +docker run --rm -p 8080:8080 helidon-standalone-quickstart-mp-jri:latest +``` + +See the start script help: + +```shell +docker run --rm helidon-standalone-quickstart-mp-jri:latest --help +``` \ No newline at end of file diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/app.yaml b/examples/quickstarts/helidon-standalone-quickstart-mp/app.yaml new file mode 100644 index 000000000..9475ca4fd --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/app.yaml @@ -0,0 +1,57 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +kind: Service +apiVersion: v1 +metadata: + name: helidon-quickstart-mp + labels: + app: helidon-quickstart-mp +spec: + type: NodePort + selector: + app: helidon-quickstart-mp + ports: + - port: 8080 + targetPort: 8080 + name: http +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helidon-quickstart-mp + labels: + app: helidon-quickstart-mp + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: helidon-quickstart-mp + version: v1 + template: + metadata: + labels: + app: helidon-quickstart-mp + version: v1 + spec: + containers: + - name: helidon-quickstart-mp + image: helidon-quickstart-mp + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/pom.xml b/examples/quickstarts/helidon-standalone-quickstart-mp/pom.xml new file mode 100644 index 000000000..7333d05b8 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/pom.xml @@ -0,0 +1,317 @@ + + + + 4.0.0 + io.helidon.examples.quickstarts + helidon-standalone-quickstart-mp + 1.0.0-SNAPSHOT + Helidon Examples Standalone Quickstart MP + + + 4.1.0-SNAPSHOT + io.helidon.microprofile.cdi.Main + + 21 + ${maven.compiler.source} + true + UTF-8 + UTF-8 + + + 3.8.1 + 3.6.0 + 2.7.5.1 + 1.6.0 + 3.0.0-M5 + 4.0.6 + 4.0.6 + 3.1.2 + 3.0.2 + 0.10.2 + 1.5.0.Final + 0.5.1 + 2.7 + 3.0.0-M5 + + + + + + io.helidon + helidon-dependencies + ${helidon.version} + pom + import + + + + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.microprofile.openapi + helidon-microprofile-openapi + + + io.helidon.microprofile.health + helidon-microprofile-health + + + io.helidon.microprofile.metrics + helidon-microprofile-metrics + + + io.helidon.logging + helidon-logging-jul + runtime + + + jakarta.json.bind + jakarta.json.bind-api + + + org.glassfish.jersey.media + jersey-media-json-binding + runtime + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + + ${project.artifactId} + + + + org.apache.maven.plugins + maven-compiler-plugin + ${version.plugin.compiler} + + + org.apache.maven.plugins + maven-surefire-plugin + ${version.plugin.surefire} + + false + + ${project.build.outputDirectory}/logging.properties + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.plugin.failsafe} + + false + true + + + + org.apache.maven.plugins + maven-dependency-plugin + ${version.plugin.dependency} + + + org.apache.maven.plugins + maven-resources-plugin + ${version.plugin.resources} + + + org.apache.maven.plugins + maven-jar-plugin + ${version.plugin.jar} + + + + true + libs + ${mainClass} + false + + + + + + io.smallrye + jandex-maven-plugin + ${version.plugin.jandex} + + + org.codehaus.mojo + exec-maven-plugin + ${version.plugin.exec} + + ${mainClass} + + + + io.helidon.build-tools + helidon-maven-plugin + ${version.plugin.helidon} + + + io.helidon.build-tools + helidon-cli-maven-plugin + ${version.plugin.helidon-cli} + + + org.graalvm.buildtools + native-maven-plugin + ${version.plugin.nativeimage} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + prepare-package + + copy-dependencies + + + ${project.build.directory}/libs + false + false + true + true + runtime + + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + jandex + + process-classes + + + + + + + + + native-image + + + + org.graalvm.buildtools + native-maven-plugin + ${version.plugin.nativeimage} + + + resource-config + package + + generateResourceConfig + + + + true + + + + build-native-image + package + + compile-no-fork + + + + true + + ${project.build.outputDirectory} + ${project.build.finalName} + false + + false + + + + --add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.configure=ALL-UNNAMED + + + + + + + + + + io.helidon.integrations.graal + helidon-mp-graal-native-image-extension + + + + + jlink-image + + + + io.helidon.build-tools + helidon-maven-plugin + + + + jlink-image + + + + + + + + + diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetResource.java b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetResource.java new file mode 100644 index 000000000..503726ae3 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetResource.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.quickstart.mp; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; + +/** + * A simple JAX-RS resource to greet you. Examples: + * + * Get default greeting message: + * curl -X GET http://localhost:8080/greet + * + * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + * + * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + * + * The message is returned as a JSON object. + */ +@Path("/greet") +@RequestScoped +public class GreetResource { + + /** + * The greeting message provider. + */ + private final GreetingProvider greetingProvider; + + /** + * Using constructor injection to get a configuration property. + * By default this gets the value from META-INF/microprofile-config + * + * @param greetingConfig the configured greeting message + */ + @Inject + public GreetResource(GreetingProvider greetingConfig) { + this.greetingProvider = greetingConfig; + } + + /** + * Return a worldly greeting message. + * + * @return {@link GreetingMessage} + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getDefaultMessage() { + return createResponse("World"); + } + + /** + * Return a greeting message using the name that was provided. + * + * @param name the name to greet + * @return {@link GreetingMessage} + */ + @Path("/{name}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public GreetingMessage getMessage(@PathParam("name") String name) { + return createResponse(name); + } + + /** + * Set the greeting to use in future messages. + * + * @param message JSON containing the new greeting + * @return {@link Response} + */ + @Path("/greeting") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @RequestBody(name = "greeting", + required = true, + content = @Content(mediaType = "application/json", + schema = @Schema(type = SchemaType.OBJECT, requiredProperties = { "greeting" }))) + @APIResponses({ + @APIResponse(name = "normal", responseCode = "204", description = "Greeting updated"), + @APIResponse(name = "missing 'greeting'", responseCode = "400", + description = "JSON did not contain setting for 'greeting'")}) + public Response updateGreeting(GreetingMessage message) { + if (message.getMessage() == null) { + GreetingMessage entity = new GreetingMessage("No greeting provided"); + return Response.status(Response.Status.BAD_REQUEST).entity(entity).build(); + } + + greetingProvider.setMessage(message.getMessage()); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + private GreetingMessage createResponse(String who) { + String msg = String.format("%s %s!", greetingProvider.getMessage(), who); + + return new GreetingMessage(msg); + } +} diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingMessage.java b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingMessage.java new file mode 100644 index 000000000..b3c8a56f9 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingMessage.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.quickstart.mp; + +/** + * POJO defining the greeting message content. + */ +@SuppressWarnings("unused") +public class GreetingMessage { + private String message; + + /** + * Create a new GreetingMessage instance. + */ + public GreetingMessage() { + } + + /** + * Create a new GreetingMessage instance. + * + * @param message message + */ + public GreetingMessage(String message) { + this.message = message; + } + + /** + * Gets the message value. + * + * @return message value + */ + public String getMessage() { + return message; + } + + /** + * Sets the message value. + * + * @param message message value to set + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingProvider.java b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingProvider.java new file mode 100644 index 000000000..b11d24624 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/GreetingProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.quickstart.mp; + +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Provider for greeting message. + */ +@ApplicationScoped +public class GreetingProvider { + private final AtomicReference message = new AtomicReference<>(); + + /** + * Create a new greeting provider, reading the message from configuration. + * + * @param message greeting to use + */ + @Inject + public GreetingProvider(@ConfigProperty(name = "app.greeting") String message) { + this.message.set(message); + } + + String getMessage() { + return message.get(); + } + + void setMessage(String message) { + this.message.set(message); + } +} diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/package-info.java b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/package-info.java new file mode 100644 index 000000000..885a43f00 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/java/io/helidon/examples/quickstart/mp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quickstart MicroProfile example. + */ +package io.helidon.examples.quickstart.mp; diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/META-INF/beans.xml b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..a0938bff7 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/META-INF/microprofile-config.properties b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..4f4497753 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Application properties. This is the default greeting +app.greeting=Hello + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +# Enable the optional MicroProfile Metrics REST.request metrics +metrics.rest-request.enabled=true diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-standalone-quickstart-mp/native-image.properties b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-standalone-quickstart-mp/native-image.properties new file mode 100644 index 000000000..72e99d2af --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-standalone-quickstart-mp/native-image.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +Args=--initialize-at-build-time=io.helidon.examples diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/logging.properties b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/logging.properties new file mode 100644 index 000000000..7b395f50e --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/src/main/resources/logging.properties @@ -0,0 +1,38 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +## Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler +# +## HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n +# +## Global logging level. Can be overridden by specific loggers +.level=INFO + +# Quiet Weld +org.jboss.level=WARNING + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.microprofile.level=INFO +#io.helidon.common.level=INFO +#org.jboss.weld=INFO diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/src/test/java/io/helidon/examples/quickstart/mp/MainTest.java b/examples/quickstarts/helidon-standalone-quickstart-mp/src/test/java/io/helidon/examples/quickstart/mp/MainTest.java new file mode 100644 index 000000000..067c6e007 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/src/test/java/io/helidon/examples/quickstart/mp/MainTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.quickstart.mp; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +class MainTest { + private final WebTarget target; + + @Inject + MainTest(WebTarget target) { + this.target = target; + } + + @Test + void testHelloWorld() { + + GreetingMessage message = target.path("/greet") + .request() + .get(GreetingMessage.class); + assertThat("default message", message.getMessage(), + is("Hello World!")); + + message = target.path("/greet/Joe") + .request() + .get(GreetingMessage.class); + assertThat("hello Joe message", message.getMessage(), + is("Hello Joe!")); + + try (Response r = target.path("/greet/greeting") + .request() + .put(Entity.entity("{\"message\" : \"Hola\"}", MediaType.APPLICATION_JSON))) { + assertThat("PUT status code", r.getStatus(), is(204)); + } + + message = target.path("/greet/Jose") + .request() + .get(GreetingMessage.class); + assertThat("hola Jose message", message.getMessage(), + is("Hola Jose!")); + + try (Response r = target.path("/metrics") + .request() + .get()) { + assertThat("GET metrics status code", r.getStatus(), is(200)); + } + + try (Response r = target.path("/health") + .request() + .get()) { + assertThat("GET health status code", r.getStatus(), is(200)); + } + } +} diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/.dockerignore b/examples/quickstarts/helidon-standalone-quickstart-se/.dockerignore new file mode 100644 index 000000000..c8b241f22 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/.dockerignore @@ -0,0 +1 @@ +target/* \ No newline at end of file diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/.gitignore b/examples/quickstarts/helidon-standalone-quickstart-se/.gitignore new file mode 100644 index 000000000..241e8042d --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/.gitignore @@ -0,0 +1,16 @@ +hs_err_pid* +target/ +.DS_Store +.idea/ +*.iws +*.ipr +*.iml +atlassian-ide-plugin.xml +nbactions.xml +nb-configuration.xml +.settings +.settings/ +.project +.classpath +*.swp +*~ \ No newline at end of file diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/Dockerfile b/examples/quickstarts/helidon-standalone-quickstart-se/Dockerfile new file mode 100644 index 000000000..6a484e96e --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/Dockerfile @@ -0,0 +1,54 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -DskipTests + +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-standalone-quickstart-se.jar ./ +COPY --from=build /helidon/target/libs ./libs + +CMD ["java", "-jar", "helidon-standalone-quickstart-se.jar"] + +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/Dockerfile.jlink b/examples/quickstarts/helidon-standalone-quickstart-se/Dockerfile.jlink new file mode 100644 index 000000000..61979ba2f --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/Dockerfile.jlink @@ -0,0 +1,49 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build to create the custom Java Runtime Image +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -Pjlink-image -DskipTests +RUN echo "done!" + +# 2nd stage, build the final image with the JRI built in the 1st stage + +FROM debian:stretch-slim +WORKDIR /helidon +COPY --from=build /helidon/target/helidon-standalone-quickstart-se-jri ./ +ENTRYPOINT ["/bin/bash", "/helidon/bin/start"] +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/Dockerfile.native b/examples/quickstarts/helidon-standalone-quickstart-se/Dockerfile.native new file mode 100644 index 000000000..deb4b2c3c --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/Dockerfile.native @@ -0,0 +1,54 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM ghcr.io/graalvm/graalvm-community:21.0.0-ol9 as build + +WORKDIR /usr/share + +# Install maven +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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Pnative-image -Dnative.image.skip -Dmaven.test.skip -Declipselink.weave.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -Pnative-image -Dnative.image.buildStatic -DskipTests + +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM scratch +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-standalone-quickstart-se . + +ENTRYPOINT ["./helidon-standalone-quickstart-se"] + +EXPOSE 8080 diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/README.md b/examples/quickstarts/helidon-standalone-quickstart-se/README.md new file mode 100644 index 000000000..8b1fdc110 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/README.md @@ -0,0 +1,161 @@ +# Helidon Examples Standalone Quickstart SE + +This project implements a simple Hello World REST service using Helidon SE with + a standalone Maven pom. + +## Build and run + +```shell +mvn package +java -jar target/helidon-standalone-quickstart-se.jar +``` + +## Exercise the application + +```shell +curl -X GET http://localhost:8080/greet +#Output: {"message":"Hello World!"} + +curl -X GET http://localhost:8080/greet/Joe +#Output: {"message":"Hello Joe!"} + +curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting + +curl -X GET http://localhost:8080/greet/Jose +#Output: {"message":"Hola Jose!"} +``` + +## Try health and metrics + +```shell +curl -s -X GET http://localhost:8080/observe/health +#Output: {"outcome":"UP",... + +# Prometheus Format +curl -s -X GET http://localhost:8080/observe/metrics +# TYPE base:gc_g1_young_generation_count gauge + +# JSON Format +curl -H 'Accept: application/json' -X GET http://localhost:8080/observe/metrics +#Output: {"base":... +``` + +## Build the Docker Image + +```shell +docker build -t helidon-standalone-quickstart-se . +``` + +## Start the application with Docker + +```shell +docker run --rm -p 8080:8080 helidon-standalone-quickstart-se:latest +``` + +Exercise the application as described above + +## Deploy the application to Kubernetes + +```shell +kubectl cluster-info # Verify which cluster +kubectl get pods # Verify connectivity to cluster +kubectl create -f app.yaml # Deply application +kubectl get service helidon-standalone-quickstart-se # Get service info +``` + +## Build a native image with GraalVM + +GraalVM allows you to compile your programs ahead-of-time into a native + executable. See https://www.graalvm.org/docs/reference-manual/aot-compilation/ + for more information. + +You can build a native executable in 2 different ways: +* With a local installation of GraalVM +* Using Docker + +### Local build + +Download Graal VM at https://www.graalvm.org/downloads. We recommend +version `23.1.0` or later. + +```shell +# Setup the environment +export GRAALVM_HOME=/path +# build the native executable +mvn package -Pnative-image +``` + +You can also put the Graal VM `bin` directory in your PATH, or pass + `-DgraalVMHome=/path` to the Maven command. + +See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin + for more information. + +Start the application: + +```shell +./target/helidon-standalone-quickstart-se +``` + +### Multi-stage Docker build + +Build the "native" Docker Image + +```shell +docker build -t helidon-standalone-quickstart-se-native -f Dockerfile.native . +``` + +Start the application: + +```shell +docker run --rm -p 8080:8080 helidon-standalone-quickstart-se-native:latest +``` + +## Build a Java Runtime Image using jlink + +You can build a custom Java Runtime Image (JRI) containing the application jars and the JDK modules +on which they depend. This image also: + +* Enables Class Data Sharing by default to reduce startup time. +* Contains a customized `start` script to simplify CDS usage and support debug and test modes. + +You can build a custom JRI in two different ways: +* Local +* Using Docker + + +### Local build + +```shell +# build the JRI +mvn package -Pjlink-image +``` + +See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-jlink-image + for more information. + +Start the application: + +```shell +./target/helidon-standalone-quickstart-se-jri/bin/start +``` + +### Multi-stage Docker build + +Build the JRI as a Docker Image + +```shell +docker build -t helidon-standalone-quickstart-se-jri -f Dockerfile.jlink . +``` + +Start the application: + +```shell +docker run --rm -p 8080:8080 helidon-standalone-quickstart-se-jri:latest +``` + +See the start script help: + +```shell +docker run --rm helidon-standalone-quickstart-se-jri:latest --help +``` \ No newline at end of file diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/app.yaml b/examples/quickstarts/helidon-standalone-quickstart-se/app.yaml new file mode 100644 index 000000000..de62a2772 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/app.yaml @@ -0,0 +1,57 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +kind: Service +apiVersion: v1 +metadata: + name: helidon-quickstart-se + labels: + app: helidon-quickstart-se +spec: + type: NodePort + selector: + app: helidon-quickstart-se + ports: + - port: 8080 + targetPort: 8080 + name: http +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helidon-quickstart-se + labels: + app: helidon-quickstart-se + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: helidon-quickstart-se + version: v1 + template: + metadata: + labels: + app: helidon-quickstart-se + version: v1 + spec: + containers: + - name: helidon-quickstart-se + image: helidon-quickstart-se + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/pom.xml b/examples/quickstarts/helidon-standalone-quickstart-se/pom.xml new file mode 100644 index 000000000..cf5db8259 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/pom.xml @@ -0,0 +1,307 @@ + + + + + 4.0.0 + io.helidon.examples.quickstarts + helidon-standalone-quickstart-se + 1.0.0-SNAPSHOT + Helidon Examples Standalone Quickstart SE + + + 4.1.0-SNAPSHOT + test + io.helidon.examples.quickstart.se.Main + + 21 + ${maven.compiler.source} + true + UTF-8 + UTF-8 + + + 3.8.1 + 3.6.0 + 1.6.0 + 3.0.0-M5 + 4.0.6 + 4.0.6 + 3.0.2 + 0.10.2 + 1.5.0.Final + 0.5.1 + 2.7 + 3.0.0-M5 + + + + + + io.helidon + helidon-dependencies + ${helidon.version} + pom + import + + + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.openapi + helidon-openapi + + + io.helidon.config + helidon-config-yaml + + + io.helidon.health + helidon-health-checks + + + jakarta.json + jakarta.json-api + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.webclient + helidon-webclient + test + + + org.hamcrest + hamcrest-all + test + + + + + ${project.artifactId} + + + + org.apache.maven.plugins + maven-compiler-plugin + ${version.plugin.compiler} + + + org.apache.maven.plugins + maven-surefire-plugin + ${version.plugin.surefire} + + false + + ${project.build.outputDirectory}/logging.properties + ${helidon.test.config.profile} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.plugin.failsafe} + + false + true + + + + org.apache.maven.plugins + maven-dependency-plugin + ${version.plugin.dependency} + + + org.apache.maven.plugins + maven-resources-plugin + ${version.plugin.resources} + + + org.apache.maven.plugins + maven-jar-plugin + ${version.plugin.jar} + + + + true + libs + ${mainClass} + false + + + + + + org.codehaus.mojo + exec-maven-plugin + ${version.plugin.exec} + + ${mainClass} + + + + io.helidon.build-tools + helidon-maven-plugin + ${version.plugin.helidon} + + + io.helidon.build-tools + helidon-cli-maven-plugin + ${version.plugin.helidon-cli} + + + org.graalvm.buildtools + native-maven-plugin + ${version.plugin.nativeimage} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + prepare-package + + copy-dependencies + + + ${project.build.directory}/libs + false + false + true + true + runtime + + + + + + + + + + native-image + + + + org.graalvm.buildtools + native-maven-plugin + ${version.plugin.nativeimage} + + + resource-config + package + + generateResourceConfig + + + + true + + + + build-native-image + package + + compile-no-fork + + + + true + + ${project.build.outputDirectory} + ${project.build.finalName} + false + + false + + + + --add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.configure=ALL-UNNAMED + + + + + + + + + + io.helidon.integrations.graal + helidon-graal-native-image-extension + + + + + jlink-image + + + + io.helidon.build-tools + helidon-maven-plugin + + + + jlink-image + + + + + + + + + diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java new file mode 100644 index 000000000..225fd686f --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.quickstart.se; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.config.Config; +import io.helidon.http.Status; +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.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * 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 JSON object. + */ +public class GreetService implements HttpService { + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + /** + * The config value for the key {@code greeting}. + */ + private final AtomicReference greeting = new AtomicReference<>(); + + GreetService() { + this(Config.global().get("app")); + } + + GreetService(Config appConfig) { + greeting.set(appConfig.get("greeting").asString().orElse("Ciao")); + } + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.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().get("name"); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + String msg = String.format("%s %s!", greeting.get(), name); + + JsonObject returnObject = JSON.createObjectBuilder() + .add("message", msg) + .build(); + response.send(returnObject); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + if (!jo.containsKey("greeting")) { + JsonObject jsonErrorObject = JSON.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting.set(jo.getString("greeting")); + response.status(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) { + updateGreetingFromJson(request.content().as(JsonObject.class), response); + } +} diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java new file mode 100644 index 000000000..90c83aa50 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.quickstart.se; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +/** + * The application main class. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + // load logging configuration + LogConfig.configureRuntime(); + + // initialize global config from default configuration + Config config = Config.create(); + Config.global(config); + + WebServer server = WebServer.builder() + .config(config.get("server")) + .routing(Main::routing) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + /** + * Updates HTTP Routing and registers observe providers. + */ + static void routing(HttpRouting.Builder routing) { + Config config = Config.global(); + + routing.register("/greet", new GreetService()); + } +} diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/package-info.java b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/package-info.java new file mode 100644 index 000000000..d282464f9 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quickstart demo application + *

+ * Start with {@link io.helidon.examples.quickstart.se.Main} class. + * + * @see io.helidon.examples.quickstart.se.Main + */ +package io.helidon.examples.quickstart.se; diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-standalone-quickstart-se/helidon-example-reflection-config.json b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-standalone-quickstart-se/helidon-example-reflection-config.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-standalone-quickstart-se/helidon-example-reflection-config.json @@ -0,0 +1 @@ +[] diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-standalone-quickstart-se/native-image.properties b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-standalone-quickstart-se/native-image.properties new file mode 100644 index 000000000..cfcdd3985 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/META-INF/native-image/io.helidon.examples.quickstarts/helidon-standalone-quickstart-se/native-image.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +Args=-H:ReflectionConfigurationResources=${.}/helidon-example-reflection-config.json diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/application.yaml b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/application.yaml new file mode 100644 index 000000000..87dc9c312 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/application.yaml @@ -0,0 +1,22 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + greeting: "Hello" + +server: + port: 8080 + host: 0.0.0.0 diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/logging.properties b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/logging.properties new file mode 100644 index 000000000..717a25c4a --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/resources/logging.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.level=INFO diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/src/test/java/io/helidon/examples/quickstart/se/MainTest.java b/examples/quickstarts/helidon-standalone-quickstart-se/src/test/java/io/helidon/examples/quickstart/se/MainTest.java new file mode 100644 index 000000000..76acf0e23 --- /dev/null +++ b/examples/quickstarts/helidon-standalone-quickstart-se/src/test/java/io/helidon/examples/quickstart/se/MainTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.quickstart.se; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import jakarta.json.JsonObject; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class MainTest { + + private final Http1Client client; + + protected MainTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.routing(builder); + } + + @Test + void testRootRoute() { + try (Http1ClientResponse response = client.get("/greet").request()) { + assertThat(response.status(), is(Status.OK_200)); + JsonObject json = response.as(JsonObject.class); + assertThat(json.getString("message"), is("Hello World!")); + } + } + + @Test + void testHealthObserver() { + try (Http1ClientResponse response = client.get("/observe/health").request()) { + assertThat(response.status(), is(Status.NO_CONTENT_204)); + } + } + @Test + void testDeadlockHealthCheck() { + try (Http1ClientResponse response = client.get("/observe/health/live/deadlock").request()) { + assertThat(response.status(), is(Status.NO_CONTENT_204)); + } + } + + @Test + void testMetricsObserver() { + try (Http1ClientResponse response = client.get("/observe/metrics").request()) { + assertThat(response.status(), is(Status.OK_200)); + } + } +} diff --git a/examples/quickstarts/pom.xml b/examples/quickstarts/pom.xml new file mode 100644 index 000000000..487fb3891 --- /dev/null +++ b/examples/quickstarts/pom.xml @@ -0,0 +1,39 @@ + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + io.helidon.examples.quickstarts + examples-quickstarts-project + Helidon Examples Quickstart + pom + + + helidon-quickstart-se + helidon-quickstart-mp + helidon-standalone-quickstart-mp + helidon-standalone-quickstart-se + + diff --git a/examples/security/README.md b/examples/security/README.md new file mode 100644 index 000000000..e6b14226b --- /dev/null +++ b/examples/security/README.md @@ -0,0 +1,4 @@ + +# Helidon SE Security Examples + + diff --git a/examples/security/attribute-based-access-control/README.md b/examples/security/attribute-based-access-control/README.md new file mode 100644 index 000000000..972cb44e0 --- /dev/null +++ b/examples/security/attribute-based-access-control/README.md @@ -0,0 +1,12 @@ +# Helidon Security ABAC Example + +JAX-RS (Jersey) example for attribute based access control. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-security-abac.jar +``` + +Open endpoints listen in the server's output in a browser. diff --git a/examples/security/attribute-based-access-control/pom.xml b/examples/security/attribute-based-access-control/pom.xml new file mode 100644 index 000000000..48e6ef37c --- /dev/null +++ b/examples/security/attribute-based-access-control/pom.xml @@ -0,0 +1,90 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.security + helidon-examples-security-abac + 1.0.0-SNAPSHOT + Helidon Examples Security ABAC + + + Example of attribute based access control. + + + + io.helidon.examples.security.abac.AbacJerseyMain + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.security.abac + helidon-security-abac-policy-el + + + org.glassfish + jakarta.el + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacApplication.java b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacApplication.java new file mode 100644 index 000000000..18da44565 --- /dev/null +++ b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacApplication.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.security.abac; + +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * Application class of this MP application. + */ +@ApplicationScoped +@ApplicationPath("/rest") +public class AbacApplication extends Application { + @Override + public Set> getClasses() { + return Set.of(AbacResource.class, AbacExplicitResource.class); + } +} diff --git a/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacExplicitResource.java b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacExplicitResource.java new file mode 100644 index 000000000..81e2be5be --- /dev/null +++ b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacExplicitResource.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.security.abac; + +import java.time.DayOfWeek; + +import io.helidon.security.AuthorizationResponse; +import io.helidon.security.SecurityContext; +import io.helidon.security.SubjectType; +import io.helidon.security.abac.policy.PolicyValidator; +import io.helidon.security.abac.scope.ScopeValidator; +import io.helidon.security.abac.time.TimeValidator; +import io.helidon.security.annotations.Authenticated; +import io.helidon.security.annotations.Authorized; + +import jakarta.json.JsonString; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * Explicit authorization resource - authorization must be called by programmer. + */ +@Path("/explicit") +@TimeValidator.TimeOfDay(from = "08:15:00", to = "12:00:00") +@TimeValidator.TimeOfDay(from = "12:30:00", to = "17:30:00") +@TimeValidator.DaysOfWeek({DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY}) +@ScopeValidator.Scope("calendar_read") +@ScopeValidator.Scope("calendar_edit") +@PolicyValidator.PolicyStatement("${env.time.year >= 2017 && object.owner == subject.principal.id}") +@Authenticated +public class AbacExplicitResource { + /** + * A resource method to demonstrate explicit authorization. + * + * @param context security context (injected) + * @return "fine, sir" string; or a description of authorization failure + */ + @GET + @Authorized(explicit = true) + @AtnProvider.Authentication(value = "user", + roles = {"user_role"}, + scopes = {"calendar_read", "calendar_edit"}) + @AtnProvider.Authentication(value = "service", + type = SubjectType.SERVICE, + roles = {"service_role"}, + scopes = {"calendar_read", "calendar_edit"}) + public Response process(@Context SecurityContext context) { + SomeResource res = new SomeResource("user"); + AuthorizationResponse atzResponse = context.authorize(res); + + if (atzResponse.isPermitted()) { + //do the update + return Response.ok().entity("fine, sir").build(); + } else { + return Response.status(Response.Status.FORBIDDEN) + .entity(atzResponse.description().orElse("Access not granted")) + .build(); + } + } + + /** + * A resource method to demonstrate explicit authorization - this should fail, as we do not call authorization. + * + * @param context security context (injected) + * @param object a JSON string + * @return "fine, sir" string; or a description of authorization failure + */ + @POST + @Path("/deny") + @Authorized(explicit = true) + @AtnProvider.Authentication(value = "user", + roles = {"user_role"}, + scopes = {"calendar_read", "calendar_edit"}) + @AtnProvider.Authentication(value = "service", + type = SubjectType.SERVICE, + roles = {"service_role"}, + scopes = {"calendar_read", "calendar_edit"}) + @Consumes(MediaType.APPLICATION_JSON) + public Response fail(@Context SecurityContext context, JsonString object) { + return Response.ok("This should not work").build(); + } + + /** + * Example resource. + */ + public static class SomeResource { + private String id; + private String owner; + private String message; + + private SomeResource(String owner) { + this.id = "id"; + this.owner = owner; + this.message = "Unit test"; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} diff --git a/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacJerseyMain.java b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacJerseyMain.java new file mode 100644 index 000000000..c21e7490f --- /dev/null +++ b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacJerseyMain.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.abac; + +import io.helidon.config.Config; +import io.helidon.microprofile.server.Server; + +/** + * Jersey example for Attribute based access control. + */ +public final class AbacJerseyMain { + private static Server server; + + private AbacJerseyMain() { + } + + /** + * Main method of example. No arguments required, no configuration required. + * + * @param args empty is OK + * @throws Throwable if server fails to start + */ + public static void main(String[] args) throws Throwable { + server = startIt(); + } + + static Server startIt() { + Config config = Config.create(); + + Server server = Server.builder() + .config(config) + .port(8080) + .build() + .start(); + + System.out.printf("Started server on localhost:%d%n", server.port()); + System.out.println(); + System.out.println("***********************"); + System.out.println("** Endpoints: **"); + System.out.println("***********************"); + System.out.println("Using declarative authorization (ABAC):"); + System.out.printf(" http://localhost:%1$d/rest/attributes%n", server.port()); + System.out.println("Using explicit authorization (ABAC):"); + System.out.printf(" http://localhost:%1$d/rest/explicit%n", server.port()); + + return server; + } +} diff --git a/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacResource.java b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacResource.java new file mode 100644 index 000000000..60c483f77 --- /dev/null +++ b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AbacResource.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.security.abac; + +import java.time.DayOfWeek; + +import io.helidon.security.SubjectType; +import io.helidon.security.abac.policy.PolicyValidator; +import io.helidon.security.abac.role.RoleValidator; +import io.helidon.security.abac.scope.ScopeValidator; +import io.helidon.security.abac.time.TimeValidator; +import io.helidon.security.annotations.Authenticated; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +/** + * Annotation only resource. + */ +@Path("/attributes") +@TimeValidator.TimeOfDay(from = "08:15:00", to = "12:00:00") +@TimeValidator.TimeOfDay(from = "12:30:00", to = "17:30:00") +@TimeValidator.DaysOfWeek({DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY}) +@ScopeValidator.Scope("calendar_read") +@ScopeValidator.Scope("calendar_edit") +@RoleValidator.Roles("user_role") +@RoleValidator.Roles(value = "service_role", subjectType = SubjectType.SERVICE) +@PolicyValidator.PolicyStatement("${env.time.year >= 2017}") +@Authenticated +public class AbacResource { + /** + * A resource method to demonstrate if access was successful or not. + * + * @return "hello" + */ + @GET + @AtnProvider.Authentication(value = "user", + roles = {"user_role"}, + scopes = {"calendar_read", "calendar_edit"}) + @AtnProvider.Authentication(value = "service", + type = SubjectType.SERVICE, + roles = {"service_role"}, + scopes = {"calendar_read", "calendar_edit"}) + public String process() { + return "hello"; + } + + /** + * A resource method to demonstrate if access was successful or not. + * + * @return "hello" + */ + @GET + @Path("/deny") + @PolicyValidator.PolicyStatement("${env.time.year < 2017}") + @AtnProvider.Authentication(value = "user", scopes = {"calendar_read"}) + @AtnProvider.Authentication(value = "service", + type = SubjectType.SERVICE, + roles = {"service_role"}, + scopes = {"calendar_read", "calendar_edit"}) + public String deny() { + return "hello"; + } +} diff --git a/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AtnProvider.java b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AtnProvider.java new file mode 100644 index 000000000..e90cdb777 --- /dev/null +++ b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/AtnProvider.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.abac; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; + +import io.helidon.security.AuthenticationResponse; +import io.helidon.security.EndpointConfig; +import io.helidon.security.Grant; +import io.helidon.security.Principal; +import io.helidon.security.ProviderRequest; +import io.helidon.security.Role; +import io.helidon.security.SecurityLevel; +import io.helidon.security.Subject; +import io.helidon.security.SubjectType; +import io.helidon.security.spi.AuthenticationProvider; + +/** + * Example authentication provider that reads annotation to create a subject. + */ +public class AtnProvider implements AuthenticationProvider { + @Override + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { + List securityLevels = providerRequest.endpointConfig().securityLevels(); + ListIterator listIterator = securityLevels.listIterator(securityLevels.size()); + Subject user = null; + Subject service = null; + while (listIterator.hasPrevious()) { + SecurityLevel securityLevel = listIterator.previous(); + List authenticationAnnots = securityLevel + .filterAnnotations(Authentications.class, EndpointConfig.AnnotationScope.METHOD); + + List authentications = new LinkedList<>(); + authenticationAnnots.forEach(atn -> authentications.addAll(Arrays.asList(atn.value()))); + + + if (!authentications.isEmpty()) { + for (Authentication authentication : authentications) { + if (authentication.type() == SubjectType.USER) { + user = buildSubject(authentication); + } else { + service = buildSubject(authentication); + } + } + break; + } + } + return AuthenticationResponse.success(user, service); + } + + private Subject buildSubject(Authentication authentication) { + Subject.Builder subjectBuilder = Subject.builder(); + + subjectBuilder.principal(Principal.create(authentication.value())); + for (String role : authentication.roles()) { + subjectBuilder.addGrant(Role.create(role)); + } + for (String scope : authentication.scopes()) { + subjectBuilder.addGrant(Grant.builder() + .name(scope) + .type("scope") + .build()); + } + + return subjectBuilder.build(); + } + + @Override + public Collection> supportedAnnotations() { + return Set.of(Authentication.class); + } + + /** + * Authentication annotation. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + @Documented + @Inherited + @Repeatable(Authentications.class) + public @interface Authentication { + /** + * Name of the principal. + * + * @return principal name + */ + String value(); + + /** + * Type of the subject, defaults to user. + * + * @return type + */ + SubjectType type() default SubjectType.USER; + + /** + * Granted roles. + * @return array of roles + */ + String[] roles() default ""; + + /** + * Granted scopes. + * @return array of scopes + */ + String[] scopes() default ""; + } + + /** + * Repeatable annotation for {@link Authentication}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + @Documented + @Inherited + public @interface Authentications { + /** + * Repeating annotation. + * @return annotations + */ + Authentication[] value(); + } +} diff --git a/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/package-info.java b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/package-info.java new file mode 100644 index 000000000..2d3e86539 --- /dev/null +++ b/examples/security/attribute-based-access-control/src/main/java/io/helidon/examples/security/abac/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of Attribute based access control (ABAC). + */ +package io.helidon.examples.security.abac; diff --git a/examples/security/attribute-based-access-control/src/main/resources/META-INF/beans.xml b/examples/security/attribute-based-access-control/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..a0938bff7 --- /dev/null +++ b/examples/security/attribute-based-access-control/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/security/attribute-based-access-control/src/main/resources/application.yaml b/examples/security/attribute-based-access-control/src/main/resources/application.yaml new file mode 100644 index 000000000..a6b15df00 --- /dev/null +++ b/examples/security/attribute-based-access-control/src/main/resources/application.yaml @@ -0,0 +1,88 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# It is possible to provide default config values, here. +# + +server: + port: 8380 + +security: + providers: + - abac: + # prepares environment + # executes attribute validations + # validates that attributes were processed + # grants/denies access to resource + # + #### + # Combinations: + # # Will fail if any attribute is not validated and if any has failed validation + # fail-on-unvalidated: true + # fail-if-none-validated: true + # + # # Will fail if there is one or more attributes present and NONE of them is validated or if any has failed validation + # # Will NOT fail if there is at least one validated attribute and any number of not validated attributes (and NONE failed) + # fail-on-unvalidated: false + # fail-if-none-validated: true + # + # # Will fail if there is any attribute that failed validation + # # Will NOT fail if there are no failed validation or if there are NONE validated + # fail-on-unvalidated: false + # fail-if-none-validated: false + #### + # fail if an attribute was not validated (e.g. we do not know, whether it is valid or not) + # defaults to true + fail-on-unvalidated: true + # fail if none of the attributes were validated + # defaults to true + fail-if-none-validated: true +# policy-validator: +# validators: +# - class: "io.helidon.security.abac.policy.DefaultPolicyValidator" +# my-custom-policy-engine: +# some-key: "some value" +# another-key: "another value" + - atn: + class: "io.helidon.examples.security.abac.AtnProvider" + web-server: + paths: + - path: "/query" + audit: true + - path: "/noRoles" + methods: ["get"] + authenticate: true + - path: "/user[/{*}]" + methods: ["get"] + # implies authentication and authorization + abac: + scopes: ["calendar_read", "calendar_edit"] + time: + time-of-day: + - from: "08:15:00" + to: "12:00:00" + - from: "12:30" + to: "17:30" + days-of-week: ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"] + policy: + #statement: "hasScopes('calendar_read','calendar_edit') AND timeOfDayBetween('8:15', '17:30')" + #ref: "service/policy_id" + statement: "object.owner == subject.principal.id" + resource: "com.oracle.ResourceProvider" + + + diff --git a/examples/security/attribute-based-access-control/src/main/resources/logging.properties b/examples/security/attribute-based-access-control/src/main/resources/logging.properties new file mode 100644 index 000000000..85cd92104 --- /dev/null +++ b/examples/security/attribute-based-access-control/src/main/resources/logging.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +.level=INFO +AUDIT.level=FINEST diff --git a/examples/security/basic-auth-with-static-content/README.md b/examples/security/basic-auth-with-static-content/README.md new file mode 100644 index 000000000..051ca33d8 --- /dev/null +++ b/examples/security/basic-auth-with-static-content/README.md @@ -0,0 +1,31 @@ +# Web Server Integration and Basic Authentication + +This example demonstrates integration of Web Server +based application with Security component and Basic authentication (from HttpAuthProvider), including +protection of a static resource. + +## Contents + +There are two examples with exactly the same behavior: +1. BasicExampleMain - shows how to programmatically secure application +2. BasicExampleConfigMain - shows how to secure application with configuration + 1. see src/main/resources/application.yaml for configuration + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-security-webserver-basic-auth.jar +``` + +Try the application: + +The application starts at the `8080` port +```shell +curl http://localhost:8080/public +curl -u "jill:changeit" http://localhost:8080/noRoles +curl -u "john:changeit" http://localhost:8080/user +curl -u "jack:changeit" http://localhost:8080/admin +curl -v -u "john:changeit" http://localhost:8080/deny +curl -u "jack:changeit" http://localhost:8080/noAuthn +``` diff --git a/examples/security/basic-auth-with-static-content/pom.xml b/examples/security/basic-auth-with-static-content/pom.xml new file mode 100644 index 000000000..0c77fc98f --- /dev/null +++ b/examples/security/basic-auth-with-static-content/pom.xml @@ -0,0 +1,112 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.security + helidon-examples-security-webserver-basic-auth + 1.0.0-SNAPSHOT + Helidon Examples Security HTTP Basic Auth with Static Content + + + This example demonstrates Integration of Web Server based application with Security component, HTTP Basic + Authentication, and static content support + + + + io.helidon.examples.security.basicauth.BasicExampleConfigMain + + + + + io.helidon.webserver + helidon-webserver-security + + + io.helidon.config + helidon-config-encryption + + + io.helidon.security.providers + helidon-security-providers-http-auth + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-static-content + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webclient + helidon-webclient-security + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-core + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/examples/security/basicauth/BasicExampleBuilderMain.java b/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/examples/security/basicauth/BasicExampleBuilderMain.java new file mode 100644 index 000000000..b3577f3a7 --- /dev/null +++ b/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/examples/security/basicauth/BasicExampleBuilderMain.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.basicauth; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import io.helidon.http.HttpMediaTypes; +import io.helidon.logging.common.LogConfig; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; +import io.helidon.security.providers.httpauth.SecureUserStore; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.security.SecurityFeature; +import io.helidon.webserver.staticcontent.StaticContentService; + +/** + * Example using {@link io.helidon.common.Builder} approach instead of configuration based approach. + */ +public final class BasicExampleBuilderMain { + // simple approach to user storage - for real world, use data store... + private static final Map USERS = new HashMap<>(); + + static { + USERS.put("jack", new MyUser("jack", "changeit".toCharArray(), Set.of("user", "admin"))); + USERS.put("jill", new MyUser("jill", "changeit".toCharArray(), Set.of("user"))); + USERS.put("john", new MyUser("john", "changeit".toCharArray(), Set.of())); + } + + private BasicExampleBuilderMain() { + } + + /** + * Entry point, starts the server. + * + * @param args not used + */ + public static void main(String[] args) { + LogConfig.initClass(); + + WebServerConfig.Builder builder = WebServer.builder() + .port(8080); + 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 + + Signature example: from builder + + "Users: + jack/password in roles: user, admin + jill/password in roles: user + john/password in no roles + + *********************** + ** Endpoints: ** + *********************** + + No authentication: http://localhost:8080/public + No roles required, authenticated: http://localhost:8080/noRoles + User role required: http://localhost:8080/user + Admin role required: http://localhost:8080/admin + Always forbidden (uses role nobody is in), audited: http://localhost:8080/deny + Admin role required, authenticated, authentication optional, audited \ + (always forbidden - challenge is not returned as authentication is optional): http://localhost:8080/noAuthn + Static content, requires user role: http://localhost:8080/static/index.html + + """, TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS)); + } + + static void setup(WebServerConfig.Builder server) { + server.featuresDiscoverServices(false) + // only add security feature (as we do not use configuration, only features listed here will be available) + .addFeature(SecurityFeature.builder() + .security(buildSecurity()) + .defaults(SecurityFeature.authenticate()) + .build()) + .routing(routing -> routing + // must be configured first, to protect endpoints + .any("/static[/{*}]", SecurityFeature.rolesAllowed("user")) + .register("/static", StaticContentService.create("/WEB")) + .get("/noRoles", SecurityFeature.enforce()) + .get("/user[/{*}]", SecurityFeature.rolesAllowed("user")) + .get("/admin", SecurityFeature.rolesAllowed("admin")) + // audit is not enabled for GET methods by default + .get("/deny", SecurityFeature.rolesAllowed("deny").audit()) + // roles allowed imply authn and authz + .any("/noAuthn", SecurityFeature.rolesAllowed("admin") + .authenticationOptional() + .audit()) + .get("/{*}", (req, res) -> { + Optional securityContext = req.context().get(SecurityContext.class); + res.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8); + res.send("Hello, you are: \n" + securityContext + .map(ctx -> ctx.user().orElse(SecurityContext.ANONYMOUS).toString()) + .orElse("Security context is null")); + })); + } + + private static Security buildSecurity() { + return Security.builder() + .addAuthenticationProvider( + HttpBasicAuthProvider.builder() + .realm("helidon") + .userStore(buildUserStore()), + "http-basic-auth") + .build(); + } + + private static SecureUserStore buildUserStore() { + return login -> Optional.ofNullable(USERS.get(login)); + } + + private record MyUser(String login, char[] password, Set roles) implements SecureUserStore.User { + + @Override + public boolean isPasswordValid(char[] password) { + return Arrays.equals(password(), password); + } + } +} diff --git a/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/examples/security/basicauth/BasicExampleConfigMain.java b/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/examples/security/basicauth/BasicExampleConfigMain.java new file mode 100644 index 000000000..4f8fcaf39 --- /dev/null +++ b/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/examples/security/basicauth/BasicExampleConfigMain.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.basicauth; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import io.helidon.config.Config; +import io.helidon.http.HttpMediaTypes; +import io.helidon.logging.common.LogConfig; +import io.helidon.security.SecurityContext; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.staticcontent.StaticContentService; + +/** + * Example using configuration based approach. + */ +public final class BasicExampleConfigMain { + private BasicExampleConfigMain() { + } + + /** + * Entry point, starts the server. + * + * @param args not used + */ + public static void main(String[] args) { + LogConfig.initClass(); + + WebServerConfig.Builder builder = WebServer.builder() + .port(8080); + 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 + + Signature example: from builder + + "Users: + jack/password in roles: user, admin + jill/password in roles: user + john/password in no roles + + *********************** + ** Endpoints: ** + *********************** + + No authentication: http://localhost:8080/public + No roles required, authenticated: http://localhost:8080/noRoles + User role required: http://localhost:8080/user + Admin role required: http://localhost:8080/admin + Always forbidden (uses role nobody is in), audited: http://localhost:8080/deny + Admin role required, authenticated, authentication optional, audited \ + (always forbidden - challenge is not returned as authentication is optional): http://localhost:8080/noAuthn + Static content, requires user role: http://localhost:8080/static/index.html + + """, TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS)); + } + + static void setup(WebServerConfig.Builder server) { + Config config = Config.create(); + + server.config(config.get("server")) + .routing(routing -> routing + .register("/static", StaticContentService.create("/WEB")) + .get("/{*}", (req, res) -> { + Optional securityContext = req.context().get(SecurityContext.class); + res.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8); + res.send("Hello, you are: \n" + securityContext + .map(ctx -> ctx.user().orElse(SecurityContext.ANONYMOUS).toString()) + .orElse("Security context is null")); + })); + } +} diff --git a/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/examples/security/basicauth/package-info.java b/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/examples/security/basicauth/package-info.java new file mode 100644 index 000000000..75a43f06a --- /dev/null +++ b/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/examples/security/basicauth/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of basic authentication used with static content. + */ +package io.helidon.examples.security.basicauth; diff --git a/examples/security/basic-auth-with-static-content/src/main/resources/WEB/index.html b/examples/security/basic-auth-with-static-content/src/main/resources/WEB/index.html new file mode 100644 index 000000000..fe9a6928f --- /dev/null +++ b/examples/security/basic-auth-with-static-content/src/main/resources/WEB/index.html @@ -0,0 +1,43 @@ + + + + + +Hello, this is a static resource loaded from classpath. +

+The following endpoints are available on this server: +

+ +The following users are configured: +
    +
  • jack/password in user and admin roles
  • +
  • jill/password in user roles
  • +
  • john/password that has no roles
  • +
+ + + diff --git a/examples/security/basic-auth-with-static-content/src/main/resources/application.yaml b/examples/security/basic-auth-with-static-content/src/main/resources/application.yaml new file mode 100644 index 000000000..da4d6065f --- /dev/null +++ b/examples/security/basic-auth-with-static-content/src/main/resources/application.yaml @@ -0,0 +1,63 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# To configure an explicit port: +# server.port: 8080 + +server: + features: + security: + # Configuration of security integration with web server + defaults: + authenticate: true + paths: + - path: "/noRoles" + methods: [ "get" ] + - path: "/user[/{*}]" + methods: [ "get" ] + roles-allowed: [ "user" ] + - path: "/admin" + methods: [ "get" ] + roles-allowed: [ "admin" ] + - path: "/deny" + methods: [ "get" ] + roles-allowed: [ "deny" ] + audit: true + - path: "/noAuthn" + roles-allowed: [ "admin" ] + authentication-optional: true + audit: true + - path: "/static[/{*}]" + roles-allowed: "user" + +security: + config: + # Configuration of secured config (encryption of passwords in property files) + # Set to true for production - if set to true, clear text passwords will cause failure + require-encryption: false + providers: + - http-basic-auth: + realm: "helidon" + users: + - login: "jack" + password: "${CLEAR=changeit}" + roles: [ "user", "admin" ] + - login: "jill" + password: "${CLEAR=changeit}" + roles: [ "user" ] + - login: "john" + password: "${CLEAR=changeit}" + roles: [ ] diff --git a/examples/security/basic-auth-with-static-content/src/main/resources/logging.properties b/examples/security/basic-auth-with-static-content/src/main/resources/logging.properties new file mode 100644 index 000000000..7952beeb8 --- /dev/null +++ b/examples/security/basic-auth-with-static-content/src/main/resources/logging.properties @@ -0,0 +1,21 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n +#All log level details +.level=INFO +AUDIT.level=FINEST diff --git a/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleBuilderTest.java b/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleBuilderTest.java new file mode 100644 index 000000000..8891ae22d --- /dev/null +++ b/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleBuilderTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.basicauth; + +import java.net.URI; + +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webserver.WebServerConfig; + +/** + * Unit test for {@link BasicExampleBuilderMain}. + */ +public class BasicExampleBuilderTest extends BasicExampleTest { + + public BasicExampleBuilderTest(URI uri) { + super(uri); + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + BasicExampleConfigMain.setup(server); + } +} diff --git a/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleConfigTest.java b/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleConfigTest.java new file mode 100644 index 000000000..795c39935 --- /dev/null +++ b/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleConfigTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.basicauth; + +import java.net.URI; + +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webserver.WebServerConfig; + +/** + * Unit test for {@link BasicExampleConfigMain}. + */ +public class BasicExampleConfigTest extends BasicExampleTest { + + public BasicExampleConfigTest(URI uri) { + super(uri); + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + BasicExampleConfigMain.setup(server); + } +} diff --git a/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleTest.java b/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleTest.java new file mode 100644 index 000000000..34874e6ab --- /dev/null +++ b/examples/security/basic-auth-with-static-content/src/test/java/io/helidon/examples/security/basicauth/BasicExampleTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.basicauth; + +import java.net.URI; +import java.util.Set; + +import io.helidon.http.HeaderNames; +import io.helidon.http.Status; +import io.helidon.security.EndpointConfig; +import io.helidon.security.Security; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webclient.security.WebClientSecurity; +import io.helidon.webserver.testing.junit5.ServerTest; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Abstract class with tests for this example (used by programmatic and config based tests). + */ +@ServerTest +public abstract class BasicExampleTest { + + private final Http1Client client; + + protected BasicExampleTest(URI uri) { + Security security = Security.builder() + .addProvider(HttpBasicAuthProvider.builder()) + .build(); + + WebClientSecurity securityService = WebClientSecurity.create(security); + + client = Http1Client.builder() + .baseUri(uri) + .addService(securityService) + .build(); + } + + //now for the tests + @Test + public void testPublic() { + //Must be accessible without authentication + try (Http1ClientResponse response = client.get().uri("/public").request()) { + assertThat(response.status(), is(Status.OK_200)); + String entity = response.entity().as(String.class); + assertThat(entity, containsString("")); + } + } + + @Test + public void testNoRoles() { + String uri = "/noRoles"; + + testNotAuthorized(uri); + + //Must be accessible with authentication - to everybody + testProtected(uri, "jack", "changeit", Set.of("admin", "user"), Set.of()); + testProtected(uri, "jill", "changeit", Set.of("user"), Set.of("admin")); + testProtected(uri, "john", "changeit", Set.of(), Set.of("admin", "user")); + } + + @Test + public void testUserRole() { + String uri = "/user"; + + testNotAuthorized(uri); + + //Jack and Jill allowed (user role) + testProtected(uri, "jack", "changeit", Set.of("admin", "user"), Set.of()); + testProtected(uri, "jill", "changeit", Set.of("user"), Set.of("admin")); + testProtectedDenied(uri, "john", "changeit"); + } + + @Test + public void testAdminRole() { + String uri = "/admin"; + + testNotAuthorized(uri); + + //Only jack is allowed - admin role... + testProtected(uri, "jack", "changeit", Set.of("admin", "user"), Set.of()); + testProtectedDenied(uri, "jill", "changeit"); + testProtectedDenied(uri, "john", "changeit"); + } + + @Test + public void testDenyRole() { + String uri = "/deny"; + + testNotAuthorized(uri); + + // nobody has the correct role + testProtectedDenied(uri, "jack", "changeit"); + testProtectedDenied(uri, "jill", "changeit"); + testProtectedDenied(uri, "john", "changeit"); + } + + @Test + public void getNoAuthn() { + String uri = "/noAuthn"; + //Must NOT be accessible without authentication + try (Http1ClientResponse response = client.get().uri(uri).request()) { + + // authentication is optional, so we are not challenged, only forbidden, as the role can never be there... + assertThat(response.status(), is(Status.FORBIDDEN_403)); + } + } + + private void testNotAuthorized(String uri) { + //Must NOT be accessible without authentication + try (Http1ClientResponse response = client.get().uri(uri).request()) { + + assertThat(response.status(), is(Status.UNAUTHORIZED_401)); + String header = response.headers().get(HeaderNames.WWW_AUTHENTICATE).value(); + + assertThat(header.toLowerCase(), containsString("basic")); + assertThat(header, containsString("helidon")); + } + } + + private Http1ClientResponse callProtected(String uri, String username, String password) { + // here we call the endpoint + return client.get() + .uri(uri) + .property(EndpointConfig.PROPERTY_OUTBOUND_ID, username) + .property(EndpointConfig.PROPERTY_OUTBOUND_SECRET, password) + .request(); + } + + @SuppressWarnings("SameParameterValue") + private void testProtectedDenied(String uri, String username, String password) { + try (Http1ClientResponse response = callProtected(uri, username, password)) { + assertThat(response.status(), is(Status.FORBIDDEN_403)); + } + } + + @SuppressWarnings("SameParameterValue") + private void testProtected(String uri, + String username, + String password, + Set expectedRoles, + Set invalidRoles) { + + try (Http1ClientResponse response = callProtected(uri, username, password)) { + + String entity = response.entity().as(String.class); + assertThat(response.status(), is(Status.OK_200)); + + // check login + assertThat(entity, containsString("id='" + username + "'")); + // check roles + expectedRoles.forEach(role -> assertThat(entity, containsString(":" + role))); + invalidRoles.forEach(role -> assertThat(entity, not(containsString(":" + role)))); + } + } +} diff --git a/examples/security/google-login/README.md b/examples/security/google-login/README.md new file mode 100644 index 000000000..b4f5c355a --- /dev/null +++ b/examples/security/google-login/README.md @@ -0,0 +1,25 @@ +# Integration with Google login button + +This example demonstrates Integration with Google login button on a web page. + +## Contents + +There are two examples with exactly the same behavior +1. builder - shows how to programmatically secure application +2. config - shows how to secure application with configuration + 1. see src/main/resources/application.conf + +There is a static web page in src/main/resources/WEB with a page to login to Google. + +This example requires a Google client id to run. +Update the following files with your client id (it should support http://localhost:8080): +1. src/main/resources/application.yaml - set security.properties.google-client-id or override it in a file in ~/helidon/examples.yaml +2. src/main/resources/WEB/index.html - update the meta tag in header with name "google-signin-client_id" +3. src/main/java/io/helidon/security/examples/google/GoogleBuilderMain.java - update the client id in builder of provider + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-security-google-login.jar +``` diff --git a/examples/security/google-login/pom.xml b/examples/security/google-login/pom.xml new file mode 100644 index 000000000..a3524993e --- /dev/null +++ b/examples/security/google-login/pom.xml @@ -0,0 +1,112 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.security + helidon-examples-security-google-login + 1.0.0-SNAPSHOT + Helidon Examples Security Google Login + + + Example of Google login button integration with Security. + + + + io.helidon.examples.security.google.GoogleConfigMain + + + + + io.helidon.webserver + helidon-webserver-security + + + io.helidon.security.providers + helidon-security-providers-google-login + + + io.helidon.config + helidon-config-encryption + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-static-content + + + io.helidon.bundles + helidon-bundles-config + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + io.helidon.config + helidon-config-testing + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-core + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/security/google-login/src/main/java/io/helidon/examples/security/google/GoogleBuilderMain.java b/examples/security/google-login/src/main/java/io/helidon/examples/security/google/GoogleBuilderMain.java new file mode 100644 index 000000000..7682e1edd --- /dev/null +++ b/examples/security/google-login/src/main/java/io/helidon/examples/security/google/GoogleBuilderMain.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.google; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import io.helidon.http.HttpMediaTypes; +import io.helidon.logging.common.LogConfig; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; +import io.helidon.security.providers.google.login.GoogleTokenProvider; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.context.ContextFeature; +import io.helidon.webserver.security.SecurityFeature; +import io.helidon.webserver.staticcontent.StaticContentService; + +/** + * Google login button example main class using builders. + */ +@SuppressWarnings({"SpellCheckingInspection", "DuplicatedCode"}) +public final class GoogleBuilderMain { + + private GoogleBuilderMain() { + } + + /** + * 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.featuresDiscoverServices(false) + .addFeature(ContextFeature.create()) + .addFeature(SecurityFeature.builder() + .security(security) + .build()) + .routing(routing -> routing + .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/examples/security/google-login/src/main/java/io/helidon/examples/security/google/GoogleConfigMain.java b/examples/security/google-login/src/main/java/io/helidon/examples/security/google/GoogleConfigMain.java new file mode 100644 index 000000000..28dec85a8 --- /dev/null +++ b/examples/security/google-login/src/main/java/io/helidon/examples/security/google/GoogleConfigMain.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.google; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import io.helidon.config.Config; +import io.helidon.http.HttpMediaTypes; +import io.helidon.logging.common.LogConfig; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.staticcontent.StaticContentService; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * Google login button example main class using configuration. + */ +@SuppressWarnings("DuplicatedCode") +public final class GoogleConfigMain { + + private GoogleConfigMain() { + } + + /** + * 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) { + Config config = buildConfig(); + server.config(config.get("server")) + .routing(routing -> routing + .get("/rest/profile", (req, res) -> { + Optional securityContext = req.context().get(SecurityContext.class); + res.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8); + res.send("Response from config based service, you are: \n" + securityContext + .flatMap(SecurityContext::user) + .map(Subject::toString) + .orElse("Security context is null")); + }) + .register(StaticContentService.create("/WEB"))); + } + + private static Config buildConfig() { + return Config.builder() + .sources( + // you can use this file to override the defaults built-in + file(System.getProperty("user.home") + "/helidon/conf/examples.yaml").optional(), + // in jar file (see src/main/resources/application.yaml) + classpath("application.yaml")) + .build(); + } +} diff --git a/examples/security/google-login/src/main/java/io/helidon/examples/security/google/package-info.java b/examples/security/google-login/src/main/java/io/helidon/examples/security/google/package-info.java new file mode 100644 index 000000000..eccb6f444 --- /dev/null +++ b/examples/security/google-login/src/main/java/io/helidon/examples/security/google/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of integration of Google Login button with security. + */ +package io.helidon.examples.security.google; diff --git a/examples/security/google-login/src/main/resources/WEB/index.html b/examples/security/google-login/src/main/resources/WEB/index.html new file mode 100644 index 000000000..11075666c --- /dev/null +++ b/examples/security/google-login/src/main/resources/WEB/index.html @@ -0,0 +1,86 @@ + + + + + + Google Login Provider + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +

Login to Google.

+
+
+
+ + +
+
+
+ + + diff --git a/examples/security/google-login/src/main/resources/WEB/static/js/google-app.js b/examples/security/google-login/src/main/resources/WEB/static/js/google-app.js new file mode 100644 index 000000000..53bb91e98 --- /dev/null +++ b/examples/security/google-login/src/main/resources/WEB/static/js/google-app.js @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(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/examples/security/google-login/src/main/resources/application.yaml b/examples/security/google-login/src/main/resources/application.yaml new file mode 100644 index 000000000..4f9e6034f --- /dev/null +++ b/examples/security/google-login/src/main/resources/application.yaml @@ -0,0 +1,50 @@ +# +# Copyright (c) 2016, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +security: + config.require-encryption: false + properties: + # This example loads overriding properties from ~/helidon/examples.yaml + # You may configure correct values in that file to leave this content intact + google-client-id: "your-app-id.apps.googleusercontent.com" + proxy-host: "" + providers: + - google-login: + # Create your own application in Google developer console + # Also update the client id configured in header of index.html + # Detailed how-to for login button (including links how to create an application): + # https://developers.google.com/identity/sign-in/web/sign-in + client-id: "${security.properties.google-client-id}" + # Defaults for Helidon + # realm: "helidon" + # Configure proxy host if needed + proxy-host: "${security.properties.proxy-host}" + # proxy-port: 80 + + # This is the default for GoogleTokenProvider + #token: + # header: "Authorization" + # or do not specify - then the whole header is considered to be the token value + # prefix: "bearer " + # optional alternative - looking for first matching group + # regexp: "bearer (.*)" + #} + web-server: + paths: + - path: "/rest/profile" + methods: ["get"] + authenticate: true + diff --git a/examples/security/google-login/src/main/resources/logging.properties b/examples/security/google-login/src/main/resources/logging.properties new file mode 100644 index 000000000..1d0ec2a9d --- /dev/null +++ b/examples/security/google-login/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n +.level=INFO +AUDIT.level=FINEST diff --git a/examples/security/google-login/src/test/java/io/helidon/examples/security/google/GoogleBuilderMainTest.java b/examples/security/google-login/src/test/java/io/helidon/examples/security/google/GoogleBuilderMainTest.java new file mode 100644 index 000000000..7ec553f77 --- /dev/null +++ b/examples/security/google-login/src/test/java/io/helidon/examples/security/google/GoogleBuilderMainTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.google; + +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webserver.WebServerConfig; + +/** + * Unit test for {@link GoogleBuilderMain}. + */ +@ServerTest +public class GoogleBuilderMainTest extends GoogleMainTest { + + GoogleBuilderMainTest(Http1Client client) { + super(client); + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + GoogleBuilderMain.setup(server); + } +} diff --git a/examples/security/google-login/src/test/java/io/helidon/examples/security/google/GoogleConfigMainTest.java b/examples/security/google-login/src/test/java/io/helidon/examples/security/google/GoogleConfigMainTest.java new file mode 100644 index 000000000..15a606bbb --- /dev/null +++ b/examples/security/google-login/src/test/java/io/helidon/examples/security/google/GoogleConfigMainTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.google; + +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webserver.WebServerConfig; + +/** + * Unit test for {@link GoogleConfigMain}. + */ +@ServerTest +public class GoogleConfigMainTest extends GoogleMainTest { + + GoogleConfigMainTest(Http1Client client) { + super(client); + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + GoogleConfigMain.setup(server); + } +} diff --git a/examples/security/google-login/src/test/java/io/helidon/examples/security/google/GoogleMainTest.java b/examples/security/google-login/src/test/java/io/helidon/examples/security/google/GoogleMainTest.java new file mode 100644 index 000000000..8eaf2f9a8 --- /dev/null +++ b/examples/security/google-login/src/test/java/io/helidon/examples/security/google/GoogleMainTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.google; + +import io.helidon.http.HeaderNames; +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; + +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; + +/** + * Google login common unit tests. + */ +public abstract class GoogleMainTest { + private final Http1Client client; + + GoogleMainTest(Http1Client client) { + this.client = client; + } + + @Test + public void testEndpoint() { + try (Http1ClientResponse response = client.get("/rest/profile").request()) { + + assertThat(response.status(), is(Status.UNAUTHORIZED_401)); + assertThat(response.headers().first(HeaderNames.WWW_AUTHENTICATE), + optionalValue(is("Bearer realm=\"helidon\",scope=\"openid profile email\""))); + } + } +} diff --git a/examples/security/idcs-login/README.md b/examples/security/idcs-login/README.md new file mode 100644 index 000000000..5076dea86 --- /dev/null +++ b/examples/security/idcs-login/README.md @@ -0,0 +1,76 @@ +# Security integration with IDCS + +This example demonstrates integration with IDCS (Oracle identity service, integrated with Open ID Connect provider). + +## Contents + +This project contains two samples, one (IdcsMain.java) which is configured via the application.yaml file and a second example (IdcsBuilderMain.java) which is configured in code. + +When configured the example exposes two HTTP endpoints `/jersey`, a rest endpoint protected by an IDCS application (with two scopes) and a second endpoint (/rest/profile) which is not protected. + +### IDCS Configuration + +Edit application.yaml for IdcsMain.java or OidcConfig variable definition for IdcsBuilderMain.java sample + +1. Log in to the IDCS console and create a new application of type "confidential app" +2. Within **Resources** + 1. Create two resources called `first_scope` and `second_scope` + 2. Primary Audience = `http://localhost:7987/"` (ensure there is a trailing /) +3. Within **Client Configuration** + 1. Register a client + 2. Allowed Grant Types = Client Credentials,JWT Assertion, Refresh Token, Authorization Code + 3. Check "Allow non-HTTPS URLs" + 4. Set Redirect URL to `http://localhost:7987/oidc/redirect` + 5. Client Type = Confidential + 6. Add all Scopes defined in the resources section + 7. Set allowed operations to `Introspect` + 8. Set Post Logout Redirect URL to `http://localhost:7987/loggedout` + +Ensure you save and *activate* the application + +### Code Configuration + +Edit application.yaml for IdcsMain.java or OidcConfig variable definition for IdcsBuilderMain.java sample + + 1. idcs-uri : Base URL of your idcs instance, usually something like https://idcs-.identity.oraclecloud.com + 2. idcs-client-id : This is obtained from your IDCS application in the IDCS console + 3. idcs-client-secret : This is obtained from your IDCS application in the IDCS console + 4. frontend-uri : This is the base URL of your application when run, e.g. `http://localhost:7987` + 5. proxy-host : Your proxy server if needed + 6. scope-audience : This is the scope audience which MUST match the primary audience in the IDCS resource, recommendation is not to have a trailing slash (/) + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-security-oidc.jar +``` + +Try the endpoints: + +3. Open http://localhost:7987/rest/profile in your browser. This should present + you with a response highlighting your logged in role (null) correctly as you are not logged in +4. Open `http://localhost:7987/oidc/logout` in your browser. This will log you out from your IDCS and Helidon sessions + +## Calling Sample from Postman + +Now that everything is setup it is possible to call the sample from tools like postman + +1. Create new REST call for your service , e.g. `http://localhost:7987/jersey` +2. Set authentication to oauth 2.0 +3. Press the "Get New Access Token" button + 1. Grant Type should be "Password Credentials" + 2. IDCS Access token URI should be `https://.identity.oraclecloud.com/oauth2/v1/token` + 3. ClientID and Client Secret should be obtained from IDCS and is the same that is configured in the app + 4. Scopes should be a "space" delimited list of scopes prefixed by audience , e.g. `http://localhost:7987/first_scope http://localhost:7987/second_scope` + 5. Client Authentication "Send as Basic Auth Header" +4. Request Token (If you get an error check the postman developer console) +5. Once you have a token ensure you press the "Use token" button +6. Execute your rest call + +## Troubleshooting + +#### Upon redirect to IDCS login page you receive a message indicating the scope isn't found + +- Check that *both* scope names in JerseyResource (first_scope and second_scope) exist in your IDCS application +- Check that the `primary audience` config value in IDCS contains a trailing / and the `front-end-url` in the config file does not diff --git a/examples/security/idcs-login/pom.xml b/examples/security/idcs-login/pom.xml new file mode 100644 index 000000000..28576d2a3 --- /dev/null +++ b/examples/security/idcs-login/pom.xml @@ -0,0 +1,133 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.security + helidon-examples-security-oidc + 1.0.0-SNAPSHOT + Helidon Examples Security IDCS Login + + + Example of login with IDCS using the OIDC provider, storing the identity in a cookie + + + + io.helidon.examples.security.idcs.IdcsMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.http.media + helidon-http-media-jsonp + + + + io.helidon.security.providers + helidon-security-providers-oidc + + + + io.helidon.security.providers + helidon-security-providers-idcs-mapper + + + + io.helidon.security.providers + helidon-security-providers-abac + + + + io.helidon.security.abac + helidon-security-abac-scope + + + + io.helidon.security.abac + helidon-security-abac-role + + + + io.helidon.webserver + helidon-webserver-security + + + + io.helidon.config + helidon-config-encryption + + + io.helidon.config + helidon-config-yaml + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-core + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/security/idcs-login/src/main/java/io/helidon/examples/security/idcs/IdcsBuilderMain.java b/examples/security/idcs-login/src/main/java/io/helidon/examples/security/idcs/IdcsBuilderMain.java new file mode 100644 index 000000000..ba3ec82f3 --- /dev/null +++ b/examples/security/idcs-login/src/main/java/io/helidon/examples/security/idcs/IdcsBuilderMain.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.idcs; + +import java.net.URI; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import io.helidon.common.context.Contexts; +import io.helidon.config.Config; +import io.helidon.http.HttpMediaTypes; +import io.helidon.logging.common.LogConfig; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; +import io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider; +import io.helidon.security.providers.oidc.OidcFeature; +import io.helidon.security.providers.oidc.OidcProvider; +import io.helidon.security.providers.oidc.common.OidcConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * IDCS Login example main class using configuration . + */ +@SuppressWarnings("HttpUrlsUsage") +public final class IdcsBuilderMain { + + // do not change this constant, unless you modify configuration + // of IDCS application redirect URI + static final int PORT = 7987; + + private IdcsBuilderMain() { + } + + /** + * Start the example. + * + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build(); + + long t = System.nanoTime(); + server.start(); + long time = System.nanoTime() - t; + + System.out.printf(""" + Server started in %2$d ms + + Started server on localhost:%1$d + You can access this example at http://localhost:%1$d/rest/profile + + Check application.yaml in case you are behind a proxy to configure it + """, server.port(), TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS)); + } + + static void setup(WebServerConfig.Builder server) { + Config config = buildConfig(); + + OidcConfig oidcConfig = OidcConfig.builder() + .clientId("clientId.of.your.application") + .clientSecret("clientSecret.of.your.application") + .identityUri(URI.create( + "https://idcs-tenant-id.identity.oracle.com")) + //.proxyHost("proxy.proxy.com") + .frontendUri("http://your.host:your.port") + // tell us it is IDCS, so we can modify the behavior + .serverType("idcs") + .build(); + + Security security = Security.builder() + .addProvider(OidcProvider.create(oidcConfig)) + .addProvider(IdcsRoleMapperProvider.builder() + .config(config) + .oidcConfig(oidcConfig)) + .build(); + // security needs to be available for other features, such as server security feature + Contexts.globalContext().register(security); + + server.port(PORT) + .routing(routing -> routing + // IDCS requires a web resource for redirects + .addFeature(OidcFeature.create(config)) + // web server does not (yet) have possibility to configure routes in config files, so explicit... + .get("/rest/profile", (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")); + })); + } + + private static Config buildConfig() { + return Config.builder() + .sources( + // you can use this file to override the defaults built-in + file(System.getProperty("user.home") + "/helidon/conf/examples.yaml").optional(), + // in jar file (see src/main/resources/application.yaml) + classpath("application.yaml")) + .build(); + } +} diff --git a/examples/security/idcs-login/src/main/java/io/helidon/examples/security/idcs/IdcsMain.java b/examples/security/idcs-login/src/main/java/io/helidon/examples/security/idcs/IdcsMain.java new file mode 100644 index 000000000..6669fdea5 --- /dev/null +++ b/examples/security/idcs-login/src/main/java/io/helidon/examples/security/idcs/IdcsMain.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.idcs; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import io.helidon.common.context.Contexts; +import io.helidon.config.Config; +import io.helidon.http.HttpMediaTypes; +import io.helidon.logging.common.LogConfig; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; +import io.helidon.security.providers.oidc.OidcFeature; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * IDCS Login example main class using configuration . + */ +public final class IdcsMain { + + private IdcsMain() { + } + + /** + * Start the example. + * + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build(); + + long t = System.nanoTime(); + server.start(); + long time = System.nanoTime() - t; + + System.out.printf(""" + Server started in %2$d ms + + Started server on localhost:%1$d + You can access this example at http://localhost:%1$d/rest/profile + + Check application.yaml in case you are behind a proxy to configure it + """, server.port(), TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS)); + } + + static void setup(WebServerConfig.Builder server) { + Config config = buildConfig(); + + Security security = Security.create(config.get("security")); + // this is needed for proper encryption/decryption of cookies + Contexts.globalContext().register(security); + + server.config(config) + .routing(routing -> routing + // IDCS requires a web resource for redirects + .addFeature(OidcFeature.create(config)) + // web server does not (yet) have possibility to configure routes in config files, so explicit... + .get("/rest/profile", (req, res) -> { + Optional securityContext = req.context().get(SecurityContext.class); + res.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8); + res.send("Response from config based service, you are: \n" + securityContext + .flatMap(SecurityContext::user) + .map(Subject::toString) + .orElse("Security context is null")); + }) + .get("/loggedout", (req, res) -> res.send("You have been logged out"))); + } + + private static Config buildConfig() { + return Config.builder() + .sources( + // you can use this file to override the defaults built-in + file(System.getProperty("user.home") + "/helidon/conf/examples.yaml").optional(), + // in jar file (see src/main/resources/application.yaml) + classpath("application.yaml")) + .build(); + } +} diff --git a/examples/security/idcs-login/src/main/java/io/helidon/examples/security/idcs/package-info.java b/examples/security/idcs-login/src/main/java/io/helidon/examples/security/idcs/package-info.java new file mode 100644 index 000000000..804d3deb6 --- /dev/null +++ b/examples/security/idcs-login/src/main/java/io/helidon/examples/security/idcs/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example showcasing integration of web server with IDCS server, using Open ID Connect security provider. + * There is another example in "microprofile" directory that shows the same integration for + * a MicroProfile based application. + */ +package io.helidon.examples.security.idcs; diff --git a/examples/security/idcs-login/src/main/resources/application.yaml b/examples/security/idcs-login/src/main/resources/application.yaml new file mode 100644 index 000000000..3143f33f2 --- /dev/null +++ b/examples/security/idcs-login/src/main/resources/application.yaml @@ -0,0 +1,67 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 7987 + features: + security: + # protected paths on the web server - do not include paths served by Jersey, as those are protected directly + paths: + - path: "/rest/profile" + methods: [ "get" ] + authenticate: true + roles-allowed: [ "my_admins" ] + # abac: + # scopes: ["first_scope", "second_scope"] + +security: + config.require-encryption: false + properties: + # This is a nice way to be able to override this with local properties or env-vars + idcs-uri: "https://your-tenant-id.identity.oracle.com" + idcs-client-id: "your-client-id" + idcs-client-secret: "${CLEAR=changeit}" + proxy-host: "" + providers: + - abac: + # Adds ABAC Provider - it does not require any configuration + - oidc: + client-id: "${security.properties.idcs-client-id}" + client-secret: "${security.properties.idcs-client-secret}" + identity-uri: "${security.properties.idcs-uri}" + # A prefix used for custom scopes + scope-audience: "http://localhost:7987/test-application" + proxy-host: "${security.properties.proxy-host}" + # Used as a base for redirects back to us (based on Host header now, so no need to explicitly define it) + # If explicitly defined, will override host header + # frontend-uri: "http://localhost:7987" + # support for non-public signature JWK (and maybe other IDCS specific handling) + server-type: "idcs" + logout-enabled: true + # Can define just a path, host will be taken from header + post-logout-uri: "/loggedout" + # We want to redirect to login page (and token can be received either through cookie or header) + redirect: true + - idcs-role-mapper: + multitenant: false + oidc-config: + # we must repeat IDCS configuration, as in this case + # IDCS serves both as open ID connect authenticator and + # as a role mapper. Using minimal configuration here + client-id: "${security.properties.idcs-client-id}" + client-secret: "${security.properties.idcs-client-secret}" + identity-uri: "${security.properties.idcs-uri}" + diff --git a/examples/security/idcs-login/src/main/resources/logging.properties b/examples/security/idcs-login/src/main/resources/logging.properties new file mode 100644 index 000000000..7844632da --- /dev/null +++ b/examples/security/idcs-login/src/main/resources/logging.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +.level=INFO +AUDIT.level=FINEST +io.helidon.security.level=FINEST + diff --git a/examples/security/outbound-override/README.md b/examples/security/outbound-override/README.md new file mode 100644 index 000000000..c4692698b --- /dev/null +++ b/examples/security/outbound-override/README.md @@ -0,0 +1,21 @@ + +# Helidon Security Outbound Override Example + +Example that propagates identity, and on one endpoint explicitly +sets the username and password. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-security-outbound-override.jar +``` + +Try the endpoints (port is random, shall be replaced accordingly): +```shell +export PORT=35973 +curl -u "jack:changeit" http://localhost:${PORT}/propagate +curl -u "jack:changeit" http://localhost:${PORT}/override +curl -u "jill:changeit" http://localhost:${PORT}/propagate +curl -u "jill:changeit" http://localhost:${PORT}/override +``` diff --git a/examples/security/outbound-override/pom.xml b/examples/security/outbound-override/pom.xml new file mode 100644 index 000000000..6c5ce221e --- /dev/null +++ b/examples/security/outbound-override/pom.xml @@ -0,0 +1,97 @@ + + + + + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.security + 4.0.0 + helidon-examples-security-outbound-override + 1.0.0-SNAPSHOT + Helidon Examples Security Outbound Override + + + io.helidon.security.examples.outbound.OutboundOverrideExample + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-security + + + io.helidon.webclient + helidon-webclient + + + io.helidon.webclient + helidon-webclient-security + + + io.helidon.security.providers + helidon-security-providers-http-auth + + + io.helidon.security.providers + helidon-security-providers-jwt + + + io.helidon.bundles + helidon-bundles-config + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/JwtOverrideService.java b/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/JwtOverrideService.java new file mode 100644 index 000000000..5ef6988f0 --- /dev/null +++ b/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/JwtOverrideService.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.security.examples.outbound; + +import io.helidon.security.EndpointConfig; +import io.helidon.security.SecurityContext; +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; + +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(EndpointConfig.PROPERTY_OUTBOUND_ID, "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/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/OutboundOverrideExample.java b/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/OutboundOverrideExample.java new file mode 100644 index 000000000..b79d5519a --- /dev/null +++ b/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/OutboundOverrideExample.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.security.examples.outbound; + +import java.util.concurrent.TimeUnit; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.security.Principal; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.context.ContextFeature; +import io.helidon.webserver.security.SecurityHttpFeature; + +/** + * 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 and password
  • + *
  • one that uses basic authentication both to authenticate users and to propagate identity.
  • + *
+ */ +public final class OutboundOverrideExample { + + private OutboundOverrideExample() { + } + + /** + * 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 %3d ms + + *********************** + ** Endpoints: ** + *********************** + + http://localhost:%1d/propagate + http://localhost:%1d/override + + Backend service started on: http://localhost:%2d/hello + + """, TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS), + server.port(), server.port(), server.port("backend")); + } + + static void setup(WebServerConfig.Builder server) { + Config clientConfig = Config.create(ConfigSources.classpath("client-service.yaml")); + Config backendConfig = Config.create(ConfigSources.classpath("backend-service.yaml")); + + // as we use the security http feature directly, we cannot use discovered security feature + // this is a unique case where we combine two sets of server set-ups in a single webserver + server.featuresDiscoverServices(false) + // context feature is a pre-requisite of security + .addFeature(ContextFeature.create()) + .config(clientConfig.get("security")) + .routing(routing -> routing + .addFeature(SecurityHttpFeature.create(clientConfig.get("security.web-server"))) + .register(new OverrideService())) + + // backend that prints the current user + .putSocket("backend", socket -> socket + .routing(routing -> routing + .addFeature(SecurityHttpFeature.create(backendConfig.get("security.web-server"))) + .get("/hello", (req, res) -> { + String username = req.context() + .get(SecurityContext.class) + .flatMap(SecurityContext::user) + .map(Subject::principal) + .map(Principal::getName) + .orElse("Anonymous"); + res.send(username); + }))); + } +} diff --git a/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/OutboundOverrideJwtExample.java b/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/OutboundOverrideJwtExample.java new file mode 100644 index 000000000..635fa5444 --- /dev/null +++ b/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/OutboundOverrideJwtExample.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.security.examples.outbound; + +import java.util.concurrent.TimeUnit; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.http.HeaderNames; +import io.helidon.security.Principal; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.context.ContextFeature; +import io.helidon.webserver.security.SecurityHttpFeature; + +/** + * 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 OutboundOverrideJwtExample { + + private OutboundOverrideJwtExample() { + } + + /** + * 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")); + + // as we use the security http feature directly, we cannot use discovered security feature + // this is a unique case where we combine two sets of server set-ups in a single webserver + server.featuresDiscoverServices(false) + // context feature is a pre-requisite of security + .addFeature(ContextFeature.create()) + .routing(routing -> routing + .addFeature(SecurityHttpFeature.create(clientConfig.get("security.web-server"))) + .register(new JwtOverrideService())) + + // backend that prints the current user + .putSocket("backend", socket -> socket + .routing(routing -> routing + .addFeature(SecurityHttpFeature.create(backendConfig.get("security.web-server"))) + .get("/hello", (req, res) -> { + + // This is the token. It should be bearer + req.headers().first(HeaderNames.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/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/OverrideService.java b/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/OverrideService.java new file mode 100644 index 000000000..94acb5daa --- /dev/null +++ b/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/OverrideService.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.security.examples.outbound; + +import io.helidon.security.EndpointConfig; +import io.helidon.security.SecurityContext; +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; + +class OverrideService 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(EndpointConfig.PROPERTY_OUTBOUND_ID, "jill") + .property(EndpointConfig.PROPERTY_OUTBOUND_SECRET, "changeit") + .requestEntity(String.class); + + res.send("You are: " + context.userName() + ", backend service returned: " + result + "\n"); + } + + 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 + "\n"); + } + +} diff --git a/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/package-info.java b/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/package-info.java new file mode 100644 index 000000000..05900c049 --- /dev/null +++ b/examples/security/outbound-override/src/main/java/io/helidon/security/examples/outbound/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example to show how to override default settings of an outbound provider. + */ +package io.helidon.security.examples.outbound; diff --git a/examples/security/outbound-override/src/main/resources/backend-service-jwt.yaml b/examples/security/outbound-override/src/main/resources/backend-service-jwt.yaml new file mode 100644 index 000000000..1faec0755 --- /dev/null +++ b/examples/security/outbound-override/src/main/resources/backend-service-jwt.yaml @@ -0,0 +1,28 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +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/examples/security/outbound-override/src/main/resources/backend-service.yaml b/examples/security/outbound-override/src/main/resources/backend-service.yaml new file mode 100644 index 000000000..e40cfd46a --- /dev/null +++ b/examples/security/outbound-override/src/main/resources/backend-service.yaml @@ -0,0 +1,33 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +security: + providers: + - http-basic-auth: + users: + - login: "jack" + password: "changeit" + roles: ["user", "admin"] + - login: "jill" + password: "changeit" + roles: ["user"] + web-server: + defaults: + authenticate: true + paths: + - path: "/hello" + methods: ["get"] + roles-allowed: "user" diff --git a/examples/security/outbound-override/src/main/resources/client-service-jwt.yaml b/examples/security/outbound-override/src/main/resources/client-service-jwt.yaml new file mode 100644 index 000000000..c4d11dba6 --- /dev/null +++ b/examples/security/outbound-override/src/main/resources/client-service-jwt.yaml @@ -0,0 +1,63 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +security: + provider-policy: + type: "COMPOSITE" + authentication: + - name: "http-basic-auth" + outbound: + - name: "jwt" + providers: + - http-basic-auth: + users: + - login: "john" + password: "changeit" + roles: ["admin"] + - login: "jack" + password: "changeit" + roles: ["user", "admin"] + - login: "jill" + password: "changeit" + 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/examples/security/outbound-override/src/main/resources/client-service.yaml b/examples/security/outbound-override/src/main/resources/client-service.yaml new file mode 100644 index 000000000..0219ab30e --- /dev/null +++ b/examples/security/outbound-override/src/main/resources/client-service.yaml @@ -0,0 +1,41 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +security: + providers: + - http-basic-auth: + users: + - login: "john" + password: "changeit" + roles: ["admin"] + - login: "jack" + password: "changeit" + roles: ["user", "admin"] + - login: "jill" + password: "changeit" + roles: ["user"] + 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/examples/security/outbound-override/src/main/resources/logging.properties b/examples/security/outbound-override/src/main/resources/logging.properties new file mode 100644 index 000000000..051148a14 --- /dev/null +++ b/examples/security/outbound-override/src/main/resources/logging.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n + +.level = INFO diff --git a/examples/security/outbound-override/src/main/resources/signing-jwk.json b/examples/security/outbound-override/src/main/resources/signing-jwk.json new file mode 100644 index 000000000..09df45ed5 --- /dev/null +++ b/examples/security/outbound-override/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/examples/security/outbound-override/src/main/resources/verifying-jwk.json b/examples/security/outbound-override/src/main/resources/verifying-jwk.json new file mode 100644 index 000000000..5bfc90302 --- /dev/null +++ b/examples/security/outbound-override/src/main/resources/verifying-jwk.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "kty": "oct", + "kid": "helidon", + "alg": "HS256", + "key_ops": [ + "sign", + "verify" + ], + "k": "FdFYFzERwC2uCBB46pZQi4GG85LujR8obt-KWRBICVQ" + } + ] +} \ No newline at end of file diff --git a/examples/security/outbound-override/src/test/java/io/helidon/security/examples/outbound/OutboundOverrideExampleTest.java b/examples/security/outbound-override/src/test/java/io/helidon/security/examples/outbound/OutboundOverrideExampleTest.java new file mode 100644 index 000000000..fabb17b17 --- /dev/null +++ b/examples/security/outbound-override/src/test/java/io/helidon/security/examples/outbound/OutboundOverrideExampleTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.security.examples.outbound; + +import java.net.URI; + +import io.helidon.security.EndpointConfig; +import io.helidon.security.Security; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.security.WebClientSecurity; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +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 OutboundOverrideExampleTest { + + private final Http1Client client; + + OutboundOverrideExampleTest(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) { + OutboundOverrideExample.setup(server); + } + + @Test + public void testOverrideExample() { + String value = client.get() + .path("/override") + .property(EndpointConfig.PROPERTY_OUTBOUND_ID, "jack") + .property(EndpointConfig.PROPERTY_OUTBOUND_SECRET, "changeit") + .requestEntity(String.class); + + assertThat(value, is("You are: jack, backend service returned: jill\n")); + } + + @Test + public void testPropagateExample() { + String value = client.get() + .path("/propagate") + .property(EndpointConfig.PROPERTY_OUTBOUND_ID, "jack") + .property(EndpointConfig.PROPERTY_OUTBOUND_SECRET, "changeit") + .requestEntity(String.class); + + assertThat(value, is("You are: jack, backend service returned: jack\n")); + } +} diff --git a/examples/security/outbound-override/src/test/java/io/helidon/security/examples/outbound/OutboundOverrideJwtExampleTest.java b/examples/security/outbound-override/src/test/java/io/helidon/security/examples/outbound/OutboundOverrideJwtExampleTest.java new file mode 100644 index 000000000..d99934259 --- /dev/null +++ b/examples/security/outbound-override/src/test/java/io/helidon/security/examples/outbound/OutboundOverrideJwtExampleTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.security.examples.outbound; + +import java.net.URI; + +import io.helidon.security.EndpointConfig; +import io.helidon.security.Security; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; +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.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +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 OutboundOverrideJwtExampleTest { + + private final Http1Client client; + + OutboundOverrideJwtExampleTest(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) { + OutboundOverrideJwtExample.setup(server); + } + + @Test + public void testOverrideExample() { + try (Http1ClientResponse response = client.get() + .path("/override") + .property(EndpointConfig.PROPERTY_OUTBOUND_ID, "jack") + .property(EndpointConfig.PROPERTY_OUTBOUND_SECRET, "changeit") + .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(EndpointConfig.PROPERTY_OUTBOUND_ID, "jack") + .property(EndpointConfig.PROPERTY_OUTBOUND_SECRET, "changeit") + .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/examples/security/pom.xml b/examples/security/pom.xml new file mode 100644 index 000000000..cf6f0b097 --- /dev/null +++ b/examples/security/pom.xml @@ -0,0 +1,51 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + io.helidon.examples.security + helidon-examples-security-project + 1.0.0-SNAPSHOT + Helidon Examples Security + pom + + + Examples of Helidon Security usage and integrations + + + + attribute-based-access-control + basic-auth-with-static-content + google-login + idcs-login + outbound-override + programmatic + spi-examples + vaults + webserver-digest-auth + webserver-signatures + + diff --git a/examples/security/programmatic/README.md b/examples/security/programmatic/README.md new file mode 100644 index 000000000..cc9017e63 --- /dev/null +++ b/examples/security/programmatic/README.md @@ -0,0 +1,10 @@ +# Helidon Security Example + +Example of manually using the security APIs. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-security-nohttp-programmatic.jar +``` diff --git a/examples/security/programmatic/pom.xml b/examples/security/programmatic/pom.xml new file mode 100644 index 000000000..567319ce1 --- /dev/null +++ b/examples/security/programmatic/pom.xml @@ -0,0 +1,62 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.security + helidon-examples-security-nohttp-programmatic + 1.0.0-SNAPSHOT + Helidon Examples Security No-HTTP programmatic + + + Example of programmatic security without an HTTP resource. + + + + io.helidon.examples.security.programmatic.ProgrammaticSecurity + + + + + io.helidon.security + helidon-security + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/security/programmatic/src/main/java/io/helidon/examples/security/programmatic/MyProvider.java b/examples/security/programmatic/src/main/java/io/helidon/examples/security/programmatic/MyProvider.java new file mode 100644 index 000000000..5fc6e3410 --- /dev/null +++ b/examples/security/programmatic/src/main/java/io/helidon/examples/security/programmatic/MyProvider.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.programmatic; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; + +import io.helidon.security.AuthenticationResponse; +import io.helidon.security.AuthorizationResponse; +import io.helidon.security.EndpointConfig; +import io.helidon.security.OutboundSecurityResponse; +import io.helidon.security.Principal; +import io.helidon.security.ProviderRequest; +import io.helidon.security.Role; +import io.helidon.security.SecurityEnvironment; +import io.helidon.security.Subject; +import io.helidon.security.spi.AuthenticationProvider; +import io.helidon.security.spi.AuthorizationProvider; +import io.helidon.security.spi.OutboundSecurityProvider; + +/** + * Sample provider. + */ +class MyProvider implements AuthenticationProvider, AuthorizationProvider, OutboundSecurityProvider { + + @Override + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { + //get username and password + List headers = providerRequest.env().headers().getOrDefault("authorization", List.of()); + if (headers.isEmpty()) { + return AuthenticationResponse.failed("No authorization header"); + } + + String header = headers.get(0); + if (header.toLowerCase().startsWith("basic ")) { + String base64 = header.substring(6); + String unamePwd = new String(Base64.getDecoder().decode(base64), StandardCharsets.UTF_8); + int index = unamePwd.indexOf(':'); + if (index > 0) { + String name = unamePwd.substring(0, index); + String pwd = unamePwd.substring(index + 1); + if ("aUser".equals(name)) { + //authenticate + Principal principal = Principal.create(name); + Role roleGrant = Role.create("theRole"); + + Subject subject = Subject.builder() + .principal(principal) + .addGrant(roleGrant) + .addPrivateCredential(MyPrivateCreds.class, new MyPrivateCreds(name, pwd.toCharArray())) + .build(); + + return AuthenticationResponse.success(subject); + } + } + } + + return AuthenticationResponse.failed("User not found"); + } + + @Override + public AuthorizationResponse authorize(ProviderRequest providerRequest) { + if ("CustomResourceType" + .equals(providerRequest.env().abacAttribute("resourceType").orElseThrow(() -> new IllegalArgumentException( + "Resource type is a required parameter")))) { + //supported resource + return providerRequest.securityContext() + .user() + .map(Subject::principal) + .map(Principal::getName) + .map("aUser"::equals) + .map(correct -> { + if (correct) { + return AuthorizationResponse.permit(); + } + return AuthorizationResponse.deny(); + }) + .orElse(AuthorizationResponse.deny()); + } + + return AuthorizationResponse.deny(); + } + + @Override + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { + + return providerRequest.securityContext() + .user() + .flatMap(subject -> subject.privateCredential(MyPrivateCreds.class)) + .map(myPrivateCreds -> OutboundSecurityResponse.builder() + .requestHeader("Authorization", authHeader(myPrivateCreds)) + .build() + ).orElse(OutboundSecurityResponse.abstain()); + } + + private String authHeader(MyPrivateCreds privCreds) { + String creds = privCreds.name + ":" + new String(privCreds.password); + return "basic " + Base64.getEncoder().encodeToString(creds.getBytes(StandardCharsets.UTF_8)); + } + + private static class MyPrivateCreds { + private final String name; + private final char[] password; + + private MyPrivateCreds(String name, char[] password) { + this.name = name; + this.password = password; + } + + @Override + public String toString() { + return "MyPrivateCreds: " + name; + } + } +} diff --git a/examples/security/programmatic/src/main/java/io/helidon/examples/security/programmatic/ProgrammaticSecurity.java b/examples/security/programmatic/src/main/java/io/helidon/examples/security/programmatic/ProgrammaticSecurity.java new file mode 100644 index 000000000..2fb76665d --- /dev/null +++ b/examples/security/programmatic/src/main/java/io/helidon/examples/security/programmatic/ProgrammaticSecurity.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.programmatic; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.ExecutionException; + +import io.helidon.security.AuthenticationResponse; +import io.helidon.security.AuthorizationResponse; +import io.helidon.security.OutboundSecurityResponse; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; + +/** + * This class shows how to manually secure your application. + */ +public class ProgrammaticSecurity { + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + private Security security; + + /** + * Entry point to this example. + * + * @param args no needed + * @throws ExecutionException if asynchronous security fails + * @throws InterruptedException if asynchronous security gets interrupted + */ + public static void main(String[] args) throws ExecutionException, InterruptedException { + ProgrammaticSecurity instance = new ProgrammaticSecurity(); + + /* + * Simple single threaded applications - nothing too complicated + */ + //1: initialize security component + instance.init(); + //2: login + Subject subject = instance.login(); + + //3: authorize access to restricted resource + instance.execute(); + //4: propagate identity + instance.propagate(); + + /* + * More complex - multithreaded application + */ + instance.multithreaded(subject); + + } + + private static String buildBasic(String user, String password) { + return "basic " + Base64.getEncoder() + .encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8)); + } + + private void multithreaded(Subject subject) { + Thread thread = new Thread(() -> { + try { + SecurityContext context = security.contextBuilder("newThread") + .build(); + + CONTEXT.set(context); + + //this must be done, as there is no subject (yet) for current thread (or event the login attempt may be done + //in this thread - depends on what your application wants to do... + context.runAs(subject, () -> { + //3: authorize access to restricted resource + execute(); + //4: propagate identity + propagate(); + }); + } finally { + CONTEXT.remove(); + } + }); + thread.start(); + } + + private void propagate() { + OutboundSecurityResponse response = CONTEXT.get().outboundClientBuilder().buildAndGet(); + + switch (response.status()) { + case SUCCESS: + //we should have "Authorization" header present and just need to update request headers of our outbound call + System.out.println("Authorization header: " + response.requestHeaders().get("Authorization")); + break; + case SUCCESS_FINISH: + System.out.println("Identity propagation done, request sent..."); + break; + default: + System.out.println("Failed in identity propagation provider: " + response.description().orElse(null)); + break; + } + } + + private void execute() { + SecurityContext context = CONTEXT.get(); + //check role + if (!context.isUserInRole("theRole")) { + throw new IllegalStateException("User is not in expected role"); + } + + context.env(context.env() + .derive() + .addAttribute("resourceType", "CustomResourceType")); + + //check authorization through provider + AuthorizationResponse response = context.atzClientBuilder().buildAndGet(); + + if (response.status().isSuccess()) { + //ok, process resource + System.out.println("Resource processed"); + } else { + System.out.println("You are not permitted to process resource"); + } + } + + private Subject login() { + SecurityContext securityContext = CONTEXT.get(); + securityContext.env(securityContext.env().derive() + .path("/some/path") + .header("Authorization", buildBasic("aUser", "changeit"))); + + AuthenticationResponse response = securityContext.atnClientBuilder().buildAndGet(); + + if (response.status().isSuccess()) { + return response.user().orElseThrow(() -> new IllegalStateException("No user authenticated!")); + } + + throw new RuntimeException("Failed to authenticate", response.throwable().orElse(null)); + } + + private void init() { + //binds security context to current thread + this.security = Security.builder() + .addProvider(new MyProvider(), "FirstProvider") + .build(); + CONTEXT.set(security.contextBuilder("mainThread").build()); + } + +} diff --git a/examples/security/programmatic/src/main/java/io/helidon/examples/security/programmatic/package-info.java b/examples/security/programmatic/src/main/java/io/helidon/examples/security/programmatic/package-info.java new file mode 100644 index 000000000..b41f64374 --- /dev/null +++ b/examples/security/programmatic/src/main/java/io/helidon/examples/security/programmatic/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of programmatic approach to security. + */ +package io.helidon.examples.security.programmatic; diff --git a/examples/security/programmatic/src/main/resources/logging.properties b/examples/security/programmatic/src/main/resources/logging.properties new file mode 100644 index 000000000..85cd92104 --- /dev/null +++ b/examples/security/programmatic/src/main/resources/logging.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +.level=INFO +AUDIT.level=FINEST diff --git a/examples/security/spi-examples/README.md b/examples/security/spi-examples/README.md new file mode 100644 index 000000000..15652fc7a --- /dev/null +++ b/examples/security/spi-examples/README.md @@ -0,0 +1,17 @@ +# SPI implementation + +This example demonstrates how to implement various SPIs of security component. + +Contents +-------- +The following examples are available: +1. Authentication provider +2. Authorization provider +3. Outbound provider +3. Audit provider +4. Provider selection policy + +# Running the Example + +This is an API/SPI example. It is validated through unit tests. + \ No newline at end of file diff --git a/examples/security/spi-examples/pom.xml b/examples/security/spi-examples/pom.xml new file mode 100644 index 000000000..2d50c2264 --- /dev/null +++ b/examples/security/spi-examples/pom.xml @@ -0,0 +1,82 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.security + helidon-examples-security-spi + 1.0.0-SNAPSHOT + Helidon Examples Security SPI Implementation + + + Example of implementation of custom providers and other SPI implementations + + + + 2.23.4 + + + + + io.helidon.security + helidon-security + + + io.helidon.bundles + helidon-bundles-config + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-core + test + + + org.mockito + mockito-core + ${version.lib.mockito} + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtnProviderImpl.java b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtnProviderImpl.java new file mode 100644 index 000000000..3a81320a3 --- /dev/null +++ b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtnProviderImpl.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.examples.spi; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.config.Config; +import io.helidon.security.AuthenticationResponse; +import io.helidon.security.EndpointConfig; +import io.helidon.security.Principal; +import io.helidon.security.ProviderRequest; +import io.helidon.security.Role; +import io.helidon.security.SecurityLevel; +import io.helidon.security.Subject; +import io.helidon.security.spi.AuthenticationProvider; + +/** + * Example of an authentication provider implementation. + * This is a full-blown example of a provider that requires additional configuration on a resource. + */ +public class AtnProviderImpl implements AuthenticationProvider { + @Override + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { + + // first obtain the configuration of this request + // either from annotation, custom object or config + AtnObject myObject = getCustomObject(providerRequest.endpointConfig()); + + if (null == myObject) { + // I do not have my required information, this request is probably not for me + return AuthenticationResponse.abstain(); + } + + if (myObject.isValid()) { + // now authenticate - this example just creates a subject + // based on the value (user subject) and size (group subject) + return AuthenticationResponse.success(Subject.builder() + .addPrincipal(Principal.create(myObject.getValue())) + .addGrant(Role.create("role_" + myObject.getSize())) + .build()); + } else { + return AuthenticationResponse.failed("Invalid request"); + } + } + + private AtnObject getCustomObject(EndpointConfig epConfig) { + // order I choose - this depends on type of security you implement and your choice: + // 1) custom object in request (as this must be explicitly done by a developer) + Optional opt = epConfig.instance(AtnObject.class); + if (opt.isPresent()) { + return opt.get(); + } + + // 2) configuration in request + opt = epConfig.config("atn-object").flatMap(conf -> conf.map(AtnObject::from).asOptional()); + if (opt.isPresent()) { + return opt.get(); + } + + // 3) annotations on target + List annots = new ArrayList<>(); + for (SecurityLevel securityLevel : epConfig.securityLevels()) { + annots.addAll(securityLevel.combineAnnotations(AtnAnnot.class, EndpointConfig.AnnotationScope.values())); + } + if (annots.isEmpty()) { + return null; + } else { + return AtnObject.from(annots.get(0)); + } + } + + @Override + public Collection> supportedAnnotations() { + return Set.of(AtnAnnot.class); + } + + /** + * This is an example annotation to see how to work with them. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) + @Documented + public @interface AtnAnnot { + /** + * This is an example value. + * + * @return some value + */ + String value(); + + /** + * This is an example value. + * + * @return some size + */ + int size() default 4; + } + + /** + * This is an example custom object. + * Also acts as an object to get configuration in config. + */ + public static class AtnObject { + private String value; + private int size = 4; + + /** + * Load this object instance from configuration. + * + * @param config configuration + * @return a new instance + */ + public static AtnObject from(Config config) { + AtnObject result = new AtnObject(); + config.get("value").asString().ifPresent(result::setValue); + config.get("size").asInt().ifPresent(result::setSize); + return result; + } + + static AtnObject from(AtnAnnot annot) { + AtnObject result = new AtnObject(); + result.setValue(annot.value()); + result.setSize(annot.size()); + return result; + } + + static AtnObject from(String value, int size) { + AtnObject result = new AtnObject(); + result.setValue(value); + result.setSize(size); + return result; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public boolean isValid() { + return null != value; + } + } +} diff --git a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtzProviderSync.java b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtzProviderSync.java new file mode 100644 index 000000000..8c61cd13e --- /dev/null +++ b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtzProviderSync.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.examples.spi; + +import io.helidon.security.AuthorizationResponse; +import io.helidon.security.ProviderRequest; +import io.helidon.security.spi.AuthorizationProvider; + +/** + * Authorization provider example. The most simplistic approach. + * + * @see AtnProviderImpl on how to use custom objects, config and annotations in a provider + */ +public class AtzProviderSync implements AuthorizationProvider { + @Override + public AuthorizationResponse authorize(ProviderRequest providerRequest) { + // just check the path contains the string "public", otherwise allow only if user is logged in + // if no path is defined, abstain (e.g. I do not care about such requests - I can neither allow or deny them) + return providerRequest.env().path() + .map(path -> { + if (path.contains("public")) { + return AuthorizationResponse.permit(); + } + if (providerRequest.securityContext().isAuthenticated()) { + return AuthorizationResponse.permit(); + } else { + return AuthorizationResponse.deny(); + } + }).orElse(AuthorizationResponse.abstain()); + } +} diff --git a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/Auditer.java b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/Auditer.java new file mode 100644 index 000000000..f82963a62 --- /dev/null +++ b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/Auditer.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.examples.spi; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; + +import io.helidon.security.spi.AuditProvider; + +/** + * Audit provider implementation. + */ +public class Auditer implements AuditProvider { + // BEWARE this is a memory leak. Only for example purposes and for unit-tests + private final List messages = new LinkedList<>(); + + public List getMessages() { + return messages; + } + + @Override + public Consumer auditConsumer() { + return event -> { + // just dump to stdout and store in a list + System.out.println(event.severity() + ": " + event.tracingId() + ": " + event); + messages.add(event); + }; + } + +} diff --git a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/OutboundProviderSync.java b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/OutboundProviderSync.java new file mode 100644 index 000000000..234b174c8 --- /dev/null +++ b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/OutboundProviderSync.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.examples.spi; + +import java.util.List; +import java.util.Map; + +import io.helidon.security.EndpointConfig; +import io.helidon.security.OutboundSecurityResponse; +import io.helidon.security.Principal; +import io.helidon.security.ProviderRequest; +import io.helidon.security.SecurityEnvironment; +import io.helidon.security.Subject; +import io.helidon.security.spi.OutboundSecurityProvider; + +/** + * Example of a simplistic outbound security provider. + */ +public class OutboundProviderSync implements OutboundSecurityProvider { + @Override + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { + + // let's just add current user's id as a custom header, otherwise do nothing + return providerRequest.securityContext() + .user() + .map(Subject::principal) + .map(Principal::getName) + .map(name -> OutboundSecurityResponse + .withHeaders(Map.of("X-AUTH-USER", List.of(name)))) + .orElse(OutboundSecurityResponse.abstain()); + } +} diff --git a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/ProviderSelector.java b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/ProviderSelector.java new file mode 100644 index 000000000..cfe913b29 --- /dev/null +++ b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/ProviderSelector.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.examples.spi; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +import io.helidon.security.NamedProvider; +import io.helidon.security.spi.OutboundSecurityProvider; +import io.helidon.security.spi.ProviderSelectionPolicy; +import io.helidon.security.spi.SecurityProvider; + +/** + * Simple selector of providers, just chooses first, except for outbound, where it returns all. + */ +public class ProviderSelector implements ProviderSelectionPolicy { + private final Providers providers; + private final List outboundProviders = new LinkedList<>(); + + private ProviderSelector(Providers providers) { + this.providers = providers; + + providers.getProviders(OutboundSecurityProvider.class) + .forEach(np -> outboundProviders.add(np.getProvider())); + } + + /** + * This is the function to register with security. + * + * @param providers Providers from security + * @return selector instance + * @see io.helidon.security.Security.Builder#providerSelectionPolicy(java.util.function.Function) + */ + public static ProviderSelector create(Providers providers) { + return new ProviderSelector(providers); + } + + @Override + public Optional selectProvider(Class providerType) { + List> providers = this.providers.getProviders(providerType); + + if (providers.isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(providers.get(0).getProvider()); + } + } + + @Override + public List selectOutboundProviders() { + return outboundProviders; + } + + @Override + public Optional selectProvider(Class providerType, String requestedName) { + return this.providers.getProviders(providerType) + .stream() + .filter(provider -> provider.getName().equals(requestedName)) + .findFirst() + .map(NamedProvider::getProvider); + } +} diff --git a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/package-info.java b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/package-info.java new file mode 100644 index 000000000..c427ec51d --- /dev/null +++ b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Examples of SPI implementation. + * + * @see io.helidon.security.spi.AuthenticationProvider + */ +package io.helidon.security.examples.spi; diff --git a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtnProviderSyncTest.java b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtnProviderSyncTest.java new file mode 100644 index 000000000..d7be0e6de --- /dev/null +++ b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtnProviderSyncTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.examples.spi; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.security.AuthenticationResponse; +import io.helidon.security.EndpointConfig; +import io.helidon.security.ProviderRequest; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.SecurityEnvironment; +import io.helidon.security.SecurityLevel; +import io.helidon.security.SecurityResponse; +import io.helidon.security.Subject; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit test for {@link AtnProviderImpl}. + */ +public class AtnProviderSyncTest { + private static final String VALUE = "aValue"; + private static final int SIZE = 16; + + @Test + public void testAbstain() { + SecurityContext context = mock(SecurityContext.class); + when(context.user()).thenReturn(Optional.empty()); + when(context.service()).thenReturn(Optional.empty()); + + SecurityEnvironment se = SecurityEnvironment.create(); + EndpointConfig ep = EndpointConfig.create(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.securityContext()).thenReturn(context); + when(request.env()).thenReturn(se); + when(request.endpointConfig()).thenReturn(ep); + + AtnProviderImpl provider = new AtnProviderImpl(); + + AuthenticationResponse response = provider.authenticate(request); + + assertThat(response.status(), is(SecurityResponse.SecurityStatus.ABSTAIN)); + } + + @Test + public void testAnnotationSuccess() { + AtnProviderImpl.AtnAnnot annot = new AtnProviderImpl.AtnAnnot() { + @Override + public String value() { + return VALUE; + } + + @Override + public int size() { + return SIZE; + } + + @Override + public Class annotationType() { + return AtnProviderImpl.AtnAnnot.class; + } + }; + + SecurityContext context = mock(SecurityContext.class); + when(context.user()).thenReturn(Optional.empty()); + when(context.service()).thenReturn(Optional.empty()); + + SecurityEnvironment se = SecurityEnvironment.create(); + + SecurityLevel level = SecurityLevel.create("mock") + .withClassAnnotations(Map.of(AtnProviderImpl.AtnAnnot.class, List.of(annot))) + .build(); + + EndpointConfig ep = EndpointConfig.builder() + .securityLevels(List.of(level)) + .build(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.securityContext()).thenReturn(context); + when(request.env()).thenReturn(se); + when(request.endpointConfig()).thenReturn(ep); + + testSuccess(request); + } + + @Test + public void testCustomObjectSuccess() { + AtnProviderImpl.AtnObject obj = new AtnProviderImpl.AtnObject(); + obj.setSize(SIZE); + obj.setValue(VALUE); + + SecurityContext context = mock(SecurityContext.class); + when(context.user()).thenReturn(Optional.empty()); + when(context.service()).thenReturn(Optional.empty()); + + SecurityEnvironment se = SecurityEnvironment.create(); + EndpointConfig ep = EndpointConfig.builder() + .customObject(AtnProviderImpl.AtnObject.class, obj) + .build(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.securityContext()).thenReturn(context); + when(request.env()).thenReturn(se); + when(request.endpointConfig()).thenReturn(ep); + + testSuccess(request); + } + + @Test + public void testConfigSuccess() { + Config config = Config.create( + ConfigSources.create(Map.of("value", VALUE, + "size", String.valueOf(SIZE))) + ); + + SecurityContext context = mock(SecurityContext.class); + when(context.user()).thenReturn(Optional.empty()); + when(context.service()).thenReturn(Optional.empty()); + + SecurityEnvironment se = SecurityEnvironment.create(); + EndpointConfig ep = EndpointConfig.builder() + .config("atn-object", config) + .build(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.securityContext()).thenReturn(context); + when(request.env()).thenReturn(se); + when(request.endpointConfig()).thenReturn(ep); + + + testSuccess(request); + } + + @Test + public void testFailure() { + Config config = Config.create( + ConfigSources.create(Map.of("atn-object.size", String.valueOf(SIZE))) + ); + + SecurityContext context = mock(SecurityContext.class); + when(context.user()).thenReturn(Optional.empty()); + when(context.service()).thenReturn(Optional.empty()); + + SecurityEnvironment se = SecurityEnvironment.create(); + EndpointConfig ep = EndpointConfig.builder() + .config("atn-object", config) + .build(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.securityContext()).thenReturn(context); + when(request.env()).thenReturn(se); + when(request.endpointConfig()).thenReturn(ep); + + AtnProviderImpl provider = new AtnProviderImpl(); + + AuthenticationResponse response = provider.authenticate(request); + + assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); + } + + @Test + public void integrationTest() { + Security security = Security.builder() + .addProvider(new AtnProviderImpl()) + .build(); + + // this part is usually done by container integration component + // in Jersey you have access to security context through annotations + // in Web server you have access to security context through context + SecurityContext context = security.createContext("unit-test"); + context.endpointConfig(EndpointConfig.builder() + .customObject(AtnProviderImpl.AtnObject.class, + AtnProviderImpl.AtnObject.from(VALUE, SIZE))); + AuthenticationResponse response = context.authenticate(); + + validateResponse(response); + } + + private void validateResponse(AuthenticationResponse response) { + assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); + Optional maybeuser = response.user(); + + maybeuser.ifPresentOrElse(user -> { + assertThat(user.principal().id(), is(VALUE)); + Set roles = Security.getRoles(user); + assertThat(roles.size(), is(1)); + assertThat(roles.iterator().next(), is("role_" + SIZE)); + }, () -> fail("User should have been returned")); + } + + private void testSuccess(ProviderRequest request) { + AtnProviderImpl provider = new AtnProviderImpl(); + + AuthenticationResponse response = provider.authenticate(request); + validateResponse(response); + } +} diff --git a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtzProviderSyncTest.java b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtzProviderSyncTest.java new file mode 100644 index 000000000..ce694cc50 --- /dev/null +++ b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtzProviderSyncTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.examples.spi; + +import io.helidon.security.AuthorizationResponse; +import io.helidon.security.EndpointConfig; +import io.helidon.security.ProviderRequest; +import io.helidon.security.SecurityContext; +import io.helidon.security.SecurityEnvironment; +import io.helidon.security.SecurityResponse; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit test for {@link AtzProviderSync}. + */ +public class AtzProviderSyncTest { + @Test + public void testPublic() { + SecurityEnvironment se = SecurityEnvironment.builder() + .path("/public/some/path") + .build(); + EndpointConfig ep = EndpointConfig.create(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.env()).thenReturn(se); + when(request.endpointConfig()).thenReturn(ep); + + AtzProviderSync provider = new AtzProviderSync(); + + AuthorizationResponse response = provider.authorize(request); + + assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); + } + + @Test + public void testAbstain() { + SecurityEnvironment se = SecurityEnvironment.create(); + EndpointConfig ep = EndpointConfig.create(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.env()).thenReturn(se); + when(request.endpointConfig()).thenReturn(ep); + + AtzProviderSync provider = new AtzProviderSync(); + + AuthorizationResponse response = provider.authorize(request); + + assertThat(response.status(), is(SecurityResponse.SecurityStatus.ABSTAIN)); + } + + @Test + public void testDenied() { + SecurityContext context = mock(SecurityContext.class); + when(context.isAuthenticated()).thenReturn(false); + + SecurityEnvironment se = SecurityEnvironment.builder() + .path("/private/some/path") + .build(); + EndpointConfig ep = EndpointConfig.create(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.securityContext()).thenReturn(context); + when(request.env()).thenReturn(se); + when(request.endpointConfig()).thenReturn(ep); + + AtzProviderSync provider = new AtzProviderSync(); + + AuthorizationResponse response = provider.authorize(request); + + assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); + } + + @Test + public void testPermitted() { + SecurityContext context = mock(SecurityContext.class); + when(context.isAuthenticated()).thenReturn(true); + + SecurityEnvironment se = SecurityEnvironment.builder() + .path("/private/some/path") + .build(); + EndpointConfig ep = EndpointConfig.create(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.securityContext()).thenReturn(context); + when(request.env()).thenReturn(se); + when(request.endpointConfig()).thenReturn(ep); + + AtzProviderSync provider = new AtzProviderSync(); + + AuthorizationResponse response = provider.authorize(request); + + assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); + } +} diff --git a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AuditerTest.java b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AuditerTest.java new file mode 100644 index 000000000..8b048c00b --- /dev/null +++ b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AuditerTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.examples.spi; + +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.security.AuditEvent; +import io.helidon.security.AuthorizationResponse; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.SecurityEnvironment; +import io.helidon.security.spi.AuditProvider; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Unit test for {@link Auditer}. + */ +public class AuditerTest { + @Test + public void integrateIt() throws InterruptedException { + Auditer auditer = new Auditer(); + + Security sec = Security.builder() + .addAuthorizationProvider(new AtzProviderSync()) + .addAuditProvider(auditer) + .build(); + + SecurityContext context = sec.createContext("unit-test"); + context.env(SecurityEnvironment.builder() + .path("/public/path")); + + AuthorizationResponse response = context.authorize(); + + // as auditing is asynchronous, we must give it some time to process + Thread.sleep(100); + + List messages = auditer.getMessages(); + // there should be two messages - configuration of security and authorization + + List atzEvents = messages.stream() + .filter(event -> event.eventType().startsWith(AuditEvent.AUTHZ_TYPE_PREFIX)) + .collect(Collectors.toList()); + + assertThat("We only expect a single authorization event", atzEvents.size(), is(1)); + + } +} diff --git a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/OutboundProviderSyncTest.java b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/OutboundProviderSyncTest.java new file mode 100644 index 000000000..8ba15c9c8 --- /dev/null +++ b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/OutboundProviderSyncTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.examples.spi; + +import java.util.List; +import java.util.Optional; + +import io.helidon.security.EndpointConfig; +import io.helidon.security.OutboundSecurityResponse; +import io.helidon.security.Principal; +import io.helidon.security.ProviderRequest; +import io.helidon.security.SecurityContext; +import io.helidon.security.SecurityEnvironment; +import io.helidon.security.SecurityResponse; +import io.helidon.security.Subject; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit test for {@link OutboundProviderSync}. + */ +public class OutboundProviderSyncTest { + @Test + public void testAbstain() { + SecurityContext context = mock(SecurityContext.class); + when(context.user()).thenReturn(Optional.empty()); + when(context.service()).thenReturn(Optional.empty()); + + SecurityEnvironment se = SecurityEnvironment.create(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.securityContext()).thenReturn(context); + when(request.env()).thenReturn(se); + + OutboundProviderSync ops = new OutboundProviderSync(); + OutboundSecurityResponse response = ops.outboundSecurity(request, SecurityEnvironment.create(), EndpointConfig.create()); + + assertThat(response.status(), is(SecurityResponse.SecurityStatus.ABSTAIN)); + } + + @Test + public void testSuccess() { + String username = "aUser"; + Subject subject = Subject.create(Principal.create(username)); + + SecurityContext context = mock(SecurityContext.class); + when(context.user()).thenReturn(Optional.of(subject)); + when(context.service()).thenReturn(Optional.empty()); + + SecurityEnvironment se = SecurityEnvironment.create(); + + ProviderRequest request = mock(ProviderRequest.class); + when(request.securityContext()).thenReturn(context); + when(request.env()).thenReturn(se); + + OutboundProviderSync ops = new OutboundProviderSync(); + OutboundSecurityResponse response = ops.outboundSecurity(request, SecurityEnvironment.create(), EndpointConfig.create()); + + assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); + assertThat(response.requestHeaders().get("X-AUTH-USER"), is(List.of(username))); + } +} diff --git a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/ProviderSelectorTest.java b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/ProviderSelectorTest.java new file mode 100644 index 000000000..68c5ffe30 --- /dev/null +++ b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/ProviderSelectorTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.security.examples.spi; + +import io.helidon.security.AuthorizationResponse; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.SecurityEnvironment; + +import org.junit.jupiter.api.Test; + +/** + * Unit test for {@link ProviderSelector}. + */ +public class ProviderSelectorTest { + @Test + public void integrateIt() { + Security security = Security.builder() + .providerSelectionPolicy(ProviderSelector::create) + .addProvider(new AtnProviderImpl()) + .addProvider(new AtzProviderSync()) + .build(); + + SecurityContext context = security.createContext("unit-test"); + context.env(SecurityEnvironment.builder().path("/public/path")); + + AuthorizationResponse response = context.authorize(); + + // if we reached here, the policy worked + } +} diff --git a/examples/security/vaults/README.md b/examples/security/vaults/README.md new file mode 100644 index 000000000..6ee954528 --- /dev/null +++ b/examples/security/vaults/README.md @@ -0,0 +1,34 @@ +Vaults example +---- + +This example demonstrates the use of `Security` to: +- access secrets +- generate digests (such as Signatures and HMAC) +- verify digests (dtto) +- encrypt secret text +- decrypt cipher text + +The example uses three implementations of security providers that implement these features: + +1. OCI Vault provider (supports all) +2. Config provider (supports encryption/decryption and secrets) +3. Hashicorp (HCP) Vault provider (supports all + HMAC digest) + + +# OCI Vault + +The following information/configuration is needed: + +1. `~/.oci/config` file should be present (TODO link to description how to get it) +2. A secret (for password) must be created and its OCID configured in `${oci.properties.secret-ocid}` +3. An RSA key must be created and its OCID configured in `${oci.properties.vault-rsa-key-ocid}` for signature +4. Key must be created and its OCID configured in `${oci.properties.vault-key-ocid}` for encryption + +# HCP Vault + +1. Vault address must be defined in `vault.address` +2. Vault token must be defined in `vault.token` +3. A secret must be defined in the default secrets under path `app/secret` with key `username` defining a user +4. Vault `transit` secret engine must be enabled +5. A key named `signature-key` must be created (RSA) in `transit` secret engine for signature +6. A key named `encryption-key` must be created in `transit` secret engine for encryption and HMAC diff --git a/examples/security/vaults/pom.xml b/examples/security/vaults/pom.xml new file mode 100644 index 000000000..8d326da54 --- /dev/null +++ b/examples/security/vaults/pom.xml @@ -0,0 +1,104 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.security + helidon-examples-security-vaults + 1.0.0-SNAPSHOT + Helidon Examples Security Vaults + + + This example demonstrates usage of vault implementations - OCI, Hashicorp Vault, and Config based vault + + + + io.helidon.examples.security.vaults.VaultsExampleMain + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.config + helidon-config-encryption + + + io.helidon.security + helidon-security + + + io.helidon.security.providers + helidon-security-providers-config-vault + + + io.helidon.webserver + helidon-webserver + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-kv2 + + + io.helidon.integrations.vault.secrets + helidon-integrations-vault-secrets-transit + + + io.helidon.integrations.vault.auths + helidon-integrations-vault-auths-token + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-core + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/DigestService.java b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/DigestService.java new file mode 100644 index 000000000..b4bcbf285 --- /dev/null +++ b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/DigestService.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.vaults; + +import io.helidon.security.Security; +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 static java.nio.charset.StandardCharsets.UTF_8; + +class DigestService implements HttpService { + private final Security security; + + DigestService(Security security) { + this.security = security; + } + + + @Override + public void routing(HttpRules rules) { + rules.get("/digest/{config}/{text}", this::digest) + .get("/verify/{config}/{text}/{digest:.*}", this::verify); + } + + private void digest(ServerRequest req, ServerResponse res) { + String configName = req.path().pathParameters().get("config"); + String text = req.path().pathParameters().get("text"); + + res.send(security.digest(configName, text.getBytes(UTF_8))); + } + + private void verify(ServerRequest req, ServerResponse res) { + String configName = req.path().pathParameters().get("config"); + String text = req.path().pathParameters().get("text"); + String digest = req.path().pathParameters().get("digest"); + + boolean valid = security.verifyDigest(configName, text.getBytes(UTF_8), digest); + res.send(valid ? "Valid" : "Invalid"); + } +} diff --git a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/EncryptionService.java b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/EncryptionService.java new file mode 100644 index 000000000..b983778f3 --- /dev/null +++ b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/EncryptionService.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.vaults; + +import io.helidon.security.Security; +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 static java.nio.charset.StandardCharsets.UTF_8; + +class EncryptionService implements HttpService { + private final Security security; + + EncryptionService(Security security) { + this.security = security; + } + + + @Override + public void routing(HttpRules rules) { + rules.get("/encrypt/{config}/{text:.*}", this::encrypt) + .get("/decrypt/{config}/{cipherText:.*}", this::decrypt); + } + + private void encrypt(ServerRequest req, ServerResponse res) { + String configName = req.path().pathParameters().get("config"); + String text = req.path().pathParameters().get("text"); + + res.send(security.encrypt(configName, text.getBytes(UTF_8))); + } + + private void decrypt(ServerRequest req, ServerResponse res) { + String configName = req.path().pathParameters().get("config"); + String cipherText = req.path().pathParameters().get("cipherText"); + + res.send(security.decrypt(configName, cipherText)); + } +} diff --git a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/SecretsService.java b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/SecretsService.java new file mode 100644 index 000000000..ee24c8f7b --- /dev/null +++ b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/SecretsService.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.vaults; + +import io.helidon.security.Security; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +class SecretsService implements HttpService { + private final Security security; + + SecretsService(Security security) { + this.security = security; + } + + + @Override + public void routing(HttpRules rules) { + rules.get("/{name}", this::secret); + } + + private void secret(ServerRequest req, ServerResponse res) { + String secretName = req.path().pathParameters().get("name"); + res.send(security.secret(secretName, "default-" + secretName)); + } +} diff --git a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/VaultsExampleMain.java b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/VaultsExampleMain.java new file mode 100644 index 000000000..ea8a8e545 --- /dev/null +++ b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/VaultsExampleMain.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.vaults; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.security.Security; +import io.helidon.webserver.WebServer; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * Main class of the example based on configuration. + */ +public final class VaultsExampleMain { + private VaultsExampleMain() { + } + + /** + * Start the server. + * + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + // as I cannot share my configuration of OCI, let's combine the configuration + // from my home directory with the one compiled into the jar + // when running this example, you can either update the application.yaml in resources directory + // or use the same approach + Config config = buildConfig(); + + System.out.println("This example requires a valid OCI Vault, Secret and keys configured. It also requires " + + "a Hashicorp Vault running with preconfigured data. Please see README.md"); + + Security security = Security.create(config.get("security")); + + WebServer server = WebServer.builder() + .config(config.get("server")) + .routing(routing -> routing + .register("/secrets", new SecretsService(security)) + .register("/encryption", new EncryptionService(security)) + .register("/digests", new DigestService(security))) + .build() + .start(); + + System.out.println("Server started on port: " + server.port()); + String baseAddress = "http://localhost:" + server.port() + "/"; + + System.out.println("Secrets endpoints:"); + System.out.println(); + System.out.println("OCI secret:"); + System.out.println("\t" + baseAddress + "secrets/password"); + System.out.println("Config secret:"); + System.out.println("\t" + baseAddress + "secrets/token"); + System.out.println("HCP Vault secret:"); + System.out.println("\t" + baseAddress + "secrets/username"); + System.out.println(); + + System.out.println("Encryption endpoints:"); + System.out.println("OCI encrypted:"); + System.out.println("\t" + baseAddress + "encryption/encrypt/crypto-1/text"); + System.out.println("\t" + baseAddress + "encryption/decrypt/crypto-1/cipherText"); + System.out.println("Config encrypted:"); + System.out.println("\t" + baseAddress + "encryption/encrypt/crypto-2/text"); + System.out.println("\t" + baseAddress + "encryption/decrypt/crypto-2/cipherText"); + System.out.println("HCP Vault encrypted:"); + System.out.println("\t" + baseAddress + "encryption/encrypt/crypto-3/text"); + System.out.println("\t" + baseAddress + "encryption/decrypt/crypto-3/cipherText"); + System.out.println(); + + System.out.println("Signature/HMAC endpoints:"); + System.out.println("OCI Signature:"); + System.out.println("\t" + baseAddress + "digests/digest/sig-1/text"); + System.out.println("\t" + baseAddress + "digests/verify/sig-1/text/signature"); + System.out.println("HCP Vault Signature:"); + System.out.println("\t" + baseAddress + "digests/digest/sig-2/text"); + System.out.println("\t" + baseAddress + "digests/digest/sig-2/text/signature"); + System.out.println("HCP Vault HMAC:"); + System.out.println("\t" + baseAddress + "digests/digest/hmac-1/text"); + System.out.println("\t" + baseAddress + "digests/digest/hmac-2/text/hmac"); + } + + private static Config buildConfig() { + return Config.builder() + .sources( + // you can use this file to override the defaults that are built-in + file(System.getProperty("user.home") + "/helidon/conf/examples.yaml").optional(), + // in jar file (see src/main/resources/application.yaml) + classpath("application.yaml")) + .build(); + } +} diff --git a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/package-info.java b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/package-info.java new file mode 100644 index 000000000..5dfa30def --- /dev/null +++ b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of basic vault operations available in {@link io.helidon.security.Security}. + */ +package io.helidon.examples.security.vaults; diff --git a/examples/security/vaults/src/main/resources/application.yaml b/examples/security/vaults/src/main/resources/application.yaml new file mode 100644 index 000000000..30fb258b1 --- /dev/null +++ b/examples/security/vaults/src/main/resources/application.yaml @@ -0,0 +1,92 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# All Hashicorp Vault configuration is commented out, as this PR only handles OCI +# This should be added in HCP Vault PR +# + +server: + port: 8080 + +app.token: "argb-urgx-harsps" + +vault: + token: "myroot" + address: "http://localhost:8200" + +security: + providers: + - config-vault: + master-password: "very much secret" + - vault-kv2: + address: "${vault.address}" + token: "${vault.token}" + - vault-transit: + address: "${vault.address}" + token: "${vault.token}" + # - oci-vault: + # We need either the cryptographic endpoint (recommended), or management-endpoint configured + # Also ~/.oci/config must be correctly set up, otherwise all information is required here + # The crypto endpoint may be configured per digest/encryption, as we may use more than one + # vault in a single application + # vault.cryptographic-endpoint: "${oci.properties.cryptographic-endpoint}" + # vault.management-endpoint: "${oci.properties.management-endpoint}" + secrets: + - name: "username" + provider: "vault-kv2" + config: + path: "app/secret" + key: "username" +# - name: "changeit" +# provider: "oci-vault" +# config: +# ocid: "${oci.properties.secret-ocid}" + - name: "token" + provider: "config-vault" + config: + value: "${app.token}" + digest: + # Signatures and hmac + - name: "sig-2" + provider: "vault-transit" + config: + type: "signature" + key-name: "signature-key" + - name: "hmac-1" + provider: "vault-transit" + config: + type: "hmac" + key-name: "encryption-key" +# - name: "sig-1" +# provider: "oci-vault" +# config: +# cryptographic-endpoint: "${oci.properties.cryptographic-endpoint}" +# key-ocid: "${oci.properties.vault-rsa-key-ocid}" +# algorithm: "SHA_256_RSA_PKCS_PSS" + encryption: + # encryption and decryption + - name: "crypto-3" + provider: "vault-transit" + config: + key-name: "encryption-key" +# - name: "crypto-1" +# provider: "oci-vault" +# config: +# cryptographic-endpoint: "${oci.properties.cryptographic-endpoint}" +# key-ocid: "${oci.properties.vault-key-ocid}" + - name: "crypto-2" + provider: "config-vault" diff --git a/examples/security/vaults/src/main/resources/logging.properties b/examples/security/vaults/src/main/resources/logging.properties new file mode 100644 index 000000000..8e28f63f9 --- /dev/null +++ b/examples/security/vaults/src/main/resources/logging.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.level=INFO +io.helidon.integrations.level=INFO diff --git a/examples/security/webserver-digest-auth/README.md b/examples/security/webserver-digest-auth/README.md new file mode 100644 index 000000000..6ad8f4b5d --- /dev/null +++ b/examples/security/webserver-digest-auth/README.md @@ -0,0 +1,31 @@ +# Web Server Integration and Digest Authentication + +This example demonstrates Integration of WebServer +based application with Security component and Digest authentication (from HttpAuthProvider). + +## Contents + +There are two examples with exactly the same behavior: +1. DigestExampleMain - shows how to programmatically secure application +2. DigestExampleConfigMain - shows how to secure application with configuration + 1. see src/main/resources/application.yaml for configuration + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-security-webserver-digest-auth.jar +``` + +Try the application: + +The application starts on a random port, the following assumes it is `56551` +```shell +export PORT=45351 +curl http://localhost:${PORT}/public +curl --digest -u "jill:changeit" http://localhost:${PORT}/noRoles +curl --digest -u "john:changeit" http://localhost:${PORT}/user +curl --digest -u "jack:changeit" http://localhost:${PORT}/admin +curl -v --digest -u "john:changeit" http://localhost:${PORT}/deny +curl --digest -u "jack:changeit" http://localhost:${PORT}/noAuthn +``` diff --git a/examples/security/webserver-digest-auth/pom.xml b/examples/security/webserver-digest-auth/pom.xml new file mode 100644 index 000000000..8d704b04e --- /dev/null +++ b/examples/security/webserver-digest-auth/pom.xml @@ -0,0 +1,108 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.security + helidon-examples-security-webserver-digest-auth + 1.0.0-SNAPSHOT + Helidon Examples Security Digest Authentication + + + This example demonstrates Integration of Web Server based application with Security component and Digest + authentication (from HttpAuthProvider). + + + + io.helidon.examples.security.digest.DigestExampleConfigMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-security + + + io.helidon.config + helidon-config-encryption + + + io.helidon.security.providers + helidon-security-providers-http-auth + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webclient + helidon-webclient + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-core + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/security/webserver-digest-auth/src/main/java/io/helidon/examples/security/digest/DigestExampleBuilderMain.java b/examples/security/webserver-digest-auth/src/main/java/io/helidon/examples/security/digest/DigestExampleBuilderMain.java new file mode 100644 index 000000000..72a34dbef --- /dev/null +++ b/examples/security/webserver-digest-auth/src/main/java/io/helidon/examples/security/digest/DigestExampleBuilderMain.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.digest; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import io.helidon.http.HttpMediaTypes; +import io.helidon.logging.common.LogConfig; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.providers.httpauth.HttpDigest; +import io.helidon.security.providers.httpauth.HttpDigestAuthProvider; +import io.helidon.security.providers.httpauth.SecureUserStore; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.security.SecurityFeature; + +/** + * Example of HTTP digest authentication with WebServer fully configured programmatically. + */ +@SuppressWarnings("SpellCheckingInspection") +public final class DigestExampleBuilderMain { + // simple approach to user storage - for real world, use data store... + private static final Map USERS = new HashMap<>(); + + private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); + + static { + USERS.put("jack", new MyUser("jack", "changeit".toCharArray(), Set.of("user", "admin"))); + USERS.put("jill", new MyUser("jill", "changeit".toCharArray(), Set.of("user"))); + USERS.put("john", new MyUser("john", "changeit".toCharArray(), Set.of())); + } + + private DigestExampleBuilderMain() { + } + + /** + * Starts this example. Programmatic configuration. See standard output for instructions. + * + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + WebServerConfig.Builder builder = WebServer.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 + + Users: + jack/password in roles: user, admin + jill/password in roles: user + john/password in no roles + + *********************** + ** Endpoints: ** + *********************** + + No authentication: http://localhost:%2$d/public + No roles required, authenticated: http://localhost:%2$d/noRoles + User role required: http://localhost:%2$d/user + Admin role required: http://localhost:%2$d/admin + Always forbidden (uses role nobody is in), audited: http://localhost:%2$d/deny + Admin role required, authenticated, authentication optional, audited \ + (always forbidden - challenge is not returned as authentication is optional): http://localhost:%2$d/noAuthn + + """, TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS), server.port()); + } + + static void setup(WebServerConfig.Builder server) { + server.featuresDiscoverServices(false) + .addFeature(SecurityFeature.builder() + .security(security()) + .defaults(SecurityFeature.authenticate()) + .build()) + .routing(routing -> routing + .get("/noRoles", SecurityFeature.enforce()) + .get("/user[/{*}]", SecurityFeature.rolesAllowed("user")) + .get("/admin", SecurityFeature.rolesAllowed("admin")) + // audit is not enabled for GET methods by default + .get("/deny", SecurityFeature.rolesAllowed("deny").audit()) + // roles allowed imply authn and authz + .any("/noAuthn", SecurityFeature.rolesAllowed("admin") + .authenticationOptional() + .audit()) + .get("/{*}", (req, res) -> { + Optional securityContext = req.context().get(SecurityContext.class); + res.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8); + res.send("Hello, you are: \n" + securityContext + .map(ctx -> ctx.user().orElse(SecurityContext.ANONYMOUS).toString()) + .orElse("Security context is null")); + })); + } + + private static Security security() { + return Security.builder() + .addAuthenticationProvider( + HttpDigestAuthProvider.builder() + .realm("mic") + .digestServerSecret("changeit".toCharArray()) + .userStore(buildUserStore()), + "digest-auth") + .build(); + } + + private static SecureUserStore buildUserStore() { + return login -> Optional.ofNullable(USERS.get(login)); + } + + private record MyUser(String login, char[] password, Set roles) implements SecureUserStore.User { + + private static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + @Override + public boolean isPasswordValid(char[] password) { + return Arrays.equals(password(), password); + } + + @Override + public Optional digestHa1(String realm, HttpDigest.Algorithm algorithm) { + if (algorithm != HttpDigest.Algorithm.MD5) { + throw new IllegalArgumentException("Unsupported algorithm " + algorithm); + } + String a1 = login + ":" + realm + ":" + new String(password()); + byte[] bytes = a1.getBytes(StandardCharsets.UTF_8); + MessageDigest digest; + try { + digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("MD5 algorithm should be supported", e); + } + return Optional.of(bytesToHex(digest.digest(bytes))); + } + } +} diff --git a/examples/security/webserver-digest-auth/src/main/java/io/helidon/examples/security/digest/DigestExampleConfigMain.java b/examples/security/webserver-digest-auth/src/main/java/io/helidon/examples/security/digest/DigestExampleConfigMain.java new file mode 100644 index 000000000..3499a07e8 --- /dev/null +++ b/examples/security/webserver-digest-auth/src/main/java/io/helidon/examples/security/digest/DigestExampleConfigMain.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.digest; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import io.helidon.config.Config; +import io.helidon.http.HttpMediaTypes; +import io.helidon.logging.common.LogConfig; +import io.helidon.security.SecurityContext; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; + +/** + * Example of HTTP digest authentication with Web Server fully configured in config file. + */ +public final class DigestExampleConfigMain { + private DigestExampleConfigMain() { + } + + /** + * Starts this example. Loads configuration from src/main/resources/application.conf. See standard output for instructions. + * + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + WebServerConfig.Builder builder = WebServer.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 + + Users: + jack/password in roles: user, admin + jill/password in roles: user + john/password in no roles + + *********************** + ** Endpoints: ** + *********************** + + No authentication: http://localhost:%2$d/public + No roles required, authenticated: http://localhost:%2$d/noRoles + User role required: http://localhost:%2$d/user + Admin role required: http://localhost:%2$d/admin + Always forbidden (uses role nobody is in), audited: http://localhost:%2$d/deny + Admin role required, authenticated, authentication optional, audited \ + (always forbidden - challenge is not returned as authentication is optional): http://localhost:%2$d/noAuthn + + """, TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS), server.port()); + } + + static void setup(WebServerConfig.Builder server) { + Config config = Config.create(); + server.config(config.get("server")) + .routing(routing -> routing + // web server does not (yet) have possibility to configure routes in config files, so explicit... + .get("/{*}", (req, res) -> { + Optional securityContext = req.context().get(SecurityContext.class); + res.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8); + res.send("Hello, you are: \n" + securityContext + .map(ctx -> ctx.user().orElse(SecurityContext.ANONYMOUS).toString()) + .orElse("Security context is null")); + })); + } +} diff --git a/examples/security/webserver-digest-auth/src/main/java/io/helidon/examples/security/digest/package-info.java b/examples/security/webserver-digest-auth/src/main/java/io/helidon/examples/security/digest/package-info.java new file mode 100644 index 000000000..8c57ea1c7 --- /dev/null +++ b/examples/security/webserver-digest-auth/src/main/java/io/helidon/examples/security/digest/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of Digest authentication on top of RX web server. + * + * @see io.helidon.examples.security.digest.DigestExampleConfigMain Configuration based example + * @see io.helidon.examples.security.digest.DigestExampleBuilderMain Programmatic example + */ +package io.helidon.examples.security.digest; diff --git a/examples/security/webserver-digest-auth/src/main/resources/application.yaml b/examples/security/webserver-digest-auth/src/main/resources/application.yaml new file mode 100644 index 000000000..cc933bea7 --- /dev/null +++ b/examples/security/webserver-digest-auth/src/main/resources/application.yaml @@ -0,0 +1,60 @@ +# +# Copyright (c) 2016, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + features: + security: + # Configuration of integration with web server + defaults: + authenticate: true + paths: + - path: "/noRoles" + methods: [ "get" ] + - path: "/user[/{*}]" + methods: [ "get" ] + roles-allowed: [ "user" ] + - path: "/admin" + methods: [ "get" ] + roles-allowed: [ "admin" ] + - path: "/deny" + methods: [ "get" ] + roles-allowed: [ "deny" ] + audit: true + - path: "/noAuthn" + roles-allowed: [ "admin" ] + authentication-optional: true + audit: true + + +security: + config: + # Configuration of secured config (encryption of passwords in property files) + # Set to true for production - if set to true, clear text passwords will cause failure + require-encryption: false + providers: + - http-digest-auth: + realm: "mic" + server-secret: "changeit" + users: + - login: "jack" + password: "${CLEAR=changeit}" + roles: ["user", "admin"] + - login: "jill" + password: "${CLEAR=changeit}" + roles: ["user"] + - login: "john" + password: "${CLEAR=changeit}" + roles: [] diff --git a/examples/security/webserver-digest-auth/src/main/resources/logging.properties b/examples/security/webserver-digest-auth/src/main/resources/logging.properties new file mode 100644 index 000000000..6901911c7 --- /dev/null +++ b/examples/security/webserver-digest-auth/src/main/resources/logging.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n +#All log level details +.level=WARNING +io.helidon.security.level=FINEST +AUDIT.level=FINEST diff --git a/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestAuthenticator.java b/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestAuthenticator.java new file mode 100644 index 000000000..a5efe46a9 --- /dev/null +++ b/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestAuthenticator.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.security.digest; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Poorly inspired from {@code org.glassfish.jersey.client.authentication.DigestAuthenticator}. + */ +class DigestAuthenticator { + + private static final char[] HEX_ARRAY = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + private static final Pattern KEY_VALUE_PAIR_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*(\"([^\"]+)\"|(\\w+))\\s*,?\\s*"); + + private final SecureRandom random = new SecureRandom(); + + /** + * Respond to the challenge. + * + * @param challenge response challenge + * @param uri request uri + * @param method request method + * @param username username + * @param password password + * @return authorization header value or {@code null} + */ + String authorization(String challenge, String uri, String method, String username, String password) { + DigestScheme ds = parseDigestScheme(challenge); + return ds != null ? header(ds, uri, method, username, password) : null; + } + + private String header(DigestScheme ds, String uri, String method, String username, String password) { + StringBuilder sb = new StringBuilder(100); + sb.append("Digest "); + append(sb, "username", username); + append(sb, "realm", ds.realm()); + append(sb, "nonce", ds.nonce()); + append(sb, "opaque", ds.opaque()); + append(sb, "algorithm", ds.algorithm(), false); + append(sb, "qop", ds.qop(), false); + append(sb, "uri", uri); + + String ha1; + if (ds.algorithm().equals("MD5_SESS")) { + ha1 = md5(md5(username, ds.realm(), password)); + } else { + ha1 = md5(username, ds.realm(), password); + } + + String ha2 = md5(method, uri); + String response; + if (ds.qop() == null) { + response = md5(ha1, ds.nonce(), ha2); + } else { + String cnonce = randomBytes(); // client nonce + append(sb, "cnonce", cnonce); + String nc = String.format("%08x", ds.nc.incrementAndGet()); // counter + append(sb, "nc", nc, false); + response = md5(ha1, ds.nonce(), nc, cnonce, ds.qop(), ha2); + } + append(sb, "response", response); + return sb.toString(); + } + + private static void append(StringBuilder sb, String key, String value, boolean useQuote) { + if (value == null) { + return; + } + if (sb.length() > 0) { + if (sb.charAt(sb.length() - 1) != ' ') { + sb.append(','); + } + } + sb.append(key); + sb.append('='); + if (useQuote) { + sb.append('"'); + } + sb.append(value); + if (useQuote) { + sb.append('"'); + } + } + + private static void append(StringBuilder sb, String key, String value) { + append(sb, key, value, true); + } + + private static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + int v; + for (int j = 0; j < bytes.length; j++) { + v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + private static String md5(String... tokens) { + StringBuilder sb = new StringBuilder(100); + for (String token : tokens) { + if (sb.length() > 0) { + sb.append(':'); + } + sb.append(token); + } + + MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex.getMessage()); + } + md.update(sb.toString().getBytes(StandardCharsets.UTF_8), 0, sb.length()); + byte[] md5hash = md.digest(); + return bytesToHex(md5hash); + } + + + private String randomBytes() { + byte[] bytes = new byte[4]; + random.nextBytes(bytes); + return bytesToHex(bytes); + } + + private record DigestScheme(String realm, + String nonce, + String opaque, + String qop, + String algorithm, + boolean stale, + AtomicInteger nc) { + } + + static DigestScheme parseDigestScheme(String header) { + String[] parts = header.trim().split("\\s+", 2); + if (parts.length != 2) { + return null; + } + if (!"digest".equals(parts[0].toLowerCase(Locale.ROOT))) { + return null; + } + String realm = null; + String nonce = null; + String opaque = null; + String qop = null; + String algorithm = null; + boolean stale = false; + Matcher match = KEY_VALUE_PAIR_PATTERN.matcher(parts[1]); + while (match.find()) { + // expect 4 groups (key)=("(val)" | (val)) + int nbGroups = match.groupCount(); + if (nbGroups != 4) { + continue; + } + String key = match.group(1); + String valNoQuotes = match.group(3); + String valQuotes = match.group(4); + String val = (valNoQuotes == null) ? valQuotes : valNoQuotes; + if ("qop".equals(key)) { + qop = val.contains("auth") ? "auth" : null; + } else if ("realm".equals(key)) { + realm = val; + } else if ("nonce".equals(key)) { + nonce = val; + } else if ("opaque".equals(key)) { + opaque = val; + } else if ("stale".equals(key)) { + stale = Boolean.parseBoolean(val); + } else if ("algorithm".equals(key)) { + if (val == null || val.isBlank()) { + continue; + } + val = val.trim(); + if (val.contains("MD5-sess") || val.contains("MD5-sess".toLowerCase(Locale.ROOT))) { + algorithm = "MD5-sess"; + } else { + algorithm = "MD5"; + } + } + } + return new DigestScheme(realm, nonce, opaque, qop, algorithm, stale, new AtomicInteger(0)); + } +} diff --git a/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestExampleBuilderTest.java b/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestExampleBuilderTest.java new file mode 100644 index 000000000..76f2b4153 --- /dev/null +++ b/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestExampleBuilderTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.digest; + +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.webserver.WebServerConfig; + +/** + * Unit test for {@link DigestExampleBuilderMain}. + */ +@ServerTest +public class DigestExampleBuilderTest extends DigestExampleTest { + + DigestExampleBuilderTest(Http1Client client, URI uri) { + super(client, uri); + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + DigestExampleBuilderMain.setup(server); + } +} diff --git a/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestExampleConfigTest.java b/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestExampleConfigTest.java new file mode 100644 index 000000000..0be085e84 --- /dev/null +++ b/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestExampleConfigTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.digest; + +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.webserver.WebServerConfig; + +/** + * Unit test for {@link DigestExampleConfigMain}. + */ +@ServerTest +public class DigestExampleConfigTest extends DigestExampleTest { + + DigestExampleConfigTest(Http1Client client, URI uri) { + super(client, uri); + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + DigestExampleConfigMain.setup(server); + } +} diff --git a/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestExampleTest.java b/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestExampleTest.java new file mode 100644 index 000000000..4c1aeb3fc --- /dev/null +++ b/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/DigestExampleTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.digest; + +import java.net.URI; +import java.util.Set; + +import io.helidon.http.HeaderNames; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; + +import org.junit.jupiter.api.Test; + +import static io.helidon.examples.security.digest.WebClientAuthenticationService.HTTP_AUTHENTICATION_PASSWORD; +import static io.helidon.examples.security.digest.WebClientAuthenticationService.HTTP_AUTHENTICATION_USERNAME; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Abstract class with tests for this example (used by programmatic and config based tests). + */ +public abstract class DigestExampleTest { + + private final Http1Client client; + private final Http1Client authClient; + + DigestExampleTest(Http1Client client, URI uri) { + this.client = client; + this.authClient = Http1Client.builder() + .baseUri(uri) + .addService(new WebClientAuthenticationService()) + .build(); + } + + //now for the tests + @Test + public void testPublic() { + //Must be accessible without authentication + try (Http1ClientResponse response = client.get().path("/public").request()) { + assertThat(response.status().code(), is(200)); + String entity = response.entity().as(String.class); + assertThat(entity, containsString("")); + } + } + + @Test + public void testNoRoles() { + String uri = "/noRoles"; + + testNotAuthorized(uri); + + //Must be accessible with authentication - to everybody + testProtected(uri, "jack", "changeit", Set.of("admin", "user"), Set.of()); + testProtected(uri, "jill", "changeit", Set.of("user"), Set.of("admin")); + testProtected(uri, "john", "changeit", Set.of(), Set.of("admin", "user")); + } + + @Test + public void testUserRole() { + String uri = "/user"; + + testNotAuthorized(uri); + + //Jack and Jill allowed (user role) + testProtected(uri, "jack", "changeit", Set.of("admin", "user"), Set.of()); + testProtected(uri, "jill", "changeit", Set.of("user"), Set.of("admin")); + testProtectedDenied(uri, "john", "changeit"); + } + + @Test + public void testAdminRole() { + String uri = "/admin"; + + testNotAuthorized(uri); + + //Only jack is allowed - admin role... + testProtected(uri, "jack", "changeit", Set.of("admin", "user"), Set.of()); + testProtectedDenied(uri, "jill", "changeit"); + testProtectedDenied(uri, "john", "changeit"); + } + + @Test + public void testDenyRole() { + String uri = "/deny"; + + testNotAuthorized(uri); + + // nobody has the correct role + testProtectedDenied(uri, "jack", "changeit"); + testProtectedDenied(uri, "jill", "changeit"); + testProtectedDenied(uri, "john", "changeit"); + } + + @Test + public void getNoAuthn() { + String uri = "/noAuthn"; + //Must NOT be accessible without authentication + try (Http1ClientResponse response = client.get(uri).request()) { + // authentication is optional, so we are not challenged, only forbidden, as the role can never be there... + assertThat(response.status().code(), is(403)); + + // doesn't matter, we are never challenged + testProtectedDenied(uri, "jack", "changeit"); + testProtectedDenied(uri, "jill", "changeit"); + testProtectedDenied(uri, "john", "changeit"); + } + } + + private void testNotAuthorized(String uri) { + //Must NOT be accessible without authentication + try (Http1ClientResponse response = client.get().path(uri).request()) { + assertThat(response.status().code(), is(401)); + String header = response.headers().first(HeaderNames.create("WWW-Authenticate")).orElse(null); + assertThat(header, notNullValue()); + assertThat(header.toLowerCase(), containsString("digest")); + assertThat(header, containsString("mic")); + } + } + + private Http1ClientResponse callProtected(String uri, String username, String password) { + // here we call the endpoint + return authClient + .get(uri) + .property(HTTP_AUTHENTICATION_USERNAME, username) + .property(HTTP_AUTHENTICATION_PASSWORD, password) + .request(); + } + + private void testProtectedDenied(String uri, String username, String password) { + try (Http1ClientResponse response = callProtected(uri, username, password)) { + assertThat(response.status().code(), is(403)); + } + } + + private void testProtected(String uri, + String username, + String password, + Set expectedRoles, + Set invalidRoles) { + + try (Http1ClientResponse response = callProtected(uri, username, password)) { + + assertThat(response.status().code(), is(200)); + + String entity = response.entity().as(String.class); + + // check login + assertThat(entity, containsString("id='" + username + "'")); + // check roles + expectedRoles.forEach(role -> assertThat(entity, containsString(":" + role))); + invalidRoles.forEach(role -> assertThat(entity, not(containsString(":" + role)))); + } + } + +} diff --git a/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/WebClientAuthenticationService.java b/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/WebClientAuthenticationService.java new file mode 100644 index 000000000..e80b7e334 --- /dev/null +++ b/examples/security/webserver-digest-auth/src/test/java/io/helidon/examples/security/digest/WebClientAuthenticationService.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.security.digest; + +import java.util.Map; + +import io.helidon.http.HeaderNames; +import io.helidon.http.Status; +import io.helidon.webclient.api.WebClientServiceRequest; +import io.helidon.webclient.api.WebClientServiceResponse; +import io.helidon.webclient.spi.WebClientService; + +/** + * Web client service that supports digest authentication. + * Temporary until https://github.com/helidon-io/helidon/issues/7207 is fixed. + */ +class WebClientAuthenticationService implements WebClientService { + + /** + * Property name for username. + */ + static final String HTTP_AUTHENTICATION_USERNAME = "helidon.config.client.http.auth.username"; + + /** + * Property name for password. + */ + static final String HTTP_AUTHENTICATION_PASSWORD = "helidon.config.client.http.auth.password"; + + private final DigestAuthenticator digestAuth = new DigestAuthenticator(); + + @Override + public WebClientServiceResponse handle(Chain chain, WebClientServiceRequest request) { + WebClientServiceResponse response = chain.proceed(request); + if (response.status() != Status.UNAUTHORIZED_401) { + return response; + } + Map properties = request.properties(); + String username = properties.get(HTTP_AUTHENTICATION_USERNAME); + String password = properties.get(HTTP_AUTHENTICATION_PASSWORD); + if (username == null || password == null) { + return response; + } + String challenge = response.headers().first(HeaderNames.WWW_AUTHENTICATE).orElse(null); + if (challenge == null) { + return response; + } + String uri = request.uri().path().path(); + String method = request.method().text(); + String atz = digestAuth.authorization(challenge, uri, method, username, password); + if (atz == null) { + return response; + } + request.headers().add(HeaderNames.AUTHORIZATION, atz); + return chain.proceed(request); + } +} diff --git a/examples/security/webserver-signatures/README.md b/examples/security/webserver-signatures/README.md new file mode 100644 index 000000000..a4870a885 --- /dev/null +++ b/examples/security/webserver-signatures/README.md @@ -0,0 +1,30 @@ +# Web Server Integration and HTTP Signatures + +This example demonstrates Integration of WebServer +based application with Security component and HTTP Signatures. + +## Contents + +There are two examples with exactly the same behavior +1. builder - shows how to programmatically secure application +2. config - shows how to secure application with configuration + 1. see `src/main/resources/service1.yaml` and `src/main/resources/service2.conf` for configuration +3. Each consists of two services + 1. "public" service protected by basic authentication (for simplicity) + 2. "internal" service protected by a combination of basic authentication (for user propagation) and http signature + (for service authentication) + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-security-webserver-signatures.jar +``` + +Try the endpoints (port is random, shall be replaced accordingly): +```shell +export PORT=39971 +curl -u "jack:changeit" http://localhost:${PORT}/service1 +curl -u "jill:changeit" http://localhost:${PORT}/service1-rsa +curl -v -u "john:changeit" http://localhost:${PORT}/service1 +``` diff --git a/examples/security/webserver-signatures/pom.xml b/examples/security/webserver-signatures/pom.xml new file mode 100644 index 000000000..c3dc38a62 --- /dev/null +++ b/examples/security/webserver-signatures/pom.xml @@ -0,0 +1,102 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.security + helidon-examples-security-webserver-signatures + 1.0.0-SNAPSHOT + Helidon Examples Security HTTP Signatures + + + This example demonstrates Integration of Web Server based application with Security component and HTTP + Signatures + + + + io.helidon.examples.security.signatures.SignatureExampleConfigMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-security + + + io.helidon.security.providers + helidon-security-providers-http-sign + + + io.helidon.webclient + helidon-webclient-security + + + io.helidon.bundles + helidon-bundles-security + + + io.helidon.bundles + helidon-bundles-config + + + io.helidon.config + helidon-config-hocon + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/Service1.java b/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/Service1.java new file mode 100644 index 000000000..62f2069ba --- /dev/null +++ b/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/Service1.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.security.signatures; + +import io.helidon.common.LazyValue; +import io.helidon.common.context.Contexts; +import io.helidon.http.HttpMediaTypes; +import io.helidon.http.Status; +import io.helidon.security.SecurityContext; +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; + +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() == 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/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/Service2.java b/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/Service2.java new file mode 100644 index 000000000..d76c05463 --- /dev/null +++ b/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/Service2.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.security.signatures; + +import java.util.Optional; + +import io.helidon.http.HttpMediaTypes; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +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/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/SignatureExampleBuilderMain.java b/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/SignatureExampleBuilderMain.java new file mode 100644 index 000000000..6a65aac5b --- /dev/null +++ b/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/SignatureExampleBuilderMain.java @@ -0,0 +1,242 @@ + +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.signatures; + +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.security.CompositeProviderFlag; +import io.helidon.security.CompositeProviderSelectionPolicy; +import io.helidon.security.Security; +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; +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.webserver.security.SecurityFeature; +import io.helidon.webserver.security.SecurityHttpFeature; + +/** + * Example of authentication of service with http signatures, using configuration file as much as possible. + */ +@SuppressWarnings("DuplicatedCode") +public class SignatureExampleBuilderMain { + + private static final Map USERS = new HashMap<>(); + + static { + addUser("jack", "changeit", List.of("user", "admin")); + addUser("jill", "changeit", List.of("user")); + addUser("john", "changeit", List.of()); + } + + private SignatureExampleBuilderMain() { + } + + 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) { + // as we explicitly configure SecurityHttpFeature, we must disable automated loading of security, + // as it would add another feature with different configuration + server.featuresDiscoverServices(false) + // context is a required pre-requisite of security + .addFeature(ContextFeature.create()) + .routing(SignatureExampleBuilderMain::routing1) + .putSocket("service2", socket -> socket + .routing(SignatureExampleBuilderMain::routing2)); + } + + private static void routing2(HttpRouting.Builder routing) { + SecurityHttpFeature security = SecurityHttpFeature.create(security2()) + .securityDefaults(SecurityFeature.authenticate()); + + routing.addFeature(security) + .get("/service2*", SecurityFeature.rolesAllowed("user")) + .register(new Service2()); + } + + private static void routing1(HttpRouting.Builder routing) { + SecurityHttpFeature security = SecurityHttpFeature.create(security1()) + .securityDefaults(SecurityFeature.authenticate()); + routing.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("changeit") + .build()) + .addInbound(InboundClientDefinition + .builder("service1-rsa") + .principalName("Service1 - RSA signature") + .publicKeyConfig(Keys.builder() + .keystore(k -> k + .keystore(Resource.create("keystore.p12")) + .passphrase("changeit") + .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("changeit") + .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("changeit") + .build()) + .build(); + } + + private static SecureUserStore users() { + return login -> Optional.ofNullable(USERS.get(login)); + } +} diff --git a/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/SignatureExampleConfigMain.java b/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/SignatureExampleConfigMain.java new file mode 100644 index 000000000..993c698f6 --- /dev/null +++ b/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/SignatureExampleConfigMain.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.signatures; + +import java.util.concurrent.TimeUnit; + +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.webserver.http.HttpRouting; +import io.helidon.webserver.security.SecurityHttpFeature; + +/** + * Example of authentication of service with http signatures, using configuration file as much as possible. + */ +@SuppressWarnings("DuplicatedCode") +public class SignatureExampleConfigMain { + + private SignatureExampleConfigMain() { + } + + /** + * 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 config + + 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) { + // as we explicitly configure SecurityHttpFeature, we must disable automated loading of security, + // as it would add another feature with different configuration + server.featuresDiscoverServices(false) + // context is a required pre-requisite of security + .addFeature(ContextFeature.create()) + .routing(SignatureExampleConfigMain::routing1) + .putSocket("service2", socket -> socket + .routing(SignatureExampleConfigMain::routing2)); + } + + private static void routing2(HttpRouting.Builder routing) { + // build routing (security is loaded from config) + Config config = config("service2.yaml"); + + // helper method to load both security and web server security from configuration + routing.addFeature(SecurityHttpFeature.create(config.get("security.web-server"))) + .register(new Service2()); + } + + private static void routing1(HttpRouting.Builder routing) { + // build routing (security is loaded from config) + Config config = config("service1.yaml"); + + // helper method to load both security and web server security from configuration + routing.addFeature(SecurityHttpFeature.create(config.get("security.web-server"))) + .register(new Service1()); + } + + private static Config config(String confFile) { + return Config.builder() + .sources(ConfigSources.classpath(confFile)) + .build(); + } +} diff --git a/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/package-info.java b/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/package-info.java new file mode 100644 index 000000000..1f2b49711 --- /dev/null +++ b/examples/security/webserver-signatures/src/main/java/io/helidon/examples/security/signatures/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of HTTP Signatures (both inbound and outbound). + * Based on RFC draft: https://tools.ietf + * .org/html/draft-cavage-http-signatures-03 + */ +package io.helidon.examples.security.signatures; diff --git a/examples/security/webserver-signatures/src/main/resources/keystore.p12 b/examples/security/webserver-signatures/src/main/resources/keystore.p12 new file mode 100644 index 000000000..96df59626 Binary files /dev/null and b/examples/security/webserver-signatures/src/main/resources/keystore.p12 differ diff --git a/examples/security/webserver-signatures/src/main/resources/service1.yaml b/examples/security/webserver-signatures/src/main/resources/service1.yaml new file mode 100644 index 000000000..7af4407c1 --- /dev/null +++ b/examples/security/webserver-signatures/src/main/resources/service1.yaml @@ -0,0 +1,83 @@ +# +# Copyright (c) 2016, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +security: + config: + # Configuration of secured config (encryption of passwords in property files) + # Set to true for production - if set to true, clear text passwords will cause failure + require-encryption: false + + # composite provider policy + provider-policy: + type: "COMPOSITE" + outbound: + - name: "http-basic-auth" + - name: "http-signatures" + + providers: + # Security provider - basic authentication (supports roles) - default + - http-basic-auth: + realm: "helidon" + users: + - login: "jack" + password: "${CLEAR=changeit}" + roles: ["user", "admin"] + - login: "jill" + # master password is "changeit", password is "changeit" + password: "${CLEAR=changeit}" + roles: ["user"] + - login: "john" + password: "${CLEAR=changeit}" + roles: [] + outbound: + - name: "propagate-all" + # only configured for outbound security + - http-signatures: + outbound: + - name: "service2" + hosts: ["localhost"] + paths: ["/service2"] + signature: + key-id: "service1-hmac" + hmac.secret: "${CLEAR=changeit}" + - name: "service2-rsa" + hosts: ["localhost"] + paths: ["/service2-rsa.*"] + signature: + key-id: "service1-rsa" + private-key: + keystore: + # path to keystore + resource.path: "src/main/resources/keystore.p12" + # Keystore type + # PKCS12, JSK or RSA (not really a keystore, but directly the linux style private key unencrypted) + # defaults to jdk default + type: "PKCS12" + # password of the keystore + passphrase: "changeit" + # alias of the key to sign request + key.alias: "myPrivateKey" + web-server: + # Configuration of integration with web server + defaults: + authenticate: true + paths: + - path: "/service1" + methods: ["get"] + roles-allowed: ["user"] + - path: "/service1-rsa" + methods: ["get"] + roles-allowed: ["user"] diff --git a/examples/security/webserver-signatures/src/main/resources/service2.yaml b/examples/security/webserver-signatures/src/main/resources/service2.yaml new file mode 100644 index 000000000..55f426a50 --- /dev/null +++ b/examples/security/webserver-signatures/src/main/resources/service2.yaml @@ -0,0 +1,75 @@ +# +# Copyright (c) 2016, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +security: + config: + # Configuration of secured config (encryption of passwords in property files) + # Set to true for production - if set to true, clear text passwords will cause failure + require-encryption: false + # composite provider policy + provider-policy: + type: "COMPOSITE" + authentication: + # first resolve signature, then resolve basic-auth + - name: "http-signatures" + flag: "OPTIONAL" + # must be present + - name: "http-basic-auth" + providers: + # Signatures + - http-signatures: + # only inbound configured, no outbound calls + inbound: + keys: + - key-id: "service1-hmac" + principal-name: "Service1 - HMAC signature" + hmac.secret: "${CLEAR=changeit}" + - key-id: "service1-rsa" + principal-name: "Service1 - RSA signature" + public-key: + keystore: + # path to keystore + resource.path: "src/main/resources/keystore.p12" + # Keystore type + # PKCS12 or JKS + # defaults to jdk default + # keystore-type: "PKCS12" + # password of the keystore + passphrase: "changeit" + # alias of the certificate to get public key from + cert.alias: "service_cert" + # Security provider - basic authentication (supports roles) + - http-basic-auth: + realm: "helidon" + users: + - login: "jack" + password: "${CLEAR=changeit}" + roles: [ "user", "admin" ] + - login: "jill" + password: "${CLEAR=changeit}" + roles: [ "user" ] + - login: "john" + password: "${CLEAR=changeit}" + roles: [] + web-server: + # Configuration of integration with web server + defaults: + authenticate: true + paths: + - path: "/service2" + roles-allowed: [ "user" ] + - path: "/service2-rsa" + roles-allowed: [ "user" ] \ No newline at end of file diff --git a/examples/security/webserver-signatures/src/test/java/io/helidon/examples/security/signatures/SignatureExampleBuilderMainTest.java b/examples/security/webserver-signatures/src/test/java/io/helidon/examples/security/signatures/SignatureExampleBuilderMainTest.java new file mode 100644 index 000000000..ea735703c --- /dev/null +++ b/examples/security/webserver-signatures/src/test/java/io/helidon/examples/security/signatures/SignatureExampleBuilderMainTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.signatures; + +import java.net.URI; + +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +/** + * Unit test for {@link SignatureExampleBuilderMain}. + */ +@ServerTest +public class SignatureExampleBuilderMainTest extends SignatureExampleTest { + + protected SignatureExampleBuilderMainTest(WebServer server, URI uri) { + super(server, uri); + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + SignatureExampleBuilderMain.setup(server); + } +} diff --git a/examples/security/webserver-signatures/src/test/java/io/helidon/examples/security/signatures/SignatureExampleConfigMainTest.java b/examples/security/webserver-signatures/src/test/java/io/helidon/examples/security/signatures/SignatureExampleConfigMainTest.java new file mode 100644 index 000000000..fd60ef5c5 --- /dev/null +++ b/examples/security/webserver-signatures/src/test/java/io/helidon/examples/security/signatures/SignatureExampleConfigMainTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.signatures; + +import java.net.URI; + +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; + +/** + * Unit test for {@link SignatureExampleBuilderMain}. + */ +@ServerTest +public class SignatureExampleConfigMainTest extends SignatureExampleTest { + + protected SignatureExampleConfigMainTest(WebServer server, URI uri) { + super(server, uri); + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + SignatureExampleConfigMain.setup(server); + } +} diff --git a/examples/security/webserver-signatures/src/test/java/io/helidon/examples/security/signatures/SignatureExampleTest.java b/examples/security/webserver-signatures/src/test/java/io/helidon/examples/security/signatures/SignatureExampleTest.java new file mode 100644 index 000000000..16ebbf598 --- /dev/null +++ b/examples/security/webserver-signatures/src/test/java/io/helidon/examples/security/signatures/SignatureExampleTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.security.signatures; + +import java.net.URI; +import java.util.Set; + +import io.helidon.security.Security; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; +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.webserver.testing.junit5.ServerTest; + +import org.junit.jupiter.api.Test; + +import static io.helidon.security.EndpointConfig.PROPERTY_OUTBOUND_ID; +import static io.helidon.security.EndpointConfig.PROPERTY_OUTBOUND_SECRET; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +public abstract class SignatureExampleTest { + + private final Http1Client client; + + protected SignatureExampleTest(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(PROPERTY_OUTBOUND_ID, "jack") + .property(PROPERTY_OUTBOUND_SECRET, "changeit") + .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/examples/todo-app/README.md b/examples/todo-app/README.md new file mode 100644 index 000000000..bdaae1f04 --- /dev/null +++ b/examples/todo-app/README.md @@ -0,0 +1,41 @@ +# TODO Demo Application + +This application implements todomvc[http://todomvc.com] with two microservices +implemented with Helidon MP and Helidon SE. + +## Build + +```shell +mvn clean package +docker build -t helidon-examples-todo-cassandra cassandra +``` + +## Run + +```shell +docker run -d -p 9042:9042 --name helidon-examples-todo-cassandra helidon-examples-todo-cassandra +docker run --name zipkin -d -p 9411:9411 openzipkin/zipkin +java -jar backend/target/helidon-examples-todo-backend.jar & +java -jar frontend/target/helidon-examples-todo-frontend.jar & +``` + +- Open http://localhost:8080 in your browser +- Login with a Google account +- Add some TODO entries +- Check-out the traces at http://localhost:9411 + +### HTTP proxy + +If you want to run behind an HTTP proxy: + +```shell +export security_providers_0_google_dash_login_proxy_dash_host=proxy.acme.com +export security_providers_0_google_dash_login_proxy_dash_port=80 +``` + +## Stop + +```shell +kill %1 %2 +docker rm -f zipkin helidon-examples-todo-cassandra +``` diff --git a/examples/todo-app/backend/pom.xml b/examples/todo-app/backend/pom.xml new file mode 100644 index 000000000..93a8df59c --- /dev/null +++ b/examples/todo-app/backend/pom.xml @@ -0,0 +1,178 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + + io.helidon.examples.todos + helidon-examples-todo-backend + 1.0.0-SNAPSHOT + Helidon Examples TODO Demo Backend + + + Back-end part of the application uses Helidon MP + + + + io.helidon.examples.todos.backend.Main + 3.10.2 + 4.3.1.0 + 4.9.0 + 3.0.2 + 1.32 + 1.19.1 + 1.19.1 + + + + + + com.datastax.cassandra + cassandra-driver-core + ${version.lib.cassandra} + + + io.dropwizard.metrics + metrics-core + + + + + org.yaml + snakeyaml + ${version.snakeyaml.override} + + + org.testcontainers + testcontainers-bom + ${version.testcontainers} + pom + import + + + + + + + io.helidon.microprofile.bundles + helidon-microprofile-core + + + io.helidon.microprofile + helidon-microprofile-security + + + io.helidon.security.providers + helidon-security-providers-google-login + + + io.helidon.security.providers + helidon-security-providers-http-sign + + + io.helidon.security.providers + helidon-security-providers-abac + + + io.helidon.microprofile.tracing + helidon-microprofile-tracing + + + io.helidon.tracing.providers + helidon-tracing-providers-zipkin + + + io.helidon.logging + helidon-logging-jul + runtime + + + com.datastax.cassandra + cassandra-driver-core + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + com.datastax.oss + java-driver-core + ${version.datastax.driver} + test + + + com.datastax.oss + java-driver-query-builder + ${version.datastax.driver} + test + + + com.codahale.metrics + metrics-core + ${version.codahale.metrics.core} + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + cassandra + ${version.testcontainers.cassandra} + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/DbService.java b/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/DbService.java new file mode 100644 index 000000000..07fd3fbe4 --- /dev/null +++ b/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/DbService.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.todos.backend; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.config.Config; +import io.helidon.security.SecurityException; + +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.tag.Tags; +import io.opentracing.util.GlobalTracer; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * A service showing to access a no-SQL database. + */ +@ApplicationScoped +public class DbService { + + private static final String LIST_QUERY = "select * from backend where user = ? ALLOW FILTERING"; + private static final String GET_QUERY = "select * from backend where id = ?"; + private static final String INSERT_QUERY = "insert into backend (id, user, message, completed, created)" + + " values (?, ?, ?, ?, ?)"; + private static final String UPDATE_QUERY = "update backend set message = ?, completed = ? where id = ? if user = ?"; + private static final String DELETE_QUERY = "delete from backend where id = ?"; + + private final Session session; + private final PreparedStatement listStatement; + private final PreparedStatement getStatement; + private final PreparedStatement insertStatement; + private final PreparedStatement updateStatement; + private final PreparedStatement deleteStatement; + + /** + * Create a new {@code DbService} instance. + * @param config the configuration root + */ + @Inject + public DbService(Config config) { + Cluster.Builder clusterBuilder = Cluster.builder() + .withoutMetrics(); + + Config cConfig = config.get("cassandra"); + cConfig.get("servers").asList(Config.class).stream() + .flatMap(Collection::stream) + .map(server -> server.get("host").asString().get()) + .forEach(clusterBuilder::addContactPoints); + cConfig.get("port").asInt().ifPresent(clusterBuilder::withPort); + + Cluster cluster = clusterBuilder.build(); + session = cluster.connect("backend"); + + listStatement = session.prepare(LIST_QUERY); + getStatement = session.prepare(GET_QUERY); + insertStatement = session.prepare(INSERT_QUERY); + updateStatement = session.prepare(UPDATE_QUERY); + deleteStatement = session.prepare(DELETE_QUERY); + } + + /** + * Invoke the given supplier and wrap it around with a tracing + * {@code Span}. + * @param the supplier return type + * @param tracingSpan the parent span to use + * @param operation the name of the operation + * @param supplier the supplier to invoke + * @return the object returned by the supplier + */ + private static T execute(SpanContext tracingSpan, String operation, Supplier supplier) { + Span span = startSpan(tracingSpan, operation); + + try { + return supplier.get(); + } catch (Exception e) { + Tags.ERROR.set(span, true); + span.log(Map.of("event", "error", "error.object", e)); + throw e; + } finally { + span.finish(); + } + } + + /** + * Utility method to create and start a child span of the given span. + * @param span the parent span + * @param operation the name for the new span + * @return the created span + */ + private static Span startSpan(SpanContext span, String operation) { + return GlobalTracer.get().buildSpan(operation).asChildOf(span).start(); + } + + /** + * Retrieve the TODOs entries from the database. + * @param tracingSpan the tracing span to use + * @param userId the database user id + * @return retrieved entries as {@code Iterable} + */ + Iterable list(SpanContext tracingSpan, String userId) { + return execute(tracingSpan, "cassandra::list", () -> { + BoundStatement bs = listStatement.bind(userId); + ResultSet rs = session.execute(bs); + + List result = new ArrayList<>(); + for (Row r : rs) { + result.add(Todo.fromDb(r)); + } + + return result; + }); + } + + /** + * Get the entry identified by the given ID from the database. + * @param tracingSpan the tracing span to use + * @param id the ID identifying the entry to retrieve + * @param userId the database user id + * @return retrieved entry as {@code Optional} + */ + Optional get(SpanContext tracingSpan, String id, String userId) { + return execute(tracingSpan, "cassandra::get", () -> getNoContext(id, userId)); + } + + /** + * Get the entry identified by the given ID from the database, fails if the + * entry is not associated with the given {@code userId}. + * @param id the ID identifying the entry to retrieve + * @param userId the database user id + * @return retrieved entry as {@code Optional} + */ + private Optional getNoContext(String id, String userId) { + BoundStatement bs = getStatement.bind(id); + ResultSet rs = session.execute(bs); + Row one = rs.one(); + if (null == one) { + return Optional.empty(); + } + Todo result = Todo.fromDb(one); + if (userId.equals(result.getUserId())) { + return Optional.of(result); + } + throw new SecurityException(String.format( + "User %s attempted to read record %s of another user", + userId, id)); + } + + /** + * Update the given entry in the database. + * @param tracingSpan the tracing span to use + * @param entry the entry to update + * @return {@code Optional} of updated entry if the update was successful, + * otherwise an empty {@code Optional} + */ + Optional update(SpanContext tracingSpan, Todo entry) { + return execute(tracingSpan, "cassandra::update", () -> { + //update backend set message = ? + // , completed = ? where id = ? if user = ? + BoundStatement bs = updateStatement.bind( + entry.getTitle(), + entry.getCompleted(), + entry.getId(), + entry.getUserId()); + ResultSet execute = session.execute(bs); + + if (execute.wasApplied()) { + return Optional.of(entry); + } else { + return Optional.empty(); + } + }); + } + + /** + * Delete the entry identified by the given ID in from the database. + * @param tracingSpan the tracing span to use + * @param id the ID identifying the entry to delete + * @param userId the database user id + * @return the deleted entry as {@code Optional} + */ + Optional delete(SpanContext tracingSpan, String id, String userId) { + return execute(tracingSpan, "cassandra::delete", + () -> getNoContext(id, userId) + .map(todo -> { + BoundStatement bs = deleteStatement.bind(id); + ResultSet rs = session.execute(bs); + if (!rs.wasApplied()) { + throw new RuntimeException("Failed to delete todo: " + + todo); + } + return todo; + })); + } + + /** + * Insert a new entry in the database. + * @param tracingSpan the tracing span to use + * @param entry the entry to insert + */ + void insert(SpanContext tracingSpan, Todo entry) { + execute(tracingSpan, "cassandra::insert", () -> { + BoundStatement bs = insertStatement + .bind(entry.getId(), + entry.getUserId(), + entry.getTitle(), + entry.getCompleted(), + new Date(entry.getCreated())); + + ResultSet execute = session.execute(bs); + if (!execute.wasApplied()) { + throw new RuntimeException("Failed to insert todo: " + entry); + } + return null; + }); + } +} diff --git a/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/JaxRsBackendResource.java b/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/JaxRsBackendResource.java new file mode 100644 index 000000000..0aae8d40d --- /dev/null +++ b/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/JaxRsBackendResource.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.todos.backend; + +import java.util.Collections; +import java.util.UUID; + +import io.helidon.security.Principal; +import io.helidon.security.SecurityContext; +import io.helidon.security.Subject; +import io.helidon.security.annotations.Authenticated; +import io.helidon.security.annotations.Authorized; + +import io.opentracing.Tracer; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.opentracing.Traced; + +/** + * The TODO backend REST service. + */ +@Path("/api/backend") +@Authenticated +@Authorized +@ApplicationScoped +public class JaxRsBackendResource { + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + private final DbService backendService; + private final Tracer tracer; + + /** + * Create new {@code JaxRsBackendResource} instance. + * @param dbs the database service facade to use + * @param tracer tracer to use + */ + @Inject + public JaxRsBackendResource(DbService dbs, Tracer tracer) { + this.backendService = dbs; + this.tracer = tracer; + } + + /** + * Retrieve all TODO entries. + * + * @param context security context to map the user + * @return the response with the retrieved entries as entity + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Traced(operationName = "jaxrs:list") + public Response list(@Context SecurityContext context) { + JsonArrayBuilder builder = JSON.createArrayBuilder(); + backendService.list(tracer.activeSpan().context(), getUserId(context)) + .forEach(data -> builder.add(data.forRest())); + return Response.ok(builder.build()).build(); + } + + /** + * Get the TODO entry identified by the given ID. + * @param id the ID of the entry to retrieve + * @param context security context to map the user + * @return the response with the retrieved entry as entity + */ + @GET + @Path("/{id}") + @Produces(MediaType.APPLICATION_JSON) + public Response get(@PathParam("id") String id, @Context SecurityContext context) { + return backendService + .get(tracer.activeSpan().context(), id, getUserId(context)) + .map(Todo::forRest) + .map(Response::ok) + .orElse(Response.status(Response.Status.NOT_FOUND)) + .build(); + } + + /** + * Delete the TODO entry identified by the given ID. + * @param id the id of the entry to delete + * @param context security context to map the user + * @return the response with the deleted entry as entity + */ + @DELETE + @Path("/{id}") + @Produces(MediaType.APPLICATION_JSON) + public Response delete(@PathParam("id") String id, @Context SecurityContext context) { + return backendService + .delete(tracer.activeSpan().context(), id, getUserId(context)) + .map(Todo::forRest) + .map(Response::ok) + .orElse(Response.status(Response.Status.NOT_FOUND)) + .build(); + } + + /** + * Create a new TODO entry. + * @param jsonObject the value of the new entry + * @param context security context to map the user + * @return the response ({@code 200} status if successful + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response createIt(JsonObject jsonObject, @Context SecurityContext context) { + String newId = UUID.randomUUID().toString(); + String userId = getUserId(context); + Todo newBackend = Todo.newTodoFromRest(jsonObject, userId, newId); + + backendService.insert(tracer.activeSpan().context(), newBackend); + + return Response.ok(newBackend.forRest()).build(); + } + + /** + * Update the TODO entry identified by the given ID. + * @param id the ID of the entry to update + * @param jsonObject the updated value of the entry + * @param context security context to map the user + * @return the response with the updated entry as entity + */ + @PUT + @Path("/{id}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response update(@PathParam("id") String id, JsonObject jsonObject, @Context SecurityContext context) { + return backendService + .update(tracer.activeSpan().context(), Todo.fromRest(jsonObject, getUserId(context), id)) + .map(Todo::forRest) + .map(Response::ok) + .orElse(Response.status(Response.Status.NOT_FOUND)) + .build(); + } + + /** + * Get the user id from the security context. + * @param context the security context + * @return user id found in the context or {@code } otherwise + */ + private String getUserId(SecurityContext context) { + return context.user() + .map(Subject::principal) + .map(Principal::id) + .orElse(""); + } +} diff --git a/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/Main.java b/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/Main.java new file mode 100644 index 000000000..ae04a67a9 --- /dev/null +++ b/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/Main.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.todos.backend; + +import java.util.List; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.microprofile.server.Server; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.environmentVariables; +import static io.helidon.config.ConfigSources.file; + +/** + * Main class to start the service. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments + */ + public static void main(final String[] args) { + + // load logging configuration + LogConfig.configureRuntime(); + + Config config = buildConfig(); + + // as we need to use custom filter + // we need to build Server with custom config + Server server = Server.builder() + .config(config) + .build(); + + server.start(); + } + + /** + * Load the configuration from all sources. + * @return the configuration root + */ + static Config buildConfig() { + return Config.builder() + .sources(List.of( + environmentVariables(), + // expected on development machine + // to override props for dev + file("dev.yaml").optional(), + // expected in k8s runtime + // to configure testing/production values + file("prod.yaml").optional(), + // in jar file + // (see src/main/resources/application.yaml) + classpath("application.yaml"))) + // support for passwords in configuration + //.addFilter(SecureConfigFilter.fromConfig()) + .build(); + } +} diff --git a/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/Todo.java b/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/Todo.java new file mode 100644 index 000000000..8a72c9d9f --- /dev/null +++ b/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/Todo.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.todos.backend; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.UUID; + +import com.datastax.driver.core.Row; +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonNumber; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; + +/** + * Data object for backend. + */ +public final class Todo { + + /** + * Date formatter to format the dates of the TODO entries. + */ + private static final DateTimeFormatter DATE_FORMAT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSVV"); + + /** + * Factory for creating JSON builders. + */ + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + /** + * The TODO ID. + */ + private String id; + + /** + * The user ID associated with this TODO. + */ + private String userId; + + /** + * The TODO title. + */ + private String title; + + /** + * The TODO completed flag. + */ + private Boolean completed; + + /** + * The TODO creation timestamp. + */ + private long created; + + /** + * Create a new {@code Todo} instance from a database entry in JSON format. + * @param jsonObject the database entry + * @return the created instance + */ + public static Todo fromDb(final JsonObject jsonObject) { + + Todo result = new Todo(); + result.id = jsonObject.getString("id"); + result.userId = jsonObject.getString("user"); + result.title = jsonObject.getString("message"); + result.completed = jsonObject.getBoolean("completed"); + result.created = Instant.from(DATE_FORMAT + .parse(jsonObject.getString("created"))).toEpochMilli(); + return result; + } + + /** + * Create a new {@code Todo} instance from a REST entry. + * The created entry will be new, i.e the {@code completed} flag will be set + * to {@code false} and the {@code created} timestamp set to the current + * time. + * @param jsonObject the REST entry + * @param userId the user ID associated with this entry + * @param id the entry ID + * @return the created instance + */ + public static Todo newTodoFromRest(final JsonObject jsonObject, + final String userId, + final String id) { + + Todo result = new Todo(); + result.id = id; + result.userId = userId; + result.title = jsonObject.getString("title"); + result.completed = jsonObject.getBoolean("completed", false); + result.created = System.currentTimeMillis(); + return result; + } + + /** + * Create a new {@code Todo} instance from a REST entry. + * @param jsonObject the REST entry + * @param userId the user ID associated with this entry + * @param id the entry ID + * @return the created instance + */ + public static Todo fromRest(final JsonObject jsonObject, + final String userId, + final String id) { + + Todo result = new Todo(); + result.id = id; + result.userId = userId; + result.title = jsonObject.getString("title", ""); + result.completed = jsonObject.getBoolean("completed"); + JsonNumber created = jsonObject.getJsonNumber("created"); + if (null != created) { + result.created = created.longValue(); + } + return result; + } + + /** + * Create a new {@code Todo} instance from a database entry. + * @param row the database entry + * @return the created instance + */ + public static Todo fromDb(final Row row) { + + Todo result = new Todo(); + result.id = row.getString("id"); + result.userId = row.getString("user"); + result.title = row.getString("message"); + result.completed = row.getBool("completed"); + result.created = row.getTimestamp("created").getTime(); + return result; + } + + /** + * Create a new {@code Todo} instance. + * The created entry will be new, i.e the {@code completed} flag will be set + * to {@code false} and the {@code created} timestamp set to the current + * time. + * @param userId the user ID associated with the new entry + * @param title the title for the new entry + * @return the created instance + */ + public static Todo create(final String userId, final String title) { + Todo result = new Todo(); + + result.id = UUID.randomUUID().toString(); + result.userId = userId; + result.title = title; + result.completed = false; + result.created = System.currentTimeMillis(); + + return result; + } + + /** + * Convert this {@code Todo} instance to the JSON database format. + * @return {@code JsonObject} + */ + public JsonObject forDb() { + //to store to DB + JsonObjectBuilder builder = JSON.createObjectBuilder(); + return builder.add("id", id) + .add("user", userId) + .add("message", title) + .add("completed", completed) + .add("created", created) + .build(); + } + + /** + * Convert this {@code Todo} instance to the JSON REST format. + * @return {@code JsonObject} + */ + public JsonObject forRest() { + //to send over to rest + JsonObjectBuilder builder = JSON.createObjectBuilder(); + return builder.add("id", id) + .add("user", userId) + .add("title", title) + .add("completed", completed) + .add("created", created) + .build(); + } + + /** + * Get the TODO ID. + * @return the {@code String} identifying this entry + */ + public String getId() { + return id; + } + + /** + * Get the user ID associated with this TODO. + * @return the {@code String} identifying the user + */ + public String getUserId() { + return userId; + } + + /** + * Get the TODO title. + * @return title + */ + public String getTitle() { + return title; + } + + /** + * Get the completed flag. + * @return completed flag. + */ + public Boolean getCompleted() { + return completed; + } + + /** + * Set the completed flag. + * @param iscomplete the completed flag value + */ + public void setCompleted(final boolean iscomplete) { + this.completed = iscomplete; + } + + /** + * Get the creation timestamp. + * @return timestamp + */ + public long getCreated() { + return created; + } + + @Override + public String toString() { + return "Todo{" + + "id='" + id + '\'' + + ", userId='" + userId + '\'' + + ", title='" + title + '\'' + + ", completed=" + completed + + ", created=" + created + + '}'; + } +} diff --git a/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/package-info.java b/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/package-info.java new file mode 100644 index 000000000..94501b7d1 --- /dev/null +++ b/examples/todo-app/backend/src/main/java/io/helidon/examples/todos/backend/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * TODOs Demo application backend. + */ +package io.helidon.examples.todos.backend; diff --git a/examples/todo-app/backend/src/main/resources/META-INF/beans.xml b/examples/todo-app/backend/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..ddb8316e3 --- /dev/null +++ b/examples/todo-app/backend/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/todo-app/backend/src/main/resources/application.yaml b/examples/todo-app/backend/src/main/resources/application.yaml new file mode 100644 index 000000000..8727766dd --- /dev/null +++ b/examples/todo-app/backend/src/main/resources/application.yaml @@ -0,0 +1,48 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +env: docker + +server: + port: 8854 + host: 0.0.0.0 + +tracing: + service: "todo:back" + port: 9411 + +cassandra: + port: 9042 + servers: + - host: "localhost" + +security: + config: + require-encryption: false + aes.insecure-passphrase: "changeit" + provider-policy: + type: "COMPOSITE" + authentication: + - name: "google-login" + - name: "http-signatures" + providers: + - google-login: + client-id: "1048216952820-6a6ke9vrbjlhngbc0al0dkj9qs9tqbk2.apps.googleusercontent.com" + - abac: + - http-signatures: + inbound.keys: + - key-id: "frontend" + principal-name: "Frontend Service" + hmac.secret: "${CLEAR=changeit}" diff --git a/examples/todo-app/backend/src/main/resources/logging.properties b/examples/todo-app/backend/src/main/resources/logging.properties new file mode 100644 index 000000000..b0dd4a7a0 --- /dev/null +++ b/examples/todo-app/backend/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#All attributes details +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#All log level details +.level=WARNING +io.helidon.webserver.level=INFO +io.helidon.security.level=INFO +io.helidon.tracing.level=FINE +AUDIT.level=FINEST + + diff --git a/examples/todo-app/backend/src/test/java/io/helidon/examples/todos/backend/BackendTests.java b/examples/todo-app/backend/src/test/java/io/helidon/examples/todos/backend/BackendTests.java new file mode 100644 index 000000000..e5f0316b7 --- /dev/null +++ b/examples/todo-app/backend/src/test/java/io/helidon/examples/todos/backend/BackendTests.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.todos.backend; + +import java.util.Base64; +import java.util.Properties; + +import io.helidon.config.mp.MpConfigSources; +import io.helidon.config.yaml.mp.YamlMpConfigSource; +import io.helidon.http.HeaderNames; +import io.helidon.microprofile.testing.junit5.Configuration; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.testcontainers.containers.CassandraContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@Testcontainers +@HelidonTest +@Configuration(useExisting = true) +@EnabledOnOs(OS.LINUX) // due to usage of docker with testcontainers, only Linux is enabled by default +class BackendTests { + @Container + static final CassandraContainer CASSANDRA_CONTAINER = new CassandraContainer<>("cassandra:3.11.2") + .withReuse(true); + + @Inject + private WebTarget webTarget; + + @BeforeAll + static void init() { + Properties cassandraProperties = initCassandra(); + + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + ConfigProviderResolver configResolver = ConfigProviderResolver.instance(); + + org.eclipse.microprofile.config.Config mpConfig = configResolver.getBuilder() + .withSources(YamlMpConfigSource.create(cl.getResource("test-application.yaml")), + MpConfigSources.create(cassandraProperties)) + .build(); + + configResolver.registerConfig(mpConfig, null); + } + + @AfterAll + static void stopServer() { + } + + private static Properties initCassandra() { + String host = CASSANDRA_CONTAINER.getHost(); + Integer port = CASSANDRA_CONTAINER.getMappedPort(CassandraContainer.CQL_PORT); + + Properties prop = new Properties(); + prop.put("cassandra.port", String.valueOf(port)); + prop.put("cassandra.servers.host.host", host); + + Cluster cluster = Cluster.builder() + .withoutMetrics() + .addContactPoint(host) + .withPort(port) + .build(); + + Session session = cluster.newSession(); + session.execute("CREATE KEYSPACE backend WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor' : 1};"); + session.execute( + "CREATE TABLE backend.backend (id ascii, user ascii, message ascii, completed Boolean, created timestamp, " + + "PRIMARY KEY (id));"); + session.execute("select * from backend.backend;"); + + session.close(); + cluster.close(); + + return prop; + } + + @Test + void testTodoScenario() { + String basicAuth = "Basic " + Base64.getEncoder().encodeToString("john:changeit".getBytes()); + JsonObject todo = Json.createObjectBuilder() + .add("title", "todo title") + .build(); + + // Add a new todo + JsonObject returnedTodo = webTarget + .path("/api/backend") + .request(MediaType.APPLICATION_JSON_TYPE) + .header(HeaderNames.AUTHORIZATION.defaultCase(), basicAuth) + .post(Entity.json(todo), JsonObject.class); + + assertThat(returnedTodo.getString("user"), is("john")); + assertThat(returnedTodo.getString("title"), is(todo.getString("title"))); + + // Get the todo created earlier + JsonObject fromServer = webTarget.path("/api/backend/" + returnedTodo.getString("id")) + .request(MediaType.APPLICATION_JSON_TYPE) + .header(HeaderNames.AUTHORIZATION.defaultCase(), basicAuth) + .get(JsonObject.class); + + assertThat(fromServer, is(returnedTodo)); + + // Update the todo created earlier + JsonObject updatedTodo = Json.createObjectBuilder() + .add("title", "updated title") + .add("completed", false) + .build(); + + fromServer = webTarget.path("/api/backend/" + returnedTodo.getString("id")) + .request(MediaType.APPLICATION_JSON_TYPE) + .header(HeaderNames.AUTHORIZATION.defaultCase(), basicAuth) + .put(Entity.json(updatedTodo), JsonObject.class); + + assertThat(fromServer.getString("title"), is(updatedTodo.getString("title"))); + + // Delete the todo created earlier + fromServer = webTarget.path("/api/backend/" + returnedTodo.getString("id")) + .request(MediaType.APPLICATION_JSON_TYPE) + .header(HeaderNames.AUTHORIZATION.defaultCase(), basicAuth) + .delete(JsonObject.class); + + assertThat(fromServer.getString("id"), is(returnedTodo.getString("id"))); + + // Get list of todos + JsonArray jsonValues = webTarget.path("/api/backend") + .request(MediaType.APPLICATION_JSON_TYPE) + .header(HeaderNames.AUTHORIZATION.defaultCase(), basicAuth) + .get(JsonArray.class); + + assertThat("There should be no todos on server", jsonValues.size(), is(0)); + } + +} diff --git a/examples/todo-app/backend/src/test/resources/test-application.yaml b/examples/todo-app/backend/src/test/resources/test-application.yaml new file mode 100644 index 000000000..299eed138 --- /dev/null +++ b/examples/todo-app/backend/src/test/resources/test-application.yaml @@ -0,0 +1,40 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# increase importance +config_ordinal: 500 + +# we use custom config and Helidon JUnit integration, must allow initializer +mp: + initializer: + allow: true + no-warn: true + +server: + port: 0 + host: localhost + +tracing: + service: "todo:back" + enabled: false + +security: + providers: + - http-basic-auth: + realm: "helidon" + users: + - login: "john" + password: "changeit" diff --git a/examples/todo-app/cassandra/Dockerfile b/examples/todo-app/cassandra/Dockerfile new file mode 100644 index 000000000..b41a1b500 --- /dev/null +++ b/examples/todo-app/cassandra/Dockerfile @@ -0,0 +1,27 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +FROM cassandra:3.11.16 + +ADD startup.sh / + +RUN chmod -v u=rx,og-rwx /startup.sh + +ENTRYPOINT ["/startup.sh"] + +EXPOSE 7000 7001 7199 9042 9160 + +CMD ["cassandra", "-f"] diff --git a/examples/todo-app/cassandra/startup.sh b/examples/todo-app/cassandra/startup.sh new file mode 100644 index 000000000..0cb1765d3 --- /dev/null +++ b/examples/todo-app/cassandra/startup.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +set -e + +echo 'Starting Cassandra database' +/docker-entrypoint.sh "$@" > /var/log/cassandra.log & + +echo 'Waiting for database to become available' +COUNT='1' +while test $COUNT -lt '120' && ! timeout 1 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/9042' > /dev/null 2>&1 ; do + if [ "$((COUNT%10))" -eq '0' ]; then + echo " ...$COUNT s" + fi + sleep 1 + COUNT=$((COUNT+1)) +done + +echo 'Creating todos table' +cqlsh -e " + CREATE KEYSPACE backend WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor' : 1}; + CREATE TABLE backend.backend (id ascii, user ascii, message ascii, completed Boolean, created timestamp, PRIMARY KEY (id)); + select * from backend.backend; +" \ + || true + +echo 'Opening database log file' +echo '-------------------------' +tail -f /var/log/cassandra.log diff --git a/examples/todo-app/frontend/pom.xml b/examples/todo-app/frontend/pom.xml new file mode 100644 index 000000000..736a661f4 --- /dev/null +++ b/examples/todo-app/frontend/pom.xml @@ -0,0 +1,173 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.todo + helidon-examples-todo-frontend + 1.0.0-SNAPSHOT + Helidon Examples TODO Demo Frontend + + + Front-end part of the application, uses Helidon SE + + + + io.helidon.examples.todos.frontend.Main + ${mainClass} + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver + helidon-webserver-security + + + io.helidon.webserver + helidon-webserver-static-content + + + io.helidon.webserver + helidon-webserver-access-log + + + io.helidon.metrics + helidon-metrics-api + + + io.helidon.webserver.observe + helidon-webserver-observe-tracing + + + io.helidon.webclient + helidon-webclient + + + io.helidon.webclient + helidon-webclient-security + + + io.helidon.webclient + helidon-webclient-tracing + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.common + helidon-common + + + io.helidon.tracing + helidon-tracing + + + io.helidon.tracing.providers + helidon-tracing-providers-zipkin + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.config + helidon-config-encryption + + + io.helidon.security + helidon-security + + + io.helidon.security.providers + helidon-security-providers-google-login + + + io.helidon.security.providers + helidon-security-providers-abac + + + io.helidon.security.providers + helidon-security-providers-http-sign + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + runtime + + + io.helidon.metrics + helidon-metrics-system-meters + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.security.providers + helidon-security-providers-http-auth + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/BackendServiceClient.java b/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/BackendServiceClient.java new file mode 100644 index 000000000..b66dcabee --- /dev/null +++ b/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/BackendServiceClient.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.todos.frontend; + +import java.net.URI; +import java.util.function.Supplier; + +import io.helidon.common.LazyValue; +import io.helidon.http.HttpException; +import io.helidon.http.Status.Family; +import io.helidon.http.media.jsonp.JsonpSupport; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webclient.security.WebClientSecurity; +import io.helidon.webclient.tracing.WebClientTracing; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; + +/** + * Client to invoke the backend service. + */ +final class BackendServiceClient { + + private final LazyValue client = LazyValue.create(this::createClient); + private final Supplier serviceEndpoint; + + BackendServiceClient(Supplier serviceEndpoint) { + this.serviceEndpoint = serviceEndpoint; + } + + private Http1Client createClient() { + return Http1Client.builder() + .servicesDiscoverServices(false) + .addService(WebClientTracing.create()) + .addService(WebClientSecurity.create()) + .addMediaSupport(JsonpSupport.create()) + .baseUri(serviceEndpoint.get().resolve("/api/backend")) + .build(); + } + + private Http1Client client() { + return client.get(); + } + + /** + * Retrieve all entries from the backend. + * + * @return single with all records + */ + JsonArray list() { + return processResponse(client().get().request(), JsonArray.class); + } + + /** + * Retrieve the entry identified by the given ID. + * + * @param id the ID identifying the entry to retrieve + * @return retrieved entry as a {@code JsonObject} + */ + JsonObject get(String id) { + return processResponse(client().get(id).request(), JsonObject.class); + } + + /** + * Delete the entry identified by the given ID. + * + * @param id the ID identifying the entry to delete + * @return deleted entry as a {@code JsonObject} + */ + JsonObject deleteSingle(String id) { + return processResponse(client().delete(id).request(), JsonObject.class); + } + + /** + * Create a new entry. + * + * @param json the new entry value to create as {@code JsonObject} + * @return created entry as {@code JsonObject} + */ + JsonObject create(JsonObject json) { + return processResponse(client().post().submit(json), JsonObject.class); + } + + /** + * Update an entry identified by the given ID. + * + * @param id the ID identifying the entry to update + * @param json the update entry value as {@code JsonObject} + * @return updated entry as {@code JsonObject} + */ + JsonObject update(String id, JsonObject json) { + return processResponse(client().put(id).submit(json), JsonObject.class); + } + + private T processResponse(Http1ClientResponse response, Class clazz) { + if (response.status().family() != Family.SUCCESSFUL) { + throw new HttpException("backend error", response.status()); + } + return response.entity().as(clazz); + } +} diff --git a/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/Main.java b/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/Main.java new file mode 100644 index 000000000..05a400d1d --- /dev/null +++ b/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/Main.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.todos.frontend; + +import java.net.URI; +import java.util.List; + +import io.helidon.config.Config; +import io.helidon.config.ConfigValue; +import io.helidon.config.FileSystemWatcher; +import io.helidon.http.HeaderNames; +import io.helidon.http.Status; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.staticcontent.StaticContentService; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.environmentVariables; +import static io.helidon.config.ConfigSources.file; +import static io.helidon.config.PollingStrategies.regular; +import static java.time.Duration.ofSeconds; + +/** + * Main class to start the service. + */ +public final class Main { + + private static final Long POLLING_INTERVAL = 5L; + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments + */ + public static void main(final String[] args) { + + // load logging configuration + LogConfig.configureRuntime(); + + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build(); + + // start the web server + server.start(); + System.out.println("WEB server is up! http://localhost:" + server.port()); + } + + private static void setup(WebServerConfig.Builder server) { + Config config = buildConfig(); + + ConfigValue backendEndpoint = config.get("services.backend.endpoint").as(URI.class); + + server.config(config.get("server")) + .routing(routing -> routing + // redirect POST / to GET / + .post("/", (req, res) -> { + res.header(HeaderNames.LOCATION, "/"); + res.status(Status.SEE_OTHER_303); + res.send(); + }) + // register static content support (on "/") + .register(StaticContentService.builder("/WEB").welcomeFileName("index.html")) + // register API service (on "/api") - this path is secured (see application.yaml) + .register("/api", new TodoService(new BackendServiceClient(backendEndpoint::get)))); + } + + /** + * Load the configuration from all sources. + * @return the configuration root + */ + private static Config buildConfig() { + return Config.builder() + .sources(List.of( + environmentVariables(), + // expected on development machine + // to override props for dev + file("dev.yaml") + .changeWatcher(FileSystemWatcher.create()) + .optional(), + // expected in k8s runtime + // to configure testing/production values + file("prod.yaml") + .pollingStrategy(regular( + ofSeconds(POLLING_INTERVAL))) + .optional(), + // in jar file + // (see src/main/resources/application.yaml) + classpath("application.yaml"))) + .build(); + } +} diff --git a/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/TodoService.java b/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/TodoService.java new file mode 100644 index 000000000..617cdbff4 --- /dev/null +++ b/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/TodoService.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.todos.frontend; + +import io.helidon.http.Status; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.Meter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +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; + +/** + * TODO service. + *

+ * An entry is structured as follows: + * { 'title': string, 'completed': boolean, 'id': string } + *

+ * The IDs are server generated on the initial POST operation (so they are not + * included in that case). + *

+ * Here is a summary of the operations: + * GET /api/todo: Get all entries + * GET /api/todo/{id}: Get an entry by ID + * POST /api/todo: Create a new entry, created entry is returned + * DELETE /api/todo/{id}: Delete an entry, deleted entry is returned + * PUT /api/todo/{id}: Update an entry, updated entry is returned + */ +public final class TodoService implements HttpService { + + private final BackendServiceClient bsc; + private final Counter createCounter; + private final Counter updateCounter; + private final Counter deleteCounter; + + /** + * Create a new {@code TodosHandler} instance. + * + * @param bsc the {@code BackendServiceClient} to use + */ + TodoService(BackendServiceClient bsc) { + MeterRegistry registry = Metrics.globalRegistry(); + + this.bsc = bsc; + this.createCounter = registry.getOrCreate(Counter.builder("created")); + this.updateCounter = registry.getOrCreate(Counter.builder("updates")); + this.deleteCounter = registry.getOrCreate(counterMetadata("deletes", "Number of deleted todos")); + } + + @Override + public void routing(HttpRules rules) { + rules.get("/todo/{id}", this::get) + .delete("/todo/{id}", this::delete) + .put("/todo/{id}", this::update) + .get("/todo", this::list) + .post("/todo", this::create); + } + + private Counter.Builder counterMetadata(String name, String description) { + return Counter.builder(name) + .description(description) + .baseUnit(Meter.BaseUnits.NONE); + } + + /** + * Handler for {@code POST /todo}. + * + * @param req the server request + * @param res the server response + */ + private void create(ServerRequest req, ServerResponse res) { + JsonObject jsonObject = bsc.create(req.content().as(JsonObject.class)); + createCounter.increment(); + res.status(Status.CREATED_201); + res.send(jsonObject); + } + + /** + * Handler for {@code GET /todo}. + * + * @param req the server request + * @param res the server response + */ + private void list(ServerRequest req, ServerResponse res) { + res.send(bsc.list()); + } + + /** + * Handler for {@code PUT /todo/id}. + * + * @param req the server request + * @param res the server response + */ + private void update(ServerRequest req, ServerResponse res) { + String id = req.path().pathParameters().get("id"); + JsonObject jsonObject = bsc.update(id, req.content().as(JsonObject.class)); + updateCounter.increment(); + res.send(jsonObject); + } + + /** + * Handler for {@code DELETE /todo/id}. + * + * @param req the server request + * @param res the server response + */ + private void delete(ServerRequest req, ServerResponse res) { + String id = req.path().pathParameters().get("id"); + JsonObject jsonObject = bsc.deleteSingle(id); + deleteCounter.increment(); + res.send(jsonObject); + } + + /** + * Handler for {@code GET /todo/id}. + * + * @param req the server request + * @param res the server response + */ + private void get(ServerRequest req, ServerResponse res) { + String id = req.path().pathParameters().get("id"); + res.send(bsc.get(id)); + } +} diff --git a/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/package-info.java b/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/package-info.java new file mode 100644 index 000000000..04c06c008 --- /dev/null +++ b/examples/todo-app/frontend/src/main/java/io/helidon/examples/todos/frontend/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * TODOs Demo application frontend. + */ +package io.helidon.examples.todos.frontend; diff --git a/examples/todo-app/frontend/src/main/resources/WEB/css/styles.css b/examples/todo-app/frontend/src/main/resources/WEB/css/styles.css new file mode 100644 index 000000000..7c36bb768 --- /dev/null +++ b/examples/todo-app/frontend/src/main/resources/WEB/css/styles.css @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +html, +body { + margin: 0 auto; + padding: 0; + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 600px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +input { + font-family: inherit; + font-weight: inherit; + color: inherit; + border: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + cursor: pointer; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +nav { + display: flex; + align-items: center; + padding: 0 20px 0 20px; + height: 80px; +} + +.spacer { + flex-grow: 1; +} + +.row { + display: flex; + flex-direction: row; + align-items: center; +} + +.column { + display: flex; + flex-direction: column; + align-items: center; +} + +.wrap { + display: none; +} + +.todoapp { + width: 550px; + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + text-rendering: optimizeLegibility; +} + +.new-todo { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.checkbox { + border: none; /* Mobile Safari */ + appearance: none; + opacity: 0.25; + -webkit-appearance: none; + margin: 10px; +} + +.checkbox:after { + font-family: 'Material Symbols Outlined'; + font-size: 25px; + transition: opacity 0.2s ease-out; + cursor: pointer; +} + +.checkbox:hover { + opacity: 0.5; +} + +.checkbox:active { + opacity: 0.75; +} + +.toggle-all:after { + content: "\e877"; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list .view, .todo-list .edit { + flex-grow: 1; + padding: 15px 15px 15px 60px; + line-height: 1.2; + font-size: 24px; +} + +.todo-list li.editing { + padding: 0; +} + +.todo-list li.editing .edit { + display: block; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + margin: 10px; + display: flex; + align-items: center; +} + +.toggle:after { + content: "\ef4a"; +} + +.toggle:checked:after { + content: "\e86c"; +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .actions { + display: flex; + align-items: center; + margin: 10px; +} + +.todo-list li .actions button { + margin: 5px; + opacity: 0.25; + font-size: 0; + cursor: pointer; + transition: opacity 0.2s ease-out; +} + +.todo-list li .actions button:hover { + opacity: 0.5; +} + +.todo-list li .actions button:active { + opacity: 0.75; +} + +.todo-list li .actions button:before { + font-family: 'Material Symbols Outlined'; + font-size: 25px; +} + +.todo-list li .actions .update:before { + content: '\e3c9' +} + +.todo-list li.editing .actions .update:before { + content: '\e5ca' !important; +} + +.todo-list li .actions .delete:before { + content: '\e872' +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 0; + color: #bfbfbf; + font-size: 14px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +@media screen and (-webkit-min-device-pixel-ratio: 0) { + .toggle-all, .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} + +#user-info { + display:none; +} + +#user-info > div { + display:flex; +} + +#user-info .sign-out { + margin-right: 10px; + background: white; + padding: 5px; + border: 1px solid #dadce0; + border-radius: 4px; + transition: background-color .218s, border-color .218s; +} + +#user-info .sign-out:hover { + border-color: #d2e3fc; + background-color: rgba(66,133,244,.04); +} +#user-info .sign-out:active { + background-color: rgba(66,133,244,.1); +} diff --git a/examples/todo-app/frontend/src/main/resources/WEB/index.html b/examples/todo-app/frontend/src/main/resources/WEB/index.html new file mode 100644 index 000000000..6e45e0044 --- /dev/null +++ b/examples/todo-app/frontend/src/main/resources/WEB/index.html @@ -0,0 +1,91 @@ + + + + + + Helidon TodoMVC + + + + +

+
+
+
+
+

todos

+
+ + +
+
+
+
    +
    +
    +
    +
    +
    +

    Helidon implementation of TodoMVC

    +
    +
    + + + + + + + + + diff --git a/examples/todo-app/frontend/src/main/resources/WEB/js/app.js b/examples/todo-app/frontend/src/main/resources/WEB/js/app.js new file mode 100644 index 000000000..560a075bf --- /dev/null +++ b/examples/todo-app/frontend/src/main/resources/WEB/js/app.js @@ -0,0 +1,465 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* global jQuery, Router */ + +/** + * @typedef {Object} Handlebars + * @property {function(string, function(*, *, HandlebarsOpts))} registerHelper + * @property {function(HTMLElement):function(object):string} compile + */ + +/** + * @typedef {Object} HandlebarsOpts + * @property {function(object)} fn + * @property {function(object)} inverse + */ + +/** + * @typedef {Object} Google + * @property {GoogleAccount} accounts + */ + +/** + * @typedef {Object} GoogleAccount + * @property {GoogleId} id + */ + +/** + * @typedef {Object} GoogleId + * @property {function({})} initialize + * @property {function()} prompt + * @property {function(Element, Object)} renderButton + * @property {function(string} revoke + */ + +/** + * @typedef {Object} Todo + * @property {string} title + * @property {string=} id + * @property {boolean} completed + */ + +class TodoClient { + + constructor(access_token) { + this.access_token = access_token + } + + /** + * List all entries. + * @return {Promise} + */ + list() { + return this._ajax('GET', '/api/todo'); + } + + /** + * Create a new entry. + * @property {Todo} data + * @return {Promise} + */ + create(item) { + return this._ajax('POST', '/api/todo', item); + } + + /** + * Toggle an entry. + * @param {Todo} item + * @param {boolean} completed + */ + toggle(item, completed) { + const data = {...item, completed}; + return this._ajax('PUT', `/api/todo/${item.id}`, data); + } + + /** + * Update an entry. + * @param {Todo} item + * @return {Promise} + */ + update(item) { + return this._ajax('PUT', `/api/todo/${item.id}`, item); + } + + /** + * Delete an entry. + * @param {string} id + * @return {Promise} + */ + delete(id) { + return this._ajax('DELETE', `/api/todo/${id}`); + } + + /** + * Batch requests. + * @param {Todo[]} items + * @param {function(Todo):boolean} filter + * @param {function(Todo, number): Promise} fn + * @return {Promise[]>} + */ + batch(items, filter, fn) { + const promises = []; + items.forEach((e, i) => { + if (filter(e)) { + promises.push(fn(e, i)); + } + }) + return Promise.all(promises); + } + + /** + * Toggle all items. + * @property {Todo[]} items + * @property {boolean} completed + * @return {Promise} + */ + toggleAll(items, completed) { + const result = [...items]; + return this.batch(items, e => e.completed !== completed, (e, i) => { + return this.toggle(e, completed).then(data => { + result[i] = data; + }) + }).then(() => result); + } + + /** + * Delete all completed items. + * @param {Todo[]} items + * @return {Promise} + */ + deleteCompleted(items) { + const indexes = []; + const result = [...items]; + return this.batch(items, e => e.completed, (data, index) => { + indexes.push(index); + return this._ajax('DELETE', `/api/todo/${data.id}`); + }).then(() => { + indexes.sort(); + for (let i = indexes.length - 1; i >= 0; i--) { + result.splice(indexes[i], 1); + } + return result; + }) + } + + /** + * @param {string} type + * @param {string} path + * @param {object=} data + * @return {Promise} + */ + _ajax(type, path, data) { + while (path.startsWith('/')) { + path = path.substring(1); + } + return new Promise((resolve, reject) => { + // noinspection JSUnusedGlobalSymbols + $.ajax({ + type: type, + beforeSend: (request) => { + if (this.access_token) { + request.setRequestHeader('Authorization', `Bearer ${this.access_token}`); + } + }, + url: window.location.pathname + path, + dataType: 'json', + contentType: 'application/json;charset=utf-8', + data: data && JSON.stringify(data) + }).done((resData) => { + resolve(resData); + }).fail((data, textStatus) => { + reject(textStatus); + }); + }) + } +} + +class App { + + constructor() { + this.client = null; + this.token = null; + this.todos = []; + this.todoTemplate = Handlebars.compile($('#todo-template').html()); + this.footerTemplate = Handlebars.compile($('#footer-template').html()); + this.router = new Router({ + '/:filter': (filter) => { + this.filter = filter; + this.render(); + } + }); + $('.sign-out').on('click', e => this.signOut()); + $('.new-todo').on('keyup', e => this.create(e)); + $('.toggle-all').on('change', e => this.toggleAll(e)); + $('.footer').on('click', '.clear-completed', () => this.deleteCompleted()); + $('.todo-list') + .on('change', '.toggle', e => this.toggle(e)) + .on('click', '.update', e => this.editingMode(e)) + .on('focusout', '.edit', e => this.update(e)) + .on('click', '.delete', e => this.destroy(e)); + } + + signOut() { + const google = /** @type {Google} */ (window['google']); + google.accounts.id.revoke(this.token.sub); + this.client = null; + this.token = null; + $('#user-info').fadeOut(); + $('.wrap').fadeOut(); + google.accounts.id.prompt(); + } + + /** + * Init the application. + */ + init(access_token) { + this.client = new TodoClient(access_token); + this.token = JSON.parse(atob(access_token.split(".")[1])); + const signedIn = access_token && true || false; + if (signedIn) { + this.init0().then(() => { + $('.wrap').fadeIn(); + $('#user-info').fadeIn(); + }) + } + } + + init0() { + return this.client.list().then(items => { + this.todos = items; + this.router.init('/all'); + }).catch(console.error); + } + + /** + * Render. + */ + render() { + const todos = this.getFilteredTodos(); + $('.todo-list').html(this.todoTemplate(todos.map((e, i) => { + e.index = i; + return e; + }))); + $('.main').toggle(todos.length > 0); + $('.toggle-all').prop('checked', this.getActiveTodos().length === 0); + this.renderFooter(); + $('.new-todo').focus(); + } + + /** + * Render the footer. + */ + renderFooter() { + const todoCount = this.todos.length; + const activeTodoCount = this.getActiveTodos().length; + const template = this.footerTemplate({ + activeTodoCount: activeTodoCount, + activeTodoWord: this.pluralize(activeTodoCount, 'item'), + completedTodos: todoCount - activeTodoCount, + filter: this.filter + }); + $('.footer').toggle(todoCount > 0).html(template); + } + + /** + * Pluralize the given word. + * @param {number} count + * @param {string} word + * @return {string} + */ + pluralize(count, word) { + return count === 1 ? word : word + 's'; + } + + /** + * Get the active entries. + * @return {Todo[]} + */ + getActiveTodos() { + return this.todos.filter((todo) => { + return !todo.completed; + }); + } + + /** + * Get the completed entries. + * @return {Todo[]} + */ + getCompletedTodos() { + return this.todos.filter((todo) => todo.completed); + } + + /** + * Get the entries for the current filter. + * @return {Todo[]} + */ + getFilteredTodos() { + if (this.filter === 'active') { + return this.getActiveTodos(); + } + if (this.filter === 'completed') { + return this.getCompletedTodos(); + } + return this.todos; + } + + /** + * Toggle all entries. + * @param {Event} e + */ + toggleAll(e) { + const isChecked = $(e.target).prop('checked'); + this.client.toggleAll(this.todos, isChecked).then(items => { + this.todos = items; + this.render(); + }).catch(console.error); + } + + /** + * Delete all completed entries. + */ + deleteCompleted() { + this.client.deleteCompleted(this.todos).then(items => { + this.todos = items; + this.render(); + }).catch(console.error); + } + + /** + * Create a new entry. + * @param {KeyboardEvent} e + */ + create(e) { + const input = $(e.target); + const val = input.val().trim(); + if (e.key !== "Enter" || !val) { + return; + } + input.val(''); + const item = {title: val, completed: false}; + this.client.create(item).then((data) => { + this.todos.push(data); + this.render(); + }).catch(console.error) + } + + /** + * Toggle an entry. + * @param {Event} e + * @return {Promise} + */ + toggle(e) { + const info = this.entryInfo(e.target); + const entry = this.todos[info.index]; + return this.client.toggle(entry, !entry.completed).then((data) => { + this.todos[info.index] = data; + this.render(); + }).catch(console.error); + } + + /** + * Update an entry. + * @param {{target: Element}} e + * @return {Promise} + */ + update(e) { + const input = $(e.target); + const val = input.val().trim(); + if (!val) { + this.destroy(e); + return Promise.resolve(); + } else { + const info = this.entryInfo(e.target); + const newData = {...this.todos[info.index], title: val}; + return this.client.update(newData).then(data => { + this.todos[info.index] = data; + this.render(); + }).catch(console.error); + } + } + + /** + * Destroy an entry. + * @param {{target: Element}} e + */ + destroy(e) { + const info = this.entryInfo(e.target); + this.client.delete(info.id).then(() => { + this.todos.splice(info.index, 1); + this.render(); + }).catch(console.error); + } + + /** + * Edit an entry. + * @param {Event} e + */ + editingMode(e) { + const listElt = $(e.target).closest('li'); + const editing = listElt.hasClass('editing'); + if (editing) { + const input = listElt.find('.edit'); + this.update({ + target: input[0] + }).then(() => listElt.removeClass('editing')); + } else { + const input = listElt.addClass('editing').find('.edit'); + // puts caret at end of input + const tmpStr = input.val(); + input.val(''); + input.val(tmpStr); + input.focus(); + } + } + + /** + * Get the entry info for an element. + * @param {Element} el + * @return {{index: number, id: string}} + */ + entryInfo(el) { + const id = $(el).closest('li').data('id'); + const todos = this.todos; + let i = todos.length; + while (i--) { + if (todos[i].id === id) { + return {id, index: i}; + } + } + } +} + +window.onload = () => { + const google = /** @type {Google} */ (window['google']); + const Handlebars = /** @type {Handlebars} */ (window['Handlebars']); + + Handlebars.registerHelper('eq', (a, b, options) => { + return a === b ? options.fn(this) : options.inverse(this); + }); + + const app = new App(); + + // noinspection JSUnusedGlobalSymbols,SpellCheckingInspection + google.accounts.id.initialize({ + client_id: '1048216952820-6a6ke9vrbjlhngbc0al0dkj9qs9tqbk2.apps.googleusercontent.com', + auto_select: true, + callback: response => { + app.init(response.credential) + } + }); + google.accounts.id.prompt(); +} diff --git a/examples/todo-app/frontend/src/main/resources/application.yaml b/examples/todo-app/frontend/src/main/resources/application.yaml new file mode 100644 index 000000000..a8cbb7be0 --- /dev/null +++ b/examples/todo-app/frontend/src/main/resources/application.yaml @@ -0,0 +1,63 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +env: docker + +server: + port: 8080 + features: + access-log: + enabled: true + security: + paths: + - path: "/api/{+}" + authenticate: true + +cors: + allow-origins: ["https://cdnjs.cloudflare.com", "https://raw.githubusercontent.com"] + allow-methods: ["GET"] + +tracing: + service: "todo:front" + port: 9411 + +services: + backend.endpoint: "http://localhost:8854" + +security: + config: + require-encryption: false + aes.insecure-passphrase: "changeit" + provider-policy: + type: "COMPOSITE" + authentication: + - name: "google-login" + outbound: + - name: "google-login" + - name: "http-signatures" + providers: + - google-login: + client-id: "1048216952820-6a6ke9vrbjlhngbc0al0dkj9qs9tqbk2.apps.googleusercontent.com" + outbound: + - name: "backend" + hosts: [ "localhost" ] + - abac: + - http-signatures: + outbound: + - name: "backend" + hosts: [ "localhost" ] + signature: + key-id: "frontend" + hmac.secret: "${CLEAR=changeit}" diff --git a/examples/todo-app/frontend/src/main/resources/logging.properties b/examples/todo-app/frontend/src/main/resources/logging.properties new file mode 100644 index 000000000..b0dd4a7a0 --- /dev/null +++ b/examples/todo-app/frontend/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#All attributes details +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#All log level details +.level=WARNING +io.helidon.webserver.level=INFO +io.helidon.security.level=INFO +io.helidon.tracing.level=FINE +AUDIT.level=FINEST + + diff --git a/examples/todo-app/frontend/src/test/java/io/helidon/examples/todos/frontend/TodoServiceTest.java b/examples/todo-app/frontend/src/test/java/io/helidon/examples/todos/frontend/TodoServiceTest.java new file mode 100644 index 000000000..dc412ca9c --- /dev/null +++ b/examples/todo-app/frontend/src/test/java/io/helidon/examples/todos/frontend/TodoServiceTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.todos.frontend; + +import java.net.URI; +import java.util.Base64; + +import io.helidon.config.Config; +import io.helidon.http.Header; +import io.helidon.http.HeaderNames; +import io.helidon.http.HeaderValues; +import io.helidon.http.Status; +import io.helidon.http.media.jsonp.JsonpSupport; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webclient.security.WebClientSecurity; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webserver.testing.junit5.Socket; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.config.ConfigSources.classpath; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class TodoServiceTest { + + private static final JsonObject TODO = Json.createObjectBuilder().add("msg", "todo").build(); + private static final String ENCODED_ID = Base64.getEncoder().encodeToString("john:changeit".getBytes()); + private static final Header BASIC_AUTH = HeaderValues.create(HeaderNames.AUTHORIZATION, "Basic " + ENCODED_ID); + + private static URI backendUri; + private final Http1Client client; + + TodoServiceTest(URI uri) { + this.client = Http1Client.builder() + .servicesDiscoverServices(false) + .baseUri(uri.resolve("/api/todo")) + .addMediaSupport(JsonpSupport.create()) + .addService(WebClientSecurity.create()) + .addHeader(BASIC_AUTH) + .build(); + } + + static final class BackendServiceMock implements HttpService { + + @Override + public void routing(HttpRules rules) { + rules.get("/", (req, res) -> res.send(Json.createArrayBuilder().add(TODO).build())) + .post((req, res) -> res.send(req.content().as(JsonObject.class))) + .get("/{id}", (req, res) -> res.send(TODO)) + .put("/{id}", (req, res) -> res.send(TODO)) + .delete("/{id}", (req, res) -> res.send(TODO)); + } + } + + @BeforeAll + static void init(@Socket("backend") URI uri) { + backendUri = uri; + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + Config config = Config.create(classpath("application-test.yaml")); + + server.config(config.get("server")) + .routing(routing -> routing + .register("/api", new TodoService(new BackendServiceClient(() -> backendUri)))) + .putSocket("backend", socket -> socket + .routing(routing -> routing + .register("/api/backend", new BackendServiceMock()))); + } + + @Test + void testList() { + try (Http1ClientResponse response = client.get().request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.as(JsonArray.class).getJsonObject(0), is(TODO)); + } + } + + @Test + void testCreate() { + try (Http1ClientResponse response = client.post().submit(TODO)) { + assertThat(response.status(), is(Status.CREATED_201)); + assertThat(response.as(JsonObject.class), is(TODO)); + } + } + + @Test + void testGet() { + try (Http1ClientResponse response = client.get("1").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.as(JsonObject.class), is(TODO)); + } + } + + @Test + void testDelete() { + try (Http1ClientResponse response = client.delete("1").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.as(JsonObject.class), is(TODO)); + } + } + + @Test + void testUpdate() { + try (Http1ClientResponse response = client.put("1").submit(TODO)) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.as(JsonObject.class), is(TODO)); + } + } +} diff --git a/examples/todo-app/frontend/src/test/resources/application-test.yaml b/examples/todo-app/frontend/src/test/resources/application-test.yaml new file mode 100644 index 000000000..cfc65f4c8 --- /dev/null +++ b/examples/todo-app/frontend/src/test/resources/application-test.yaml @@ -0,0 +1,31 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 0 + features: + security: + paths: + - path: "/api/{+}" + authenticate: true + +security: + providers: + - http-basic-auth: + realm: "helidon" + users: + - login: "john" + password: "changeit" diff --git a/examples/todo-app/pom.xml b/examples/todo-app/pom.xml new file mode 100644 index 000000000..39e8c0013 --- /dev/null +++ b/examples/todo-app/pom.xml @@ -0,0 +1,44 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + + io.helidon.examples.todos + example-todo-app-project + pom + 1.0.0-SNAPSHOT + Helidon Examples TODO Demo + + + TODO demo application + + + + frontend + backend + + diff --git a/examples/translator-app/README.md b/examples/translator-app/README.md new file mode 100644 index 000000000..e1eea4738 --- /dev/null +++ b/examples/translator-app/README.md @@ -0,0 +1,84 @@ +# Translator Example Application + +This application demonstrates a pseudo application composed of two microservices + implemented with Helidon SE. + +## Start Zipkin + +With Docker: +```shell +docker run --name zipkin -d -p 9411:9411 openzipkin/zipkin +``` + +```shell +curl -sSL https://zipkin.io/quickstart.sh | bash -s +java -jar zipkin.jar +``` + +With Kubernetes: +```shell +kubectl apply \ + -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/ingress-nginx-3.15.2/deploy/static/provider/cloud/deploy.yaml \ + -f ../k8s/zipkin.yaml +``` + +## Build and run + +With Docker: +```shell +docker build -t helidon-examples-translator-backend backend/ +docker build -t helidon-examples-translator-frontend frontend/ +docker run --rm -d -p 9080:9080 \ + --link zipkin \ + --name helidon-examples-translator-backend \ + helidon-examples-translator-backend:latest +docker run --rm -d -p 8080:8080 \ + --link zipkin \ + --link helidon-examples-translator-backend \ + --name helidon-examples-translator-frontend \ + helidon-examples-translator-frontend:latest +``` + +```shell +mvn package +java -jar backend/target/helidon-examples-translator-backend.jar & +java -jar frontend/target/helidon-examples-translator-frontend.jar +``` + +Try the endpoint: +```shell +curl "http://localhost:8080?q=cloud&lang=czech" +curl "http://localhost:8080?q=cloud&lang=french" +curl "http://localhost:8080?q=cloud&lang=italian" +``` + +Then check out the traces at http://localhost:9411. + +## Run with Kubernetes (docker for desktop) + +```shell +docker build -t helidon-examples-translator-backend backend/ +docker build -t helidon-examples-translator-frontend frontend/ +kubectl apply -f backend/app.yaml -f frontend/app.yaml +``` + +Try the endpoint: +```shell +curl "http://localhost/translator?q=cloud&lang=czech" +curl "http://localhost/translator?q=cloud&lang=french" +curl "http://localhost/translator?q=cloud&lang=italian" +``` + +Then check out the traces at http://localhost/zipkin. + +Stop the docker containers: +```shell +docker stop zipkin \ + helidon-examples-translator-backend \ + helidon-examples-translator-frontend +``` + +Delete the Kubernetes resources: +```shell +kubectl delete -f backend/app.yaml -f frontend/app.yaml +``` \ No newline at end of file diff --git a/examples/translator-app/backend/Dockerfile b/examples/translator-app/backend/Dockerfile new file mode 100644 index 000000000..2b7da0842 --- /dev/null +++ b/examples/translator-app/backend/Dockerfile @@ -0,0 +1,56 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -DskipTests + +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-examples-translator-backend.jar ./ +COPY --from=build /helidon/target/libs ./libs + +ENV tracing.host="zipkin" + +CMD ["java", "-jar", "helidon-examples-translator-backend.jar"] + +EXPOSE 9080 diff --git a/examples/translator-app/backend/app.yaml b/examples/translator-app/backend/app.yaml new file mode 100644 index 000000000..e4f601e6e --- /dev/null +++ b/examples/translator-app/backend/app.yaml @@ -0,0 +1,54 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helidon-examples-translator-backend + labels: + app: helidon-examples-translator-backend +spec: + replicas: 1 + template: + metadata: + labels: + app: helidon-examples-translator-backend + spec: + containers: + - name: helidon-examples-translator-backend + image: helidon-examples-translator-backend:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 9080 + env: + - name: tracing.host + value: zipkin +--- + +apiVersion: v1 +kind: Service +metadata: + name: helidon-examples-translator-backend + labels: + app: helidon-examples-translator-backend +spec: + type: ClusterIP + selector: + app: helidon-examples-translator-backend + ports: + - port: 9080 + targetPort: 9080 + name: http diff --git a/examples/translator-app/backend/pom.xml b/examples/translator-app/backend/pom.xml new file mode 100644 index 000000000..29e3076b8 --- /dev/null +++ b/examples/translator-app/backend/pom.xml @@ -0,0 +1,74 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.translator + helidon-examples-translator-backend + 1.0.0-SNAPSHOT + Helidon Examples Translator Backend + + + A translator backend example app. + + + + io.helidon.examples.translator.backend.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.config + helidon-config + + + io.helidon.tracing.providers + helidon-tracing-providers-zipkin + + + io.helidon.webserver.observe + helidon-webserver-observe-tracing + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/Main.java b/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/Main.java new file mode 100644 index 000000000..3a158de1e --- /dev/null +++ b/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/Main.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.translator.backend; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.logging.common.LogConfig; +import io.helidon.tracing.Tracer; +import io.helidon.tracing.TracerBuilder; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.tracing.TracingObserver; + +/** + * Translator application backend main class. + */ +public class Main { + + private Main() { + } + + static void setup(WebServerConfig.Builder server) { + Config config = Config.builder() + .sources(ConfigSources.environmentVariables()) + .build(); + + Tracer tracer = TracerBuilder.create(config.get("tracing")) + .serviceName("helidon-webserver-translator-backend") + .build(); + + server.port(9080) + .addFeature(ObserveFeature.builder() + .addObserver(TracingObserver.create(tracer)) + .build()) + .routing(routing -> routing + .register(new TranslatorBackendService())); + } + + /** + * The main method of Translator backend. + * + * @param args command-line args, currently ignored. + */ + public static void main(String[] args) { + // configure logging in order to not have the standard JVM defaults + LogConfig.configureRuntime(); + + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build().start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + } +} diff --git a/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/TranslatorBackendService.java b/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/TranslatorBackendService.java new file mode 100644 index 000000000..7301afea7 --- /dev/null +++ b/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/TranslatorBackendService.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.translator.backend; + +import java.util.HashMap; +import java.util.Map; + +import io.helidon.http.BadRequestException; +import io.helidon.http.NotFoundException; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * Translator backend service. + */ +@SuppressWarnings("SpellCheckingInspection") +public class TranslatorBackendService implements HttpService { + + private static final String CZECH = "czech"; + private static final String SPANISH = "spanish"; + private static final String CHINESE = "chinese"; + private static final String HINDI = "hindi"; + private static final String ITALIAN = "italian"; + private static final String FRENCH = "french"; + + private static final String SEPARATOR = "."; + private static final Map TRANSLATED_WORDS_REPOSITORY = new HashMap<>(); + + static { + //translation for word "cloud" + TRANSLATED_WORDS_REPOSITORY.put("cloud" + SEPARATOR + CZECH, "oblak"); + TRANSLATED_WORDS_REPOSITORY.put("cloud" + SEPARATOR + SPANISH, "nube"); + TRANSLATED_WORDS_REPOSITORY.put("cloud" + SEPARATOR + CHINESE, "云"); + TRANSLATED_WORDS_REPOSITORY.put("cloud" + SEPARATOR + HINDI, "बादल"); + TRANSLATED_WORDS_REPOSITORY.put("cloud" + SEPARATOR + ITALIAN, "nube"); + TRANSLATED_WORDS_REPOSITORY.put("cloud" + SEPARATOR + FRENCH, "nuage"); + + //one two three four five six seven eight nine ten + //jedna dvě tři čtyři pět šest sedm osm devět deset + //uno dos tres cuatro cinco seis siete ocho nueve diez + //一二三四五六七八九十 + //एक दो तीन चार पांच छ सात आठ नौ दस + // uno due tre quattro cinque sei sette otto nove dieci + // un deux trois quatre cinq six sept huit neuf dix + + //translation for word "one" + TRANSLATED_WORDS_REPOSITORY.put("one" + SEPARATOR + CZECH, "jedna"); + TRANSLATED_WORDS_REPOSITORY.put("one" + SEPARATOR + SPANISH, "uno"); + TRANSLATED_WORDS_REPOSITORY.put("one" + SEPARATOR + CHINESE, "一"); + TRANSLATED_WORDS_REPOSITORY.put("one" + SEPARATOR + HINDI, "एक"); + TRANSLATED_WORDS_REPOSITORY.put("one" + SEPARATOR + ITALIAN, "uno"); + TRANSLATED_WORDS_REPOSITORY.put("one" + SEPARATOR + FRENCH, "un"); + //translation for word "two" + TRANSLATED_WORDS_REPOSITORY.put("two" + SEPARATOR + CZECH, "dvě"); + TRANSLATED_WORDS_REPOSITORY.put("two" + SEPARATOR + SPANISH, "dos"); + TRANSLATED_WORDS_REPOSITORY.put("two" + SEPARATOR + CHINESE, "二"); + TRANSLATED_WORDS_REPOSITORY.put("two" + SEPARATOR + HINDI, "दो"); + TRANSLATED_WORDS_REPOSITORY.put("two" + SEPARATOR + ITALIAN, "due"); + TRANSLATED_WORDS_REPOSITORY.put("two" + SEPARATOR + FRENCH, "deux"); + //translation for word "three" + TRANSLATED_WORDS_REPOSITORY.put("three" + SEPARATOR + CZECH, "tři"); + TRANSLATED_WORDS_REPOSITORY.put("three" + SEPARATOR + SPANISH, "tres"); + TRANSLATED_WORDS_REPOSITORY.put("three" + SEPARATOR + CHINESE, "三"); + TRANSLATED_WORDS_REPOSITORY.put("three" + SEPARATOR + HINDI, "तीन"); + TRANSLATED_WORDS_REPOSITORY.put("three" + SEPARATOR + ITALIAN, "tre"); + TRANSLATED_WORDS_REPOSITORY.put("three" + SEPARATOR + FRENCH, "trois"); + //translation for word "four" + TRANSLATED_WORDS_REPOSITORY.put("four" + SEPARATOR + CZECH, "čtyři"); + TRANSLATED_WORDS_REPOSITORY.put("four" + SEPARATOR + SPANISH, "cuatro"); + TRANSLATED_WORDS_REPOSITORY.put("four" + SEPARATOR + CHINESE, "四"); + TRANSLATED_WORDS_REPOSITORY.put("four" + SEPARATOR + HINDI, "चार"); + TRANSLATED_WORDS_REPOSITORY.put("four" + SEPARATOR + ITALIAN, "quattro"); + TRANSLATED_WORDS_REPOSITORY.put("four" + SEPARATOR + FRENCH, "quatre"); + //translation for word "five" + TRANSLATED_WORDS_REPOSITORY.put("five" + SEPARATOR + CZECH, "pět"); + TRANSLATED_WORDS_REPOSITORY.put("five" + SEPARATOR + SPANISH, "cinco"); + TRANSLATED_WORDS_REPOSITORY.put("five" + SEPARATOR + CHINESE, "五"); + TRANSLATED_WORDS_REPOSITORY.put("five" + SEPARATOR + HINDI, "पांच"); + TRANSLATED_WORDS_REPOSITORY.put("five" + SEPARATOR + ITALIAN, "cinque"); + TRANSLATED_WORDS_REPOSITORY.put("five" + SEPARATOR + FRENCH, "cinq"); + //translation for word "six" + TRANSLATED_WORDS_REPOSITORY.put("six" + SEPARATOR + CZECH, "šest"); + TRANSLATED_WORDS_REPOSITORY.put("six" + SEPARATOR + SPANISH, "seis"); + TRANSLATED_WORDS_REPOSITORY.put("six" + SEPARATOR + CHINESE, "六"); + TRANSLATED_WORDS_REPOSITORY.put("six" + SEPARATOR + HINDI, "छ"); + TRANSLATED_WORDS_REPOSITORY.put("six" + SEPARATOR + ITALIAN, "sei"); + TRANSLATED_WORDS_REPOSITORY.put("six" + SEPARATOR + FRENCH, "six"); + //translation for word "seven" + TRANSLATED_WORDS_REPOSITORY.put("seven" + SEPARATOR + CZECH, "sedm"); + TRANSLATED_WORDS_REPOSITORY.put("seven" + SEPARATOR + SPANISH, "siete"); + TRANSLATED_WORDS_REPOSITORY.put("seven" + SEPARATOR + CHINESE, "七"); + TRANSLATED_WORDS_REPOSITORY.put("seven" + SEPARATOR + HINDI, "सात"); + TRANSLATED_WORDS_REPOSITORY.put("seven" + SEPARATOR + ITALIAN, "sette"); + TRANSLATED_WORDS_REPOSITORY.put("seven" + SEPARATOR + FRENCH, "sept"); + //translation for word "eight" + TRANSLATED_WORDS_REPOSITORY.put("eight" + SEPARATOR + CZECH, "osm"); + TRANSLATED_WORDS_REPOSITORY.put("eight" + SEPARATOR + SPANISH, "ocho"); + TRANSLATED_WORDS_REPOSITORY.put("eight" + SEPARATOR + CHINESE, "八"); + TRANSLATED_WORDS_REPOSITORY.put("eight" + SEPARATOR + HINDI, "आठ"); + TRANSLATED_WORDS_REPOSITORY.put("eight" + SEPARATOR + ITALIAN, "otto"); + TRANSLATED_WORDS_REPOSITORY.put("eight" + SEPARATOR + FRENCH, "huit"); + //translation for word "nine" + TRANSLATED_WORDS_REPOSITORY.put("nine" + SEPARATOR + CZECH, "devět"); + TRANSLATED_WORDS_REPOSITORY.put("nine" + SEPARATOR + SPANISH, "nueve"); + TRANSLATED_WORDS_REPOSITORY.put("nine" + SEPARATOR + CHINESE, "九"); + TRANSLATED_WORDS_REPOSITORY.put("nine" + SEPARATOR + HINDI, "नौ"); + TRANSLATED_WORDS_REPOSITORY.put("nine" + SEPARATOR + ITALIAN, "nove"); + TRANSLATED_WORDS_REPOSITORY.put("nine" + SEPARATOR + FRENCH, "neuf"); + //translation for word "ten" + TRANSLATED_WORDS_REPOSITORY.put("ten" + SEPARATOR + CZECH, "deset"); + TRANSLATED_WORDS_REPOSITORY.put("ten" + SEPARATOR + SPANISH, "diez"); + TRANSLATED_WORDS_REPOSITORY.put("ten" + SEPARATOR + CHINESE, "十"); + TRANSLATED_WORDS_REPOSITORY.put("ten" + SEPARATOR + HINDI, "दस"); + TRANSLATED_WORDS_REPOSITORY.put("ten" + SEPARATOR + ITALIAN, "dieci"); + TRANSLATED_WORDS_REPOSITORY.put("ten" + SEPARATOR + FRENCH, "dix"); + } + + @Override + public void routing(HttpRules rules) { + rules.get(this::getText); + } + + private void getText(ServerRequest req, ServerResponse res) { + + String query = req.query().first("q") + .orElseThrow(() -> new BadRequestException("missing query parameter 'q'")); + String language = req.query().first("lang") + .orElseThrow(() -> new BadRequestException("missing query parameter 'lang'")); + String translation; + translation = switch (language) { + case CZECH -> TRANSLATED_WORDS_REPOSITORY.get(query + SEPARATOR + CZECH); + case SPANISH -> TRANSLATED_WORDS_REPOSITORY.get(query + SEPARATOR + SPANISH); + case CHINESE -> TRANSLATED_WORDS_REPOSITORY.get(query + SEPARATOR + CHINESE); + case HINDI -> TRANSLATED_WORDS_REPOSITORY.get(query + SEPARATOR + HINDI); + case ITALIAN -> TRANSLATED_WORDS_REPOSITORY.get(query + SEPARATOR + ITALIAN); + case FRENCH -> TRANSLATED_WORDS_REPOSITORY.get(query + SEPARATOR + FRENCH); + default -> throw new NotFoundException(String.format( + "Language '%s' not in supported. Supported languages: %s, %s, %s, %s.", + language, + CZECH, SPANISH, CHINESE, HINDI)); + }; + + if (translation != null) { + res.send(translation); + } else { + res.status(404) + .send(String.format("Word '%s' not in the dictionary", query)); + } + } +} diff --git a/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/package-info.java b/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/package-info.java new file mode 100644 index 000000000..8bc85e4d5 --- /dev/null +++ b/examples/translator-app/backend/src/main/java/io/helidon/examples/translator/backend/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Demo examples Translator Backend package. + */ +package io.helidon.examples.translator.backend; diff --git a/examples/translator-app/backend/src/main/resources/logging.properties b/examples/translator-app/backend/src/main/resources/logging.properties new file mode 100644 index 000000000..0e4f9023c --- /dev/null +++ b/examples/translator-app/backend/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +#All attributes details +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#All log level details +.level=INFO + +io.helidon.webserver.level=FINEST +org.glassfish.jersey.internal.Errors.level=SEVERE diff --git a/examples/translator-app/frontend/Dockerfile b/examples/translator-app/frontend/Dockerfile new file mode 100644 index 000000000..d28e9c545 --- /dev/null +++ b/examples/translator-app/frontend/Dockerfile @@ -0,0 +1,57 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -Dmaven.test.skip + +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-examples-translator-frontend.jar ./ +COPY --from=build /helidon/target/libs ./libs + +ENV tracing.host="zipkin" +ENV backend.host="helidon-examples-translator-backend" + +CMD ["java", "-jar", "helidon-examples-translator-frontend.jar"] + +EXPOSE 8080 diff --git a/examples/translator-app/frontend/app.yaml b/examples/translator-app/frontend/app.yaml new file mode 100644 index 000000000..0d6a36ba1 --- /dev/null +++ b/examples/translator-app/frontend/app.yaml @@ -0,0 +1,76 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helidon-examples-translator-frontend + labels: + app: helidon-examples-translator-frontend +spec: + replicas: 1 + template: + metadata: + labels: + app: helidon-examples-translator-frontend + spec: + containers: + - name: translator-frontend + image: helidon-examples-translator-frontend:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + env: + - name: tracing.host + value: zipkin + - name: backend.host + value: helidon-examples-translator-backend + +--- + +apiVersion: v1 +kind: Service +metadata: + name: helidon-examples-translator-frontend + labels: + app: helidon-examples-translator-frontend +spec: + type: ClusterIP + ports: + - name: http + port: 8080 + selector: + app: helidon-examples-translator-frontend + sessionAffinity: None + +--- + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: helidon-examples-translator-frontend +spec: + rules: + - host: localhost + http: + paths: + - path: /translator + pathType: Prefix + backend: + service: + name: helidon-examples-translator-frontend + port: + number: 8080 diff --git a/examples/translator-app/frontend/pom.xml b/examples/translator-app/frontend/pom.xml new file mode 100644 index 000000000..b9fe162ff --- /dev/null +++ b/examples/translator-app/frontend/pom.xml @@ -0,0 +1,121 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.translator + helidon-examples-translator-frontend + 1.0.0-SNAPSHOT + Helidon Examples Translator Frontend + + + A translator frontend example app. + + + + io.helidon.examples.translator.frontend.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe-tracing + + + io.helidon.webclient + helidon-webclient + + + io.helidon.config + helidon-config + + + io.helidon.tracing.providers + helidon-tracing-providers-zipkin + + + io.helidon.common + helidon-common + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + + + + add-backend-dependency + + + !maven.test.skip + + + + + io.helidon.examples.translator + helidon-examples-translator-backend + ${project.version} + test + + + + + diff --git a/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/Main.java b/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/Main.java new file mode 100644 index 000000000..26c2c5277 --- /dev/null +++ b/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/Main.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.translator.frontend; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.logging.common.LogConfig; +import io.helidon.tracing.Tracer; +import io.helidon.tracing.TracerBuilder; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.tracing.TracingObserver; + +/** + * Translator application frontend main class. + */ +public class Main { + + private Main() { + } + + /** + * The main method of Translator frontend. + * + * @param args command-line args, currently ignored. + */ + public static void main(String[] args) { + // configure logging in order to not have the standard JVM defaults + LogConfig.configureRuntime(); + + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build().start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + } + + static void setup(WebServerConfig.Builder server) { + Config config = Config.builder() + .sources(ConfigSources.environmentVariables()) + .build(); + + Tracer tracer = TracerBuilder.create(config.get("tracing")) + .serviceName("helidon-webserver-translator-frontend") + .registerGlobal(false) + .build(); + String backendHost = config.get("backend.host").asString().orElse("localhost"); + server.addFeature(ObserveFeature.builder() + .addObserver(TracingObserver.create(tracer)) + .build()) + .routing(routing -> routing + .register(new TranslatorFrontendService(backendHost, 9080))); + } +} diff --git a/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/TranslatorFrontendService.java b/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/TranslatorFrontendService.java new file mode 100644 index 000000000..215f880b6 --- /dev/null +++ b/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/TranslatorFrontendService.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.translator.frontend; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.http.BadRequestException; +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * Translator frontend resource. + */ +public final class TranslatorFrontendService implements HttpService { + + private static final Logger LOGGER = Logger.getLogger(TranslatorFrontendService.class.getName()); + private final Http1Client client; + + /** + * Create a new instance. + * + * @param backendHostname backend service host + * @param backendPort backend service port + */ + public TranslatorFrontendService(String backendHostname, int backendPort) { + client = Http1Client.builder() + .baseUri("http://" + backendHostname + ":" + backendPort) + .build(); + } + + @Override + public void routing(HttpRules rules) { + rules.get(this::getText); + } + + private void getText(ServerRequest re, ServerResponse res) { + try { + String query = re.query() + .first("q") + .orElseThrow(() -> new BadRequestException("missing query parameter 'q'")); + + String language = re.query() + .first("lang") + .orElseThrow(() -> new BadRequestException("missing query parameter 'lang'")); + + try (Http1ClientResponse clientRes = client.get() + .queryParam("q", query) + .queryParam("lang", language) + .request()) { + + final String result; + if (clientRes.status().family() == Status.Family.SUCCESSFUL) { + result = clientRes.entity().as(String.class); + } else { + result = "Error: " + clientRes.entity().as(String.class); + } + res.send(result + "\n"); + } + } catch (RuntimeException pe) { + LOGGER.log(Level.WARNING, "Problem to call translator frontend.", pe); + res.status(503).send("Translator backend service isn't available."); + } + } +} diff --git a/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/package-info.java b/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/package-info.java new file mode 100644 index 000000000..0e591dde3 --- /dev/null +++ b/examples/translator-app/frontend/src/main/java/io/helidon/examples/translator/frontend/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Demo examples Translator Frontend package. + */ +package io.helidon.examples.translator.frontend; diff --git a/examples/translator-app/frontend/src/main/resources/logging.properties b/examples/translator-app/frontend/src/main/resources/logging.properties new file mode 100644 index 000000000..0e4f9023c --- /dev/null +++ b/examples/translator-app/frontend/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +#All attributes details +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#All log level details +.level=INFO + +io.helidon.webserver.level=FINEST +org.glassfish.jersey.internal.Errors.level=SEVERE diff --git a/examples/translator-app/frontend/src/test/java/io/helidon/examples/translator/TranslatorTest.java b/examples/translator-app/frontend/src/test/java/io/helidon/examples/translator/TranslatorTest.java new file mode 100644 index 000000000..9ec6c59c8 --- /dev/null +++ b/examples/translator-app/frontend/src/test/java/io/helidon/examples/translator/TranslatorTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.translator; + +import io.helidon.examples.translator.backend.TranslatorBackendService; +import io.helidon.examples.translator.frontend.TranslatorFrontendService; +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 org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * The TranslatorTest. + */ +@SuppressWarnings("SpellCheckingInspection") +@ServerTest +public class TranslatorTest { + + private final Http1Client client; + + public TranslatorTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + public static void setUp(WebServerConfig.Builder builder) { + builder.routing(routing -> routing. + register(new TranslatorFrontendService("localhost", 9080))) + .putSocket("backend", socket -> socket + .port(9080) + .routing(routing -> routing.register(new TranslatorBackendService()))); + } + + @Test + public void testCzech() { + try (Http1ClientResponse response = client.get() + .queryParam("q", "cloud") + .queryParam("lang", "czech") + .request()) { + + assertThat("Unexpected response! Status code: " + response.status(), + response.entity().as(String.class), is("oblak\n")); + } + } + + @Test + public void testItalian() { + try (Http1ClientResponse response = client.get() + .queryParam("q", "cloud") + .queryParam("lang", "italian") + .request()) { + + assertThat("Unexpected response! Status code: " + response.status(), + response.entity().as(String.class), is("nube\n")); + } + } + + @Test + public void testFrench() { + try (Http1ClientResponse response = client.get() + .queryParam("q", "cloud") + .queryParam("lang", "french") + .request()) { + + assertThat("Unexpected response! Status code: " + response.status(), + response.entity().as(String.class), is("nuage\n")); + } + } +} diff --git a/examples/translator-app/pom.xml b/examples/translator-app/pom.xml new file mode 100644 index 000000000..29a3de7d6 --- /dev/null +++ b/examples/translator-app/pom.xml @@ -0,0 +1,42 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + io.helidon.examples.translator + helidon-examples-translator-project + pom + Helidon Examples Translator Demo + + + A translator example app. + + + + backend + frontend + + diff --git a/examples/webclient/pom.xml b/examples/webclient/pom.xml new file mode 100644 index 000000000..a08ed72f1 --- /dev/null +++ b/examples/webclient/pom.xml @@ -0,0 +1,37 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + + io.helidon.examples.webclient + helidon-examples-webclient-project + Helidon Examples WebClient + + pom + + + standalone + + diff --git a/examples/webclient/standalone/README.md b/examples/webclient/standalone/README.md new file mode 100644 index 000000000..de61b3654 --- /dev/null +++ b/examples/webclient/standalone/README.md @@ -0,0 +1,33 @@ +# Standalone WebClient Example + +This example demonstrates how to use the Helidon SE WebClient from a +standalone Java program to connect to a server. + +## Build + +```shell +mvn package +``` + +## Run + +First, start the server: + +```shell +java -jar target/helidon-examples-webclient-standalone.jar +``` + +Note the port number that it displays. For example: + +``` +WEB server is up! http://localhost:PORT/greet +``` + +Then run the client, passing the port number. It will connect +to the server: + +```shell +export PORT=33417 +java -cp "target/classes:target/libs/*" io.helidon.examples.webclient.standalone.ClientMain ${PORT} +``` + diff --git a/examples/webclient/standalone/pom.xml b/examples/webclient/standalone/pom.xml new file mode 100644 index 000000000..ae165d577 --- /dev/null +++ b/examples/webclient/standalone/pom.xml @@ -0,0 +1,101 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + helidon-examples-webclient-standalone + 1.0.0-SNAPSHOT + Helidon Examples WebClient Standalone + + + io.helidon.examples.webclient.standalone.ServerMain + + + + + io.helidon.config + helidon-config-yaml + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.metrics + helidon-metrics-api + + + io.helidon.metrics.providers + helidon-metrics-providers-micrometer + runtime + + + io.helidon.webclient + helidon-webclient + + + io.helidon.webclient + helidon-webclient-metrics + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + + diff --git a/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/ClientMain.java b/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/ClientMain.java new file mode 100644 index 000000000..71c8dcb09 --- /dev/null +++ b/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/ClientMain.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webclient.standalone; + +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.config.Config; +import io.helidon.config.ConfigValue; +import io.helidon.http.Method; +import io.helidon.http.Status; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.webclient.api.HttpClientResponse; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.metrics.WebClientMetrics; +import io.helidon.webclient.spi.WebClientService; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple WebClient usage class. + *

    + * Each of the methods demonstrates different usage of the WebClient. + */ +public class ClientMain { + + private static final MeterRegistry METER_REGISTRY = Metrics.globalRegistry(); + private static final JsonBuilderFactory JSON_BUILDER = Json.createBuilderFactory(Map.of()); + private static final JsonObject JSON_NEW_GREETING; + + static { + JSON_NEW_GREETING = JSON_BUILDER.createObjectBuilder() + .add("greeting", "Hola") + .build(); + } + + private ClientMain() { + } + + /** + * 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() + "/greet"; + } else { + url = "http://localhost:" + Integer.parseInt(args[0]) + "/greet"; + } + + WebClient client = WebClient.builder() + .baseUri(url) + .config(config.get("client")) + .build(); + + performPutMethod(client); + performGetMethod(client); + followRedirects(client); + getResponseAsAnJsonObject(client); + saveResponseToFile(client); + clientMetricsExample(url, config); + } + + static Status performPutMethod(WebClient client) { + System.out.println("Put request execution."); + try (HttpClientResponse response = client.put("/greeting").submit(JSON_NEW_GREETING)) { + System.out.println("PUT request executed with status: " + response.status()); + return response.status(); + } + } + + static String performGetMethod(WebClient client) { + System.out.println("Get request execution."); + String result = client.get().requestEntity(String.class); + System.out.println("GET request successfully executed."); + System.out.println(result); + return result; + } + + static String followRedirects(WebClient client) { + System.out.println("Following request redirection."); + try (HttpClientResponse response = client.get("/redirect").request()) { + if (response.status() != Status.OK_200) { + throw new IllegalStateException("Follow redirection failed!"); + } + String result = response.as(String.class); + System.out.println("Redirected request successfully followed."); + System.out.println(result); + return result; + } + } + + static void getResponseAsAnJsonObject(WebClient client) { + System.out.println("Requesting from JsonObject."); + JsonObject jsonObject = client.get().requestEntity(JsonObject.class); + System.out.println("JsonObject successfully obtained."); + System.out.println(jsonObject); + } + + static void saveResponseToFile(WebClient client) { + Path file = Paths.get("test.txt"); + try { + Files.deleteIfExists(file); + } catch (IOException e) { + e.printStackTrace(); + } + + System.out.println("Downloading server response to file: " + file); + try (HttpClientResponse response = client.get().request()) { + Files.copy(response.entity().inputStream(), file); + System.out.println("Download complete!"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + static String clientMetricsExample(String url, Config config) { + //This part here is only for verification purposes, it is not needed to be done for actual usage. + String counterName = "example.metric.GET.localhost"; + Counter counter = METER_REGISTRY.getOrCreate(Counter.builder(counterName)); + System.out.println(counterName + ": " + counter.count()); + + //Creates new metric which will count all GET requests and has format of example.metric.GET. + WebClientService clientService = WebClientMetrics.counter() + .methods(Method.GET) + .nameFormat("example.metric.%1$s.%2$s") + .build(); + + //This newly created metric now needs to be registered to WebClient. + WebClient client = WebClient.builder() + .baseUri(url) + .config(config) + .addService(clientService) + .build(); + + //Perform any GET request using this newly created WebClient instance. + String result = performGetMethod(client); + //Verification for example purposes that metric has been incremented. + System.out.println(counterName + ": " + counter.count()); + return result; + } +} diff --git a/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/GreetService.java b/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/GreetService.java new file mode 100644 index 000000000..703b33c9e --- /dev/null +++ b/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/GreetService.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webclient.standalone; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.config.Config; +import io.helidon.http.HeaderNames; +import io.helidon.http.Status; +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 jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * A simple service to greet you. Examples: + *

    + * Get default greeting message: + * curl -X GET http://localhost:8080/greet + *

    + * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + *

    + * Change greeting + * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting + *

    + * The message is returned as a JSON object + */ + +public class GreetService implements HttpService { + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + /** + * The config value for the key {@code greeting}. + */ + private final AtomicReference greeting = new AtomicReference<>(); + + GreetService() { + Config config = Config.global(); + greeting.set(config.get("app.greeting").asString().orElse("Ciao")); + } + + /** + * A service registers itself by updating the routing rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.get("/", this::getDefaultMessageHandler) + .get("/redirect", this::redirect) + .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 status code of {@link io.helidon.http.Status#MOVED_PERMANENTLY_301} and the new location where should + * client redirect. + * + * @param request the server request + * @param response the server response + */ + private void redirect(ServerRequest request, + ServerResponse response) { + int port = request.context().get(WebServer.class).orElseThrow().port(); + response.headers().add(HeaderNames.LOCATION, "http://localhost:" + port + "/greet/"); + response.status(Status.MOVED_PERMANENTLY_301).send(); + } + + /** + * 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().get("name"); + sendResponse(response, name); + } + + /** + * 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) { + JsonObject jsonObject = request.content().as(JsonObject.class); + updateGreetingFromJson(jsonObject, response); + } + + private void sendResponse(ServerResponse response, String name) { + String msg = String.format("%s %s!", greeting.get(), name); + + JsonObject returnObject = JSON.createObjectBuilder() + .add("message", msg) + .build(); + response.send(returnObject); + } + + private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { + + if (!jo.containsKey("greeting")) { + JsonObject jsonErrorObject = JSON.createObjectBuilder() + .add("error", "No greeting provided") + .build(); + response.status(Status.BAD_REQUEST_400) + .send(jsonErrorObject); + return; + } + + greeting.set(jo.getString("greeting")); + response.status(Status.NO_CONTENT_204).send(); + } +} diff --git a/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/ServerMain.java b/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/ServerMain.java new file mode 100644 index 000000000..00ea2e060 --- /dev/null +++ b/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/ServerMain.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webclient.standalone; + +import io.helidon.config.Config; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * The application main class. + */ +public final class ServerMain { + + /** + * Cannot be instantiated. + */ + private ServerMain() { + } + + /** + * WebServer starting method. + * + * @param args starting arguments + */ + public static void main(String[] args) { + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + Config.global(config); + + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.build().start(); + server.context().register(server); + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + /** + * Set up the server. + * + * @param server server builder + */ + static void setup(WebServerConfig.Builder server) { + Config config = Config.global(); + server.config(config.get("server")) + .routing(ServerMain::routing); + } + + /** + * Setup routing. + * + * @param routing routing builder + */ + private static void routing(HttpRouting.Builder routing) { + routing.register("/greet", new GreetService()); + } +} diff --git a/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/package-info.java b/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/package-info.java new file mode 100644 index 000000000..b080637ed --- /dev/null +++ b/examples/webclient/standalone/src/main/java/io/helidon/examples/webclient/standalone/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Basic examples of webclient usage. + */ +package io.helidon.examples.webclient.standalone; diff --git a/examples/webclient/standalone/src/main/resources/application.yaml b/examples/webclient/standalone/src/main/resources/application.yaml new file mode 100644 index 000000000..825d1ef67 --- /dev/null +++ b/examples/webclient/standalone/src/main/resources/application.yaml @@ -0,0 +1,26 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +app: + greeting: "Hello" + +server: + port: -1 + host: 0.0.0.0 + +client: + follow-redirects: true + max-redirects: 8 \ No newline at end of file diff --git a/examples/webclient/standalone/src/main/resources/full-webclient-config.yaml b/examples/webclient/standalone/src/main/resources/full-webclient-config.yaml new file mode 100644 index 000000000..e106ffd75 --- /dev/null +++ b/examples/webclient/standalone/src/main/resources/full-webclient-config.yaml @@ -0,0 +1,75 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +client: + event-loop: + name-prefix: client-thread- + workers: 1 + connect-timeout-millis: 2000 + read-timeout-millis: 2000 + follow-redirects: true + max-redirects: 5 + user-agent: "Helidon" + cookies: + automatic-store-enabled: true + default-cookies: + - name: "env" + value: "dev" + headers: + - name: "Accept" + value: ["application/json","text/plain"] + services: + exclude: ["some.webclient.service.Provider"] + config: + metrics: + - type: METER + name-format: "client.meter.overall" + - type: TIMER + # meter per method + name-format: "client.meter.%1$s" + - methods: ["GET"] + type: COUNTER + errors: false + name-format: "client.counter.%1$s.success" + description: "Counter of successful GET requests" + - methods: ["PUT", "POST", "DELETE"] + type: COUNTER + success: false + name-format: "wc.counter.%1$s.error" + description: "Counter of failed PUT, POST and DELETE requests" + - methods: ["GET"] + type: GAUGE_IN_PROGRESS + name-format: "client.inprogress.%2$s" + description: "In progress requests to host" + tracing: + proxy: + use-system-selector: false + host: "hostName" + port: 80 + no-proxy: ["localhost:8080", ".helidon.io", "192.168.1.1"] + ssl: + server: + disable-hostname-verification: false + trust-all: false + truststore: + keystore-resource-path: "path to the keystore" + keystore-type: "JKS" + keystore-passphrase: "changeit" + trust-store: true + client: + keystore: + keystore-resource-path: "path to client keystore" + keystore-passphrase: "changeit" + trust-store: true diff --git a/examples/webclient/standalone/src/test/java/io/helidon/examples/webclient/standalone/ClientMainTest.java b/examples/webclient/standalone/src/test/java/io/helidon/examples/webclient/standalone/ClientMainTest.java new file mode 100644 index 000000000..06aed5437 --- /dev/null +++ b/examples/webclient/standalone/src/test/java/io/helidon/examples/webclient/standalone/ClientMainTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webclient.standalone; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; + +import io.helidon.config.Config; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.spi.WebClientService; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * Test for verification of WebClient example. + */ +@ServerTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ClientMainTest { + + private static final MeterRegistry METRIC_REGISTRY = Metrics.globalRegistry(); + + private final WebServer server; + private final Path testFile; + + public ClientMainTest(WebServer server) { + this.server = server; + server.context().register(server); + this.testFile = Paths.get("test.txt"); + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + Config.global(Config.create()); + ServerMain.setup(server); + } + + @AfterEach + public void afterEach() throws IOException { + Files.deleteIfExists(testFile); + } + + private WebClient client(WebClientService... services) { + Config config = Config.create(); + return WebClient.builder() + .baseUri("http://localhost:" + server.port() + "/greet") + .config(config.get("client")) + .update(it -> Stream.of(services).forEach(it::addService)) + .build(); + } + + @Test + @Order(1) + public void testPerformRedirect() { + WebClient client = client(); + String greeting = ClientMain.followRedirects(client); + assertThat(greeting, is("{\"message\":\"Hello World!\"}")); + } + + @Test + @Order(2) + public void testFileDownload() throws IOException { + WebClient client = client(); + ClientMain.saveResponseToFile(client); + assertThat(Files.exists(testFile), is(true)); + assertThat(Files.readString(testFile), is("{\"message\":\"Hello World!\"}")); + } + + @Test + @Order(3) + public void testMetricsExample() { + String counterName = "example.metric.GET.localhost"; + Counter counter = METRIC_REGISTRY.getOrCreate(Counter.builder(counterName)); + assertThat("Counter " + counterName + " has not been 0", counter.count(), is(0L)); + ClientMain.clientMetricsExample("http://localhost:" + server.port() + "/greet", Config.create()); + assertThat("Counter " + counterName + " " + "has not been 1", counter.count(), is(1L)); + } + + @Test + @Order(4) + public void testPerformPutAndGetMethod() { + WebClient client = client(); + String greeting = ClientMain.performGetMethod(client); + assertThat(greeting, is("{\"message\":\"Hello World!\"}")); + ClientMain.performPutMethod(client); + greeting = ClientMain.performGetMethod(client); + assertThat(greeting, is("{\"message\":\"Hola World!\"}")); + } + +} diff --git a/examples/webserver/README.md b/examples/webserver/README.md new file mode 100644 index 000000000..41c29e619 --- /dev/null +++ b/examples/webserver/README.md @@ -0,0 +1,3 @@ +# Helidon SE WebServer Examples + +This directory contains Helidon SE webserver examples. \ No newline at end of file diff --git a/examples/webserver/basic/pom.xml b/examples/webserver/basic/pom.xml new file mode 100644 index 000000000..25efe33a8 --- /dev/null +++ b/examples/webserver/basic/pom.xml @@ -0,0 +1,83 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.webserver + helidon-examples-webserver-basic + 1.0.0-SNAPSHOT + Helidon Examples WebServer Basic + + + io.helidon.examples.webserver.basic.BasicMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + diff --git a/examples/webserver/basic/src/main/java/io/helidon/examples/webserver/basic/BasicMain.java b/examples/webserver/basic/src/main/java/io/helidon/examples/webserver/basic/BasicMain.java new file mode 100644 index 000000000..87ec5bf0c --- /dev/null +++ b/examples/webserver/basic/src/main/java/io/helidon/examples/webserver/basic/BasicMain.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.basic; + +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +/** + * As simple as possible with a fixed port. + */ +public class BasicMain { + private BasicMain() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + /* + This would be the simplest possible server + We do not use it, as we want testability + */ + /* + WebServer.builder() + .port(8080) + .routing(router -> router.get("/*", (req, res) -> res.send("WebServer Works!"))) + .start(); + */ + WebServer.builder() + .port(8080) + .routing(BasicMain::routing) + .build() + .start(); + } + + /** + * Set up HTTP routing. + * This method is used from both unit and integration tests. + * + * @param router HTTP routing builder to configure routes for this service + */ + static void routing(HttpRouting.Builder router) { + router.get("/*", (req, res) -> res.send("WebServer Works!")); + } +} diff --git a/examples/webserver/basic/src/main/java/io/helidon/examples/webserver/basic/package-info.java b/examples/webserver/basic/src/main/java/io/helidon/examples/webserver/basic/package-info.java new file mode 100644 index 000000000..8a86a716c --- /dev/null +++ b/examples/webserver/basic/src/main/java/io/helidon/examples/webserver/basic/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Basic example. + */ +package io.helidon.examples.webserver.basic; diff --git a/examples/webserver/basic/src/main/resources/logging.properties b/examples/webserver/basic/src/main/resources/logging.properties new file mode 100644 index 000000000..161f6db0d --- /dev/null +++ b/examples/webserver/basic/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.webserver.level=INFO diff --git a/examples/webserver/basic/src/test/java/io/helidon/examples/webserver/basic/AbstractBasicRoutingTest.java b/examples/webserver/basic/src/test/java/io/helidon/examples/webserver/basic/AbstractBasicRoutingTest.java new file mode 100644 index 000000000..2f7d68648 --- /dev/null +++ b/examples/webserver/basic/src/test/java/io/helidon/examples/webserver/basic/AbstractBasicRoutingTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.basic; + +import io.helidon.webserver.testing.junit5.SetUpRoute; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webserver.http.HttpRouting; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +abstract class AbstractBasicRoutingTest { + private final Http1Client client; + + AbstractBasicRoutingTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + BasicMain.routing(builder); + } + + @Test + void testRootRoute() { + String response = client.get("/") + .request() + .as(String.class); + + assertThat(response, is("WebServer Works!")); + } + + @Test + void testOtherRoute() { + String response = client.get("/longer/path") + .request() + .as(String.class); + + assertThat(response, is("WebServer Works!")); + } +} diff --git a/examples/webserver/basic/src/test/java/io/helidon/examples/webserver/basic/BasicRoutingIT.java b/examples/webserver/basic/src/test/java/io/helidon/examples/webserver/basic/BasicRoutingIT.java new file mode 100644 index 000000000..f271ff11f --- /dev/null +++ b/examples/webserver/basic/src/test/java/io/helidon/examples/webserver/basic/BasicRoutingIT.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.basic; + +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webclient.http1.Http1Client; + +/** + * An integration test that starts the server and invokes the routing through HTTP. + */ +@ServerTest +class BasicRoutingIT extends AbstractBasicRoutingTest { + BasicRoutingIT(Http1Client client) { + super(client); + } +} diff --git a/examples/webserver/basic/src/test/java/io/helidon/examples/webserver/basic/BasicRoutingTest.java b/examples/webserver/basic/src/test/java/io/helidon/examples/webserver/basic/BasicRoutingTest.java new file mode 100644 index 000000000..0e415fc99 --- /dev/null +++ b/examples/webserver/basic/src/test/java/io/helidon/examples/webserver/basic/BasicRoutingTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.basic; + +import io.helidon.webserver.testing.junit5.DirectClient; +import io.helidon.webserver.testing.junit5.RoutingTest; + +/** + * A unit test that does not start the server and invokes the routing directly, with no network traffic. + */ +@RoutingTest +class BasicRoutingTest extends AbstractBasicRoutingTest { + BasicRoutingTest(DirectClient client) { + super(client); + } +} diff --git a/examples/webserver/basic/src/test/resources/logging-test.properties b/examples/webserver/basic/src/test/resources/logging-test.properties new file mode 100644 index 000000000..2fa545536 --- /dev/null +++ b/examples/webserver/basic/src/test/resources/logging-test.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +# java.util.logging.SimpleFormatter.format = [%1$tc] %5$s %6$s%n +java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=WARNING diff --git a/examples/webserver/basics/README.md b/examples/webserver/basics/README.md new file mode 100644 index 000000000..c146d43f7 --- /dev/null +++ b/examples/webserver/basics/README.md @@ -0,0 +1,45 @@ + +# Helidon WebServer Basic Example + +This example consists of various methods that can be selected +at runtime. Each method illustrates a different WebServer concept. +See the comments in `Main.java` for a description of the various +methods. + +## Build and run + +```shell +mvn package +``` + +To see the list of methods that are available run: + +```shell +java -DexampleName=help -jar target/helidon-examples-webserver-basics.jar +``` + +You should see output like: + +``` +Example method names: + help + h + firstRouting + startServer + routingAsFilter + parametersAndHeaders + advancedRouting + organiseCode + readContentEntity + filterAndProcessEntity + supports + errorHandling +``` + +You can then choose the method to execute by setting the `exampleName` system property: + +``` +java -DexampleName=firstRouting -jar target/helidon-examples-webserver-basics.jar +``` + +This will start the Helidon SE WebServer using the method indicated. diff --git a/examples/webserver/basics/pom.xml b/examples/webserver/basics/pom.xml new file mode 100644 index 000000000..9dfa354b9 --- /dev/null +++ b/examples/webserver/basics/pom.xml @@ -0,0 +1,85 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-basics + 1.0.0-SNAPSHOT + Helidon Examples WebServer Basics + + + Examples of elementary use of the Web Server + + + + io.helidon.examples.webserver.basics.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-static-content + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.http.media + helidon-http-media-jsonp + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/Catalog.java b/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/Catalog.java new file mode 100644 index 000000000..b75ff059b --- /dev/null +++ b/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/Catalog.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.basics; + +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * Skeleton example of catalog resource use in {@link Main} class. + */ +public class Catalog implements HttpService { + + @Override + public void routing(HttpRules rules) { + rules.get("/", this::list) + .get("/{id}", (req, res) -> getSingle(res, req.path().pathParameters().get("id"))); + } + + private void list(ServerRequest request, ServerResponse response) { + response.send("1, 2, 3, 4, 5"); + } + + private void getSingle(ServerResponse response, String id) { + response.send("Item: " + id); + } +} diff --git a/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/Main.java b/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/Main.java new file mode 100644 index 000000000..eb0760e92 --- /dev/null +++ b/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/Main.java @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.basics; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.http.HeaderName; +import io.helidon.http.HeaderNames; +import io.helidon.http.HttpException; +import io.helidon.http.ServerRequestHeaders; +import io.helidon.http.Status; +import io.helidon.http.media.EntityReader; +import io.helidon.http.media.MediaContext; +import io.helidon.http.media.MediaContextConfig; +import io.helidon.http.media.ReadableEntity; +import io.helidon.http.media.jsonp.JsonpSupport; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.ErrorHandler; +import io.helidon.webserver.http.Handler; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import io.helidon.webserver.staticcontent.StaticContentService; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; + +/** + * This example consists of few first tutorial steps of WebServer API. Each step is represented by a single method. + *

    + * Principles: + *

      + *
    • Reactive principles
    • + *
    • Reflection free
    • + *
    • Fluent
    • + *
    • Integration platform
    • + *
    + *

    + * It is also java executable main class. Use a method name as a command line parameter to execute. + */ +public class Main { + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + private static final HeaderName BAR_HEADER = HeaderNames.create("bar"); + private static final HeaderName FOO_HEADER = HeaderNames.create("foo"); + + // ---------------- EXAMPLES + + /** + * True heart of WebServer API is {@link HttpRouting}. + * It provides fluent way how to assign custom {@link Handler} to the routing rule. + * The rule consists from two main factors - HTTP method and path pattern. + *

    + * The (route) {@link Handler} is a functional interface which process HTTP {@link ServerRequest request} and + * writes to the {@link ServerResponse response}. + * + * @param routing routing builder + */ + public static void firstRouting(HttpRouting.Builder routing) { + routing.post("/firstRouting/post-endpoint", (req, res) -> res.status(Status.CREATED_201) + .send()) + .get("/firstRouting/get-endpoint", (req, res) -> res.status(Status.OK_200) + .send("Hello World!")); + } + + /** + * All routing rules (routes) are evaluated in a definition order. The {@link Handler} assigned with the first valid route + * for given request is called. It is a responsibility of each handler to process in one of the following ways: + *

      + *
    • Respond using one of {@link ServerResponse#send() ServerResponse.send(...)} method.
    • + *
    • Continue to next valid route using {@link ServerResponse#next() ServerRequest.next()} method. + * It is possible to define filtering handlers.
    • + *
    + *

    + * If no valid {@link Handler} is found then routing respond by {@code HTTP 404} code. + *

    + * If selected {@link Handler} doesn't process request than the request stacks! + *

    + * Blocking operations:
    + * For performance reason, {@link Handler} can be called directly by a selector thread. It is not good idea to block + * such thread. If request must be processed by a blocking operation then such processing should be deferred to another + * thread. + * + * @param routing routing builder + */ + public static void routingAsFilter(HttpRouting.Builder routing) { + routing.any("/routingAsFilter/*", (req, res) -> { + System.out.println(req.prologue().method() + " " + req.path()); + // Filters are just routing handlers which calls next() + res.next(); + }) + .post("/routingAsFilter/post-endpoint", (req, res) -> res.status(Status.CREATED_201) + .send()) + .get("/routingAsFilter/get-endpoint", (req, res) -> res.status(Status.OK_200) + .send("Hello World!")); + } + + /** + * {@link ServerRequest} provides access to three types of "parameters": + *

      + *
    • Headers
    • + *
    • Query parameters
    • + *
    • Path parameters - Evaluated from provided {@code path pattern}
    • + *
    + *

    + * {@link java.util.Optional Optional} API is heavily used to represent parameters optionality. + *

    + * WebServer {@link io.helidon.common.parameters.Parameters Parameters} API is used to represent fact, that headers and + * query parameters can contain multiple values. + * + * @param routing routing builder + */ + public static void parametersAndHeaders(HttpRouting.Builder routing) { + routing.get("/parametersAndHeaders/context/{id}", (req, res) -> { + StringBuilder sb = new StringBuilder(); + // Request headers + req.headers() + .first(FOO_HEADER) + .ifPresent(v -> sb.append("foo: ").append(v).append("\n")); + // Request parameters + req.query() + .first("bar") + .ifPresent(v -> sb.append("bar: ").append(v).append("\n")); + // Path parameters + sb.append("id: ").append(req.path().pathParameters().get("id")); + // Response headers + res.headers().contentType(MediaTypes.TEXT_PLAIN); + // Response entity (payload) + res.send(sb.toString()); + }); + } + + /** + * Routing rules (routes) are limited on two criteria - HTTP method and path. + * + * @param routing routing builder + */ + public static void advancedRouting(HttpRouting.Builder routing) { + routing.get("/advancedRouting/foo", (req, res) -> { + ServerRequestHeaders headers = req.headers(); + if (headers.isAccepted(MediaTypes.TEXT_PLAIN) + && headers.contains(BAR_HEADER)) { + + res.send(); + } else { + res.next(); + } + }); + } + + /** + * Larger applications with many routing rules can cause complicated readability (maintainability) if all rules are + * defined in a single fluent code. It is possible to register {@link HttpService} and organise + * the code into services and resources. {@code Service} is an interface which can register more routing rules (routes). + * + * @param routing routing builder + */ + public static void organiseCode(HttpRouting.Builder routing) { + routing.register("/organiseCode/catalog-context-path", new Catalog()); + } + + /** + * Request payload (body/entity) is represented by {@link ReadableEntity}. + * But it is more convenient to process entity in some type specific form. WebServer supports few types which can be + * used te read the whole entity: + *

      + *
    • {@code byte[]}
    • + *
    • {@code String}
    • + *
    • {@code InputStream}
    • + *
    + *

    + * Similar approach is used for the response entity. + * + * @param routing routing builder + */ + public static void readContentEntity(HttpRouting.Builder routing) { + routing.post("/readContentEntity/foo", (req, res) -> { + try { + String data = req.content().as(String.class); + System.out.println("/foo DATA: " + data); + res.send(data); + } catch (Throwable th) { + res.status(Status.BAD_REQUEST_400); + } + }) + // It is possible to use Handler.of() method to automatically cover all error states. + .post("/readContentEntity/bar", Handler.create(String.class, (data, res) -> { + System.out.println("/foo DATA: " + data); + res.send(data); + })); + } + + /** + * Use a custom {@link EntityReader reader} to convert the request content into an object of a given type. + * + * @param routing routing builder + * @param mediaContext media context builder + */ + public static void mediaReader(HttpRouting.Builder routing, MediaContextConfig.Builder mediaContext) { + routing.post("/mediaReader/create-record", Handler.create(Name.class, (name, res) -> { + System.out.println("Name: " + name); + res.status(Status.CREATED_201) + .send(name.toString()); + })); + + // add our custom Name reader + mediaContext.addMediaSupport(NameSupport.create()); + } + + /** + * Combination of filtering {@link Handler} pattern with {@link HttpService} registration capabilities + * can be used by other frameworks for the integration. WebServer is shipped with several integrated libraries (supports) + * including static content, JSON and Jersey. See {@code POM.xml} for requested dependencies. + * + * @param routing routing builder + * @param mediaContext mediaContext + */ + public static void supports(HttpRouting.Builder routing, MediaContextConfig.Builder mediaContext) { + routing.register("/supports", StaticContentService.create("/static")) + .get("/supports/hello/{what}", (req, res) -> + res.send(JSON.createObjectBuilder() + .add("message", "Hello " + req.path() + .pathParameters() + .get("what")) + .build())); + mediaContext.addMediaSupport(JsonpSupport.create()); + } + + /** + * Request processing can cause error represented by {@link Throwable}. It is possible to register custom + * {@link ErrorHandler ErrorHandlers} for specific processing. + *

    + * If error is not processed by a custom {@link ErrorHandler ErrorHandler} than default one is used. + * It responds with HTTP 500 code unless error is not represented + * by {@link HttpException HttpException}. In such case it reflects its content. + * + * @param routing routing builder + */ + public static void errorHandling(HttpRouting.Builder routing) { + routing.post("/errorHandling/compute", Handler.create(String.class, (str, res) -> { + int result = 100 / Integer.parseInt(str); + res.send("100 / " + str + " = " + result); + })) + .error(Throwable.class, (req, res, ex) -> { + ex.printStackTrace(System.out); + res.next(); + }) + .error(NumberFormatException.class, + (req, res, ex) -> res.status(Status.BAD_REQUEST_400).send()) + .error(ArithmeticException.class, + (req, res, ex) -> res.status(Status.PRECONDITION_FAILED_412).send()); + } + + + // ---------------- EXECUTION + + private static final String EXAMPLE_NAME_SYS_PROP = "exampleName"; + private static final String EXAMPLE_NAME_ENV_VAR = "EXAMPLE_NAME"; + + /** + * Prints usage instructions. + */ + public void help() { + StringBuilder hlp = new StringBuilder(); + hlp.append("java -jar example-basics.jar \n"); + hlp.append("Example method names:\n"); + Method[] methods = Main.class.getDeclaredMethods(); + for (Method method : methods) { + if (Modifier.isPublic(method.getModifiers()) && !Modifier.isStatic(method.getModifiers())) { + hlp.append(" ").append(method.getName()).append('\n'); + } + } + hlp.append('\n'); + hlp.append("Example method name can be also provided as a\n"); + hlp.append(" - -D").append(EXAMPLE_NAME_SYS_PROP).append(" jvm property.\n"); + hlp.append(" - ").append(EXAMPLE_NAME_ENV_VAR).append(" environment variable.\n"); + System.out.println(hlp); + } + + /** + * Prints usage instructions. (Shortcut to {@link #help()} method. + */ + public void h() { + help(); + } + + /** + * Java main method. + * + * @param args Command line arguments. + */ + public static void main(String[] args) { + String exampleName; + if (args.length > 0) { + exampleName = args[0]; + } else if (System.getProperty(EXAMPLE_NAME_SYS_PROP) != null) { + exampleName = System.getProperty(EXAMPLE_NAME_SYS_PROP); + } else if (System.getenv(EXAMPLE_NAME_ENV_VAR) != null) { + exampleName = System.getenv(EXAMPLE_NAME_ENV_VAR); + } else { + System.out.println("Missing example name. It can be provided as a \n" + + " - first command line argument.\n" + + " - -D" + EXAMPLE_NAME_SYS_PROP + " jvm property.\n" + + " - " + EXAMPLE_NAME_ENV_VAR + " environment variable.\n"); + System.exit(1); + return; + } + while (exampleName.startsWith("-")) { + exampleName = exampleName.substring(1); + } + String methodName = exampleName; + Method method = Arrays.stream(Main.class.getMethods()) + .filter(m -> m.getName().equals(methodName)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Missing example method named: " + methodName)); + HttpRouting.Builder routingBuilder = HttpRouting.builder(); + MediaContextConfig.Builder mediaContextBuilder = MediaContext.builder() + .mediaSupportsDiscoverServices(false); + List params = new ArrayList<>(); + for (Parameter param : method.getParameters()) { + Class paramType = param.getType(); + if (paramType.isAssignableFrom(routingBuilder.getClass())) { + params.add(routingBuilder); + } else if (paramType.isAssignableFrom(mediaContextBuilder.getClass())) { + params.add(mediaContextBuilder); + } else { + throw new IllegalStateException("Unsupported parameter type: " + paramType.getName()); + } + } + WebServer server; + try { + method.invoke(new Main(), params.toArray(new Object[0])); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + System.exit(2); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(System.out); + System.exit(100); + } + server = WebServer.builder() + .routing(routingBuilder) + .mediaContext(mediaContextBuilder.build()) + .build() + .start(); + System.out.println("Server is UP: http://localhost:" + server.port()); + } +} diff --git a/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/Name.java b/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/Name.java new file mode 100644 index 000000000..3ed630ba0 --- /dev/null +++ b/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/Name.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.basics; + +/** + * Represents a simple entity - the name. + */ +public class Name { + + private final String firstName; + private final String middleName; + private final String lastName; + + /** + * An naive implementation of name parser. + * + * @param fullName a full name + */ + public Name(String fullName) { + String[] split = fullName.split(" "); + switch (split.length) { + case 0: + throw new IllegalArgumentException("An empty name"); + case 1: + firstName = null; + middleName = null; + lastName = split[0]; + break; + case 2: + firstName = split[0]; + middleName = null; + lastName = split[1]; + break; + case 3: + firstName = split[0]; + middleName = split[1]; + lastName = split[2]; + break; + default: + throw new IllegalArgumentException("To many name parts!"); + } + } + + public String getFirstName() { + return firstName; + } + + public String getMiddleName() { + return middleName; + } + + public String getLastName() { + return lastName; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + if (firstName != null) { + result.append(firstName); + } + if (middleName != null) { + if (result.length() > 0) { + result.append(' '); + } + result.append(middleName); + } + if (lastName != null) { + if (result.length() > 0) { + result.append(' '); + } + result.append(lastName); + } + return result.toString(); + } +} diff --git a/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/NameSupport.java b/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/NameSupport.java new file mode 100644 index 000000000..c48d408ba --- /dev/null +++ b/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/NameSupport.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.basics; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import io.helidon.common.GenericType; +import io.helidon.http.Headers; +import io.helidon.http.HttpMediaType; +import io.helidon.http.media.EntityReader; +import io.helidon.http.media.MediaSupport; + +/** + * Reader for the custom media type. + */ +public class NameSupport implements MediaSupport { + + static final HttpMediaType APP_NAME = HttpMediaType.create("application/name"); + + private NameSupport() { + } + + /** + * Create a new instance. + * + * @return new instance + */ + public static NameSupport create() { + return new NameSupport(); + } + + @SuppressWarnings("unchecked") + @Override + public ReaderResponse reader(GenericType type, Headers headers) { + if (!type.rawType().equals(Name.class) + || !headers.contentType().map(APP_NAME::equals).orElse(false)) { + return ReaderResponse.unsupported(); + } + return (ReaderResponse) new ReaderResponse<>(SupportLevel.SUPPORTED, () -> new EntityReader() { + @Override + public Name read(GenericType type, InputStream stream, Headers headers) { + return read(stream, headers); + } + + @Override + public Name read(GenericType type, + InputStream stream, + Headers requestHeaders, + Headers responseHeaders) { + return read(stream, responseHeaders); + } + + private Name read(InputStream stream, Headers headers) { + Charset charset = headers.contentType() + .flatMap(HttpMediaType::charset) + .map(Charset::forName) + .orElse(StandardCharsets.UTF_8); + + try (stream) { + return new Name(new String(stream.readAllBytes(), charset)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }); + } + + @Override + public String name() { + return "name"; + } + + @Override + public String type() { + return "name"; + } +} + + + + diff --git a/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/package-info.java b/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/package-info.java new file mode 100644 index 000000000..6efde8c75 --- /dev/null +++ b/examples/webserver/basics/src/main/java/io/helidon/examples/webserver/basics/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A set of small usage examples. Start with {@link io.helidon.examples.webserver.basics.Main Main} class. + */ +package io.helidon.examples.webserver.basics; diff --git a/examples/webserver/basics/src/main/resources/static/index.html b/examples/webserver/basics/src/main/resources/static/index.html new file mode 100644 index 000000000..598a9bc68 --- /dev/null +++ b/examples/webserver/basics/src/main/resources/static/index.html @@ -0,0 +1,28 @@ + + + + + + + Just index + + +

    Example

    + + diff --git a/examples/webserver/basics/src/test/java/io/helidon/examples/webserver/basics/MainTest.java b/examples/webserver/basics/src/test/java/io/helidon/examples/webserver/basics/MainTest.java new file mode 100644 index 000000000..7d9c6b668 --- /dev/null +++ b/examples/webserver/basics/src/test/java/io/helidon/examples/webserver/basics/MainTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.basics; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.http.HeaderName; +import io.helidon.http.HeaderNames; +import io.helidon.http.media.MediaContext; +import io.helidon.http.media.MediaContextConfig; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +import org.junit.jupiter.api.Test; + +import static io.helidon.http.Status.BAD_REQUEST_400; +import static io.helidon.http.Status.CREATED_201; +import static io.helidon.http.Status.INTERNAL_SERVER_ERROR_500; +import static io.helidon.http.Status.OK_200; +import static io.helidon.http.Status.PRECONDITION_FAILED_412; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +public class MainTest { + + private static final HeaderName FOO_HEADER = HeaderNames.create("foo"); + + private final Http1Client client; + + public MainTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + MediaContextConfig.Builder mediaContext = MediaContext.builder() + .mediaSupportsDiscoverServices(false); + server.routing(routing -> { + Main.firstRouting(routing); + Main.mediaReader(routing, mediaContext); + Main.advancedRouting(routing); + Main.organiseCode(routing); + Main.routingAsFilter(routing); + Main.parametersAndHeaders(routing); + Main.errorHandling(routing); + Main.readContentEntity(routing); + Main.supports(routing, mediaContext); + }); + server.mediaContext(mediaContext.build()); + } + + @Test + public void firstRouting() { + // POST + try (Http1ClientResponse response = client.post("/firstRouting/post-endpoint").request()) { + assertThat(response.status(), is(CREATED_201)); + } + // GET + try (Http1ClientResponse response = client.get("/firstRouting/get-endpoint").request()) { + assertThat(response.status(), is(OK_200)); + } + } + + @Test + public void routingAsFilter() { + // POST + try (Http1ClientResponse response = client.post("/routingAsFilter/post-endpoint").request()) { + assertThat(response.status(), is(CREATED_201)); + } + // GET + try (Http1ClientResponse response = client.get("/routingAsFilter/get-endpoint").request()) { + assertThat(response.status(), is(OK_200)); + } + } + + @Test + public void parametersAndHeaders() { + try (Http1ClientResponse response = client.get("/parametersAndHeaders/context/aaa") + .queryParam("bar", "bbb") + .header(FOO_HEADER, "ccc") + .request()) { + + assertThat(response.status(), is(OK_200)); + String s = response.entity().as(String.class); + assertThat(s, containsString("id: aaa")); + assertThat(s, containsString("bar: bbb")); + assertThat(s, containsString("foo: ccc")); + } + } + + @Test + public void organiseCode() { + // List + try (Http1ClientResponse response = client.get("/organiseCode/catalog-context-path").request()) { + assertThat(response.status(), is(OK_200)); + assertThat(response.entity().as(String.class), is("1, 2, 3, 4, 5")); + } + + // Get by id + try (Http1ClientResponse response = client.get("/organiseCode/catalog-context-path/aaa").request()) { + assertThat(response.status(), is(OK_200)); + assertThat(response.entity().as(String.class), is("Item: aaa")); + } + } + + @Test + public void readContentEntity() { + // foo + try (Http1ClientResponse response = client.post("/readContentEntity/foo").submit("aaa")) { + assertThat(response.status(), is(OK_200)); + assertThat(response.entity().as(String.class), is("aaa")); + } + + // bar + try (Http1ClientResponse response = client.post("/readContentEntity/bar").submit("aaa")) { + assertThat(response.status(), is(OK_200)); + assertThat(response.entity().as(String.class), is("aaa")); + } + } + + @Test + public void mediaReader() { + try (Http1ClientResponse response = client.post("/mediaReader/create-record") + .contentType(NameSupport.APP_NAME) + .submit("John Smith")) { + assertThat(response.status(), is(CREATED_201)); + assertThat(response.entity().as(String.class), is("John Smith")); + } + + // Unsupported Content-Type + try (Http1ClientResponse response = client.post("/mediaReader/create-record") + .contentType(MediaTypes.TEXT_PLAIN) + .submit("John Smith")) { + assertThat(response.status(), is(INTERNAL_SERVER_ERROR_500)); + } + } + + @Test + public void supports() { + // Static content + try (Http1ClientResponse response = client.get("/supports/index.html").request()) { + assertThat(response.status(), is(OK_200)); + assertThat(response.headers().first(HeaderNames.CONTENT_TYPE).orElse(null), is(MediaTypes.TEXT_HTML.text())); + } + + // JSON + try (Http1ClientResponse response = client.get("/supports/hello/Europe").request()) { + assertThat(response.status(), is(OK_200)); + assertThat(response.entity().as(String.class), is("{\"message\":\"Hello Europe\"}")); + } + } + + @Test + public void errorHandling() { + // Valid + try (Http1ClientResponse response = client.post("/errorHandling/compute") + .contentType(MediaTypes.TEXT_PLAIN) + .submit("2")) { + assertThat(response.status(), is(OK_200)); + assertThat(response.entity().as(String.class), is("100 / 2 = 50")); + } + + // Zero + try (Http1ClientResponse response = client.post("/errorHandling/compute") + .contentType(MediaTypes.TEXT_PLAIN) + .submit("0")) { + assertThat(response.status(), is(PRECONDITION_FAILED_412)); + } + + // NaN + try (Http1ClientResponse response = client.post("/errorHandling/compute") + .contentType(MediaTypes.TEXT_PLAIN) + .submit("aaa")) { + assertThat(response.status(), is(BAD_REQUEST_400)); + } + } +} diff --git a/examples/webserver/comment-aas/README.md b/examples/webserver/comment-aas/README.md new file mode 100644 index 000000000..3918c032d --- /dev/null +++ b/examples/webserver/comment-aas/README.md @@ -0,0 +1,20 @@ +# Comments As a Service + +This application allows users to add or read short comments related to a single topic. + Topic can be anything including blog post, newspaper article, and others. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-webserver-comment-aas.jar +``` + +Try the application: + +```shell +curl http://localhost:8080/comments/java -d "I use Helidon!" +curl http://localhost:8080/comments/java -d "I use vertx" +curl http://localhost:8080/comments/java -d "I use spring" +curl http://localhost:8080/comments/java +``` diff --git a/examples/webserver/comment-aas/etc/add-comment b/examples/webserver/comment-aas/etc/add-comment new file mode 100755 index 000000000..ff3d1c2d8 --- /dev/null +++ b/examples/webserver/comment-aas/etc/add-comment @@ -0,0 +1,38 @@ +#!/bin/bash +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +if [ -f "$SCRIPT_DIR/user.txt" ]; then + export SVC_USER=`cat "$SCRIPT_DIR/user.txt"` +fi + +if [ -z "$2" ]; then + echo "Missing command line parameter! Usage: comment.sh " + exit 1 +fi + +if [ -z "$SVC_USER" ]; then + echo "Contacting service http://localhost:8080" + echo "" + curl --noproxy localhost -X POST --data "$2" http://localhost:8080/comments/$1 +else + echo "Contacting service http://localhost:8080 as $SVC_USER" + echo "" + curl --noproxy localhost -H "user-identity: $SVC_USER" -X POST --data "$2" http://localhost:8080/comments/$1 +fi +echo "" diff --git a/examples/webserver/comment-aas/etc/config.yaml b/examples/webserver/comment-aas/etc/config.yaml new file mode 100644 index 000000000..da7cd3400 --- /dev/null +++ b/examples/webserver/comment-aas/etc/config.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +anonymous-enabled: false diff --git a/examples/webserver/comment-aas/etc/create-etcd b/examples/webserver/comment-aas/etc/create-etcd new file mode 100755 index 000000000..e0ecae7e9 --- /dev/null +++ b/examples/webserver/comment-aas/etc/create-etcd @@ -0,0 +1,43 @@ +#!/bin/bash +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +export SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +export ETCD_DATA_DIRS=/tmp/etcd-data/config-gerrit-etcd-data-dirs +rm -rf $ETCD_DATA_DIRS +export CONTAINER_NAME="config-etcd" + +docker rm -f $CONTAINER_NAME +export HOST_IP=`ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'` + +docker run -d \ + -v $ETCD_DATA_DIRS:/var/data-dir \ + -p 2380:2380 \ + -p 2379:2379 \ + --name $CONTAINER_NAME \ + quay.io/coreos/etcd:v3.1.3 \ + etcd \ + -name etcd \ + -data-dir /var/data-dir \ + -advertise-client-urls http://$HOST_IP:2380 \ + -listen-client-urls http://0.0.0.0:2379 \ + -initial-advertise-peer-urls http://$HOST_IP:2380 \ + -listen-peer-urls http://0.0.0.0:2380 + +echo "Run with name $CONTAINER_NAME" + +echo "Test:" +docker exec $CONTAINER_NAME /bin/sh -c "ETCDCTL_API=3 etcdctl put my-key my-value" diff --git a/examples/webserver/comment-aas/etc/get-etcd-config b/examples/webserver/comment-aas/etc/get-etcd-config new file mode 100755 index 000000000..ee3c1f9fe --- /dev/null +++ b/examples/webserver/comment-aas/etc/get-etcd-config @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +HOST_IP=`ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'` + +echo "Retrieving configuration from ETCD:" +curl --noproxy $HOST_IP http://$HOST_IP:2379/v2/keys/comments-aas-config | python -m json.tool diff --git a/examples/webserver/comment-aas/etc/list-comments b/examples/webserver/comment-aas/etc/list-comments new file mode 100755 index 000000000..38280937a --- /dev/null +++ b/examples/webserver/comment-aas/etc/list-comments @@ -0,0 +1,38 @@ +#!/bin/bash +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +if [ -f "$SCRIPT_DIR/user.txt" ]; then + export SVC_USER=`cat "$SCRIPT_DIR/user.txt"` +fi + +if [ -z "$1" ]; then + echo "Missing command line parameter! Usage: list_comments.sh " + exit 1 +fi + +if [ -z "$SVC_USER" ]; then + echo "Contacting service http://localhost:8080" + echo "" + curl --noproxy localhost http://localhost:8080/comments/$1 +else + echo "Contacting service http://localhost:8080 as $SVC_USER" + echo "" + curl --noproxy localhost -H "user-identity: $SVC_USER" http://localhost:8080/comments/$1 +fi +echo "" diff --git a/examples/webserver/comment-aas/etc/put-etcd-config b/examples/webserver/comment-aas/etc/put-etcd-config new file mode 100755 index 000000000..afe3b329c --- /dev/null +++ b/examples/webserver/comment-aas/etc/put-etcd-config @@ -0,0 +1,27 @@ +#!/bin/bash +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +HOST_IP=`ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'` +CONFIG_FILE="$SCRIPT_DIR/config.yaml" + +if [ -f "$CONFIG_FILE" ]; then + echo "Storing configuration into ETCD: $CONFIG_FILE" + curl --noproxy $HOST_IP -vvv http://$HOST_IP:2379/v2/keys/comments-aas-config -XPUT --data-urlencode value@$CONFIG_FILE +else + echo "Configuration file $CONFIG_FILE not found. DO NOT IMPORT INTO ETCD!" +fi diff --git a/examples/webserver/comment-aas/etc/start-etcd b/examples/webserver/comment-aas/etc/start-etcd new file mode 100755 index 000000000..3fa8af56e --- /dev/null +++ b/examples/webserver/comment-aas/etc/start-etcd @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +CONTAINER_NAME="config-etcd" +docker start $CONTAINER_NAME diff --git a/examples/webserver/comment-aas/etc/stop-comment-service b/examples/webserver/comment-aas/etc/stop-comment-service new file mode 100755 index 000000000..0bfdcd994 --- /dev/null +++ b/examples/webserver/comment-aas/etc/stop-comment-service @@ -0,0 +1,33 @@ +#!/bin/bash +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +if [ -f "$SCRIPT_DIR/user.txt" ]; then + export SVC_USER=`cat "$SCRIPT_DIR/user.txt"` +fi + +if [ -z "$SVC_USER" ]; then + echo "Contacting service http://localhost:8080" + echo "" + curl --noproxy localhost -X POST http://localhost:8080/mgmt/shutdown +else + echo "Contacting service http://localhost:8080 as $SVC_USER" + echo "" + curl --noproxy localhost -H "user-identity: $SVC_USER" -X POST http://localhost:8080/mgmt/shutdown +fi +echo "" diff --git a/examples/webserver/comment-aas/etc/stop-etcd b/examples/webserver/comment-aas/etc/stop-etcd new file mode 100755 index 000000000..8f81778c2 --- /dev/null +++ b/examples/webserver/comment-aas/etc/stop-etcd @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +CONTAINER_NAME="config-etcd" +docker stop $CONTAINER_NAME diff --git a/examples/webserver/comment-aas/etc/switch-user b/examples/webserver/comment-aas/etc/switch-user new file mode 100755 index 000000000..356fd3518 --- /dev/null +++ b/examples/webserver/comment-aas/etc/switch-user @@ -0,0 +1,26 @@ +#!/bin/bash +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +if [ -z "$1" ]; then + echo "set empty context user!" + rm -f "$SCRIPT_DIR/user.txt" +else + echo "set context user: $1" + echo "$1" > "$SCRIPT_DIR/user.txt" +fi diff --git a/examples/webserver/comment-aas/pom.xml b/examples/webserver/comment-aas/pom.xml new file mode 100644 index 000000000..69702f02b --- /dev/null +++ b/examples/webserver/comment-aas/pom.xml @@ -0,0 +1,91 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-comment-aas + 1.0.0-SNAPSHOT + Helidon Examples WebServer CommentsAAS + + Comments As A Service example application + + + io.helidon.examples.webserver.comments.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.http + helidon-http + + + io.helidon.common + helidon-common + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/CommentsService.java b/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/CommentsService.java new file mode 100644 index 000000000..b57bd70af --- /dev/null +++ b/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/CommentsService.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.comments; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +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; + +/** + * Basic service for comments. + */ +public class CommentsService implements HttpService { + + private final ConcurrentHashMap> topicsAndComments = new ConcurrentHashMap<>(); + + @Override + public void routing(HttpRules rules) { + rules.get("/{topic}", this::handleListComments) + .post("/{topic}", this::handleAddComment); + } + + private void handleListComments(ServerRequest req, ServerResponse resp) { + String topic = req.path().pathParameters().get("topic"); + resp.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8); + resp.send(listComments(topic)); + } + + private void handleAddComment(ServerRequest req, ServerResponse res) { + String topic = req.path().pathParameters().get("topic"); + String userName = req.context().get("user", String.class).orElse("anonymous"); + String msg = req.content().as(String.class); + addComment(msg, userName, topic); + res.send(); + } + + /** + * Adds new comment into the comment-room. + * + * @param message a comment message + * @param fromUser a user alias of the comment author + */ + void addComment(String message, String fromUser, String toTopic) { + ProfanityDetector.detectProfanity(message); + if (fromUser == null) { + fromUser = "anonymous"; + } + List comments = + topicsAndComments.computeIfAbsent(toTopic, k -> Collections.synchronizedList(new ArrayList<>())); + comments.add(new Comment(fromUser, message)); + } + + /** + * List all comments in original order as a string with single comment on the line. + * + * @param roomName a name of the room + * @return all comments, line-by-line + */ + String listComments(String roomName) { + List comments = topicsAndComments.get(roomName); + if (comments != null) { + return comments.stream() + .map(String::valueOf) + .collect(Collectors.joining("\n")); + } + return ""; + } + + private record Comment(String userName, String message) { + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + if (userName != null) { + result.append(userName); + } + result.append(": "); + result.append(message); + return result.toString(); + } + } +} diff --git a/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/Main.java b/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/Main.java new file mode 100644 index 000000000..1ed5267e8 --- /dev/null +++ b/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/Main.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.comments; + +import java.util.Optional; + +import io.helidon.config.Config; +import io.helidon.http.HeaderName; +import io.helidon.http.HeaderNames; +import io.helidon.http.HttpException; +import io.helidon.http.Status; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * Application java main class. + *

    + *

    The COMMENTS-As-a-Service application example demonstrates Web Server in its integration role. + * It integrates various components including Configuration and Security. + *

    + *

    This WEB application provides possibility to store and read comment related to various topics. + */ +public final class Main { + + static final HeaderName USER_IDENTITY_HEADER = HeaderNames.create("user-identity"); + + private Main() { + } + + /** + * A java main class. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder); + WebServer server = builder.port(8080).build().start(); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/comments"); + } + + static void routing(HttpRouting.Builder routing, boolean acceptAnonymousUsers) { + // Filter that translates user identity header into the contextual "user" information + routing.any((req, res) -> { + String user = req.headers() + .first(USER_IDENTITY_HEADER) + .or(() -> acceptAnonymousUsers ? Optional.of("anonymous") : Optional.empty()) + .orElseThrow(() -> new HttpException("Anonymous access is forbidden!", Status.FORBIDDEN_403)); + + req.context().register("user", user); + res.next(); + }) + // Main service logic part is registered as a separated class to "/comments" context root + .register("/comments", new CommentsService()) + // Error handling for argot expressions. + .error(ProfanityException.class, (req, res, ex) -> { + res.status(Status.NOT_ACCEPTABLE_406); + res.send("Expressions like '" + ex.getObfuscatedProfanity() + "' are unacceptable!"); + }) + .error(HttpException.class, (req, res, ex) -> { + if (ex.status() == Status.FORBIDDEN_403) { + res.status(ex.status()); + res.send(ex.getMessage()); + } else { + res.next(); + } + }); + } + + static void setup(WebServerConfig.Builder server) { + // Load configuration + Config config = Config.create(); + + server.config(config) + .routing(r -> routing(r, config.get("anonymous-enabled").asBoolean().orElse(false))); + } +} diff --git a/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/ProfanityDetector.java b/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/ProfanityDetector.java new file mode 100644 index 000000000..885a3cd23 --- /dev/null +++ b/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/ProfanityDetector.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.comments; + +/** + * Simple profanity detection utility class. + */ +class ProfanityDetector { + private static final String[] BANNED_TERMS = new String[] {"spring", "nodejs", "vertx"}; + + private ProfanityDetector() { + } + + /** + * Detects if a message contains one of the profane words. + * + * @param message a message to check. + */ + static void detectProfanity(String message) { + if (message == null) { + return; + } + message = message.toLowerCase(); + for (String bannedTerm : BANNED_TERMS) { + if (message.contains(bannedTerm)) { + throw new ProfanityException(bannedTerm); + } + } + } +} diff --git a/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/ProfanityException.java b/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/ProfanityException.java new file mode 100644 index 000000000..ba35ba128 --- /dev/null +++ b/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/ProfanityException.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.comments; + +/** + * Thrown to indicate that a message contains illegal argot word. + */ +public class ProfanityException extends IllegalArgumentException { + + private final String profanity; + + /** + * Creates new instance. + * + * @param profanity an illegal argot word. + */ + public ProfanityException(String profanity) { + super("Do not use such an ugly word as '" + obfuscate(profanity) + "'!"); + this.profanity = profanity; + } + + /** + * Returns an illegal argot word! + * + * @return an argot word + */ + public String getProfanity() { + return profanity; + } + + /** + * Returns an illegal argot word in obfuscated form! + * + * @return an argot word in obfuscated form + */ + public String getObfuscatedProfanity() { + return obfuscate(profanity); + } + + private static String obfuscate(String argot) { + if (argot == null || argot.length() < 2) { + return argot; + } + if (argot.length() == 2) { + return argot.charAt(0) + "*"; + } + return argot.charAt(0) + "*" + argot.charAt(argot.length() - 1); + } +} diff --git a/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/package-info.java b/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/package-info.java new file mode 100644 index 000000000..55b35969e --- /dev/null +++ b/examples/webserver/comment-aas/src/main/java/io/helidon/examples/webserver/comments/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The COMMENTS-As-a-Service application example demonstrates Web Server in its integration role. + * It integrates various components including Configuration and Security. + * + *

    This WEB application provides possibility to store and read comment related to various topics. + */ +package io.helidon.examples.webserver.comments; diff --git a/examples/webserver/comment-aas/src/main/resources/application.yaml b/examples/webserver/comment-aas/src/main/resources/application.yaml new file mode 100644 index 000000000..869b357d9 --- /dev/null +++ b/examples/webserver/comment-aas/src/main/resources/application.yaml @@ -0,0 +1,21 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +webserver: + port: 8080 + +# If true then anonymous access is enabled. +anonymous-enabled: true diff --git a/examples/webserver/comment-aas/src/test/java/io/helidon/examples/webserver/comments/CommentsServiceTest.java b/examples/webserver/comment-aas/src/test/java/io/helidon/examples/webserver/comments/CommentsServiceTest.java new file mode 100644 index 000000000..da601b64a --- /dev/null +++ b/examples/webserver/comment-aas/src/test/java/io/helidon/examples/webserver/comments/CommentsServiceTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.comments; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.DirectClient; +import io.helidon.webserver.testing.junit5.RoutingTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.isEmptyString; + + +/** + * Tests {@link CommentsService}. + */ +@RoutingTest +public class CommentsServiceTest { + + private final DirectClient client; + + public CommentsServiceTest(DirectClient client) { + this.client = client; + } + + @SetUpRoute + static void setup(HttpRouting.Builder routing) { + Main.routing(routing, true); + } + + @Test + public void addAndGetComments() { + CommentsService service = new CommentsService(); + assertThat(service.listComments("one"), isEmptyString()); + assertThat(service.listComments("two"), isEmptyString()); + + service.addComment("aaa", null, "one"); + assertThat(service.listComments("one"), is("anonymous: aaa")); + assertThat(service.listComments("two"), isEmptyString()); + + service.addComment("bbb", "Foo", "one"); + assertThat(service.listComments("one"), is("anonymous: aaa\nFoo: bbb")); + assertThat(service.listComments("two"), isEmptyString()); + + service.addComment("bbb", "Bar", "two"); + assertThat(service.listComments("one"), is("anonymous: aaa\nFoo: bbb")); + assertThat(service.listComments("two"), is("Bar: bbb")); + } + + @Test + public void testRouting() { + try (Http1ClientResponse response = client.get("/comments/one").request()) { + assertThat(response.status(), is(Status.OK_200)); + } + try (Http1ClientResponse response = client.post("/comments/one") + .contentType(MediaTypes.TEXT_PLAIN) + .submit("aaa")) { + + assertThat(response.status(), is(Status.OK_200)); + } + + try (Http1ClientResponse response = client.get("/comments/one").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.entity().as(String.class), is("anonymous: aaa")); + } + } + +} diff --git a/examples/webserver/comment-aas/src/test/java/io/helidon/examples/webserver/comments/MainTest.java b/examples/webserver/comment-aas/src/test/java/io/helidon/examples/webserver/comments/MainTest.java new file mode 100644 index 000000000..1ad04f6cc --- /dev/null +++ b/examples/webserver/comment-aas/src/test/java/io/helidon/examples/webserver/comments/MainTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.comments; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.DirectClient; +import io.helidon.webserver.testing.junit5.RoutingTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests {@link Main} class. + */ +@RoutingTest +public class MainTest { + + private final DirectClient client; + + public MainTest(DirectClient client) { + this.client = client; + } + + @SetUpRoute + static void setup(HttpRouting.Builder routing) { + Main.routing(routing, false); + } + + @Test + public void argot() { + try (Http1ClientResponse response = client.post("/comments/one") + .header(Main.USER_IDENTITY_HEADER, "Joe") + .contentType(MediaTypes.TEXT_PLAIN) + .submit("Spring framework is the BEST!")) { + + assertThat(response.status(), is(Status.NOT_ACCEPTABLE_406)); + } + } + + @Test + public void anonymousDisabled() { + try (Http1ClientResponse response = client.get("/comment/one").request()) { + assertThat(response.status(), is(Status.FORBIDDEN_403)); + } + } +} diff --git a/examples/webserver/echo/pom.xml b/examples/webserver/echo/pom.xml new file mode 100644 index 000000000..186fdaaf3 --- /dev/null +++ b/examples/webserver/echo/pom.xml @@ -0,0 +1,70 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.webserver + helidon-examples-webserver-echo + 1.0.0-SNAPSHOT + Helidon Examples WebServer Echo + + + io.helidon.examples.webserver.echo.EchoMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webclient + helidon-webclient + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/EchoClient.java b/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/EchoClient.java new file mode 100644 index 000000000..a86d3dc9a --- /dev/null +++ b/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/EchoClient.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.echo; + +import io.helidon.http.Header; +import io.helidon.http.HeaderValues; +import io.helidon.http.Headers; +import io.helidon.webclient.api.HttpClientRequest; +import io.helidon.webclient.api.HttpClientResponse; +import io.helidon.webclient.api.WebClient; + +/** + * A client that invokes the echo server. + */ +public class EchoClient { + private static final Header HEADER = HeaderValues.create("MY-HEADER", "header-value"); + private static final Header HEADERS = HeaderValues.create("MY-HEADERS", "ha", "hb", "hc"); + + private EchoClient() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + WebClient client = WebClient.create(); + + HttpClientRequest request = client.get("http://localhost:8080/echo;param={param}/{name}"); + + try (HttpClientResponse response = request.pathParam("name", "param-placeholder") + .pathParam("param", "path-param-placeholder") + .queryParam("query-param", "single_value") + .queryParam("query-params", "a", "b", "c") + .header(HEADER) + .header(HEADERS) + .request()) { + + Headers headers = response.headers(); + for (Header header : headers) { + System.out.println("Header: " + header.name() + "=" + header.value()); + } + System.out.println("Entity:"); + System.out.println(response.as(String.class)); + } + } +} diff --git a/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/EchoMain.java b/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/EchoMain.java new file mode 100644 index 000000000..29745bd2c --- /dev/null +++ b/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/EchoMain.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.echo; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Set; + +import io.helidon.common.parameters.Parameters; +import io.helidon.common.uri.UriQuery; +import io.helidon.http.Header; +import io.helidon.http.Headers; +import io.helidon.http.RoutedPath; +import io.helidon.http.Status; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * Echo example. + */ +public class EchoMain { + private EchoMain() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + WebServer.builder() + .port(8080) + .host("127.0.0.1") + .routing(router -> router + .get("/echo/{param}", EchoMain::echo) + ) + .build() + .start(); + } + + private static void echo(ServerRequest req, ServerResponse res) { + RoutedPath path = req.path(); + UriQuery query = req.query(); + Headers headers = req.headers(); + + Parameters pathParams = path.matrixParameters(); + Parameters templateParams = path.pathParameters(); + Set queryNames = query.names(); + + for (String pathParamName : pathParams.names()) { + res.header("R-PATH_PARAM_" + pathParamName, pathParams.get(pathParamName)); + } + + for (String paramName : templateParams.names()) { + res.header("R-PATH_" + paramName, templateParams.get(paramName)); + } + + for (String queryName : queryNames) { + res.header("R-QUERY_" + queryName, query.all(queryName).toString()); + } + + for (Header header : headers) { + res.header("R-" + header.name(), header.allValues().toString()); + } + + try (InputStream inputStream = req.content().inputStream(); + OutputStream outputStream = res.outputStream()) { + inputStream.transferTo(outputStream); + } catch (Exception e) { + res.status(Status.INTERNAL_SERVER_ERROR_500).send("failed: " + e.getMessage()); + } + } +} diff --git a/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/package-info.java b/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/package-info.java new file mode 100644 index 000000000..df99607cb --- /dev/null +++ b/examples/webserver/echo/src/main/java/io/helidon/examples/webserver/echo/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Echo example. + */ +package io.helidon.examples.webserver.echo; diff --git a/examples/webserver/fault-tolerance/pom.xml b/examples/webserver/fault-tolerance/pom.xml new file mode 100644 index 000000000..528f0ff7a --- /dev/null +++ b/examples/webserver/fault-tolerance/pom.xml @@ -0,0 +1,80 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.webserver + helidon-examples-webserver-fault-tolerance + 1.0.0-SNAPSHOT + Helidon Examples WebServer FT + + + Application demonstrates Fault tolerance used in webserver. + + + + io.helidon.examples.webserver.faulttolerance.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.fault-tolerance + helidon-fault-tolerance + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/fault-tolerance/src/main/java/io/helidon/examples/webserver/faulttolerance/FtService.java b/examples/webserver/fault-tolerance/src/main/java/io/helidon/examples/webserver/faulttolerance/FtService.java new file mode 100644 index 000000000..cf9521a5d --- /dev/null +++ b/examples/webserver/fault-tolerance/src/main/java/io/helidon/examples/webserver/faulttolerance/FtService.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.faulttolerance; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.faulttolerance.Async; +import io.helidon.faulttolerance.Bulkhead; +import io.helidon.faulttolerance.CircuitBreaker; +import io.helidon.faulttolerance.Fallback; +import io.helidon.faulttolerance.Retry; +import io.helidon.faulttolerance.Timeout; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * Simple service to demonstrate fault tolerance. + */ +public class FtService implements HttpService { + + private final Async async; + private final Bulkhead bulkhead; + private final CircuitBreaker breaker; + private final Fallback fallback; + private final Retry retry; + private final Timeout timeout; + + FtService() { + this.async = Async.create(); + this.bulkhead = Bulkhead.builder() + .queueLength(1) + .limit(1) + .name("helidon-example-bulkhead") + .build(); + this.breaker = CircuitBreaker.builder() + .volume(4) + .errorRatio(40) + .successThreshold(1) + .delay(Duration.ofSeconds(5)) + .build(); + this.fallback = Fallback.create(b -> b.fallback(this::fallbackToMethod)); + this.retry = Retry.builder() + .retryPolicy(Retry.DelayingRetryPolicy.noDelay(3)) + .build(); + this.timeout = Timeout.create(Duration.ofMillis(100)); + } + + @Override + public void routing(HttpRules rules) { + rules.get("/async", this::asyncHandler) + .get("/bulkhead/{millis}", this::bulkheadHandler) + .get("/circuitBreaker/{success}", this::circuitBreakerHandler) + .get("/fallback/{success}", this::fallbackHandler) + .get("/retry/{count}", this::retryHandler) + .get("/timeout/{millis}", this::timeoutHandler); + } + + private void timeoutHandler(ServerRequest request, ServerResponse response) { + long sleep = Long.parseLong(request.path().pathParameters().get("millis")); + response.send(timeout.invoke(() -> sleep(sleep))); + } + + private void retryHandler(ServerRequest request, ServerResponse response) { + int count = Integer.parseInt(request.path().pathParameters().get("count")); + + AtomicInteger call = new AtomicInteger(1); + AtomicInteger failures = new AtomicInteger(); + + String msg = retry.invoke(() -> { + int current = call.getAndIncrement(); + if (current < count) { + failures.incrementAndGet(); + return failure(); + } + return "calls/failures: " + current + "/" + failures.get(); + }); + response.send(msg); + } + + private void fallbackHandler(ServerRequest request, ServerResponse response) { + boolean success = "true".equalsIgnoreCase(request.path().pathParameters().get("success")); + if (success) { + response.send(fallback.invoke(this::data)); + } else { + response.send(fallback.invoke(this::failure)); + } + } + + private void circuitBreakerHandler(ServerRequest request, ServerResponse response) { + boolean success = "true".equalsIgnoreCase(request.path().pathParameters().get("success")); + if (success) { + response.send(breaker.invoke(this::data)); + } else { + response.send(breaker.invoke(this::failure)); + } + } + + private void bulkheadHandler(ServerRequest request, ServerResponse response) { + long sleep = Long.parseLong(request.path().pathParameters().get("millis")); + response.send(bulkhead.invoke(() -> sleep(sleep))); + } + + private void asyncHandler(ServerRequest request, ServerResponse response) { + response.send(async.invoke(this::data).join()); + } + + private String failure() { + throw new RuntimeException("failure"); + } + + private String sleep(long sleepMillis) { + try { + Thread.sleep(sleepMillis); + } catch (InterruptedException ignored) { + } + return "Slept for " + sleepMillis + " ms"; + } + + private String data() { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + return "blocked for 100 millis"; + } + + private String fallbackToMethod(Throwable e) { + return "Failed back because of " + e.getMessage(); + } + +} diff --git a/examples/webserver/fault-tolerance/src/main/java/io/helidon/examples/webserver/faulttolerance/Main.java b/examples/webserver/fault-tolerance/src/main/java/io/helidon/examples/webserver/faulttolerance/Main.java new file mode 100644 index 000000000..5cbd1ba10 --- /dev/null +++ b/examples/webserver/fault-tolerance/src/main/java/io/helidon/examples/webserver/faulttolerance/Main.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.faulttolerance; + +import io.helidon.faulttolerance.BulkheadException; +import io.helidon.faulttolerance.CircuitBreakerOpenException; +import io.helidon.faulttolerance.TimeoutException; +import io.helidon.http.Status; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * Main class of Fault tolerance example. + */ +public final class Main { + // utility class + private Main() { + } + + /** + * Start the example. + * + * @param args start arguments are ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + WebServerConfig.Builder builder = WebServer.builder().port(8079); + setup(builder); + WebServer server = builder.build().start(); + + System.out.println("Server started on " + "http://localhost:" + server.port()); + } + + static void setup(WebServerConfig.Builder server) { + server.routing(Main::routing); + } + + static void routing(HttpRouting.Builder routing) { + routing.register("/ft", new FtService()) + .error(BulkheadException.class, + (req, res, ex) -> res.status(Status.SERVICE_UNAVAILABLE_503).send("bulkhead")) + .error(CircuitBreakerOpenException.class, + (req, res, ex) -> res.status(Status.SERVICE_UNAVAILABLE_503).send("circuit breaker")) + .error(TimeoutException.class, + (req, res, ex) -> res.status(Status.REQUEST_TIMEOUT_408).send("timeout")) + .error(Throwable.class, + (req, res, ex) -> res.status(Status.INTERNAL_SERVER_ERROR_500) + .send(ex.getClass().getName() + ": " + ex.getMessage())); + } +} diff --git a/examples/webserver/fault-tolerance/src/main/java/io/helidon/examples/webserver/faulttolerance/package-info.java b/examples/webserver/fault-tolerance/src/main/java/io/helidon/examples/webserver/faulttolerance/package-info.java new file mode 100644 index 000000000..79f828763 --- /dev/null +++ b/examples/webserver/fault-tolerance/src/main/java/io/helidon/examples/webserver/faulttolerance/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of Fault Tolerance usage in webserver. + */ +package io.helidon.examples.webserver.faulttolerance; diff --git a/examples/webserver/fault-tolerance/src/main/resources/logging.properties b/examples/webserver/fault-tolerance/src/main/resources/logging.properties new file mode 100644 index 000000000..62a490106 --- /dev/null +++ b/examples/webserver/fault-tolerance/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO +io.helidon.microprofile.server.level=INFO diff --git a/examples/webserver/fault-tolerance/src/test/java/io/helidon/examples/webserver/faulttolerance/MainTest.java b/examples/webserver/fault-tolerance/src/test/java/io/helidon/examples/webserver/faulttolerance/MainTest.java new file mode 100644 index 000000000..e1dd3f963 --- /dev/null +++ b/examples/webserver/fault-tolerance/src/test/java/io/helidon/examples/webserver/faulttolerance/MainTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.faulttolerance; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.DirectClient; +import io.helidon.webserver.testing.junit5.RoutingTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@RoutingTest +class MainTest { + + private final DirectClient client; + + MainTest(DirectClient client) { + this.client = client; + } + + @SetUpRoute + static void setup(HttpRouting.Builder routing) { + Main.routing(routing); + } + + @Test + void testAsync() { + try (Http1ClientResponse response = client.get("/ft/async").request()) { + assertThat(response.as(String.class), is("blocked for 100 millis")); + } + } + + @Test + void testBulkhead() throws InterruptedException { + // bulkhead is configured for limit of 1 and queue of 1, so third + // request should fail + + ExecutorService executor = Executors.newFixedThreadPool(2); + executor.submit(() -> client.get("/ft/bulkhead/10000").request().close()); + executor.submit(() -> client.get("/ft/bulkhead/10000").request().close()); + + // I want to make sure the above is connected + Thread.sleep(300); + + try (Http1ClientResponse response = client.get("/ft/bulkhead/10000").request()) { + // registered an error handler in Main + assertThat(response.status(), is(Status.SERVICE_UNAVAILABLE_503)); + assertThat(response.as(String.class), is("bulkhead")); + } finally { + executor.close(); + } + } + + @Test + void testCircuitBreaker() { + try (Http1ClientResponse response = client.get("/ft/circuitBreaker/true").request()) { + assertThat(response.as(String.class), is("blocked for 100 millis")); + } + + // error ratio is 20% within 10 request + // should work after first + try (Http1ClientResponse ignored = client.get("/ft/circuitBreaker/false").request(); + Http1ClientResponse response = client.get("/ft/circuitBreaker/true").request()) { + + assertThat(response.as(String.class), is("blocked for 100 millis")); + } + + // should open after second + client.get("/ft/circuitBreaker/false").request().close(); + + try (Http1ClientResponse response = client.get("/ft/circuitBreaker/true").request()) { + + // registered an error handler in Main + assertThat(response.status(), is(Status.SERVICE_UNAVAILABLE_503)); + assertThat(response.as(String.class), is("circuit breaker")); + } + } + + @Test + void testFallback() { + try (Http1ClientResponse response = client.get("/ft/fallback/true").request()) { + assertThat(response.as(String.class), is("blocked for 100 millis")); + } + + try (Http1ClientResponse response = client.get("/ft/fallback/false").request()) { + assertThat(response.as(String.class), is("Failed back because of failure")); + } + } + + @Test + void testRetry() { + try (Http1ClientResponse response = client.get("/ft/retry/1").request()) { + assertThat(response.as(String.class), is("calls/failures: 1/0")); + } + + try (Http1ClientResponse response = client.get("/ft/retry/2").request()) { + assertThat(response.as(String.class), is("calls/failures: 2/1")); + } + + try (Http1ClientResponse response = client.get("/ft/retry/3").request()) { + assertThat(response.as(String.class), is("calls/failures: 3/2")); + } + + try (Http1ClientResponse response = client.get("/ft/retry/4").request()) { + // no error handler specified + assertThat(response.status(), is(Status.INTERNAL_SERVER_ERROR_500)); + assertThat(response.as(String.class), is("java.lang.RuntimeException: failure")); + } + } + + @Test + void testTimeout() { + try (Http1ClientResponse response = client.get("/ft/timeout/10").request()) { + assertThat(response.as(String.class), is("Slept for 10 ms")); + } + + try (Http1ClientResponse response = client.get("/ft/timeout/1000").request()) { + // error handler specified in Main + assertThat(response.status(), is(Status.REQUEST_TIMEOUT_408)); + assertThat(response.as(String.class), is("timeout")); + } + } +} \ No newline at end of file diff --git a/examples/webserver/imperative/pom.xml b/examples/webserver/imperative/pom.xml new file mode 100644 index 000000000..cc11a465a --- /dev/null +++ b/examples/webserver/imperative/pom.xml @@ -0,0 +1,93 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.webserver + helidon-examples-webserver-imperative + 1.0.0-SNAPSHOT + Helidon Examples WebServer Imperative + Example of imperative ("pure" programmatic) approach using Helidon SE + + + io.helidon.examples.webserver.imperative.ImperativeMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + diff --git a/examples/webserver/imperative/src/main/java/io/helidon/examples/webserver/imperative/ImperativeMain.java b/examples/webserver/imperative/src/main/java/io/helidon/examples/webserver/imperative/ImperativeMain.java new file mode 100644 index 000000000..bb8da9c5b --- /dev/null +++ b/examples/webserver/imperative/src/main/java/io/helidon/examples/webserver/imperative/ImperativeMain.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.imperative; + +import io.helidon.config.Config; +import io.helidon.http.Method; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +/** + * Main class of this example, starts the server. + */ +public final class ImperativeMain { + private ImperativeMain() { + } + + /** + * Start the example. + * + * @param args ignored + */ + public static void main(String[] args) { + Config config = Config.create(); + Config.global(config); + + WebServer server = WebServer.create(ws -> ws.config(config.get("server")) + .routing(ImperativeMain::routing)) + .start(); + + System.out.println("Server started. Server configuration: " + server.prototype()); + } + + private static void routing(HttpRouting.Builder routing) { + Method list = Method.create("LIST"); + + routing.get("/", (req, res) -> res.send("Hello World!")) + .route(list, "/", (req, res) -> res.send("lll")) + .route(list, (req, res) -> res.send("listed")); + } +} diff --git a/examples/webserver/imperative/src/main/java/io/helidon/examples/webserver/imperative/package-info.java b/examples/webserver/imperative/src/main/java/io/helidon/examples/webserver/imperative/package-info.java new file mode 100644 index 000000000..b2dbabd47 --- /dev/null +++ b/examples/webserver/imperative/src/main/java/io/helidon/examples/webserver/imperative/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example showing usage of HTTP web-server with pure imperative programming (no injection, no inversion of control). + */ +package io.helidon.examples.webserver.imperative; diff --git a/examples/webserver/imperative/src/main/resources/application.yaml b/examples/webserver/imperative/src/main/resources/application.yaml new file mode 100644 index 000000000..b4fe53abe --- /dev/null +++ b/examples/webserver/imperative/src/main/resources/application.yaml @@ -0,0 +1,23 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + host: "localhost" + port: 8080 + sockets: + - name: "https" + host: "localhost" + port: 8081 diff --git a/examples/webserver/multiport/README.md b/examples/webserver/multiport/README.md new file mode 100644 index 000000000..56b941bf4 --- /dev/null +++ b/examples/webserver/multiport/README.md @@ -0,0 +1,37 @@ +# Helidon WebServer Multiple Port Example + +It is common when deploying a microservice to run your service on +multiple ports so that you can control the visibility of your +service's endpoints. For example, you might want to use three ports: + +- 8080: public REST endpoints of application +- 8081: private REST endpoints of application +- 8082: admin endpoints for health, metrics, etc. + +This lets you expose only the public endpoints via your +ingress controller or load balancer. + +This example shows a Helidon application running on three ports +as described above. + +The ports are configured in `application.yaml` by using named sockets. + +Separate routing is defined for each named socket in `Main.java` + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-webserver-multiport.jar +``` +## Exercise the application + +```shell +curl -X GET http://localhost:8080/hello + +curl -X GET http://localhost:8081/private/hello + +curl -X GET http://localhost:8082/observe/health + +curl -X GET http://localhost:8082/observe/metrics +``` diff --git a/examples/webserver/multiport/pom.xml b/examples/webserver/multiport/pom.xml new file mode 100644 index 000000000..ce5366126 --- /dev/null +++ b/examples/webserver/multiport/pom.xml @@ -0,0 +1,106 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-multiport + 1.0.0-SNAPSHOT + Helidon Examples WebServer Multiple Ports + + + io.helidon.webserver.examples.multiport.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe + + + io.helidon.config + helidon-config-yaml + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.health + helidon-health-checks + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + + + io.helidon.metrics + helidon-metrics-system-meters + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/multiport/src/main/java/io/helidon/webserver/examples/multiport/Main.java b/examples/webserver/multiport/src/main/java/io/helidon/webserver/examples/multiport/Main.java new file mode 100644 index 000000000..fe6621c1c --- /dev/null +++ b/examples/webserver/multiport/src/main/java/io/helidon/webserver/examples/multiport/Main.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.examples.multiport; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * The application main class. + */ +public final class Main { + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(final String[] args) { + // load logging configuration + LogConfig.configureRuntime(); + + // By default, this will pick up application.yaml from the classpath + Config config = Config.create(); + Config.global(config); + + WebServer server = WebServer.builder() + .config(config.get("server")) + .update(Main::setup) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + } + + /** + * Set up the server. + */ + static void setup(WebServerConfig.Builder server) { + // Build server using three ports: + // default public port, admin port, private port + server.routing(Main::publicRouting) + .routing("private", Main::privateSocket); + } + + /** + * Set up private socket. + */ + static void privateSocket(HttpRouting.Builder routing) { + routing.get("/private/hello", (req, res) -> res.send("Private Hello!!")); + } + + /** + * Set up public routing. + */ + static void publicRouting(HttpRouting.Builder routing) { + routing.get("/hello", (req, res) -> res.send("Public Hello!!")); + } +} diff --git a/examples/webserver/multiport/src/main/java/io/helidon/webserver/examples/multiport/package-info.java b/examples/webserver/multiport/src/main/java/io/helidon/webserver/examples/multiport/package-info.java new file mode 100644 index 000000000..cfc753a3d --- /dev/null +++ b/examples/webserver/multiport/src/main/java/io/helidon/webserver/examples/multiport/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Application that exposes multiple ports. + */ +package io.helidon.webserver.examples.multiport; diff --git a/examples/webserver/multiport/src/main/resources/application.yaml b/examples/webserver/multiport/src/main/resources/application.yaml new file mode 100644 index 000000000..8335d53ef --- /dev/null +++ b/examples/webserver/multiport/src/main/resources/application.yaml @@ -0,0 +1,28 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +server: + port: 8080 + host: "localhost" + sockets: + - name: "private" + port: 8081 + bind-address: "localhost" + - name: "admin" + port: 8082 + bind-address: "localhost" + features: + observe: + sockets: "admin" diff --git a/examples/webserver/multiport/src/main/resources/logging.properties b/examples/webserver/multiport/src/main/resources/logging.properties new file mode 100644 index 000000000..c916a0505 --- /dev/null +++ b/examples/webserver/multiport/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/webserver/multiport/src/test/java/io/helidon/webserver/examples/multiport/MainTest.java b/examples/webserver/multiport/src/test/java/io/helidon/webserver/examples/multiport/MainTest.java new file mode 100644 index 000000000..e4fc32d61 --- /dev/null +++ b/examples/webserver/multiport/src/test/java/io/helidon/webserver/examples/multiport/MainTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.examples.multiport; + +import java.util.stream.Stream; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +public class MainTest { + + private final Http1Client client; + private final int publicPort; + private final int privatePort; + private final int adminPort; + + public MainTest(WebServer server) { + client = Http1Client.builder().build(); + publicPort = server.port(); + privatePort = server.port("private"); + adminPort = server.port("admin"); + } + + int port(Params params) { + return switch (params.socket) { + case PUBLIC -> publicPort; + case ADMIN -> adminPort; + case PRIVATE -> privatePort; + }; + } + + @SetUpServer + public static void setup(WebServerConfig.Builder server) { + // Use test configuration so we can have ports allocated dynamically + Config config = Config.just(ConfigSources.classpath("application-test.yaml")); + server.config(config.get("server")) + .update(Main::setup); + } + + static Stream initParams() { + final String PUBLIC_PATH = "/hello"; + final String PRIVATE_PATH = "/private/hello"; + final String HEALTH_PATH = "/health"; + final String METRICS_PATH = "/health"; + + return Stream.of( + new Params(Socket.PUBLIC, PUBLIC_PATH, Status.OK_200), + new Params(Socket.PUBLIC, PRIVATE_PATH, Status.NOT_FOUND_404), + new Params(Socket.PUBLIC, HEALTH_PATH, Status.NOT_FOUND_404), + new Params(Socket.PUBLIC, METRICS_PATH, Status.NOT_FOUND_404), + + new Params(Socket.PRIVATE, PUBLIC_PATH, Status.NOT_FOUND_404), + new Params(Socket.PRIVATE, PRIVATE_PATH, Status.OK_200), + new Params(Socket.PRIVATE, HEALTH_PATH, Status.NOT_FOUND_404), + new Params(Socket.PRIVATE, METRICS_PATH, Status.NOT_FOUND_404), + + new Params(Socket.ADMIN, PUBLIC_PATH, Status.NOT_FOUND_404), + new Params(Socket.ADMIN, PRIVATE_PATH, Status.NOT_FOUND_404), + new Params(Socket.ADMIN, HEALTH_PATH, Status.OK_200), + new Params(Socket.ADMIN, METRICS_PATH, Status.OK_200)); + } + + @MethodSource("initParams") + @ParameterizedTest + public void portAccessTest(Params params) { + // Verifies we can access endpoints only on the proper port + client.get() + .uri("http://localhost:" + port(params)) + .path(params.path) + .request() + .close(); + } + + @Test + public void portTest() { + try (Http1ClientResponse response = client.get() + .uri("http://localhost:" + publicPort) + .path("/hello") + .request()) { + assertThat(response.as(String.class), is("Public Hello!!")); + } + + try (Http1ClientResponse response = client.get() + .uri("http://localhost:" + privatePort) + .path("/private/hello") + .request()) { + assertThat(response.as(String.class), is("Private Hello!!")); + } + } + + private record Params(Socket socket, String path, Status httpStatus) { + + @Override + public String toString() { + return path + " @" + socket + " should return " + httpStatus; + } + } + + private enum Socket { + PUBLIC, + ADMIN, + PRIVATE + } + +} diff --git a/examples/webserver/multiport/src/test/resources/application-test.yaml b/examples/webserver/multiport/src/test/resources/application-test.yaml new file mode 100644 index 000000000..2730bb315 --- /dev/null +++ b/examples/webserver/multiport/src/test/resources/application-test.yaml @@ -0,0 +1,25 @@ +# +# Copyright (c) 2021, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +server: + port: 0 + host: "localhost" + sockets: + - name: "private" + port: 0 + bind-address: "localhost" + - name: "admin" + port: 0 + bind-address: "localhost" diff --git a/examples/webserver/mutual-tls/README.md b/examples/webserver/mutual-tls/README.md new file mode 100644 index 000000000..066190167 --- /dev/null +++ b/examples/webserver/mutual-tls/README.md @@ -0,0 +1,47 @@ +# Mutual TLS Example + +This application demonstrates use of client certificates to +authenticate HTTP client. + +## Build + +```shell +mvn package +``` + +## Run + +Run the _config_ variant (default) of the application: + +```shell +java -jar target/helidon-examples-webserver-mutual-tls.jar +``` + +Run the _programmatic_ variant of the application: + +```shell +mvn exec:java -Dexec.mainClass=io.helidon.examples.webserver.mtls.ServerBuilderMain +``` + +## Exercise the application + +Using `curl`: + +```shell +openssl pkcs12 -in src/main/resources/client.p12 -nodes -legacy -passin pass:changeit -nokeys -out /tmp/chain.pem +openssl pkcs12 -in src/main/resources/client.p12 -nodes -legacy -passin pass:changeit -nokeys -cacerts -out /tmp/ca.pem +openssl pkcs12 -in src/main/resources/client.p12 -nodes -legacy -passin pass:changeit -nocerts -out /tmp/key.pem +curl --key /tmp/key.pem --cert /tmp/chain.pem --cacert /tmp/ca.pem https://localhost:443 --pass changeit +``` + +Using Helidon WebClient setup with configuration: + +```shell +mvn exec:java -Dexec.mainClass=io.helidon.examples.webserver.mtls.ClientConfigMain +``` + +Using Helidon WebClient setup programmatically: + +```shell +mvn exec:java -Dexec.mainClass=io.helidon.examples.webserver.mtls.ClientBuilderMain +``` diff --git a/examples/webserver/mutual-tls/automatic-store-generator.sh b/examples/webserver/mutual-tls/automatic-store-generator.sh new file mode 100644 index 000000000..03d7da1ca --- /dev/null +++ b/examples/webserver/mutual-tls/automatic-store-generator.sh @@ -0,0 +1,163 @@ +#!/bin/bash -e +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Windows Mingwin fix for path resolving +#export MSYS_NO_PATHCONV=1 + +NAME="" +TYPE=PKCS12 +SINGLE=true + +createCertificatesAndStores() { + mkdir out + echo 'Generating new key stores...' + keytool -genkeypair -keyalg RSA -keysize 2048 -alias root-ca -dname "CN=$NAME-CA" -validity 21650 -keystore ca.jks -storepass changeit -keypass changeit -deststoretype pkcs12 -ext KeyUsage=digitalSignature,keyEncipherment,keyCertSign -ext ExtendedKeyUsage=serverAuth,clientAuth -ext BasicConstraints=ca:true,PathLen:3 + keytool -genkeypair -keyalg RSA -keysize 2048 -alias server -dname "CN=localhost" -validity 21650 -keystore server.jks -storepass changeit -keypass changeit -deststoretype pkcs12 + keytool -genkeypair -keyalg RSA -keysize 2048 -alias client -dname "C=CZ,CN=$NAME-client,OU=Prague,O=Oracle" -validity 21650 -keystore client.jks -storepass changeit -keypass changeit -deststoretype pkcs12 + echo 'Obtaining client and server certificates...' + keytool -exportcert -keystore client.jks -storepass changeit -alias client -rfc -file client.cer + keytool -exportcert -keystore server.jks -storepass changeit -alias server -rfc -file server.cer + echo 'Generating CSR for client and server...' + keytool -certreq -keystore server.jks -alias server -keypass changeit -storepass changeit -keyalg rsa -file server.csr + keytool -certreq -keystore client.jks -alias client -keypass changeit -storepass changeit -keyalg rsa -file client.csr + echo 'Obtaining CA pem and key...' + keytool -importkeystore -srckeystore ca.jks -destkeystore ca.p12 -srcstoretype jks -deststoretype pkcs12 -srcstorepass changeit -deststorepass changeit + openssl pkcs12 -in ca.p12 -out ca.key -nocerts -passin pass:changeit -passout pass:changeit + openssl pkcs12 -in ca.p12 -out ca.pem -nokeys -passin pass:changeit -passout pass:changeit + echo 'Signing client and server certificates...' + openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client-signed.cer -days 21650 -passin pass:changeit + openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server-signed.cer -sha256 -days 21650 -passin pass:changeit + echo 'Replacing server and client certificates with the signed ones...' + keytool -importkeystore -srckeystore client.jks -destkeystore client.p12 -srcstoretype jks -deststoretype pkcs12 -srcstorepass changeit -deststorepass changeit + openssl pkcs12 -in client.p12 -nodes -out client-private.key -nocerts -passin pass:changeit + openssl pkcs12 -export -in client-signed.cer -inkey client-private.key -out client-signed.p12 -name client -passout pass:changeit + keytool -delete -alias client -keystore client.jks -storepass changeit + keytool -importkeystore -srckeystore client-signed.p12 -srcstoretype PKCS12 -destkeystore client.jks -srcstorepass changeit -deststorepass changeit + keytool -importkeystore -srckeystore server.jks -destkeystore server.p12 -srcstoretype jks -deststoretype pkcs12 -srcstorepass changeit -deststorepass changeit + openssl pkcs12 -in server.p12 -nodes -out server-private.key -nocerts -passin pass:changeit + openssl pkcs12 -export -in server-signed.cer -inkey server-private.key -out server-signed.p12 -name server -passout pass:changeit + keytool -delete -alias server -keystore server.jks -storepass changeit + keytool -importkeystore -srckeystore server-signed.p12 -srcstoretype PKCS12 -destkeystore server.jks -srcstorepass changeit -deststorepass changeit + + echo "Importing CA cert to the client and server stores..." + if [ "$SINGLE" = true ] ; then + keytool -v -trustcacerts -keystore client.jks -importcert -file ca.pem -alias root-ca -storepass changeit -noprompt + keytool -v -trustcacerts -keystore server.jks -importcert -file ca.pem -alias root-ca -storepass changeit -noprompt + else + keytool -v -trustcacerts -keystore client-truststore.jks -importcert -file ca.pem -alias root-ca -storepass changeit -noprompt + keytool -v -trustcacerts -keystore server-truststore.jks -importcert -file ca.pem -alias root-ca -storepass changeit -noprompt + fi + + echo "Changing aliases to 1..." + keytool -changealias -alias server -destalias 1 -keypass changeit -keystore server.jks -storepass changeit + keytool -changealias -alias client -destalias 1 -keypass changeit -keystore client.jks -storepass changeit + + echo "Generating requested type of stores..." + if [ "$TYPE" = PKCS12 ] || [ "$TYPE" = P12 ] ; then + keytool -importkeystore -srckeystore client.jks -destkeystore out/client.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass changeit -deststorepass changeit + keytool -importkeystore -srckeystore server.jks -destkeystore out/server.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass changeit -deststorepass changeit + if [ "$SINGLE" = false ] ; then + keytool -importkeystore -srckeystore server-truststore.jks -destkeystore out/server-truststore.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass changeit -deststorepass changeit + keytool -importkeystore -srckeystore client-truststore.jks -destkeystore out/client-truststore.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass changeit -deststorepass changeit + fi + else + mv client.jks out/client.jks + mv server.jks out/server.jks + if [ "$SINGLE" = false ] ; then + mv client-truststore.jks out/client-truststore.jks + mv server-truststore.jks out/server-truststore.jks + fi + fi +} + +removeAllPreviouslyCreatedStores() { + echo 'Removing all of previously created items...' + + rm -fv ca.key + rm -fv ca.jks + rm -fv ca.p12 + rm -fv ca.pem + rm -fv ca.srl + rm -fv server.jks + rm -fv server.cer + rm -fv server.csr + rm -fv server.p12 + rm -fv server-private.key + rm -fv server-signed.cer + rm -fv server-signed.p12 + rm -fv server-truststore.jks + rm -fv client.cer + rm -fv client.csr + rm -fv client.p12 + rm -fv client-private.key + rm -fv client-signed.cer + rm -fv client-signed.p12 + rm -fv client.jks + rm -fv client-truststore.jks + rm -rf out + + echo 'Clean up finished' +} + +while [ "$1" != "" ]; do + case $1 in + -n | --name ) shift + NAME=$1 + ;; + -t | --type ) shift + TYPE=$1 + ;; + -s | --single ) shift + SINGLE=$1 + ;; + -h | --help ) echo "Some cool help" + exit + ;; + * ) echo "ERROR: Invalid parameter" $1 + exit 1 + esac + shift +done +if [ -z "$NAME" ]; then + echo "ERROR: Please specify the name of Organization/Application by parameter -n | --name" + exit 1 +else + echo "Generating certs for Organization/Application "$NAME +fi +case $TYPE in + JKS | P12 | PKCS12 ) + echo "Output file will be of type" $TYPE + ;; + *) + echo 'ERROR: Invalid output type' $TYPE + echo 'Only JKS | P12 | PKCS12 supported' + return 1 +esac +case $SINGLE in + true ) + echo "Truststore and private key will be in single file" + ;; + false ) + echo "Truststore and private key will be in separate files" + ;; + *) + echo "ERROR: Only value true/false valid in single parameter! Current " $SINGLE + exit 1 +esac + +removeAllPreviouslyCreatedStores +createCertificatesAndStores diff --git a/examples/webserver/mutual-tls/pom.xml b/examples/webserver/mutual-tls/pom.xml new file mode 100644 index 000000000..59524e793 --- /dev/null +++ b/examples/webserver/mutual-tls/pom.xml @@ -0,0 +1,88 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-mutual-tls + 1.0.0-SNAPSHOT + Helidon Examples WebServer Mutual TLS + + + Application demonstrates the use of mutual TLS with WebServer and WebClient + + + + io.helidon.examples.webserver.mtls.ServerConfigMain + + + + + + io.helidon.webclient + helidon-webclient + + + io.helidon.webserver + helidon-webserver + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ClientBuilderMain.java b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ClientBuilderMain.java new file mode 100644 index 000000000..071d64631 --- /dev/null +++ b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ClientBuilderMain.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.mtls; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.pki.Keys; +import io.helidon.common.tls.Tls; +import io.helidon.common.tls.TlsClientAuth; +import io.helidon.webclient.http1.Http1Client; + +/** + * Setting up {@link io.helidon.webclient.api.WebClient} to support mutual TLS via builder. + */ +public class ClientBuilderMain { + + private ClientBuilderMain() { + } + + /** + * Start the example. + * This example executes two requests by Helidon {@link io.helidon.webclient.api.WebClient} which are configured + * by the {@link io.helidon.webclient.api.WebClientConfig.Builder}. + *

    + * You have to execute either {@link ServerBuilderMain} or {@link ServerConfigMain} for this to work. + *

    + * If any of the ports has been changed, you have to update ports in this main method also. + * + * @param args start arguments are ignored + */ + public static void main(String[] args) { + Http1Client client = createClient(); + + System.out.println("Contacting unsecured endpoint!"); + System.out.println("Response: " + callUnsecured(client, 8080)); + + System.out.println("Contacting secured endpoint!"); + System.out.println("Response: " + callSecured(client, 443)); + } + + static Http1Client createClient() { + Keys keyConfig = Keys.builder() + .keystore(store -> store + .trustStore(true) + .keystore(Resource.create("client.p12")) + .passphrase("changeit")) + .build(); + return Http1Client.builder() + .tls(Tls.builder() + .endpointIdentificationAlgorithm("NONE") + .clientAuth(TlsClientAuth.REQUIRED) + .privateKey(keyConfig) + .privateKeyCertChain(keyConfig) + .trust(keyConfig) + .build()) + .build(); + } + + static String callUnsecured(Http1Client client, int port) { + return client.get("http://localhost:" + port) + .requestEntity(String.class); + } + + static String callSecured(Http1Client client, int port) { + return client.get("https://localhost:" + port) + .requestEntity(String.class); + } +} diff --git a/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ClientConfigMain.java b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ClientConfigMain.java new file mode 100644 index 000000000..27ea14c38 --- /dev/null +++ b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ClientConfigMain.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.mtls; + +import io.helidon.config.Config; +import io.helidon.webclient.http1.Http1Client; + +/** + * Setting up {@link io.helidon.webclient.api.WebClient} to support mutual TLS via configuration. + */ +public class ClientConfigMain { + + private ClientConfigMain() { + } + + /** + * Start the example. + * This example executes two requests by Helidon {@link io.helidon.webclient.api.WebClient} which are configured + * by the configuration. + *

    + * You have to execute either {@link ServerBuilderMain} or {@link ServerConfigMain} for this to work. + *

    + * If any of the ports has been changed, you have to update ports in this main method also. + * + * @param args start arguments are ignored + */ + public static void main(String[] args) { + Config config = Config.create(); + Http1Client client = Http1Client.builder() + .config(config.get("client")) + .build(); + + System.out.println("Contacting unsecured endpoint!"); + System.out.println("Response: " + callUnsecured(client, 8080)); + + System.out.println("Contacting secured endpoint!"); + System.out.println("Response: " + callSecured(client, 443)); + + } + + static String callUnsecured(Http1Client client, int port) { + return client.get("http://localhost:" + port) + .requestEntity(String.class); + } + + static String callSecured(Http1Client client, int port) { + return client.get("https://localhost:" + port) + .requestEntity(String.class); + } +} diff --git a/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/SecureService.java b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/SecureService.java new file mode 100644 index 000000000..04f7d924f --- /dev/null +++ b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/SecureService.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.mtls; + +import io.helidon.http.HeaderValues; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; + +import static io.helidon.http.HeaderNames.X_HELIDON_CN; + +class SecureService implements HttpService { + @Override + public void routing(HttpRules rules) { + rules.any((req, res) -> { + // close to avoid re-using cached connections on the client side + res.header(HeaderValues.CONNECTION_CLOSE); + res.send("Hello " + req.headers().get(X_HELIDON_CN).get() + "!"); + }); + } +} diff --git a/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ServerBuilderMain.java b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ServerBuilderMain.java new file mode 100644 index 000000000..6f1878aa1 --- /dev/null +++ b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ServerBuilderMain.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.mtls; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.pki.Keys; +import io.helidon.common.tls.TlsClientAuth; +import io.helidon.webserver.ListenerConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * Setting up {@link WebServer} to support mutual TLS via builder. + */ +public class ServerBuilderMain { + + private ServerBuilderMain() { + } + + /** + * Start the example. + * This will start Helidon {@link WebServer} which is configured by the {@link WebServerConfig.Builder}. + * There will be two sockets running: + *

      + *
    • {@code 8080} - without TLS protection + *
    • {@code 443} - with TLS protection + *

    + * Both of the ports mentioned above are default ports for this example and can be changed by updating + * values in this method. + * + * @param args start arguments are ignored + */ + public static void main(String[] args) { + WebServerConfig.Builder builder = WebServer.builder() + .port(8080) + .putSocket("secured", socket -> socket.port(443)); + setup(builder); + WebServer server = builder.build().start(); + System.out.printf(""" + WebServer is up! + Unsecured: http://localhost:%1$d + Secured: https://localhost:%2$d + """, server.port(), server.port("secured")); + } + + static void setup(WebServerConfig.Builder server) { + server.routing(ServerBuilderMain::plainRouting) + .putSocket("secured", socket -> securedSocket(server, socket)); + } + + static void plainRouting(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> res.send("Hello world unsecured!")); + } + + private static void securedSocket(WebServerConfig.Builder server, ListenerConfig.Builder socket) { + Keys keyConfig = Keys.builder() + .keystore(store -> store + .trustStore(true) + .keystore(Resource.create("server.p12")) + .passphrase("changeit")) + .build(); + + socket.from(server.sockets().get("secured")) + .tls(tls -> tls + .endpointIdentificationAlgorithm("NONE") + .clientAuth(TlsClientAuth.REQUIRED) + .trust(keyConfig) + .privateKey(keyConfig) + .privateKeyCertChain(keyConfig)) + .routing(routing -> routing + .register("/", new SecureService())); + } +} diff --git a/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ServerConfigMain.java b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ServerConfigMain.java new file mode 100644 index 000000000..b1accc104 --- /dev/null +++ b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/ServerConfigMain.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.mtls; + +import io.helidon.config.Config; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * Setting up {@link WebServer} to support mutual TLS via configuration. + */ +public class ServerConfigMain { + + private ServerConfigMain() { + } + + /** + * Start the example. + * This will start Helidon {@link WebServer} which is configured by the configuration. + * There will be two sockets running: + *

      + *
    • {@code 8080} - without TLS protection + *
    • {@code 443} - with TLS protection + *

    + * Both of the ports mentioned above are default ports for this example and can be changed via configuration file. + * + * @param args start arguments are ignored + */ + public static void main(String[] args) { + Config config = Config.create(); + WebServerConfig.Builder builder = WebServer.builder(); + setup(builder, config.get("server")); + WebServer server = builder.build().start(); + System.out.printf(""" + WebServer is up! + Unsecured: http://localhost:%1$d + Secured: https://localhost:%2$d + """, server.port(), server.port("secured")); + } + + static void setup(WebServerConfig.Builder server, Config config) { + server.config(config) + .routing(ServerConfigMain::plainRouting) + .putSocket("secured", socket -> socket + .from(server.sockets().get("secured")) + .routing(routing -> routing + .register("/", new SecureService()))); + } + + private static void plainRouting(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> res.send("Hello world unsecured!")); + } +} diff --git a/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/package-info.java b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/package-info.java new file mode 100644 index 000000000..29f989d35 --- /dev/null +++ b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of mutual TLS configuration for {@link io.helidon.webserver.WebServer} and {@link io.helidon.webclient.WebClient}, + * using both {@link io.helidon.config.Config} and builder based approach. + */ +package io.helidon.examples.webserver.mtls; diff --git a/examples/webserver/mutual-tls/src/main/resources/application.yaml b/examples/webserver/mutual-tls/src/main/resources/application.yaml new file mode 100644 index 000000000..959a9d9b0 --- /dev/null +++ b/examples/webserver/mutual-tls/src/main/resources/application.yaml @@ -0,0 +1,50 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + sockets: + - name: "secured" + port: 443 + tls: + endpoint-identification-algorithm: "NONE" + client-auth: "REQUIRED" + trust: + keystore: + passphrase: "changeit" + trust-store: true + resource: + resource-path: "server.p12" + private-key: + keystore: + passphrase: "changeit" + resource: + resource-path: "server.p12" + +client: + tls: + client-auth: "REQUIRED" + trust: + keystore: + passphrase: "changeit" + trust-store: true + resource: + resource-path: "client.p12" + private-key: + keystore: + passphrase: "changeit" + resource: + resource-path: "client.p12" diff --git a/examples/webserver/mutual-tls/src/main/resources/client.p12 b/examples/webserver/mutual-tls/src/main/resources/client.p12 new file mode 100644 index 000000000..9529b6722 Binary files /dev/null and b/examples/webserver/mutual-tls/src/main/resources/client.p12 differ diff --git a/examples/webserver/mutual-tls/src/main/resources/logging.properties b/examples/webserver/mutual-tls/src/main/resources/logging.properties new file mode 100644 index 000000000..1d28bd7d8 --- /dev/null +++ b/examples/webserver/mutual-tls/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2023, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +.level=INFO diff --git a/examples/webserver/mutual-tls/src/main/resources/server.p12 b/examples/webserver/mutual-tls/src/main/resources/server.p12 new file mode 100644 index 000000000..5fc1fba05 Binary files /dev/null and b/examples/webserver/mutual-tls/src/main/resources/server.p12 differ diff --git a/examples/webserver/mutual-tls/src/test/java/io/helidon/examples/webserver/mtls/MutualTlsExampleBuilderTest.java b/examples/webserver/mutual-tls/src/test/java/io/helidon/examples/webserver/mtls/MutualTlsExampleBuilderTest.java new file mode 100644 index 000000000..a6e8c1d64 --- /dev/null +++ b/examples/webserver/mutual-tls/src/test/java/io/helidon/examples/webserver/mtls/MutualTlsExampleBuilderTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.mtls; + +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; + +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; + +/** + * Test of mutual TLS example. + */ +@ServerTest +public class MutualTlsExampleBuilderTest { + + private final WebServer server; + + public MutualTlsExampleBuilderTest(WebServer server) { + this.server = server; + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + server.port(0); + server.putSocket("secured", it -> it + .from(server.sockets().get("secured")) + .port(0)); + ServerBuilderMain.setup(server); + } + + @Test + public void testBuilderAccessSuccessful() { + Http1Client client = ClientBuilderMain.createClient(); + MatcherAssert.assertThat(ClientBuilderMain.callUnsecured(client, server.port()), is("Hello world unsecured!")); + MatcherAssert.assertThat(ClientBuilderMain.callSecured(client, server.port("secured")), is("Hello Helidon-client!")); + } +} diff --git a/examples/webserver/mutual-tls/src/test/java/io/helidon/examples/webserver/mtls/MutualTlsExampleConfigTest.java b/examples/webserver/mutual-tls/src/test/java/io/helidon/examples/webserver/mtls/MutualTlsExampleConfigTest.java new file mode 100644 index 000000000..734f25177 --- /dev/null +++ b/examples/webserver/mutual-tls/src/test/java/io/helidon/examples/webserver/mtls/MutualTlsExampleConfigTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.mtls; + +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.config.OverrideSources; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; + +import org.junit.jupiter.api.Test; + +import static io.helidon.examples.webserver.mtls.ClientConfigMain.callSecured; +import static io.helidon.examples.webserver.mtls.ClientConfigMain.callUnsecured; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Test of mutual TLS example. + */ +@ServerTest +public class MutualTlsExampleConfigTest { + + private static Config config; + private final WebServer server; + private final Http1Client client; + + public MutualTlsExampleConfigTest(WebServer server) { + this.server = server; + this.client = Http1Client.builder().config(config.get("client")).build(); + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + config = Config.builder() + .sources(ConfigSources.classpath("application.yaml")) + .overrides(() -> OverrideSources.create(Map.of( + "server.port", "0", + "server.sockets.*.port", "0" + ))) + .build(); + ServerConfigMain.setup(server, config.get("server")); + } + + @Test + public void testConfigAccessSuccessful() { + assertThat(callUnsecured(client, server.port()), is("Hello world unsecured!")); + assertThat(callSecured(client, server.port("secured")), is("Hello Helidon-client!")); + } +} \ No newline at end of file diff --git a/examples/webserver/observe/pom.xml b/examples/webserver/observe/pom.xml new file mode 100644 index 000000000..aca1efbd8 --- /dev/null +++ b/examples/webserver/observe/pom.xml @@ -0,0 +1,115 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.webserver + helidon-examples-webserver-observe + 1.0.0-SNAPSHOT + Helidon Examples WebServer Observe + + + io.helidon.examples.webserver.observe.ObserveMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.webserver.observe + helidon-webserver-observe-config + + + io.helidon.webserver.observe + helidon-webserver-observe-info + + + io.helidon.webserver.observe + helidon-webserver-observe-log + + + io.helidon.webserver + helidon-webserver-security + + + io.helidon.security.providers + helidon-security-providers-http-auth + + + io.helidon.webserver + helidon-webserver-context + + + io.helidon.config + helidon-config-yaml + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + diff --git a/examples/webserver/observe/src/main/java/io/helidon/examples/webserver/observe/ObserveMain.java b/examples/webserver/observe/src/main/java/io/helidon/examples/webserver/observe/ObserveMain.java new file mode 100644 index 000000000..6568c4e81 --- /dev/null +++ b/examples/webserver/observe/src/main/java/io/helidon/examples/webserver/observe/ObserveMain.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.observe; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +/** + * Register observe support with all available observers and NO security. + * Some observers may disclose secret or private information and should be protected, use with care (and security). + */ +public class ObserveMain { + private ObserveMain() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + // load logging + LogConfig.configureRuntime(); + + Config config = Config.create(); + + WebServer server = WebServer.builder() + .config(config.get("server")) + .routing(ObserveMain::routing) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + } + + /** + * Set up HTTP routing. + * This method is used from tests as well. + * + * @param router HTTP routing builder + */ + static void routing(HttpRouting.Builder router) { + router.get("/", (req, res) -> res.send("WebServer Works!")); + } +} diff --git a/examples/webserver/observe/src/main/java/io/helidon/examples/webserver/observe/package-info.java b/examples/webserver/observe/src/main/java/io/helidon/examples/webserver/observe/package-info.java new file mode 100644 index 000000000..c6bf2e052 --- /dev/null +++ b/examples/webserver/observe/src/main/java/io/helidon/examples/webserver/observe/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Observability example. + */ +package io.helidon.examples.webserver.observe; diff --git a/examples/webserver/observe/src/main/resources/application.yaml b/examples/webserver/observe/src/main/resources/application.yaml new file mode 100644 index 000000000..32fcd606c --- /dev/null +++ b/examples/webserver/observe/src/main/resources/application.yaml @@ -0,0 +1,44 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + host: "127.0.0.1" + port: 8080 + features: + security: + paths: + - path: "/observe/log/*" + authenticate: true + - path: "/observe/config/*" + authenticate: true + observe: + observers: + info: + values: + name: "Observe Example" + version: "1.0.0" + +security: + providers: + - http-basic-auth: + users: + - login: "admin" + password: "changeit" + roles: ["observe"] +app: + greeting: "Hello!" + app-secret: "Do not print this in observe" + app-password: "Do not print this in observe" diff --git a/examples/webserver/observe/src/main/resources/logging.properties b/examples/webserver/observe/src/main/resources/logging.properties new file mode 100644 index 000000000..308e2fda1 --- /dev/null +++ b/examples/webserver/observe/src/main/resources/logging.properties @@ -0,0 +1,24 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.webserve.level=INFO + +java.util.logging.ConsoleHandler.level=ALL +# to enable security audit logging for Helidon: +# AUDIT.level=FINEST diff --git a/examples/webserver/observe/src/test/java/io/helidon/examples/webserver/observe/AbstractObserveTest.java b/examples/webserver/observe/src/test/java/io/helidon/examples/webserver/observe/AbstractObserveTest.java new file mode 100644 index 000000000..18ac8fdd0 --- /dev/null +++ b/examples/webserver/observe/src/test/java/io/helidon/examples/webserver/observe/AbstractObserveTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.observe; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +abstract class AbstractObserveTest { + private final Http1Client client; + + protected AbstractObserveTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + ObserveMain.routing(builder); + } + + @Test + void testRootRoute() { + String response = client.get("/") + .request() + .as(String.class); + + assertThat(response, is("WebServer Works!")); + } + + @Test + void testConfigObserver() { + try (Http1ClientResponse response = client.get("/observe/config/profile").request()) { + // this requires basic authentication + assertThat(response.status(), is(Status.UNAUTHORIZED_401)); + } + } + + @Test + void testHealthObserver() { + try (Http1ClientResponse response = client.get("/observe/health").request()) { + assertThat(response.status(), is(Status.NO_CONTENT_204)); + } + } + + @Test + void testInfoObserver() { + try (Http1ClientResponse response = client.get("/observe/info").request()) { + assertThat(response.status(), is(Status.OK_200)); + } + } +} diff --git a/examples/webserver/observe/src/test/java/io/helidon/examples/webserver/observe/ObserveRoutingIT.java b/examples/webserver/observe/src/test/java/io/helidon/examples/webserver/observe/ObserveRoutingIT.java new file mode 100644 index 000000000..a7889600a --- /dev/null +++ b/examples/webserver/observe/src/test/java/io/helidon/examples/webserver/observe/ObserveRoutingIT.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.observe; + +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webclient.http1.Http1Client; + +@ServerTest +class ObserveRoutingIT extends AbstractObserveTest { + ObserveRoutingIT(Http1Client client) { + super(client); + } +} diff --git a/examples/webserver/observe/src/test/java/io/helidon/examples/webserver/observe/ObserveRoutingTest.java b/examples/webserver/observe/src/test/java/io/helidon/examples/webserver/observe/ObserveRoutingTest.java new file mode 100644 index 000000000..c8cddad27 --- /dev/null +++ b/examples/webserver/observe/src/test/java/io/helidon/examples/webserver/observe/ObserveRoutingTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.observe; + +import io.helidon.webserver.testing.junit5.DirectClient; +import io.helidon.webserver.testing.junit5.RoutingTest; + +@RoutingTest +class ObserveRoutingTest extends AbstractObserveTest { + ObserveRoutingTest(DirectClient client) { + super(client); + } +} + diff --git a/examples/webserver/observe/src/test/resources/logging-test.properties b/examples/webserver/observe/src/test/resources/logging-test.properties new file mode 100644 index 000000000..2fa545536 --- /dev/null +++ b/examples/webserver/observe/src/test/resources/logging-test.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +# java.util.logging.SimpleFormatter.format = [%1$tc] %5$s %6$s%n +java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=WARNING diff --git a/examples/webserver/opentracing/Dockerfile b/examples/webserver/opentracing/Dockerfile new file mode 100644 index 000000000..6a08ba21e --- /dev/null +++ b/examples/webserver/opentracing/Dockerfile @@ -0,0 +1,56 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# 1st stage, build the app +FROM container-registry.oracle.com/java/jdk-no-fee-term: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 + +# Create a first layer to cache the "Maven World" in the local repository. +# Incremental docker builds will always resume after that, unless you update +# the pom +ADD pom.xml . +RUN mvn package -Dmaven.test.skip + +# Do the Maven build! +# Incremental docker builds will resume here when you change sources +ADD src src +RUN mvn package -DskipTests + +RUN echo "done!" + +# 2nd stage, build the runtime image +FROM container-registry.oracle.com/java/jdk-no-fee-term:21 +WORKDIR /helidon + +# Copy the binary built in the 1st stage +COPY --from=build /helidon/target/helidon-examples-webserver-opentracing.jar ./ +COPY --from=build /helidon/target/libs ./libs + +ENV tracing.host="zipkin" + +CMD ["java", "-jar", "helidon-examples-webserver-opentracing.jar"] + +EXPOSE 8080 diff --git a/examples/webserver/opentracing/README.md b/examples/webserver/opentracing/README.md new file mode 100644 index 000000000..ed0ad12e7 --- /dev/null +++ b/examples/webserver/opentracing/README.md @@ -0,0 +1,39 @@ +# Opentracing Example Application + +## Start Zipkin + +With Docker: +```shell +docker run --name zipkin -d -p 9411:9411 openzipkin/zipkin +``` +With JDK: +```shell +curl -sSL https://zipkin.io/quickstart.sh | bash -s +java -jar zipkin.jar +``` + +## Build and run + +With Docker: +```shell +docker build -t helidon-webserver-opentracing-example . +docker run --rm -d --link zipkin --name helidon-webserver-opentracing-example \ + -p 8080:8080 helidon-webserver-opentracing-example:latest +``` +With JDK: +```shell +mvn package +java -jar target/helidon-examples-webserver-opentracing.jar +``` + +Try the endpoint: +```shell +curl http://localhost:8080/test +``` + +Then check out the traces at http://localhost:9411. + +Stop the docker containers: +```shell +docker stop zipkin helidon-webserver-opentracing-example +``` diff --git a/examples/webserver/opentracing/pom.xml b/examples/webserver/opentracing/pom.xml new file mode 100644 index 000000000..1bb46c4d5 --- /dev/null +++ b/examples/webserver/opentracing/pom.xml @@ -0,0 +1,124 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-opentracing + 1.0.0-SNAPSHOT + Helidon Examples WebServer OpenTracing + + + An example app with Open Tracing support. + + + + io.helidon.examples.webserver.opentracing.Main + 2.23.4 + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe-tracing + + + io.helidon.logging + helidon-logging-common + + + io.helidon.config + helidon-config + + + io.helidon.tracing.providers + helidon-tracing-providers-zipkin + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.testcontainers + junit-jupiter + test + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.webclient + helidon-webclient + test + + + org.hamcrest + hamcrest-all + test + + + jakarta.json + jakarta.json-api + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + io.helidon.http.media + helidon-http-media-jsonp + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/opentracing/src/main/java/io/helidon/examples/webserver/opentracing/Main.java b/examples/webserver/opentracing/src/main/java/io/helidon/examples/webserver/opentracing/Main.java new file mode 100644 index 000000000..3282b3b76 --- /dev/null +++ b/examples/webserver/opentracing/src/main/java/io/helidon/examples/webserver/opentracing/Main.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.opentracing; + +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.logging.common.LogConfig; +import io.helidon.tracing.Tracer; +import io.helidon.tracing.TracerBuilder; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.tracing.TracingObserver; + +/** + * The application uses Open Tracing and sends the collected data to ZipKin. + * + * @see io.helidon.tracing.TracerBuilder + * @see io.helidon.tracing.providers.zipkin.ZipkinTracerBuilder + */ +public final class Main { + + private Main() { + } + + /** + * Run the OpenTracing application. + * + * @param args not used + */ + public static void main(String[] args) { + + // configure logging in order to not have the standard JVM defaults + LogConfig.configureRuntime(); + + WebServer server = setupServer(WebServerConfig.builder(), 9411); + + System.out.println("Started at http://localhost:" + server.port()); + } + + static WebServer setupServer(WebServerConfig.Builder builder, int port) { + Config config = Config.builder() + .sources(ConfigSources.create(Map.of("host", "localhost", + "port", "8080"))) + .build(); + + Tracer tracer = TracerBuilder.create("demo-first") + .collectorPort(port) + .registerGlobal(true) + .build(); + + return builder + .config(config) + .addFeature(ObserveFeature.builder() + .addObserver(TracingObserver.create(tracer)) + .build()) + .routing(routing -> routing + .get("/test", (req, res) -> res.send("Hello World!")) + .post("/hello", (req, res) -> res.send("Hello: " + req.content().as(String.class)))) + .build() + .start(); + } +} diff --git a/examples/webserver/opentracing/src/main/java/io/helidon/examples/webserver/opentracing/package-info.java b/examples/webserver/opentracing/src/main/java/io/helidon/examples/webserver/opentracing/package-info.java new file mode 100644 index 000000000..6a2bc93f9 --- /dev/null +++ b/examples/webserver/opentracing/src/main/java/io/helidon/examples/webserver/opentracing/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * An example of WebServer app supporting Open Tracing. + * + * @see io.helidon.examples.webserver.opentracing.Main + */ +package io.helidon.examples.webserver.opentracing; diff --git a/examples/webserver/opentracing/src/main/resources/logging.properties b/examples/webserver/opentracing/src/main/resources/logging.properties new file mode 100644 index 000000000..8b1a686e5 --- /dev/null +++ b/examples/webserver/opentracing/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2017, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +#All attributes details +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#All log level details +.level=FINEST + +io.helidon.webserver.level=FINEST +org.glassfish.jersey.internal.Errors.level=SEVERE diff --git a/examples/webserver/opentracing/src/test/java/io/helidon/examples/webserver/opentracing/MainTest.java b/examples/webserver/opentracing/src/test/java/io/helidon/examples/webserver/opentracing/MainTest.java new file mode 100644 index 000000000..9079fd48c --- /dev/null +++ b/examples/webserver/opentracing/src/test/java/io/helidon/examples/webserver/opentracing/MainTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.opentracing; + +import io.helidon.http.Status; +import io.helidon.http.media.jsonp.JsonpSupport; +import io.helidon.webclient.api.HttpClientResponse; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServer; + +import jakarta.json.JsonArray; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static io.helidon.common.testing.junit5.MatcherWithRetry.assertThatWithRetry; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@Testcontainers(disabledWithoutDocker = true) +public class MainTest { + private static WebClient client; + private static WebServer server; + private static Http1Client zipkinClient; + + @Container + private static final GenericContainer container = new GenericContainer<>("openzipkin/zipkin") + .withExposedPorts(9411) + .waitingFor(Wait.forHttp("/health").forPort(9411)); + + @BeforeAll + static void checkContainer() { + server = Main.setupServer(WebServer.builder(), container.getMappedPort(9411)); + client = WebClient.create(config -> config.baseUri("http://localhost:" + server.port()) + .addMediaSupport(JsonpSupport.create())); + zipkinClient = Http1Client.create(config -> config + .baseUri("http://localhost:" + container.getMappedPort(9411))); + } + + @AfterAll + static void close() { + if (server != null) { + server.stop(); + } + } + + @Test + void test() { + try (Http1ClientResponse response = zipkinClient.get("/zipkin/api/v2/traces").request()) { + JsonArray array = response.as(JsonArray.class); + assertThat(response.status(), is(Status.OK_200)); + assertThat(array.isEmpty(), is(true)); + } + + try (HttpClientResponse response = client.get("test").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.as(String.class), is("Hello World!")); + } + + assertThatWithRetry("Traces must contains service name", + MainTest::getZipkinTraces, containsString("demo-first")); + assertThatWithRetry("Traces must contains pinged endpoint", + MainTest::getZipkinTraces, containsString(client.get("test").uri().toString())); + } + + private static String getZipkinTraces() { + try (Http1ClientResponse response = zipkinClient.get("/zipkin/api/v2/traces").request()) { + assertThat(response.status(), is(Status.OK_200)); + return response.as(String.class); + } + } +} diff --git a/examples/webserver/pom.xml b/examples/webserver/pom.xml new file mode 100644 index 000000000..1986c6522 --- /dev/null +++ b/examples/webserver/pom.xml @@ -0,0 +1,54 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-project + 1.0.0-SNAPSHOT + Helidon Examples WebServer + pom + + + basic + basics + comment-aas + echo + fault-tolerance + imperative + multiport + mutual-tls + observe + opentracing + protocols + static-content + streaming + tls + tracing + tutorial + websocket + + diff --git a/examples/webserver/protocols/pom.xml b/examples/webserver/protocols/pom.xml new file mode 100644 index 000000000..3b3c34b6b --- /dev/null +++ b/examples/webserver/protocols/pom.xml @@ -0,0 +1,114 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.webserver + helidon-examples-webserver-protocols + 1.0.0-SNAPSHOT + Helidon Examples WebServer Protocols + + + io.helidon.examples.webserver.protocols.ProtocolsMain + + + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-grpc + + + io.helidon.webclient + helidon-webclient-http2 + + + io.helidon.webserver + helidon-webserver-websocket + + + io.helidon.webserver + helidon-webserver-http2 + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + + + compile + compile-custom + + + + + com.google.protobuf:protoc:${version.lib.google-protobuf}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${version.lib.grpc}:exe:${os.detected.classifier} + + + + + + diff --git a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java new file mode 100644 index 000000000..07e7c5adf --- /dev/null +++ b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.protocols; + +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.pki.Keys; +import io.helidon.common.tls.Tls; +import io.helidon.examples.grpc.strings.Strings; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.grpc.GrpcRouting; +import io.helidon.webserver.http1.Http1Route; +import io.helidon.webserver.http2.Http2Route; +import io.helidon.webserver.websocket.WsRouting; +import io.helidon.websocket.WsListener; +import io.helidon.websocket.WsSession; + +import io.grpc.stub.StreamObserver; + +import static io.helidon.http.Method.GET; +import static io.helidon.webserver.grpc.ResponseHelper.complete; + +/** + * Example showing supported protocols. + */ +public class ProtocolsMain { + private static final byte[] RESPONSE = "Hello from WebServer!".getBytes(StandardCharsets.UTF_8); + + private ProtocolsMain() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + Keys privateKeyConfig = privateKey(); + + Tls tls = Tls.builder() + .privateKey(privateKeyConfig.privateKey().get()) + .privateKeyCertChain(privateKeyConfig.certChain()) + .build(); + + WebServer.builder() + .port(8080) + .host("127.0.0.1") + .putSocket("https", + builder -> builder.port(8081) + .host("127.0.0.1") + .tls(tls) + .receiveBufferSize(4096) + .backlog(8192) + ) + .routing(router -> router + .get("/", (req, res) -> res.send(RESPONSE)) + .route(Http1Route.route(GET, "/versionspecific", (req, res) -> res.send("HTTP/1.1 route"))) + .route(Http2Route.route(GET, "/versionspecific", (req, res) -> res.send("HTTP/2 route")))) + .addRouting(GrpcRouting.builder() + .unary(Strings.getDescriptor(), + "StringService", + "Upper", + ProtocolsMain::grpcUpper)) + .addRouting(WsRouting.builder() + .endpoint("/tyrus/echo", ProtocolsMain::wsEcho)) + .build() + .start(); + } + + private static void grpcUpper(Strings.StringMessage request, StreamObserver observer) { + String requestText = request.getText(); + System.out.println("grpc request: " + requestText); + complete(observer, Strings.StringMessage.newBuilder() + .setText(requestText.toUpperCase(Locale.ROOT)) + .build()); + } + + private static WsListener wsEcho() { + return new WsListener() { + @Override + public void onMessage(WsSession session, String text, boolean last) { + session.send(text, last); + System.out.println("websocket request " + text); + } + + @Override + public void onClose(WsSession session, int status, String reason) { + System.out.println("websocket closed (" + status + " " + reason + ")"); + } + }; + } + + private static Keys privateKey() { + String password = "helidon"; + + return Keys.builder() + .keystore(keystore -> keystore + .keystore(Resource.create("certificate.p12")) + .passphrase(password)) + .build(); + } +} diff --git a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/package-info.java b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/package-info.java new file mode 100644 index 000000000..a8fe5174b --- /dev/null +++ b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Various supported protocols example. + */ +package io.helidon.examples.webserver.protocols; diff --git a/examples/webserver/protocols/src/main/proto/events.proto b/examples/webserver/protocols/src/main/proto/events.proto new file mode 100644 index 000000000..2ec3af772 --- /dev/null +++ b/examples/webserver/protocols/src/main/proto/events.proto @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +syntax = "proto3"; +option java_package = "io.helidon.webserver.grpc.events"; + +import "google/protobuf/empty.proto"; + +service EventService { + rpc Send (Message) returns (google.protobuf.Empty) {} + rpc Events (stream EventRequest) returns (stream EventResponse) {} +} + +message Message { + string text = 2; +} + +message EventRequest { + int64 id = 1; + enum Action { + SUBSCRIBE = 0; + UNSUBSCRIBE = 1; + } + Action action = 2; +} + +message EventResponse { + oneof response_type { + Subscribed subscribed = 1; + Unsubscribed unsubscribed = 2; + Event event = 3; + } +} + +message Subscribed { + int64 id = 1; +} + +message Unsubscribed { + int64 id = 1; +} + +message Event { + int64 id = 1; + string text = 2; +} diff --git a/examples/webserver/protocols/src/main/proto/strings.proto b/examples/webserver/protocols/src/main/proto/strings.proto new file mode 100644 index 000000000..70691545d --- /dev/null +++ b/examples/webserver/protocols/src/main/proto/strings.proto @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +syntax = "proto3"; +option java_package = "io.helidon.examples.grpc.strings"; + +service StringService { + rpc Upper (StringMessage) returns (StringMessage) {} + rpc Lower (StringMessage) returns (StringMessage) {} + rpc Split (StringMessage) returns (stream StringMessage) {} + rpc Join (stream StringMessage) returns (StringMessage) {} + rpc Echo (stream StringMessage) returns (stream StringMessage) {} +} + +message StringMessage { + string text = 1; +} diff --git a/examples/webserver/protocols/src/main/resources/certificate.p12 b/examples/webserver/protocols/src/main/resources/certificate.p12 new file mode 100644 index 000000000..b2cb83427 Binary files /dev/null and b/examples/webserver/protocols/src/main/resources/certificate.p12 differ diff --git a/examples/webserver/protocols/src/main/resources/logging.properties b/examples/webserver/protocols/src/main/resources/logging.properties new file mode 100644 index 000000000..161f6db0d --- /dev/null +++ b/examples/webserver/protocols/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.webserver.level=INFO diff --git a/examples/webserver/static-content/README.md b/examples/webserver/static-content/README.md new file mode 100644 index 000000000..45b6395a1 --- /dev/null +++ b/examples/webserver/static-content/README.md @@ -0,0 +1,13 @@ +# Static Content Example + +This application demonstrates use of the StaticContentService to serve static files + together with a simple REST service. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-webserver-static-content.jar +``` + +Open http://localhost:8080 in your browser. diff --git a/examples/webserver/static-content/pom.xml b/examples/webserver/static-content/pom.xml new file mode 100644 index 000000000..8ea9e7ad2 --- /dev/null +++ b/examples/webserver/static-content/pom.xml @@ -0,0 +1,86 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-static-content + 1.0.0-SNAPSHOT + Helidon Examples WebServer Static Content + + + Application demonstrates combination of the static content with a simple REST API. It counts accesses and display it + on the WEB page. + + + + io.helidon.examples.webserver.staticcontent.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-static-content + + + io.helidon.http.media + helidon-http-media-jsonp + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/static-content/src/main/java/io/helidon/examples/webserver/staticcontent/CounterService.java b/examples/webserver/static-content/src/main/java/io/helidon/examples/webserver/staticcontent/CounterService.java new file mode 100644 index 000000000..8a6887efd --- /dev/null +++ b/examples/webserver/static-content/src/main/java/io/helidon/examples/webserver/staticcontent/CounterService.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.staticcontent; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.LongAdder; + +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.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; + +/** + * Counts access to the WEB service. + */ +public class CounterService implements HttpService { + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + private final LongAdder allAccessCounter = new LongAdder(); + private final AtomicInteger apiAccessCounter = new AtomicInteger(); + + @Override + public void routing(HttpRules rules) { + rules.any(this::handleAny) + .get("/api/counter", this::handleGet); + } + + private void handleAny(ServerRequest request, ServerResponse response) { + allAccessCounter.increment(); + response.next(); + } + + private void handleGet(ServerRequest request, ServerResponse response) { + int apiAcc = apiAccessCounter.incrementAndGet(); + JsonObject result = JSON.createObjectBuilder() + .add("all", allAccessCounter.longValue()) + .add("api", apiAcc) + .build(); + response.send(result); + } +} diff --git a/examples/webserver/static-content/src/main/java/io/helidon/examples/webserver/staticcontent/Main.java b/examples/webserver/static-content/src/main/java/io/helidon/examples/webserver/staticcontent/Main.java new file mode 100644 index 000000000..c278a7207 --- /dev/null +++ b/examples/webserver/static-content/src/main/java/io/helidon/examples/webserver/staticcontent/Main.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.staticcontent; + +import io.helidon.http.Header; +import io.helidon.http.HeaderNames; +import io.helidon.http.HeaderValues; +import io.helidon.http.Status; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.staticcontent.StaticContentService; + +/** + * The application main class. + */ +public final class Main { + private static final Header UI_REDIRECT = HeaderValues.createCached(HeaderNames.LOCATION, "/ui"); + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + // load logging configuration + LogConfig.configureRuntime(); + + WebServer server = WebServer.builder() + .port(8080) + .routing(Main::routing) + .build() + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); + } + + /** + * Updates HTTP Routing. + */ + static void routing(HttpRouting.Builder routing) { + routing.any("/", (req, res) -> { + // showing the capability to run on any path, and redirecting from root + res.status(Status.MOVED_PERMANENTLY_301); + res.headers().set(UI_REDIRECT); + res.send(); + }) + .register("/ui", CounterService::new) + .register("/ui", StaticContentService.builder("WEB") + .welcomeFileName("index.html") + .build()); + } +} diff --git a/examples/webserver/static-content/src/main/java/io/helidon/examples/webserver/staticcontent/package-info.java b/examples/webserver/static-content/src/main/java/io/helidon/examples/webserver/staticcontent/package-info.java new file mode 100644 index 000000000..f401ed62d --- /dev/null +++ b/examples/webserver/static-content/src/main/java/io/helidon/examples/webserver/staticcontent/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Application demonstrates combination of the static content with a simple REST API. It counts accesses and display it + * on the WEB page. + *

    + * Start with {@link io.helidon.examples.webserver.staticcontent.Main} class. + * + * @see io.helidon.examples.webserver.staticcontent.Main + */ +package io.helidon.examples.webserver.staticcontent; diff --git a/examples/webserver/static-content/src/main/resources/WEB/css/app.css b/examples/webserver/static-content/src/main/resources/WEB/css/app.css new file mode 100644 index 000000000..028dee61d --- /dev/null +++ b/examples/webserver/static-content/src/main/resources/WEB/css/app.css @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +body { + padding-top: 2rem; +} diff --git a/examples/webserver/static-content/src/main/resources/WEB/index.html b/examples/webserver/static-content/src/main/resources/WEB/index.html new file mode 100644 index 000000000..413268e45 --- /dev/null +++ b/examples/webserver/static-content/src/main/resources/WEB/index.html @@ -0,0 +1,88 @@ + + + + + + + + + + Static Content Example + + + + + + + +

    + + +
    +
    +

    Hello!

    +

    +
    +
    + +
    + +
    +
    +

    Heading

    +

    Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.

    +

    View details »

    +
    +
    +

    Heading

    +

    Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.

    +

    View details »

    +
    +
    +

    Heading

    +

    Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

    +

    View details »

    +
    +
    + +
    + +
    +

    © Oracle 2017

    +
    +
    + + + + + + + diff --git a/examples/webserver/static-content/src/main/resources/WEB/js/app.js b/examples/webserver/static-content/src/main/resources/WEB/js/app.js new file mode 100644 index 000000000..f132a7087 --- /dev/null +++ b/examples/webserver/static-content/src/main/resources/WEB/js/app.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +$(document).ready(function() { + $.get("api/counter", function (data) { + $("#hello-message").html("This page was reloaded " + data.api + " times. " + + "This WebServer already served " + data.all + " requests."); + }); +}); diff --git a/examples/webserver/static-content/src/test/java/io/helidon/examples/webserver/staticcontent/MainTest.java b/examples/webserver/static-content/src/test/java/io/helidon/examples/webserver/staticcontent/MainTest.java new file mode 100644 index 000000000..4791580d6 --- /dev/null +++ b/examples/webserver/static-content/src/test/java/io/helidon/examples/webserver/staticcontent/MainTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.staticcontent; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import jakarta.json.JsonNumber; +import jakarta.json.JsonObject; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class MainTest { + + private final Http1Client client; + + protected MainTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.routing(builder); + } + + @Test + void testUi() { + assertThat(allCounter(), is(1)); + try (Http1ClientResponse response = client.get("/ui/index.html").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.headers().contentType().orElseThrow().text(), is("text/html")); + } + try (Http1ClientResponse response = client.get("/ui/css/app.css").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.headers().contentType().orElseThrow().text(), is("text/css")); + } + try (Http1ClientResponse response = client.get("/ui/js/app.js").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.headers().contentType().orElseThrow().text(), is("text/javascript")); + } + assertThat(allCounter(), is(5)); // includes /ui/api/counter calls + } + + private int allCounter() { + try (Http1ClientResponse response = client.get("/ui/api/counter").request()) { + assertThat(response.status(), is(Status.OK_200)); + JsonNumber number = (JsonNumber) response.as(JsonObject.class).get("all"); + return number.intValue(); + } + } +} diff --git a/examples/webserver/streaming/README.md b/examples/webserver/streaming/README.md new file mode 100644 index 000000000..29bf45340 --- /dev/null +++ b/examples/webserver/streaming/README.md @@ -0,0 +1,24 @@ +# Streaming Example + +This application is an example of a very simple streaming service. It leverages the +fact that Helidon uses virtual threads to perform simple input/output stream blocking +operations in the endpoint handlers. As a result, this service runs in constant space instead +of proportional to the size of the file being uploaded or downloaded. + +There are two endpoints: + +- `upload` : uploads a file to the service +- `download` : downloads the previously uploaded file + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-webserver-streaming.jar +``` + +Upload a file and download it back with `curl`: +```shell +curl --data-binary "@large-file.bin" http://localhost:8080/upload +curl http://localhost:8080/download --output myfile.bin +``` diff --git a/examples/webserver/streaming/large-file.bin b/examples/webserver/streaming/large-file.bin new file mode 100644 index 000000000..909e3e3e9 Binary files /dev/null and b/examples/webserver/streaming/large-file.bin differ diff --git a/examples/webserver/streaming/pom.xml b/examples/webserver/streaming/pom.xml new file mode 100644 index 000000000..c9e7bac75 --- /dev/null +++ b/examples/webserver/streaming/pom.xml @@ -0,0 +1,88 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-streaming + 1.0.0-SNAPSHOT + Helidon Examples WebServer Streaming + + + Application that demonstrates how to write a service that uploads and downloads + a file using chunks, in a streaming manner. + + + + io.helidon.examples.webserver.streaming.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.webclient + helidon-webclient + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/Main.java b/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/Main.java new file mode 100644 index 000000000..dc587a7e6 --- /dev/null +++ b/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/Main.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.streaming; + +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * Class Main. Entry point to streaming application. + */ +public class Main { + + private Main() { + } + + /** + * Setup {@link HttpRouting}. + */ + static void routing(HttpRouting.Builder routing) { + routing.register(new StreamingService()); + } + + /** + * A java main class. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + WebServerConfig.Builder builder = WebServer.builder().port(8080); + builder.routing(Main::routing); + WebServer server = builder.build().start(); + System.out.println("Steaming service is up at http://localhost:" + server.port()); + } +} diff --git a/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/StreamingService.java b/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/StreamingService.java new file mode 100644 index 000000000..81c83523d --- /dev/null +++ b/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/StreamingService.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.streaming; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.logging.Logger; + +import io.helidon.http.Status; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * StreamingService class. + * Uses a {@link java.io.InputStream} and {@link java.io.OutputStream} for uploading and downloading files. + */ +public class StreamingService implements HttpService { + private static final Logger LOGGER = Logger.getLogger(StreamingService.class.getName()); + + // Last file uploaded (or default). Since we don't do any locking + // when operating on the file this example is not safe for concurrent requests. + private volatile Path filePath; + + StreamingService() { + } + + @Override + public void routing(HttpRules rules) { + rules.get("/download", this::download) + .post("/upload", this::upload); + } + + private void upload(ServerRequest request, ServerResponse response) { + LOGGER.info("Entering upload ... " + Thread.currentThread()); + try { + Path tempFilePath = Files.createTempFile("large-file", ".tmp"); + Files.copy(request.content().inputStream(), tempFilePath, StandardCopyOption.REPLACE_EXISTING); + filePath = tempFilePath; + response.send("File was stored as " + tempFilePath); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + LOGGER.info("Exiting upload after uploading " + filePath.toFile().length() + " bytes..."); + } + + private void download(ServerRequest request, ServerResponse response) { + LOGGER.info("Entering download ..." + Thread.currentThread()); + if (filePath == null) { + response.status(Status.BAD_REQUEST_400).send("No file to download. Please upload file first."); + return; + } + long length = filePath.toFile().length(); + response.headers().contentLength(length); + try { + Files.copy(filePath, response.outputStream()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + LOGGER.info("Exiting download after serving " + length + " bytes..."); + } +} + diff --git a/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/package-info.java b/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/package-info.java new file mode 100644 index 000000000..3e18160eb --- /dev/null +++ b/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + *

    + * Start with {@link io.helidon.examples.webserver.streaming.Main} class. + * + * @see io.helidon.examples.webserver.streaming.Main + */ +package io.helidon.examples.webserver.streaming; diff --git a/examples/webserver/streaming/src/test/java/io/helidon/examples/webserver/streaming/MainTest.java b/examples/webserver/streaming/src/test/java/io/helidon/examples/webserver/streaming/MainTest.java new file mode 100644 index 000000000..50ada67eb --- /dev/null +++ b/examples/webserver/streaming/src/test/java/io/helidon/examples/webserver/streaming/MainTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.streaming; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MainTest { + + private final Http1Client client; + + private final String TEST_DATA_1 = "Test Data 1"; + private final String TEST_DATA_2 = "Test Data 2"; + + protected MainTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.routing(builder); + } + + @Test + @Order(0) + void testBadRequest() { + try (Http1ClientResponse response = client.get("/download").request()) { + assertThat(response.status(), is(Status.BAD_REQUEST_400)); + } + } + + @Test + @Order(1) + void testUpload1() { + try (Http1ClientResponse response = client.post("/upload").submit(TEST_DATA_1)) { + assertThat(response.status(), is(Status.OK_200)); + } + } + + @Test + @Order(2) + void testDownload1() { + try (Http1ClientResponse response = client.get("/download").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.as(String.class), is(TEST_DATA_1)); + } + } + + @Test + @Order(3) + void testUpload2() { + try (Http1ClientResponse response = client.post("/upload").submit(TEST_DATA_2)) { + assertThat(response.status(), is(Status.OK_200)); + } + } + + @Test + @Order(4) + void testDownload2() { + try (Http1ClientResponse response = client.get("/download").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.as(String.class), is(TEST_DATA_2)); + } + } +} diff --git a/examples/webserver/threads/README.md b/examples/webserver/threads/README.md new file mode 100644 index 000000000..303946010 --- /dev/null +++ b/examples/webserver/threads/README.md @@ -0,0 +1,98 @@ +# Helidon SE Threading Example + +Helidon's adoption of virtual threads has eliminated a lot of the headaches +of thread pools and thread pool tuning. But there are still cases where using +application specific executors is desirable. This example illustrates two +such cases: + +1. Using a virtual thread executor to execute multiple tasks in parallel. +2. Using a platform thread executor to execute long-running CPU intensive operations. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-webserver-threads.jar +``` + +## Exercise the application + +__Compute:__ +```shell +curl -X GET http://localhost:8080/thread/compute/5 +``` +The `compute` endpoint runs a costly floating point computation using a platform thread. +Increase the number to make the computation more costly (and take longer). + +The request returns the results of the computation (not important!). + +__Fanout:__ +```shell +curl -X GET http://localhost:8080/thread/fanout/5 +``` +The `fanout` endpoint simulates a fanout of remote calls that are run in parallel using +virtual threads. Each remote call invokes the server's `sleep` endpoint sleeping anywhere from +0 to 4 seconds. Since the remote requests are executed in parallel the curl request should not +take longer than 4 seconds to return. Increase the number to have more remote calls made +in parallel. + +The request returns a list of numbers showing the sleep value of each remote client call. + +__Sleep:__ +```shell +curl -X GET http://localhost:8080/thread/sleep/4 +``` +This is a simple endpoint that just sleeps for the specified number of seconds. It is +used by the `fanout` endpoint. + +The request returns the number of seconds requested to sleep. + +## Further Discussion + +### Use Case 1: Virtual Threads: Executing Tasks in Parallel + +Sometimes an endpoint needs to perform multiple blocking operations in parallel: +querying a database, calling another service, etc. Virtual threads are a +good fit for this because they are lightweight and do not consume platform +threads when performing blocking operations (like network I/O). + +The `fanout` endpoint in this example demonstrates this use case. You pass the endpoint +the number of parallel tasks to execute and it simulates remote client calls by using +the Helidon WebClient to call the `sleep` endpoint on the server. + +### Use Case 2: Platform Threads: Executing a CPU Intensive Task + +If you have an endpoint that performs an in-memory, CPU intensive task, then +platform threads might be a better match. This is because a virtual thread would be pinned to +a platform thread throughout the computation -- potentially causing unbounded consumption +of platform threads. Instead, the example uses a small, bounded pool of platform +threads to perform computations. Bounded meaning that the number of threads and the +size of the work queue are both limited and will reject work when they fill up. +This gives the application tight control over the resources allocated to these CPU intensive tasks. + +The `compute` endpoint in this example demonstrates this use case. You pass the endpoint +the number of times you want to make the computation, and it uses a small bounded pool +of platform threads to execute the task. + +### Use of Helidon's ThreadPoolSupplier and Configuration + +This example uses `io.helidon.common.configurable.ThreadPoolSupplier` to create the +two executors used in the example. This provides a few benefits: + +1. ThreadPoolSupplier supports a number of tuning parameters that enable us to configure a small, bounded threadpool. +2. You can drive the thread pool configuration via Helidon config -- see this example's `application.yaml` +3. You get propagation of Helidon's Context which supports Helidon's features as well as direct use by the application. + +### Logging + +In `logging.properties` the log level for `io.helidon.common.configurable.ThreadPool` +is increased so that you can see the values used to configure the platform thread pool. +When you start the application you will see a line like +``` +ThreadPool 'application-platform-executor-thread-pool-1' {corePoolSize=1, maxPoolSize=2, + queueCapacity=10, growthThreshold=1000, growthRate=0%, averageQueueSize=0.00, peakQueueSize=0, averageActiveThreads=0.00, peakPoolSize=0, currentPoolSize=0, completedTasks=0, failedTasks=0, rejectedTasks=0} +``` +This reflects the configuration of the platform thread pool created by the application +and used by the `compute` endpoint. At most the thread pool will consume two platform +threads for computations. The work queue is limited to 10 entries to allow for small +bursts of requests. diff --git a/examples/webserver/threads/pom.xml b/examples/webserver/threads/pom.xml new file mode 100644 index 000000000..f194ac9eb --- /dev/null +++ b/examples/webserver/threads/pom.xml @@ -0,0 +1,84 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-threads + 1.0.0-SNAPSHOT + + + io.helidon.examples.webserver.threads.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webclient + helidon-webclient + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/Main.java b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/Main.java new file mode 100644 index 000000000..41e008633 --- /dev/null +++ b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/Main.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.threads; + +import io.helidon.logging.common.LogConfig; +import io.helidon.config.Config; +import io.helidon.webclient.api.WebClient; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; + +/** + * The application main class. + */ +public class Main { + + static WebClient webclient; + + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * @param args command line arguments. + */ + public static void main(String[] args) { + + // load logging configuration + LogConfig.configureRuntime(); + + // initialize global config from default configuration + Config config = Config.create(); + Config.global(config); + + WebServer webserver = WebServer.builder() + .config(config.get("server")) + .routing(Main::routing) + .build() + .start(); + + // Construct webclient here using port of running server + webclient = WebClient.builder() + .baseUri("http://localhost:" + webserver.port() + "/thread") + .build(); + + System.out.println("WEB server is up! http://localhost:" + webserver.port() + "/thread"); + } + + + /** + * Updates HTTP Routing. + */ + static void routing(HttpRouting.Builder routing) { + routing + .register("/thread", new ThreadService()); + } +} \ No newline at end of file diff --git a/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/ThreadService.java b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/ThreadService.java new file mode 100644 index 000000000..60ece4b13 --- /dev/null +++ b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/ThreadService.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.threads; + +import java.lang.System.Logger.Level; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; + +import io.helidon.common.configurable.ThreadPoolSupplier; +import io.helidon.config.Config; +import io.helidon.http.Status; +import io.helidon.webclient.api.ClientResponseTyped; +import io.helidon.webclient.api.WebClient; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +class ThreadService implements HttpService { + + private static final System.Logger LOGGER = System.getLogger(ThreadService.class.getName()); + private static final Random rand = new Random(System.currentTimeMillis()); + + // ThreadPool of platform threads. + private static ExecutorService platformExecutorService; + // Executor of virtual threads. + private static ExecutorService virtualExecutorService; + + /** + * The config value for the key {@code greeting}. + */ + + ThreadService() { + this(Config.global().get("app")); + } + + ThreadService(Config appConfig) { + /* + * We create two executor services. One is a thread pool of platform threads. + * The second is a virtual thread executor service. + * See `application.yaml` for configuration of each of these. + */ + ThreadPoolSupplier platformThreadSupplier = ThreadPoolSupplier.builder() + .config(appConfig.get("application-platform-executor")) + .build(); + platformExecutorService = platformThreadSupplier.get(); + + ThreadPoolSupplier virtualThreadSupplier = ThreadPoolSupplier.builder() + .config(appConfig.get("application-virtual-executor")) + .build(); + virtualExecutorService = virtualThreadSupplier.get(); + } + + @Override + public void routing(HttpRules rules) { + rules + .get("/compute", this::computeHandler) + .get("/compute/{iterations}", this::computeHandler) + .get("/fanout", this::fanOutHandler) + .get("/fanout/{count}", this::fanOutHandler) + .get("/sleep", this::sleepHandler) + .get("/sleep/{seconds}", this::sleepHandler); + } + + /** + * Perform a CPU intensive operation. + * The optional path parameter controls the number of iterations of the computation. The more + * iterations the longer it will take. + * + * @param request server request + * @param response server response + */ + private void computeHandler(ServerRequest request, ServerResponse response) { + int iterations = request.path().pathParameters().first("iterations").asInt().orElse(1); + try { + // We execute the computation on a platform thread. This prevents unbounded obstruction of virtual + // threads, plus provides us the ability to limit the number of concurrent computation requests + // we handle by limiting the thread pool work queue length (as defined in application.yaml) + Future future = platformExecutorService.submit(() -> compute(iterations)); + response.send(future.get().toString()); + } catch (RejectedExecutionException e) { + // Work queue is full! We reject the request + LOGGER.log(Level.WARNING, e); + response.status(Status.SERVICE_UNAVAILABLE_503).send("Server busy"); + } catch (ExecutionException | InterruptedException e) { + LOGGER.log(Level.ERROR, e); + response.status(Status.INTERNAL_SERVER_ERROR_500).send(); + } + } + + /** + * Sleep for a specified number of seconds. + * The optional path parameter controls the number of seconds to sleep. Defaults to 1 + * + * @param request server request + * @param response server response + */ + private void sleepHandler(ServerRequest request, ServerResponse response) { + int seconds = request.path().pathParameters().first("seconds").asInt().orElse(1); + response.send(String.valueOf(sleep(seconds))); + } + + /** + * Fan out a number of remote requests in parallel. + * The optional path parameter controls the number of parallel requests to make. + * + * @param request server request + * @param response server response + */ + private void fanOutHandler(ServerRequest request, ServerResponse response) { + int count = request.path().pathParameters().first("count").asInt().orElse(1); + LOGGER.log(Level.INFO, "Fanning out " + count + " parallel requests"); + // We simulate multiple client requests running in parallel by calling our sleep endpoint. + try { + // For this we use our virtual thread based executor. We submit the work and save the Futures + var futures = new ArrayList>(); + for (int i = 0; i < count; i++) { + futures.add(virtualExecutorService.submit(() -> callRemote(rand.nextInt(5)))); + } + + // After work has been submitted we loop through the future and block getting the results. + // We aggregate the results in a list of Strings + var responses = new ArrayList(); + for (var future : futures) { + try { + responses.add(future.get()); + } catch (InterruptedException e) { + responses.add(e.getMessage()); + } + } + + // All parallel calls are complete! + response.send(String.join(":", responses)); + } catch (ExecutionException e) { + LOGGER.log(Level.ERROR, e); + response.status(Status.INTERNAL_SERVER_ERROR_500).send(); + } + } + + /** + * Simulate a remote client call by calling this server's sleep endpoint + * + * @param seconds number of seconds the endpoint should sleep. + * @return string response from client + */ + private String callRemote(int seconds) { + LOGGER.log(Level.INFO, Thread.currentThread() + ": Calling remote sleep for " + seconds + "s"); + WebClient client = Main.webclient; + ClientResponseTyped response = client.get("/sleep/" + seconds).request(String.class); + if (response.status().equals(Status.OK_200)) { + return response.entity(); + } + return response.status().toString(); + } + + /** + * Sleep current thread + * + * @param seconds number of seconds to sleep + * @return number of seconds requested to sleep + */ + private int sleep(int seconds) { + try { + Thread.sleep(seconds * 1_000L); + } catch (InterruptedException e) { + LOGGER.log(Level.WARNING, e); + } + return seconds; + } + + /** + * Perform a CPU intensive computation + * + * @param iterations: number of times to perform computation + * @return result of computation + */ + private double compute(int iterations) { + LOGGER.log(Level.INFO, Thread.currentThread() + ": Computing with " + iterations + " iterations"); + double d = 123456789.123456789 * rand.nextInt(100); + for (int i = 0; i < iterations; i++) { + for (int n = 0; n < 1_000_000; n++) { + for (int j = 0; j < 5; j++) { + d = Math.tan(d); + d = Math.atan(d); + } + } + } + return d; + } +} diff --git a/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/package-info.java b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/package-info.java new file mode 100644 index 000000000..f7dfc7e8a --- /dev/null +++ b/examples/webserver/threads/src/main/java/io/helidon/examples/webserver/threads/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.threads; diff --git a/examples/webserver/threads/src/main/resources/application.yaml b/examples/webserver/threads/src/main/resources/application.yaml new file mode 100644 index 000000000..05ee0f283 --- /dev/null +++ b/examples/webserver/threads/src/main/resources/application.yaml @@ -0,0 +1,29 @@ +# +# Copyright (c) 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8080 + host: 0.0.0.0 + +app: + application-platform-executor: + thread-name-prefix: "application-platform-executor-" + core-pool-size: 1 + max-pool-size: 2 + queue-capacity: 10 + application-virtual-executor: + thread-name-prefix: "application-virtual-executor-" + virtual-threads: true diff --git a/examples/webserver/threads/src/main/resources/logging.properties b/examples/webserver/threads/src/main/resources/logging.properties new file mode 100644 index 000000000..5fddb7f3d --- /dev/null +++ b/examples/webserver/threads/src/main/resources/logging.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO + +io.helidon.common.configurable.ThreadPool.level=ALL diff --git a/examples/webserver/threads/src/test/java/io/helidon/examples/webserver/threads/MainTest.java b/examples/webserver/threads/src/test/java/io/helidon/examples/webserver/threads/MainTest.java new file mode 100644 index 000000000..bbe78c10f --- /dev/null +++ b/examples/webserver/threads/src/test/java/io/helidon/examples/webserver/threads/MainTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.threads; + +import io.helidon.http.Status; +import io.helidon.webclient.api.HttpClientResponse; +import io.helidon.webclient.api.WebClient; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class MainTest { + private final WebClient client; + + protected MainTest(WebClient client) { + this.client = client; + Main.webclient = this.client; // Needed for ThreadService to make calls + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.routing(builder); + } + + @Test + void testFanOut() { + try (HttpClientResponse response = client.get("/thread/fanout/2").request()) { + assertThat(response.status(), is(Status.OK_200)); + } + } + + @Test + void testCompute() { + try (HttpClientResponse response = client.get("/thread/compute").request()) { + assertThat(response.status(), is(Status.OK_200)); + } + } +} \ No newline at end of file diff --git a/examples/webserver/tls/pom.xml b/examples/webserver/tls/pom.xml new file mode 100644 index 000000000..7831001e7 --- /dev/null +++ b/examples/webserver/tls/pom.xml @@ -0,0 +1,86 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.webserver + helidon-examples-webserver-tls + 1.0.0-SNAPSHOT + Helidon Examples WebServer TLS + + + Application demonstrates TLS configuration using a builder + and config. + + + + io.helidon.examples.webserver.tls.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/tls/src/main/java/io/helidon/examples/webserver/tls/Main.java b/examples/webserver/tls/src/main/java/io/helidon/examples/webserver/tls/Main.java new file mode 100644 index 000000000..88660353f --- /dev/null +++ b/examples/webserver/tls/src/main/java/io/helidon/examples/webserver/tls/Main.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tls; + +import io.helidon.common.configurable.Resource; +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * Main class of TLS example. + */ +public final class Main { + + // utility class + private Main() { + } + + /** + * Start the example. + * This will start two Helidon WebServers, both protected by TLS - one configured from config, one using a builder. + * Port of the servers will be configured from config, to be able to switch to an ephemeral port for tests. + * + * @param args start arguments are ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + Config config = Config.create(); + + WebServerConfig.Builder builder1 = WebServer.builder(); + setupConfigBased(builder1, config); + WebServer server1 = builder1.build().start(); + System.out.println("Started config based WebServer on https://localhost:" + server1.port()); + + WebServerConfig.Builder builder2 = WebServer.builder(); + setupBuilderBased(builder2); + WebServer server2 = builder2.build().start(); + System.out.println("Started builder based WebServer on http://localhost:" + server2.port()); + } + + static void setupBuilderBased(WebServerConfig.Builder server) { + server.routing(Main::routing) + .tls(tls -> tls + .privateKey(key -> key + .keystore(store -> store + .passphrase("changeit") + .keystore(Resource.create("server.p12")))) + .privateKeyCertChain(key -> key + .keystore(store -> store + .passphrase("changeit") + .keystore(Resource.create("server.p12"))))); + } + + static void setupConfigBased(WebServerConfig.Builder server, Config config) { + server.config(config).routing(Main::routing); + } + + static void routing(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> res.send("Hello!")); + } +} diff --git a/examples/webserver/tls/src/main/java/io/helidon/examples/webserver/tls/package-info.java b/examples/webserver/tls/src/main/java/io/helidon/examples/webserver/tls/package-info.java new file mode 100644 index 000000000..c187e3c51 --- /dev/null +++ b/examples/webserver/tls/src/main/java/io/helidon/examples/webserver/tls/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of TLS configuration for webserver, using both {@link io.helidon.config.Config} and builder based approach. + */ +package io.helidon.examples.webserver.tls; diff --git a/examples/webserver/tls/src/main/resources/application.yaml b/examples/webserver/tls/src/main/resources/application.yaml new file mode 100644 index 000000000..dcbd77028 --- /dev/null +++ b/examples/webserver/tls/src/main/resources/application.yaml @@ -0,0 +1,27 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +config-based: + port: 8080 + tls: + private-key: + keystore: + passphrase: "changeit" + resource: + resource-path: "server.p12" + +builder-based: + port: 8081 diff --git a/examples/webserver/tls/src/main/resources/logging.properties b/examples/webserver/tls/src/main/resources/logging.properties new file mode 100644 index 000000000..c9eacb5c4 --- /dev/null +++ b/examples/webserver/tls/src/main/resources/logging.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO diff --git a/examples/webserver/tls/src/main/resources/server.p12 b/examples/webserver/tls/src/main/resources/server.p12 new file mode 100644 index 000000000..a692efcf9 Binary files /dev/null and b/examples/webserver/tls/src/main/resources/server.p12 differ diff --git a/examples/webserver/tls/src/test/java/io/helidon/examples/webserver/tls/BuilderBasedTest.java b/examples/webserver/tls/src/test/java/io/helidon/examples/webserver/tls/BuilderBasedTest.java new file mode 100644 index 000000000..fe107210a --- /dev/null +++ b/examples/webserver/tls/src/test/java/io/helidon/examples/webserver/tls/BuilderBasedTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tls; + +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; + +class BuilderBasedTest extends TestBase { + + BuilderBasedTest(WebServer server) { + super(server); + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + Main.setupBuilderBased(server); + } +} diff --git a/examples/webserver/tls/src/test/java/io/helidon/examples/webserver/tls/ConfigBasedTest.java b/examples/webserver/tls/src/test/java/io/helidon/examples/webserver/tls/ConfigBasedTest.java new file mode 100644 index 000000000..3a2a94667 --- /dev/null +++ b/examples/webserver/tls/src/test/java/io/helidon/examples/webserver/tls/ConfigBasedTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tls; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; + +class ConfigBasedTest extends TestBase { + + ConfigBasedTest(WebServer server) { + super(server); + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + Config config = Config.create(ConfigSources.classpath("test-application.yaml"), + ConfigSources.classpath("application.yaml")); + Main.setupConfigBased(server, config.get("config-based")); + } +} diff --git a/examples/webserver/tls/src/test/java/io/helidon/examples/webserver/tls/TestBase.java b/examples/webserver/tls/src/test/java/io/helidon/examples/webserver/tls/TestBase.java new file mode 100644 index 000000000..1500baef7 --- /dev/null +++ b/examples/webserver/tls/src/test/java/io/helidon/examples/webserver/tls/TestBase.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tls; + +import io.helidon.common.tls.Tls; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webserver.WebServer; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +abstract class TestBase { + + private final Http1Client client; + + TestBase(WebServer server) { + this.client = Http1Client.builder() + .baseUri("https://localhost:" + server.port()) + .tls(Tls.builder().trustAll(true).build()) + .build(); + } + + @Test + void testSsl() { + assertThat(client.get().requestEntity(String.class), is("Hello!")); + } +} diff --git a/examples/webserver/tls/src/test/resources/test-application.yaml b/examples/webserver/tls/src/test/resources/test-application.yaml new file mode 100644 index 000000000..7a722539a --- /dev/null +++ b/examples/webserver/tls/src/test/resources/test-application.yaml @@ -0,0 +1,19 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +config-based: + # switch to available ephemeral port for tests + port: 0 \ No newline at end of file diff --git a/examples/webserver/tracing/pom.xml b/examples/webserver/tracing/pom.xml new file mode 100644 index 000000000..7d9d3c812 --- /dev/null +++ b/examples/webserver/tracing/pom.xml @@ -0,0 +1,95 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + + io.helidon.examples.webserver + helidon-examples-webserver-tracing + 1.0.0-SNAPSHOT + Helidon Examples WebServer Tracing + + + io.helidon.examples.webserver.tracing.TracingMain + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-http2 + + + io.helidon.webserver.observe + helidon-webserver-observe-tracing + + + io.helidon.webclient + helidon-webclient + + + io.helidon.webclient + helidon-webclient-tracing + + + io.helidon.tracing.providers + helidon-tracing-providers-jaeger + + + io.helidon.config + helidon-config-yaml + + + io.helidon.logging + helidon-logging-jul + runtime + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/tracing/src/main/java/io/helidon/examples/webserver/tracing/TracingMain.java b/examples/webserver/tracing/src/main/java/io/helidon/examples/webserver/tracing/TracingMain.java new file mode 100644 index 000000000..8925d387c --- /dev/null +++ b/examples/webserver/tracing/src/main/java/io/helidon/examples/webserver/tracing/TracingMain.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tracing; + +import io.helidon.logging.common.LogConfig; +import io.helidon.tracing.Span; +import io.helidon.tracing.Tracer; +import io.helidon.tracing.TracerBuilder; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.tracing.WebClientTracing; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.Handler; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import io.helidon.webserver.http1.Http1Route; +import io.helidon.webserver.http2.Http2Route; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.tracing.TracingObserver; + +import static io.helidon.http.Method.GET; + +/** + * Tracing example. + */ +public class TracingMain { + private TracingMain() { + } + + /** + * Main method. + * @param args ignored + */ + public static void main(String[] args) { + LogConfig.configureRuntime(); + + Tracer tracer = TracerBuilder.create("helidon") + .build(); + + WebServer.builder() + .port(8080) + .host("127.0.0.1") + .addFeature(ObserveFeature.builder() + .addObserver(TracingObserver.create(tracer)) + .build()) + .routing(router -> router + .route(Http1Route.route(GET, "/versionspecific", new TracedHandler(tracer, "HTTP/1.1 route"))) + .route(Http2Route.route(GET, "/versionspecific", new TracedHandler(tracer, "HTTP/2 route"))) + .get("/client", new ClientHandler(tracer))) + .build() + .start(); + } + + private static class ClientHandler implements Handler { + private final WebClient client; + + private ClientHandler(Tracer tracer) { + this.client = WebClient.builder() + .baseUri("http://localhost:8080/versionspecific") + .servicesDiscoverServices(false) + .addService(WebClientTracing.create(tracer)) + .build(); + } + + @Override + public void handle(ServerRequest req, ServerResponse res) { + res.send(client.get() + .requestEntity(String.class)); + } + } + + private static class TracedHandler implements Handler { + private final Tracer tracer; + private final String message; + + private TracedHandler(Tracer tracer, String message) { + this.tracer = tracer; + this.message = message; + } + + @Override + public void handle(ServerRequest req, ServerResponse res) { + Span span = tracer.spanBuilder("custom-span") + .start(); + try { + span.addEvent("my nice log"); + res.send(message); + } finally { + span.end(); + } + } + } +} diff --git a/examples/webserver/tracing/src/main/java/io/helidon/examples/webserver/tracing/package-info.java b/examples/webserver/tracing/src/main/java/io/helidon/examples/webserver/tracing/package-info.java new file mode 100644 index 000000000..75dd943de --- /dev/null +++ b/examples/webserver/tracing/src/main/java/io/helidon/examples/webserver/tracing/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tracing example. + */ +package io.helidon.examples.webserver.tracing; diff --git a/examples/webserver/tracing/src/main/resources/application.yaml b/examples/webserver/tracing/src/main/resources/application.yaml new file mode 100644 index 000000000..1e8f796e9 --- /dev/null +++ b/examples/webserver/tracing/src/main/resources/application.yaml @@ -0,0 +1,20 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +tracing: + service: "helidon-server" + sampler-type: "const" + sampler-param: 1 \ No newline at end of file diff --git a/examples/webserver/tracing/src/main/resources/logging.properties b/examples/webserver/tracing/src/main/resources/logging.properties new file mode 100644 index 000000000..161f6db0d --- /dev/null +++ b/examples/webserver/tracing/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2022, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.webserver.level=INFO diff --git a/examples/webserver/tutorial/README.md b/examples/webserver/tutorial/README.md new file mode 100644 index 000000000..89c9e6d6d --- /dev/null +++ b/examples/webserver/tutorial/README.md @@ -0,0 +1,10 @@ +# Tutorial Server + +This application demonstrates various WebServer use cases together and in its complexity. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-webserver-tutorial.jar +``` diff --git a/examples/webserver/tutorial/pom.xml b/examples/webserver/tutorial/pom.xml new file mode 100644 index 000000000..9dadd4ec4 --- /dev/null +++ b/examples/webserver/tutorial/pom.xml @@ -0,0 +1,84 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-tutorial + 1.0.0-SNAPSHOT + Helidon Examples WebServer Tutorial + + + A tutorial documentation server. + It serves various tutorial articles designed based on project examples. + It is also a complex example demonstrating various web server features. + + + + io.helidon.examples.webserver.tutorial.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/Comment.java b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/Comment.java new file mode 100644 index 000000000..5adca4dd4 --- /dev/null +++ b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/Comment.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.tutorial; + +/** + * Represents a single comment. + */ +record Comment(User user, String message) { + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + if (user != null) { + result.append(user.alias()); + } + result.append(": "); + result.append(message); + return result.toString(); + } +} diff --git a/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/CommentService.java b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/CommentService.java new file mode 100644 index 000000000..2dedee5f1 --- /dev/null +++ b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/CommentService.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tutorial; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +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; + +/** + * Basic service for comments. + */ +class CommentService implements HttpService { + + private final ConcurrentHashMap> data = new ConcurrentHashMap<>(); + + @Override + public void routing(HttpRules rules) { + rules.get("/{room-id}", this::getComments) + .post("/{room-id}", this::addComment); + } + + private void getComments(ServerRequest req, ServerResponse resp) { + String roomId = req.path().pathParameters().get("room-id"); + resp.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8); + resp.send(getComments(roomId)); + } + + /** + * Returns all comments for the room or an empty list if room doesn't exist. + * + * @param roomId a room ID + * @return a list of comments + */ + List getComments(String roomId) { + if (roomId == null || roomId.isEmpty()) { + return Collections.emptyList(); + } + + List result = data.get(roomId); + return result == null ? Collections.emptyList() : result; + } + + private void addComment(ServerRequest req, ServerResponse res) { + String roomId = req.path().pathParameters().get("room-id"); + User user = req.context().get(User.class).orElse(User.ANONYMOUS); + String msg = req.content().as(String.class); + addComment(roomId, user, msg); + res.send(); + } + + /** + * Adds new comment into the comment-room. + * + * @param roomName a name of the comment-room + * @param user a user who provides the comment + * @param message a comment message + */ + void addComment(String roomName, User user, String message) { + if (user == null) { + user = User.ANONYMOUS; + } + List comments = data.computeIfAbsent(roomName, k -> Collections.synchronizedList(new ArrayList<>())); + comments.add(new Comment(user, message)); + } + +} diff --git a/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/CommentSupport.java b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/CommentSupport.java new file mode 100644 index 000000000..3ff003c72 --- /dev/null +++ b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/CommentSupport.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.tutorial; + +import java.util.List; + +import io.helidon.common.GenericType; +import io.helidon.http.Headers; +import io.helidon.http.WritableHeaders; +import io.helidon.http.media.MediaSupport; + +class CommentSupport implements MediaSupport { + + private static final GenericType> COMMENT_TYPE = new GenericType<>() {}; + + @Override + @SuppressWarnings("unchecked") + public WriterResponse writer(GenericType type, + Headers requestHeaders, + WritableHeaders responseHeaders) { + if (!COMMENT_TYPE.rawType().isAssignableFrom(type.rawType())) { + return WriterResponse.unsupported(); + } + return (WriterResponse) new WriterResponse<>(SupportLevel.SUPPORTED, CommentWriter::new); + } + + @Override + public String name() { + return "comment"; + } + + @Override + public String type() { + return "comment"; + } +} diff --git a/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/CommentWriter.java b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/CommentWriter.java new file mode 100644 index 000000000..c03c1fa67 --- /dev/null +++ b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/CommentWriter.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.webserver.tutorial; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.common.GenericType; +import io.helidon.http.Headers; +import io.helidon.http.HttpMediaType; +import io.helidon.http.WritableHeaders; +import io.helidon.http.media.EntityWriter; + +class CommentWriter implements EntityWriter> { + + @Override + public void write(GenericType> type, + List comments, + OutputStream os, + Headers requestHeaders, + WritableHeaders responseHeaders) { + + write(comments, os, responseHeaders); + } + + @Override + public void write(GenericType> type, + List comments, + OutputStream os, + WritableHeaders headers) { + + write(comments, os, headers); + } + + private void write(List comments, OutputStream os, Headers headers) { + String str = comments.stream() + .map(Comment::toString) + .collect(Collectors.joining("\n")); + + Charset charset = headers.contentType() + .flatMap(HttpMediaType::charset) + .map(Charset::forName) + .orElse(StandardCharsets.UTF_8); + + try { + os.write(str.getBytes(charset)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/Main.java b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/Main.java new file mode 100644 index 000000000..902e4f24d --- /dev/null +++ b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/Main.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tutorial; + +import io.helidon.http.HttpMediaTypes; +import io.helidon.http.media.MediaContext; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http.HttpRouting; + +/** + * Application java main class. + * + *

    The TUTORIAL application demonstrates various WebServer use cases together and in its complexity. + *

    It also serves web server tutorial articles composed of live examples. + */ +public final class Main { + + private Main() { + } + + /** + * Set up the routing. + * + * @param routing routing builder + */ + static void routing(HttpRouting.Builder routing) { + routing.any(new UserFilter()) + .register("/article", new CommentService()) + .post("/mgmt/shutdown", (req, res) -> { + res.headers().contentType(HttpMediaTypes.PLAINTEXT_UTF_8); + res.send("Shutting down TUTORIAL server. Good bye!\n"); + req.context() + .get(WebServer.class) + .orElseThrow() + .stop(); + }); + } + + /** + * Set up the server. + * + * @param server server builder + */ + static void setup(WebServerConfig.Builder server) { + server.routing(Main::routing) + .contentEncoding(encoding -> encoding.addContentEncoding(new UpperXEncodingProvider())) + .mediaContext(MediaContext.builder() + .addMediaSupport(new CommentSupport()) + .build()); + } + + /** + * A java main class. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + // Create a web server instance + int port = 8080; + if (args.length > 0) { + try { + port = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) { + port = 0; + } + } + + WebServerConfig.Builder builder = WebServer.builder().port(port); + setup(builder); + WebServer server = builder.build().start(); + server.context().register(server); + + System.out.printf(""" + TUTORIAL server is up! http://localhost:%1$d" + Call POST on 'http://localhost:%1$d/mgmt/shutdown' to STOP the server! + """, server.port()); + } +} diff --git a/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/UpperXEncodingProvider.java b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/UpperXEncodingProvider.java new file mode 100644 index 000000000..5e11a61dc --- /dev/null +++ b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/UpperXEncodingProvider.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tutorial; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +import io.helidon.http.encoding.ContentDecoder; +import io.helidon.http.encoding.ContentEncoder; +import io.helidon.http.encoding.ContentEncoding; + +/** + * All 'x' must be upper case. + *

    + * This is a naive implementation. + */ +public final class UpperXEncodingProvider implements ContentEncoding { + + @Override + public Set ids() { + return Set.of("upper-x"); + } + + @Override + public boolean supportsEncoding() { + return false; + } + + @Override + public boolean supportsDecoding() { + return true; + } + + @Override + public ContentDecoder decoder() { + return UpperXInputStream::new; + } + + @Override + public ContentEncoder encoder() { + return ContentEncoder.NO_OP; + } + + @Override + public String name() { + return "upper-x"; + } + + @Override + public String type() { + return "upper-x"; + } + + /** + * All 'x' must be upper case. + *

    + * This is a naive implementation. + */ + static final class UpperXInputStream extends FilterInputStream { + + UpperXInputStream(InputStream is) { + super(is); + } + + @Override + public int read() throws IOException { + int c = super.read(); + return c == 'x' ? 'X' : c; + } + } +} diff --git a/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/User.java b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/User.java new file mode 100644 index 000000000..047e880e4 --- /dev/null +++ b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/User.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tutorial; + +import io.helidon.webserver.http.HttpRouting; + +/** + * Represents an immutable user. + * + *

    {@link UserFilter} can be registered on Web Server {@link HttpRouting Routing} to provide valid {@link User} + * instance on the request context. + */ +public final class User { + + /** + * Represents an anonymous user. + */ + public static final User ANONYMOUS = new User(); + + private final boolean authenticated; + private final String alias; + private final boolean anonymous; + + /** + * Creates new instance non-anonymous user. + * + * @param authenticated an authenticated is {@code true} if this user identity was validated + * @param alias an alias represents the name of the user which is visible for others + */ + User(boolean authenticated, String alias) { + this.authenticated = authenticated; + this.alias = alias; + this.anonymous = false; + } + + /** + * Creates an unauthenticated user. + * + * @param alias an alias represents the name of the user which is visible for others + */ + User(String alias) { + this(false, alias); + } + + /** + * Creates an anonymous user instance. + */ + private User() { + this.anonymous = true; + this.authenticated = false; + this.alias = "anonymous"; + } + + /** + * Indicate if this user identity was validated. + * + * @return {@code true} if validated + */ + public boolean isAuthenticated() { + return authenticated; + } + + /** + * Get the name of the user which is visible for others. + * + * @return alias + */ + public String alias() { + return alias; + } + + /** + * Indicate if this user is anonymous. + * + * @return {@code true} if anonymous + */ + public boolean isAnonymous() { + return anonymous; + } +} diff --git a/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/UserFilter.java b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/UserFilter.java new file mode 100644 index 000000000..5c851642f --- /dev/null +++ b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/UserFilter.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tutorial; + +import io.helidon.webserver.http.Handler; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * If used as a {@link HttpRouting} {@link Handler} then assign valid {@link User} instance on the request + * {@link io.helidon.common.context.Context context}. + */ +public class UserFilter implements Handler { + + @Override + public void handle(ServerRequest req, ServerResponse res) { + // Register as a supplier. + // Thanks to it, user instance is resolved ONLY if it is requested in downstream handlers. + req.context().supply(User.class, + () -> req.headers() + .cookies() + .first("Unauthenticated-User-Alias") + .map(User::new) + .orElse(User.ANONYMOUS)); + res.next(); + } +} diff --git a/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/package-info.java b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/package-info.java new file mode 100644 index 000000000..ee8993b81 --- /dev/null +++ b/examples/webserver/tutorial/src/main/java/io/helidon/examples/webserver/tutorial/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A tutorial documentation server. It serves various tutorial articles designed based on project examples. + * + *

    It is also a complex example demonstrating various web server features. + */ +package io.helidon.examples.webserver.tutorial; diff --git a/examples/webserver/tutorial/src/main/resources/logging.properties b/examples/webserver/tutorial/src/main/resources/logging.properties new file mode 100644 index 000000000..ecfe90ddb --- /dev/null +++ b/examples/webserver/tutorial/src/main/resources/logging.properties @@ -0,0 +1,33 @@ +# +# Copyright (c) 2018, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO diff --git a/examples/webserver/tutorial/src/test/java/io/helidon/examples/webserver/tutorial/CommentServiceTest.java b/examples/webserver/tutorial/src/test/java/io/helidon/examples/webserver/tutorial/CommentServiceTest.java new file mode 100644 index 000000000..3112eddc3 --- /dev/null +++ b/examples/webserver/tutorial/src/test/java/io/helidon/examples/webserver/tutorial/CommentServiceTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tutorial; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; + +/** + * Tests {@link CommentService}. + */ +@ServerTest +class CommentServiceTest { + + private final Http1Client client; + + CommentServiceTest(Http1Client client) { + this.client = client; + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + Main.setup(server); + } + + @Test + void addAndGetComments() { + CommentService service = new CommentService(); + assertThat(service.getComments("one"), empty()); + assertThat(service.getComments("two"), empty()); + service.addComment("one", null, "aaa"); + assertThat(service.getComments("one"), hasSize(1)); + assertThat(service.getComments("two"), empty()); + service.addComment("one", null, "bbb"); + assertThat(service.getComments("one"), hasSize(2)); + assertThat(service.getComments("two"), empty()); + service.addComment("two", null, "bbb"); + assertThat(service.getComments("one"), hasSize(2)); + assertThat(service.getComments("two"), hasSize(1)); + } + + @Test + void testRouting() { + try (Http1ClientResponse response = client.get("/article/one").request()) { + assertThat(response.status(), is(Status.OK_200)); + } + + // Add first comment + try (Http1ClientResponse response = client.post("/article/one") + .contentType(MediaTypes.TEXT_PLAIN) + .submit("aaa")) { + assertThat(response.status(), is(Status.OK_200)); + } + + try (Http1ClientResponse response = client.get("/article/one").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.entity().as(String.class), is("anonymous: aaa")); + } + + + // Add second comment + try (Http1ClientResponse response = client.post("/article/one") + .contentType(MediaTypes.TEXT_PLAIN) + .submit("bbb")) { + assertThat(response.status(), is(Status.OK_200)); + } + + try (Http1ClientResponse response = client.get("/article/one").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.entity().as(String.class), is("anonymous: aaa\nanonymous: bbb")); + } + } +} diff --git a/examples/webserver/tutorial/src/test/java/io/helidon/examples/webserver/tutorial/MainTest.java b/examples/webserver/tutorial/src/test/java/io/helidon/examples/webserver/tutorial/MainTest.java new file mode 100644 index 000000000..51daab0a0 --- /dev/null +++ b/examples/webserver/tutorial/src/test/java/io/helidon/examples/webserver/tutorial/MainTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tutorial; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests {@link Main}. + */ +@ServerTest +public class MainTest { + + private final WebServer server; + private final Http1Client client; + + public MainTest(WebServer server, Http1Client client) { + server.context().register(server); + this.server = server; + this.client = client; + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + Main.setup(server); + } + + @Test + public void testShutDown() throws InterruptedException { + try (Http1ClientResponse response = client.post("/mgmt/shutdown").request()) { + assertThat(response.status(), is(Status.OK_200)); + } + // there may be some delay between the request being completed, and the server shutting down + // let's give it a second to shut down, then fail + for (int i = 0; i < 10; i++) { + if (server.isRunning()) { + Thread.sleep(100); + } else { + break; + } + } + assertThat(server.isRunning(), is(false)); + } +} diff --git a/examples/webserver/tutorial/src/test/java/io/helidon/examples/webserver/tutorial/UserFilterTest.java b/examples/webserver/tutorial/src/test/java/io/helidon/examples/webserver/tutorial/UserFilterTest.java new file mode 100644 index 000000000..e7b71c1b3 --- /dev/null +++ b/examples/webserver/tutorial/src/test/java/io/helidon/examples/webserver/tutorial/UserFilterTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2017, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.tutorial; + +import io.helidon.http.HeaderNames; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.DirectClient; +import io.helidon.webserver.testing.junit5.RoutingTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests {@link UserFilter}. + */ +@RoutingTest +public class UserFilterTest { + + private final DirectClient client; + + UserFilterTest(DirectClient client) { + this.client = client; + } + + @SetUpRoute + static void setup(HttpRouting.Builder routing) { + routing.any(new UserFilter()) + .any((req, res) -> res.send(req.context() + .get(User.class) + .orElse(User.ANONYMOUS) + .alias())); + } + + @Test + public void filter() { + try (Http1ClientResponse response = client.get().request()) { + assertThat(response.entity().as(String.class), is("anonymous")); + } + + try (Http1ClientResponse response = client.get() + .header(HeaderNames.COOKIE, "Unauthenticated-User-Alias=Foo") + .request()) { + assertThat(response.entity().as(String.class), is("Foo")); + } + } +} diff --git a/examples/webserver/websocket/README.md b/examples/webserver/websocket/README.md new file mode 100644 index 000000000..6a1c48eb4 --- /dev/null +++ b/examples/webserver/websocket/README.md @@ -0,0 +1,13 @@ +# WebSocket Example + +This application demonstrates use of websockets and REST. + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-webserver-websocket.jar +``` + +Open http://localhost:8080/web/index.html in your browser. +`` diff --git a/examples/webserver/websocket/pom.xml b/examples/webserver/websocket/pom.xml new file mode 100644 index 000000000..b68e8a287 --- /dev/null +++ b/examples/webserver/websocket/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.1.0-SNAPSHOT + + io.helidon.examples.webserver + helidon-examples-webserver-websocket + 1.0.0-SNAPSHOT + Helidon Examples WebServer WebSocket + + + Application demonstrates the use of websockets and REST. + + + + io.helidon.examples.webserver.websocket.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-static-content + + + io.helidon.webserver + helidon-webserver-websocket + + + io.helidon.logging + helidon-logging-jul + runtime + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-websocket + test + + + io.helidon.webclient + helidon-webclient-websocket + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/Main.java b/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/Main.java new file mode 100644 index 000000000..7c15600de --- /dev/null +++ b/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/Main.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.websocket; + +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.staticcontent.StaticContentService; +import io.helidon.webserver.websocket.WsRouting; + +/** + * Application demonstrates combination of websocket and REST. + */ +public class Main { + + private Main() { + } + + static void setup(WebServerConfig.Builder server) { + StaticContentService staticContent = StaticContentService.builder("/WEB") + .welcomeFileName("index.html") + .build(); + MessageQueueService messageQueueService = new MessageQueueService(); + server.routing(routing -> routing + .register("/web", staticContent) + .register("/rest", messageQueueService)) + .addRouting(WsRouting.builder() + .endpoint("/websocket/board", new MessageBoardEndpoint())); + } + + /** + * A java main class. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + WebServerConfig.Builder builder = WebServer.builder().port(8080); + setup(builder); + WebServer server = builder.build().start(); + System.out.println("WEB server is up! http://localhost:" + server.port() + "/web"); + } +} diff --git a/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/MessageBoardEndpoint.java b/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/MessageBoardEndpoint.java new file mode 100644 index 000000000..7600bee0e --- /dev/null +++ b/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/MessageBoardEndpoint.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.websocket; + +import io.helidon.websocket.WsListener; +import io.helidon.websocket.WsSession; + +/** + * Class MessageBoardEndpoint. + */ +public class MessageBoardEndpoint implements WsListener { + private final MessageQueue messageQueue = MessageQueue.instance(); + + @Override + public void onMessage(WsSession session, String text, boolean last) { + // Send all messages in the queue + if (text.equals("SEND")) { + while (!messageQueue.isEmpty()) { + session.send(messageQueue.pop(), last); + } + } + } +} diff --git a/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/MessageQueue.java b/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/MessageQueue.java new file mode 100644 index 000000000..caa71d4f5 --- /dev/null +++ b/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/MessageQueue.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.websocket; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Class MessageQueue. + */ +public class MessageQueue { + + private static final MessageQueue INSTANCE = new MessageQueue(); + + private Queue queue = new ConcurrentLinkedQueue<>(); + + /** + * Return singleton instance of this class. + * + * @return Singleton. + */ + public static MessageQueue instance() { + return INSTANCE; + } + + private MessageQueue() { + } + + /** + * Push string on stack. + * + * @param s String to push. + */ + public void push(String s) { + queue.add(s); + } + + /** + * Pop string from stack. + * + * @return The string or {@code null}. + */ + public String pop() { + return queue.poll(); + } + + /** + * Check if stack is empty. + * + * @return Outcome of test. + */ + public boolean isEmpty() { + return queue.isEmpty(); + } + + /** + * Peek at stack without changing it. + * + * @return String peeked or {@code null}. + */ + public String peek() { + return queue.peek(); + } +} diff --git a/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/MessageQueueService.java b/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/MessageQueueService.java new file mode 100644 index 000000000..e575a8527 --- /dev/null +++ b/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/MessageQueueService.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.websocket; + +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** + * Class MessageQueueResource. + */ +public class MessageQueueService implements HttpService { + + private final MessageQueue messageQueue = MessageQueue.instance(); + + @Override + public void routing(HttpRules routingRules) { + routingRules.post("/board", this::handlePost); + } + + private void handlePost(ServerRequest request, ServerResponse response) { + messageQueue.push(request.content().as(String.class)); + response.status(204).send(); + } +} diff --git a/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/package-info.java b/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/package-info.java new file mode 100644 index 000000000..dbc784b6b --- /dev/null +++ b/examples/webserver/websocket/src/main/java/io/helidon/examples/webserver/websocket/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Application demonstrates combination of the websocket and REST. + * + *

    + * Start with {@link io.helidon.examples.webserver.websocket.Main} class. + * + * @see io.helidon.examples.webserver.websocket.Main + */ +package io.helidon.examples.webserver.websocket; diff --git a/examples/webserver/websocket/src/main/resources/WEB/index.html b/examples/webserver/websocket/src/main/resources/WEB/index.html new file mode 100644 index 000000000..b597a6d9b --- /dev/null +++ b/examples/webserver/websocket/src/main/resources/WEB/index.html @@ -0,0 +1,81 @@ + + + + + + + + + + + +

    +
    +

    +

    + +

    +

    + +

    +

    +

    History

    +
    +
    + + diff --git a/examples/webserver/websocket/src/main/resources/logging.properties b/examples/webserver/websocket/src/main/resources/logging.properties new file mode 100644 index 000000000..62a490106 --- /dev/null +++ b/examples/webserver/websocket/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2020, 2024 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO +io.helidon.microprofile.server.level=INFO diff --git a/examples/webserver/websocket/src/test/java/io/helidon/examples/webserver/websocket/MessageBoardTest.java b/examples/webserver/websocket/src/test/java/io/helidon/examples/webserver/websocket/MessageBoardTest.java new file mode 100644 index 000000000..998f0e8be --- /dev/null +++ b/examples/webserver/websocket/src/test/java/io/helidon/examples/webserver/websocket/MessageBoardTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020, 2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.webserver.websocket; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webclient.websocket.WsClient; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import io.helidon.websocket.WsCloseCodes; +import io.helidon.websocket.WsListener; +import io.helidon.websocket.WsSession; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Class MessageBoardTest. + */ +@ServerTest +public class MessageBoardTest { + private static final Logger LOGGER = Logger.getLogger(MessageBoardTest.class.getName()); + + private static final String[] MESSAGES = {"Whisky", "Tango", "Foxtrot"}; + private final WsClient wsClient; + private final Http1Client client; + + MessageBoardTest(Http1Client client, WsClient wsClient) { + this.client = client; + this.wsClient = wsClient; + } + + @SetUpServer + static void setup(WebServerConfig.Builder server) { + Main.setup(server); + } + + @Test + public void testBoard() throws InterruptedException { + // Post messages using REST resource + for (String message : MESSAGES) { + try (Http1ClientResponse response = client.post("/rest/board").submit(message)) { + assertThat(response.status(), is(Status.NO_CONTENT_204)); + LOGGER.info("Posting message '" + message + "'"); + } + } + + // Now connect to message board using WS and them back + + CountDownLatch messageLatch = new CountDownLatch(MESSAGES.length); + + wsClient.connect("/websocket/board", new WsListener() { + @Override + public void onMessage(WsSession session, String text, boolean last) { + LOGGER.info("Client OnMessage called '" + text + "'"); + messageLatch.countDown(); + if (messageLatch.getCount() == 0) { + session.close(WsCloseCodes.NORMAL_CLOSE, "Bye!"); + } + } + + @Override + public void onOpen(WsSession session) { + session.send("SEND", false); + } + }); + + // Wait until all messages are received + assertThat(messageLatch.await(1000000, TimeUnit.SECONDS), is(true)); + } +} diff --git a/pom.xml b/pom.xml index 5f8017e23..213a68632 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,6 @@ io.helidon helidon-dependencies 4.1.0-SNAPSHOT - io.helidon.examples helidon-example-code-project