diff --git a/.gitignore b/.gitignore index bb39c50bea0..000016a72ef 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,5 @@ node/ # Other *~ user.txt +ObjectStore/ +PutObjectStoreDirHere/ diff --git a/3RD-PARTY.txt b/3RD-PARTY.txt index 612310294b5..b76e960ba78 100644 --- a/3RD-PARTY.txt +++ b/3RD-PARTY.txt @@ -2220,3 +2220,23 @@ from the source code management (SCM) system project uses. -----------------jackson-annotations 2.9.8 ----------------------- COPYRIGHT: Copyright (c) 2007- Tatu Saloranta, tatu.saloranta@iki.fi LICENSE: Apache 2.0 + +======================= +SnakeYAML 1.24 +======================= +Copyright (c) 2008, http://www.snakeyaml.org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +LICENSE: Apache 2.0 + diff --git a/bom/pom.xml b/bom/pom.xml index 644ebdcd439..9160ed57a60 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -52,6 +52,17 @@ helidon-webserver-test-support ${project.version} + + + io.helidon.grpc + helidon-grpc-client + ${project.version} + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + io.helidon.media @@ -170,6 +181,11 @@ helidon-security-providers-http-sign ${project.version} + + io.helidon.security.integration + helidon-security-integration-grpc + ${project.version} + io.helidon.security.integration helidon-security-integration-jersey @@ -186,10 +202,10 @@ ${project.version} - io.helidon.security.providers - helidon-security-providers-abac - ${project.version} - + io.helidon.security.providers + helidon-security-providers-abac + ${project.version} + io.helidon.security helidon-security-abac-time @@ -318,6 +334,11 @@ helidon-common-key-util ${project.version} + + io.helidon.common + helidon-common-service-loader + ${project.version} + @@ -398,11 +419,36 @@ helidon-integrations-cdi-datasource-hikaricp ${project.version} + + io.helidon.integrations.cdi + helidon-integrations-cdi-eclipselink + ${project.version} + io.helidon.integrations.cdi helidon-integrations-cdi-jedis ${project.version} + + io.helidon.integrations.cdi + helidon-integrations-cdi-jpa + ${project.version} + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jpa-weld + ${project.version} + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta + ${project.version} + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + ${project.version} + io.helidon.integrations.cdi helidon-integrations-cdi-oci-objectstorage diff --git a/common/common/src/main/java/io/helidon/common/Prioritized.java b/common/common/src/main/java/io/helidon/common/Prioritized.java new file mode 100644 index 00000000000..5d6ef845dce --- /dev/null +++ b/common/common/src/main/java/io/helidon/common/Prioritized.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.common; + +/** + * Interface to define that this class is a class with priority. + * One of the uses is for services loaded by a ServiceLoader. + *

+ * A {@code Prioritized} with lower priority number is more significant than a {@code Prioritized} with a + * higher priority number. + *

+ * For cases where priority is the same, implementation must define ordering of such {@code Prioritized}. + *

+ * Negative priorities are not allowed and services using priorities should throw an + * {@link java.lang.IllegalArgumentException} if such a priority is used (unless such a service + * documents the specific usage of a negative priority) + *

+ * A {@code Prioritized} with priority {@code 1} is more significant (will be returned before) priority {@code 2}. + */ +@FunctionalInterface +public interface Prioritized { + /** + * Default priority for any prioritized component (whether it implements this interface + * or uses {@code javax.annotation.Priority} annotation. + */ + int DEFAULT_PRIORITY = 5000; + + /** + * Priority of this class (maybe because it is defined + * dynamically, so it cannot be defined by an annotation). + * If not dynamic, you can use the {@code javax.annotation.Priority} + * annotation rather then implementing this interface as long as + * it is supported by the library using this {@code Prioritized}. + * + * @return the priority of this service, must be a non-negative number + */ + int priority(); +} diff --git a/common/common/src/main/templates/io/helidon/common/Version.java b/common/common/src/main/templates/io/helidon/common/Version.java index bac7bfa39b4..55a9987a2d3 100644 --- a/common/common/src/main/templates/io/helidon/common/Version.java +++ b/common/common/src/main/templates/io/helidon/common/Version.java @@ -26,12 +26,17 @@ public class Version { */ public static final String VERSION = "${project.version}"; + /** + * Revision Number. + */ + public static final String REVISION = "${buildNumber}"; + /** * Display version * - * @param args + * @param args Ignored */ public static void main(String[] args) { - System.out.println(VERSION); + System.out.println(VERSION + " " + REVISION); } } diff --git a/common/configurable/src/main/java/io/helidon/common/configurable/ResourceUtil.java b/common/configurable/src/main/java/io/helidon/common/configurable/ResourceUtil.java index c2559ce77a3..d63f85f7576 100644 --- a/common/configurable/src/main/java/io/helidon/common/configurable/ResourceUtil.java +++ b/common/configurable/src/main/java/io/helidon/common/configurable/ResourceUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -90,7 +90,7 @@ static InputStream toIs(URI uri) { try { return uri.toURL().openStream(); } catch (IOException e) { - throw new ResourceException("Failed to open strem to uri: " + uri, e); + throw new ResourceException("Failed to open stream to uri: " + uri, e); } } @@ -105,7 +105,7 @@ static InputStream toIs(URI uri, Proxy proxy) { try { return uri.toURL().openConnection(proxy).getInputStream(); } catch (IOException e) { - throw new ResourceException("Failed to open strem to uri: " + uri, e); + throw new ResourceException("Failed to open stream to uri: " + uri, e); } } diff --git a/common/pom.xml b/common/pom.xml index ac982542ef5..fc67643272f 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -36,5 +36,6 @@ configurable key-util http + service-loader diff --git a/common/service-loader/pom.xml b/common/service-loader/pom.xml new file mode 100644 index 00000000000..63e64013945 --- /dev/null +++ b/common/service-loader/pom.xml @@ -0,0 +1,62 @@ + + + + + + helidon-common-project + io.helidon.common + 1.0.4-SNAPSHOT + + 4.0.0 + + helidon-common-service-loader + Helidon Common Service Loader + + + Service loader utilities to extend functionality of + Java Service loader. + + + + + io.helidon.common + helidon-common + ${project.version} + + + javax.annotation + javax.annotation-api + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.mockito + mockito-core + test + + + \ No newline at end of file diff --git a/common/service-loader/src/main/java/io/helidon/common/serviceloader/HelidonServiceLoader.java b/common/service-loader/src/main/java/io/helidon/common/serviceloader/HelidonServiceLoader.java new file mode 100644 index 00000000000..8ffea1d4c51 --- /dev/null +++ b/common/service-loader/src/main/java/io/helidon/common/serviceloader/HelidonServiceLoader.java @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.common.serviceloader; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import javax.annotation.Priority; + +import io.helidon.common.Prioritized; + +/** + * Helidon specific support for Java Service Loaders. + *

+ * This service loader: + *

+ *

+ * Note on priority handling + *

+ * Service priority is defined by: + *

+ * Example: + *
+ * {@literal @}Priority(4500)
+ * public class MyServiceImpl implements Service, Prioritized {
+ *     public int priority() {
+ *         return 6200;
+ *     }
+ * }
+ * 
+ * Such a service would have a priority of {@code 6200} as that is more significant than the annotation. + *

+ * A service with lower priority number is returned before a service with a higher priority number. + * Services with the same priority have order defined by the order they are in the configured services + * and then as they are loaded from the {@link java.util.ServiceLoader}. + * Negative priorities are not allowed. + * A service with priority {@code 1} will be returned before a service with priority {@code 2}. + * + * @param Type of the service to be loaded + * @see java.util.ServiceLoader + * @see #builder(java.util.ServiceLoader) + */ +public final class HelidonServiceLoader implements Iterable { + /** + * System property used to exclude some implementation from the list of services that are configured for Java Service + * loader or services that are registered using {@link io.helidon.common.serviceloader.HelidonServiceLoader.Builder}. + */ + public static final String SYSTEM_PROPERTY_EXCLUDE = "io.helidon.common.serviceloader.exclude"; + + private static final Logger LOGGER = Logger.getLogger(HelidonServiceLoader.class.getName()); + + private final List services; + + /** + * Create a builder for customizable service loader. + * + * @param serviceLoader the Java Service loader used to get service implementations + * @param type of the service + * @return a new fluent API builder + */ + public static Builder builder(ServiceLoader serviceLoader) { + return new Builder<>(serviceLoader); + } + + /** + * Create a prioritized service loader from a Java Service loader. + * + * @param serviceLoader the Java service loader + * @param type of the service + * @return service loader with exclusions defined by system properties and no custom services + */ + public static HelidonServiceLoader create(ServiceLoader serviceLoader) { + Builder builder = builder(serviceLoader); + return builder.build(); + } + + private HelidonServiceLoader(List services) { + this.services = new LinkedList<>(services); + } + + @Override + public Iterator iterator() { + return Collections.unmodifiableList(services) + .iterator(); + } + + @Override + public void forEach(Consumer action) { + this.services.forEach(action); + } + + /** + * Provides a list of service implementations in prioritized order. + * + * @return list of service implementations + */ + public List asList() { + return new LinkedList<>(this.services); + } + + /** + * Fluent api builder for {@link io.helidon.common.serviceloader.HelidonServiceLoader}. + * + * @param type of the service to be loaded + */ + public static final class Builder implements io.helidon.common.Builder> { + private final ServiceLoader serviceLoader; + private final List> customServices = new LinkedList>(); + private final Set excludedServiceClasses = new HashSet<>(); + private boolean useSysPropExclude = true; + private boolean useSystemServiceLoader = true; + private boolean replaceImplementations = true; + + private Builder(ServiceLoader serviceLoader) { + this.serviceLoader = serviceLoader; + } + + @Override + public HelidonServiceLoader build() { + // first merge the lists together + List> services = new LinkedList<>(customServices); + if (useSystemServiceLoader) { + Set uniqueImplementations = new HashSet<>(); + + if (replaceImplementations) { + customServices.stream() + .map(ServiceWithPriority::instanceClassName) + .forEach(uniqueImplementations::add); + } + + serviceLoader.forEach(service -> { + if (replaceImplementations) { + if (!uniqueImplementations.contains(service.getClass().getName())) { + services.add(new ServiceWithPriority<>(service)); + } + } else { + services.add(new ServiceWithPriority<>(service)); + } + }); + } + + if (useSysPropExclude) { + addSystemExcludes(); + } + List> withoutExclusions = services.stream() + .filter(this::notExcluded) + .collect(Collectors.toList()); + + // order by priority + return new HelidonServiceLoader<>(orderByPriority(withoutExclusions)); + } + + /** + * When configured to use system excludes, system property {@value #SYSTEM_PROPERTY_EXCLUDE} is used to get the + * comma separated list of service implementations to exclude them from the loaded list. + *

+ * This defaults to {@code true}. + * + * @param useSysPropExclude whether to use a system property to exclude service implementations + * @return updated builder instance + */ + public Builder useSystemExcludes(boolean useSysPropExclude) { + this.useSysPropExclude = useSysPropExclude; + return this; + } + + /** + * When configured to use Java Service loader, then the result is a combination of all service implementations + * loaded from the Java Service loader and those added by {@link #addService(Object)} or {@link #addService(Object, int)}. + * When set to {@code false} the Java Service loader is ignored. + *

+ * This defaults to {@code true}. + * + * @param useServiceLoader whether to use the Java Service loader + * @return updated builder instance + */ + public Builder useSystemServiceLoader(boolean useServiceLoader) { + this.useSystemServiceLoader = useServiceLoader; + return this; + } + + /** + * When configured to replace implementations, then a service implementation configured through + * {@link #addService(Object)} + * will replace the same implementation loaded from the Java Service loader (compared by fully qualified class name). + *

+ * This defaults to {@code true}. + * + * @param replace whether to replace service instances loaded by java service loader with the ones provided + * through builder methods + * @return updated builder instance + */ + public Builder replaceImplementations(boolean replace) { + this.replaceImplementations = replace; + return this; + } + + /** + * Add a custom service implementation to the list of services. + * + * @param service a new service instance + * @return updated builder instance + */ + public Builder addService(T service) { + this.customServices.add(new ServiceWithPriority<>(service)); + return this; + } + + /** + * Add a custom service implementation to the list of services with a custom priority. + * + * @param service a new service instance + * @param priority priority to use when ordering service instances + * @return updated builder instance + */ + public Builder addService(T service, int priority) { + this.customServices.add(new ServiceWithPriority<>(service, priority)); + return this; + } + + /** + * Add an excluded implementation class - if such a service implementation is configured (either through + * Java Service loader or through {@link #addService(Object)}), it would be ignored. + * + * @param excluded excluded implementation class + * @return updated builder instance + */ + public Builder addExcludedClass(Class excluded) { + excludedServiceClasses.add(excluded.getName()); + return this; + } + + /** + * Add an excluded implementation class - if such a service implementation is configured (either through + * Java Service loader or through {@link #addService(Object)}), it would be ignored. + * + * @param excludeName excluded implementation class name + * @return updated builder instance + */ + public Builder addExcludedClassName(String excludeName) { + excludedServiceClasses.add(excludeName); + return this; + } + + private boolean notExcluded(ServiceWithPriority service) { + String className = service.instance.getClass().getName(); + if (excludedServiceClasses.contains(className)) { + LOGGER.finest(() -> "Excluding service implementation " + className); + return false; + } + return true; + } + + private List orderByPriority(List> services) { + services.sort(ServiceWithPriority.COMPARATOR); + + List result = services.stream() + .map(ServiceWithPriority::instance) + .collect(Collectors.toList()); + + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.finest("Final order of enabled service implementations for service: " + serviceLoader); + result.stream() + .map(Object::getClass) + .map(Class::getName) + .forEach(LOGGER::finest); + } + + return result; + } + + private void addSystemExcludes() { + String excludes = System.getProperty(SYSTEM_PROPERTY_EXCLUDE); + if (null == excludes) { + return; + } + + for (String exclude : excludes.split(",")) { + LOGGER.finest(() -> "Adding exclude from system properties: " + exclude); + addExcludedClassName(exclude); + } + } + + private static final class ServiceWithPriority { + public static final Comparator> COMPARATOR = Comparator + .comparingInt(ServiceWithPriority::priority); + + private final T instance; + private final int priority; + + private ServiceWithPriority(T instance, int priority) { + this.instance = instance; + this.priority = priority; + + if (priority < 0) { + throw new IllegalArgumentException("Service: " + + instance.getClass().getName() + + " declares a negative priority, which is not allowed. Priority: " + + priority); + } + } + + private ServiceWithPriority(T service) { + this(service, findPriority(service)); + } + + private int priority() { + return priority; + } + + private T instance() { + return instance; + } + + private String instanceClassName() { + return instance.getClass().getName(); + } + + private static int findPriority(Object o) { + if (o instanceof Prioritized) { + return ((Prioritized) o).priority(); + } + Priority prio = o.getClass().getAnnotation(Priority.class); + if (null == prio) { + return Prioritized.DEFAULT_PRIORITY; + } + return prio.value(); + } + + @Override + public String toString() { + return instance.toString(); + } + } + } +} diff --git a/common/service-loader/src/main/java/io/helidon/common/serviceloader/package-info.java b/common/service-loader/src/main/java/io/helidon/common/serviceloader/package-info.java new file mode 100644 index 00000000000..8e7027c71aa --- /dev/null +++ b/common/service-loader/src/main/java/io/helidon/common/serviceloader/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 Service loader extension. + */ +package io.helidon.common.serviceloader; diff --git a/common/service-loader/src/main/java9/module-info.java b/common/service-loader/src/main/java9/module-info.java new file mode 100644 index 00000000000..37fb8a0feca --- /dev/null +++ b/common/service-loader/src/main/java9/module-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 Common Service Loader. + */ +module io.helidon.common.serviceloader { + requires java.logging; + requires io.helidon.common; + requires java.annotation; + + exports io.helidon.common.serviceloader; +} diff --git a/common/service-loader/src/test/java/io/helidon/common/serviceloader/HelidonServiceLoaderTest.java b/common/service-loader/src/test/java/io/helidon/common/serviceloader/HelidonServiceLoaderTest.java new file mode 100644 index 00000000000..9e4d99fb76a --- /dev/null +++ b/common/service-loader/src/test/java/io/helidon/common/serviceloader/HelidonServiceLoaderTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.serviceloader; + +import java.util.List; +import java.util.ServiceLoader; + +import org.junit.jupiter.api.BeforeAll; +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.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit test for {@link io.helidon.common.serviceloader.HelidonServiceLoader}. + */ +class HelidonServiceLoaderTest { + private static ServiceLoader javaLoader; + + @BeforeAll + static void initClass() { + javaLoader = ServiceLoader.load(ServiceInterface.class); + } + + @Test + void testJavaLoader() { + List loaded = HelidonServiceLoader.create(javaLoader).asList(); + + assertThat(loaded, hasSize(2)); + assertThat(loaded.get(0).message(), is(ServiceImpl2.class.getName())); + assertThat(loaded.get(1).message(), is(ServiceImpl1.class.getName())); + } + + @Test + void testCustomService() { + List loaded = HelidonServiceLoader.builder(javaLoader) + .addService(new ServiceImpl3()) + .build() + .asList(); + + assertThat(loaded, hasSize(3)); + assertThat(loaded.get(0).message(), is(ServiceImpl2.class.getName())); + assertThat(loaded.get(1).message(), is(ServiceImpl3.class.getName())); + assertThat(loaded.get(2).message(), is(ServiceImpl1.class.getName())); + } + + @Test + void testCustomServiceWithCustomPrio() { + List loaded = HelidonServiceLoader.builder(javaLoader) + .addService(new ServiceImpl3(), 0) + .build() + .asList(); + + assertThat(loaded, hasSize(3)); + assertThat(loaded.get(0).message(), is(ServiceImpl3.class.getName())); + assertThat(loaded.get(1).message(), is(ServiceImpl2.class.getName())); + assertThat(loaded.get(2).message(), is(ServiceImpl1.class.getName())); + } + + @Test + void testExcludeService() { + List loaded = HelidonServiceLoader.builder(javaLoader) + .addService(new ServiceImpl3()) + .addExcludedClass(ServiceImpl2.class) + .build() + .asList(); + + assertThat(loaded, hasSize(2)); + assertThat(loaded.get(0).message(), is(ServiceImpl3.class.getName())); + assertThat(loaded.get(1).message(), is(ServiceImpl1.class.getName())); + } + + @Test + void testExcludeServiceNames() { + List loaded = HelidonServiceLoader.builder(javaLoader) + .addService(new ServiceImpl3()) + .addExcludedClassName(ServiceImpl1.class.getName()) + .addExcludedClassName(ServiceImpl3.class.getName()) + .build() + .asList(); + + assertThat(loaded, hasSize(1)); + assertThat(loaded.get(0).message(), is(ServiceImpl2.class.getName())); + } + + @Test + void testWithoutSystemServiceLoader() { + List loaded = HelidonServiceLoader.builder(javaLoader) + .addService(new ServiceImpl3()) + .addService(new ServiceImpl2()) + .useSystemServiceLoader(false) + .build() + .asList(); + + assertThat(loaded, hasSize(2)); + assertThat(loaded.get(0).message(), is(ServiceImpl2.class.getName())); + assertThat(loaded.get(1).message(), is(ServiceImpl3.class.getName())); + } + + @Test + void testUniqueImplementations() { + String TEST_STRING = "custom messsage"; + + List loaded = HelidonServiceLoader.builder(javaLoader) + .addService(new ServiceImpl2(TEST_STRING)) + .build() + .asList(); + + assertThat(loaded, hasSize(2)); + assertThat(loaded.get(0).message(), is(TEST_STRING)); + assertThat(loaded.get(1).message(), is(ServiceImpl1.class.getName())); + } + + @Test + void testNoUniqueImplementations() { + String TEST_STRING = "custom messsage"; + + List loaded = HelidonServiceLoader.builder(javaLoader) + .addService(new ServiceImpl2(TEST_STRING), 11) + .replaceImplementations(false) + .build() + .asList(); + + assertThat(loaded, hasSize(3)); + assertThat(loaded.get(0).message(), is(TEST_STRING)); + assertThat(loaded.get(1).message(), is(ServiceImpl2.class.getName())); + assertThat(loaded.get(2).message(), is(ServiceImpl1.class.getName())); + } + + @Test + void testNegativePrioFails() { + assertThrows(IllegalArgumentException.class, () -> HelidonServiceLoader.builder(javaLoader) + .addService(new ServiceImpl2("something"), -11) + .replaceImplementations(false) + .build() + .asList()); + } + + @Test + void testZeropPrioWorks() { + HelidonServiceLoader.builder(javaLoader) + .addService(new ServiceImpl2("something"), 0) + .replaceImplementations(false) + .build() + .asList(); + } + +} + diff --git a/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceImpl1.java b/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceImpl1.java new file mode 100644 index 00000000000..5abf5d6cea2 --- /dev/null +++ b/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceImpl1.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.common.serviceloader; + +import javax.annotation.Priority; + +/** + * A service implementation. + */ +@Priority(47) +public class ServiceImpl1 implements ServiceInterface { + @Override + public String message() { + return getClass().getName(); + } +} diff --git a/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceImpl2.java b/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceImpl2.java new file mode 100644 index 00000000000..8e540fc9a31 --- /dev/null +++ b/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceImpl2.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.common.serviceloader; + +import io.helidon.common.Prioritized; + +/** + * A service implementation. + */ +public class ServiceImpl2 implements ServiceInterface, Prioritized { + private final String message; + + public ServiceImpl2() { + this.message = ServiceImpl2.class.getName(); + } + + public ServiceImpl2(String message) { + this.message = message; + } + + @Override + public String message() { + return message; + } + + @Override + public int priority() { + return 12; + } +} diff --git a/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceImpl3.java b/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceImpl3.java new file mode 100644 index 00000000000..3249e01bbf3 --- /dev/null +++ b/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceImpl3.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.common.serviceloader; + +import javax.annotation.Priority; + +/** + * A service implementation. + */ +@Priority(22) +public class ServiceImpl3 implements ServiceInterface { + @Override + public String message() { + return getClass().getName(); + } +} diff --git a/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceInterface.java b/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceInterface.java new file mode 100644 index 00000000000..e10398c4601 --- /dev/null +++ b/common/service-loader/src/test/java/io/helidon/common/serviceloader/ServiceInterface.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.common.serviceloader; + +/** + * Testing Java Service loader service interface. + */ +public interface ServiceInterface { + String message(); +} diff --git a/common/service-loader/src/test/resources/META-INF/services/io.helidon.common.serviceloader.ServiceInterface b/common/service-loader/src/test/resources/META-INF/services/io.helidon.common.serviceloader.ServiceInterface new file mode 100644 index 00000000000..cef74a665ec --- /dev/null +++ b/common/service-loader/src/test/resources/META-INF/services/io.helidon.common.serviceloader.ServiceInterface @@ -0,0 +1,18 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.common.serviceloader.ServiceImpl1 +io.helidon.common.serviceloader.ServiceImpl2 diff --git a/config/etcd/pom.xml b/config/etcd/pom.xml index b7d5f5447d5..9c81092892b 100644 --- a/config/etcd/pom.xml +++ b/config/etcd/pom.xml @@ -133,15 +133,6 @@ org.xolstice.maven.plugins protobuf-maven-plugin - - - com.google.protobuf:protoc:${version.lib.protobuf.java}:exe:${os.detected.classifier} - - grpc-java - - io.grpc:protoc-gen-grpc-java:${version.lib.grpc}:exe:${os.detected.classifier} - - diff --git a/config/yaml/src/main/java9/module-info.java b/config/yaml/src/main/java9/module-info.java index 7b4fef1ea17..add8dcfaf84 100644 --- a/config/yaml/src/main/java9/module-info.java +++ b/config/yaml/src/main/java9/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ requires java.logging; - requires snakeyaml; + requires org.yaml.snakeyaml; requires transitive io.helidon.config; requires io.helidon.common; diff --git a/docs-internal/context.md b/docs-internal/context.md new file mode 100644 index 00000000000..e08a16152da --- /dev/null +++ b/docs-internal/context.md @@ -0,0 +1,90 @@ +# Helidon Context Support Proposal + +Provide a way to propagate Context across thread boundaries. +We already use the WebServer request context to store and retrieve +information (such as in security). + +This proposal would allow us to use the context: + +- in any thread managed (or wrapped) by Helidon +- to propagate information all the way from server inbound + request to a client outbound request +- to handle correctly things such as parent SpanContext in tracing + where we currently have access only to the overall WebServer + parent span + +## Problem + +We currently have context available only when we can reach +ServerRequest. There are cases, where we need to propagate +context information and where dependency on WebServer is not desired, +such as in Helidon DB. +Also with introduction of gRPC server, we have another source +of contexts, that should not depend on http modules. + +## Proposal + +This proposal defines: + +- new common API +- updates to existing modules + +### New API + +A new module `helidon-common-context` is to be created. +Public API consists of: + +- `io.helidon.common.context.Context` - a copy of existing `ContextualRegistry` in `http`, + to be used as the main point of registering and retrieving contextual values +- `io.helidon.common.context.Contexts` - a utility class with helpful static methods + to work with `Context`: + - `Optional context()`: to retrieve current context (if there is one) + - `ExecutorService wrap(ExecutorService)`: to wrap any executor service and create a context-aware executor service + - `ScheduledExecutorService wrap(ScheduledExecutorService)`: to wrap any scheduled executor service and create a + context-aware scheduled executor service + - `void inContext(Context, Runnable)`: to execute a runnable in a context in current thread + - ` T inContext(Context, Callable)`: to execute a callable in a context in current thread +- `io.helidon.common.context.ExecutorException` - unchecked exception to use in this module + +The new implementation can be source code compatible with previous + Helidon versions, as long as we keep the `ContextualRegistry` in + our `http` module and it extends the new `Context` interface. + +### Updates to existing modules + +Each module that creates a new context (e.g. WebServer and gRPC server when starting processing of a new request) + should execute subsequent methods in that context using `Contexts.inContext()`. + +Each module that hands processing over to an executor service should wrap that +executor service using `Contexts.wrap()`. + +Modules that are interested in using the context should retrieve +the current context using `Contexts.context()`. + +## Examples + +New context created (`RequestRouting.next()`): +```java +Contexts.inContext(nextRequest.context(), () -> nextItem.handlerRoute + .handler() + .accept(nextRequest, nextResponse)); +``` + +Processing through an executor service (`JerseySupport`): +```java +ExecutorService executorService = (service != null) + ? service + : Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); + +this.service = Contexts.wrap(executorService); + +//... +service.submit(() -> {...}); +``` + +Using the context (`HelidonDb`): +```java +Executors.context() + .ifPresent(context -> interceptors + .forEach(interceptor -> interceptor.statement(context, statementName, statement, statementFuture))); +``` \ No newline at end of file diff --git a/docs/src/main/docs/extensions/01_overview.adoc b/docs/src/main/docs/extensions/01_overview.adoc index 618f70fb7b0..e2330e1e562 100644 --- a/docs/src/main/docs/extensions/01_overview.adoc +++ b/docs/src/main/docs/extensions/01_overview.adoc @@ -51,4 +51,11 @@ Create and inject a Jedis pool in your application code. Create and inject an Oracle Cloud Infrastructure Object Storage client in your application code. -- + +[CARD] +.Java Transaction API objects +[link=extensions/05_cdi_jta.adoc] +-- +Use the Java Transaction API in your application code. +-- ==== diff --git a/docs/src/main/docs/extensions/05_cdi_jta.adoc b/docs/src/main/docs/extensions/05_cdi_jta.adoc new file mode 100644 index 00000000000..ea04e913efd --- /dev/null +++ b/docs/src/main/docs/extensions/05_cdi_jta.adoc @@ -0,0 +1,84 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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 extension for JTA +:description: Helidon CDI extension for JTA +:keywords: helidon, java, microservices, microprofile, extensions, cdi, jta + +This https://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#spi[CDI +portable extension] provides support for JTA (Java Transaction API) +transactions in your Helidon MicroProfile applications. + +== Prerequsites + +Declare the following dependency fragment in your project's `pom.xml`: + +[source,xml] +---- + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + runtime + + + + javax.transaction + javax.transaction-api + provided + +---- + +== Declaring a method to be transactional + +The following example shows how to declare a transactional method. + +[source,java] +.Transactional method declaration +---- +@Transactional(Transactional.TxType.REQUIRED) +public void doSomethingTransactionally() { + +} +---- + +The extension ensures that a transaction is started before and +committed after the method executes. If the method throws an +exception, the transaction will be rolled back. + +You can further specify the transactional behavior of the extension by +using different instances of the `Transactional` annotation. For more +information, see the +https://static.javadoc.io/javax.transaction/javax.transaction-api/1.2/javax/transaction/Transactional.html[`Transactional` +annotation documentation]. + +Transactional method support is implemented by CDI interception +facilities. Among other things, this means that the method to which +you apply the `Transactional` annotation must not be `private` and +must in all other ways be a _business method_. See the +https://jcp.org/aboutJava/communityprocess/mrel/jsr318/index3.html[Java +Interceptors specification] for more details. + +During a transactional method invocation, the extension makes the +following objects available for injection via the `Inject` annotation: + +* https://static.javadoc.io/javax.transaction/javax.transaction-api/1.2/javax/transaction/UserTransaction.html[`UserTransaction`] +* https://static.javadoc.io/javax.transaction/javax.transaction-api/1.2/javax/transaction/Transaction.html[`Transaction`] +* https://static.javadoc.io/javax.transaction/javax.transaction-api/1.2/javax/transaction/UserTransactionManager.html[`TransactionManager`] +* https://static.javadoc.io/javax.transaction/javax.transaction-api/1.2/javax/transaction/UserTransactionSynchronizationRegistry.html[`TransactionSynchronizationRegistry`] + + diff --git a/docs/src/main/docs/grpc/01_introduction.adoc b/docs/src/main/docs/grpc/01_introduction.adoc new file mode 100644 index 00000000000..91ce8e196da --- /dev/null +++ b/docs/src/main/docs/grpc/01_introduction.adoc @@ -0,0 +1,80 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + +:pagename: grpc-server-introduction +:description: Helidon gRPC Server Introduction +:keywords: helidon, grpc, java + += gRPC Server Introduction + +Helidon gRPC Server provides a framework for creating link:http://grpc.io/[gRPC] applications. + +=== _Experimental Feature_ +The Helidon gRPC feature is currently experimental and the APIs are subject to changes until gRPC support is stabilized. + +== Quick Start + +Here is the code for a minimalist gRPC application that runs on a default port (1408): + +[source,java] +---- + public static void main(String[] args) throws Exception { + GrpcServer grpcServer = GrpcServer + .create(GrpcRouting.builder() + .register(new HelloService()) // <1> + .build()) + .start() // <2> + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); // <3> + + System.out.println("gRPC Server started at: http://localhost:" + grpcServer.port()); // <4> + } + + static class HelloService implements GrpcService { <5> + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.unary("SayHello", ((request, responseObserver) -> complete(responseObserver, "Hello World!"))); // <6> + } + } +---- + +<1> Register gRPC service. +<2> Start the server. +<3> Wait for the server to start while throwing possible errors as exceptions. +<4> The server is bound to a default port (1408). +<5> Implement the simplest possible gRPC service. +<6> Add unary method `HelloService/SayHello` to the service definition. + +The example above deploys a very simple service to the gRPC server that by default uses Java serialization to marshall +requests and responses. We will look into deployment of "standard" gRPC services that use Protobuf for request and +response marshalling, as well as how you can configure custom marshallers, later in this document. + +== Maven Coordinates + +The <> page describes how you +should declare dependency management for Helidon applications. Then declare the following dependency in your project: + +[source,xml,subs="verbatim,attributes"] +---- + + io.helidon.grpc + helidon-grpc-server + +---- + +<1> Dependency on gRPC Server. diff --git a/docs/src/main/docs/grpc/02_configuration.adoc b/docs/src/main/docs/grpc/02_configuration.adoc new file mode 100644 index 00000000000..bc5a08319cf --- /dev/null +++ b/docs/src/main/docs/grpc/02_configuration.adoc @@ -0,0 +1,69 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + +:javadoc-base-url-api: {javadoc-base-url}?io/helidon/grpc/server +:pagename: grpc-server-configuration +:description: Helidon gRPC Server Configuration +:keywords: helidon, grpc, java, configuration + += gRPC Server Configuration + +Configure the gRPC Server using the Helidon configuration framework, either programmatically +or via a configuration file. + +== Configuring the gRPC Server in your code + +The easiest way to configure the gRPC Server is in your application code. + +[source,java] +---- +GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .port(8080) + .build(); +GrpcServer grpcServer = GrpcServer.create(configuration, routing); +---- + +== Configuring the gRPC Server in a configuration file + +You can also define the configuration in a file. + +[source,hocon] +.GrpcServer configuration file `application.yaml` +---- +grpcserver: + port: 3333 +---- + +Then, in your application code, load the configuration from that file. + +[source,java] +.GrpcServer initialization using the `application.conf` file located on the classpath +---- +GrpcServerConfiguration configuration = GrpcServerConfiguration.create( + Config.builder() + .sources(classpath("application.conf")) + .build()); + +GrpcServer grpcServer = GrpcServer.create(configuration, routing); +---- + +== Configuration options + +See all configuration options + link:{javadoc-base-url-api}/GrpcServerConfiguration.html[here]. + diff --git a/docs/src/main/docs/grpc/03_routing.adoc b/docs/src/main/docs/grpc/03_routing.adoc new file mode 100644 index 00000000000..0bd14cc3707 --- /dev/null +++ b/docs/src/main/docs/grpc/03_routing.adoc @@ -0,0 +1,102 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + +:pagename: grpc-server-routing +:description: Helidon gRPC Server Routing +:keywords: helidon, grpc, java + += gRPC Server Routing + +Unlike Webserver, which allows you to route requests based on path expression +and the HTTP verb, gRPC server always routes requests based on the service and +method name. This makes routing configuration somewhat simpler -- all you need +to do is register your services: + +[source,java] +---- + private static GrpcRouting createRouting(Config config) { + return GrpcRouting.builder() + .register(new GreetService(config)) // <1> + .register(new EchoService()) // <2> + .register(new MathService()) // <3> + .build(); + } +---- + +<1> Register `GreetService` instance. +<2> Register `EchoService` instance. +<3> Register `MathService` instance. + +Both "standard" gRPC services that implement `io.grpc.BindableService` interface +(typically implemented by extending generated server-side stub and overriding +its methods), and Helidon gRPC services that implement +`io.helidon.grpc.server.GrpcService` interface can be registered. + +The difference is that Helidon gRPC services allow you to customize behavior +down to the method level, and provide a number of useful helper methods that +make service implementation easier, as we'll see in a moment. + +== Customizing Service Definitions + +When registering a service, regardless of its type, you can customize its +descriptor by providing configuration consumer as a second argument to the +`register` method. + +This is particularly useful when registering standard `BindableService` +instances, as it allows you to add certain Helidon-specific behaviors, such as +<<06_health_checks.adoc, health checks>> and <<07_metrics.adoc, metrics>> to them: + +[source,java] +---- + private static GrpcRouting createRouting(Config config) { + return GrpcRouting.builder() + .register(new GreetService(config)) + .register(new EchoService(), service -> { + service.healthCheck(CustomHealthChecks::echoHealthCheck) // <1> + .metered(); // <2> + }) + .build(); + } +---- + +<1> Add custom health check to the service. +<2> Specify that all the calls to service methods should be metered. + +== Specifying Global Interceptors + +`GrpcRouting` also allows you to specify <<05_interceptors.adoc, custom interceptors>> +that will be applied to all registered services. + +This is useful to configure features such as tracing, security and metrics collection, +and we provide built-in interceptors for those purposes that you can simply register +with the routing definition: + +[source,java] +---- + private static GrpcRouting createRouting(Config config) { + return GrpcRouting.builder() + .intercept(GrpcMetrics.timed()) // <1> + .register(new GreetService(config)) + .register(new EchoService()) + .register(new MathService()) + .build(); + } +---- + +<1> Register `GrpcMetrics` interceptor that will collect timers for all methods of + all services (but can be overridden at the individual service or even method level). diff --git a/docs/src/main/docs/grpc/04_service_implementation.adoc b/docs/src/main/docs/grpc/04_service_implementation.adoc new file mode 100644 index 00000000000..d1ae3b962dd --- /dev/null +++ b/docs/src/main/docs/grpc/04_service_implementation.adoc @@ -0,0 +1,163 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + +:javadoc-base-url-api: {javadoc-base-url}?io/helidon/grpc/server +:pagename: grpc-server-service-implementation +:description: Helidon gRPC Service Implementation +:keywords: helidon, grpc, java + += Service Implementation + +While Helidon gRPC Server allows you to deploy any standard gRPC service that +implements `io.grpc.BindableService` interface, including services generated +from the Protobuf IDL files (and even allows you to customize them to a certain +extent), using Helidon gRPC framework to implement your services has a number of +benefits: + +* It allows you to define both HTTP and gRPC services using similar programming + model, simplifying learning curve for developers. + +* It provides a number of helper methods that make service implementation + significantly simpler. + +* It allows you to configure some of the Helidon value-added features, such + as <<08_security.adoc, security>> and <<07_metrics.adoc, metrics collection>> + down to the method level. + +* It allows you to easily specify custom marshaller for requests and + responses if Protobuf does not satisfy your needs. + +* It provides built in support for <<06_health_checks.adoc, health checks>>. + +== Service Implementation Basics + +At the very basic level, all you need to do in order to implement a Helidon +gRPC service is create a class that implements `io.helidon.grpc.server.GrpcService` +interface and define one or more methods for the service: + +[source,java] +---- +class EchoService implements GrpcService { + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.unary("Echo", this::echo); // <1> + } + + /** + * Echo the message back to the caller. + * + * @param request the echo request containing the message to echo + * @param observer the response observer + */ + public void echo(String request, StreamObserver observer) { // <2> + complete(observer, request); // <3> + } +} +---- + +<1> Define unary method `Echo` and map it to the `this::echo` handler. +<2> Create a handler for the `Echo` method. +<3> Send the request string back to the client by completing response observer. + +NOTE: The `complete` method shown in the example above is just one of many helper + methods available in the `GrpcService` class. See the full list + link:{javadoc-base-url-api}/GrpcService.html[here]. + +The example above implements a service with a single unary method, which will be +exposed at the `EchoService/Echo' endpoint. The service does not explicitly define +a marshaller for requests and responses, so Java serialization will be used as a +default. + +Unfortunately, this implies that you will have to implement clients by hand and +configure them to use the same marshaller as the server. Obviously, one of the +major selling points of gRPC is that it makes it easy to generate clients for a +number of languages (as long as you use Protobuf for marshalling), so let's see +how we would implement Protobuf enabled Helidon gRPC service. + +== Implementing Protobuf Services + +In order to implement Protobuf-based service, you would follow the official +link:https://grpc.io/docs/quickstart/java.html[instructions] on the gRPC +web site, which boil down to the following: + +==== Define the Service IDL + +For this example, we will re-implement the `EchoService` above as a Protobuf +service in `echo.proto` file. + +[source, proto] +---- +syntax = "proto3"; +option java_package = "org.example.services.echo"; + +service EchoService { + rpc Echo (EchoRequest) returns (EchoResponse) {} +} + +message EchoRequest { + string message = 1; +} + +message EchoResponse { + string message = 1; +} +---- + +Based on this IDL, the gRPC compiler will generate message classes (`EchoRequest` +and `EchoResponse`), client stubs that can be used to make RPC calls to the server, +as well as the base class for the server-side service implementation. + +We can ignore the last one, and implement the service using Helidon gRPC framework +instead. + +==== Implement the Service + +The service implementation will be very similar to our original implementation: + +[source,java] +---- +class EchoService implements GrpcService { + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.proto(Echo.getDescriptor()) // <1> + .unary("Echo", this::echo); // <2> + } + + /** + * Echo the message back to the caller. + * + * @param request the echo request containing the message to echo + * @param observer the response observer + */ + public void echo(Echo.EchoRequest request, StreamObserver observer) { // <3> + String message = request.getMessage(); // <4> + Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build(); // <5> + complete(observer, response); // <6> + } +} +---- + +<1> Specify proto descriptor in order to provide necessary type information and + enable Protobuf marshalling. +<2> Define unary method `Echo` and map it to the `this::echo` handler. +<3> Create a handler for the `Echo` method, using Protobuf message types for request and response. +<4> Extract message string from the request. +<5> Create the response containing extracted message. +<6> Send the response back to the client by completing response observer. diff --git a/docs/src/main/docs/grpc/05_interceptors.adoc b/docs/src/main/docs/grpc/05_interceptors.adoc new file mode 100644 index 00000000000..4def7c98793 --- /dev/null +++ b/docs/src/main/docs/grpc/05_interceptors.adoc @@ -0,0 +1,123 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + +:pagename: grpc-server-interceptors +:description: Helidon gRPC Service Interceptors +:keywords: helidon, grpc, java + += Interceptors + +Helidon gRPC allows you to configure standard `io.grpc.ServerInterceptor`s. + +For example, you could implement an interceptor that logs each RPC call: + +[source,java] +---- +class LoggingInterceptor implements ServerInterceptor { // <1> + + private static final Logger LOG = Logger.getLogger(LoggingInterceptor.class.getName()); + + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata metadata, + ServerCallHandler handler) { + + LOG.info(() -> "CALL: " + call.getMethodDescriptor()); // <2> + return handler.startCall(call, metadata); // <3> + } +} +---- + +<1> Implement `io.grpc.ServerInterceptor` +<2> Implement the logging logic +<3> Start intercepted call + +== Registering Interceptors + +You can register interceptors globally, in which case they will be applied to all +methods of all services, by simply adding them to the `GrpcRouting` instance: + +[source,java] +---- +private static GrpcRouting createRouting(Config config) { + return GrpcRouting.builder() + .intercept(new LoggingInterceptor()) // <1> + .register(new GreetService(config)) + .register(new EchoService()) + .build(); +} +---- + +<1> Adds `LoggingInterceptor` to all methods of `GreetService` and `EchoService` + +You can also register an interceptor for a specific service, either by implementing +`GrpcService.update` method: + +[source,java] +---- +public class MyService implements GrpcService { + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.intercept(new LoggingInterceptor()) // <1> + .unary("MyMethod", this::myMethod); + } + + private void myMethod(ReqT request, StreamObserver observer) { + // do something + } +} +---- + +<1> Adds `LoggingInterceptor` to all methods of `MyService` + +Or by configuring `ServiceDescriptor` externally, when creating `GrpcRouting`, which +allows you to add interceptors to plain `io.grpc.BindableService` services as well: + +[source,java] +---- +private static GrpcRouting createRouting(Config config) { + return GrpcRouting.builder() + .register(new GreetService(config), cfg -> cfg.intercept(new LoggingInterceptor())) // <1> + .register(new EchoService()) + .build(); +} +---- + +<1> Adds `LoggingInterceptor` to all methods of `GreetService` only + +Finally, you can also register an interceptor at the method level: + +[source,java] +---- +public class MyService implements GrpcService { + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.unary("MyMethod", + this::myMethod, + cfg -> cfg.intercept(new LoggingInterceptor())); // <1> + } + + private void myMethod(ReqT request, StreamObserver observer) { + // do something + } +} +---- + +<1> Adds `LoggingInterceptor` to `MyService::MyMethod` only diff --git a/docs/src/main/docs/grpc/06_health_checks.adoc b/docs/src/main/docs/grpc/06_health_checks.adoc new file mode 100644 index 00000000000..2d54bd0d140 --- /dev/null +++ b/docs/src/main/docs/grpc/06_health_checks.adoc @@ -0,0 +1,116 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + +:pagename: grpc-server-health-checks +:description: Helidon gRPC Service Health Checks +:keywords: helidon, grpc, java + += Service Health Checks + +Helidon gRPC services provide a built-in support for Helidon Health Checks. + +Unless a custom health check is implemented by the service developer, each service +deployed to the gRPC server will be provisioned with a default health check, which +always returns status of `UP`. + +This allows all services, including the ones that don't have a meaningful health check, +to show up in the health report (or to be queried for health) without service developer +having to do anything. + +However, services that do need custom health checks can easily define one, +directly within `GrpcService` implementation: + +[source,java] +---- +public class MyService implements GrpcService { + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.unary("MyMethod", this::myMethod) + .healthCheck(this::healthCheck); // <1> + } + + private HealthCheckResponse healthCheck() { + boolean fUp = isMyServiceUp(); // <2> + return HealthCheckResponse + .named(name()) // <3> + .state(fUp) // <4> + .withData("ts", System.currentTimeMillis()) // <5> + .build(); + } + + private void myMethod(ReqT request, StreamObserver observer) { + // do something + } +} +---- + +<1> Configure a custom health check for the service +<2> Determine service status +<3> Use service name as a health check name for consistency +<4> Use determined service status +<5> Optionally, provide additional metadata + +You can also define custom health check for an existing service, including plain +`io.grpc.BindableService` implementations, using service configurer inside the +`GrpcRouting` deefinition: + +[source,java] +---- +private static GrpcRouting createRouting() { + return GrpcRouting.builder() + .register(new EchoService(), cfg -> cfg.healthCheck(MyCustomHealthChecks::echoHealthCheck)) // <1> + .build(); +} +---- + +<1> Configure custom health check for an existing or legacy service + +== Exposing Health Checks + +All gRPC service health checks are managed by the Helidon gRPC Server, and are +automatically exposed to the gRPC clients using custom implementation of the +standard gRPC `HealthService` API. + +However, they can also be exposed to REST clients via standard Helidon/Microprofile +`/health` endpoint: + +[source,java] +---- + GrpcServer grpcServer = GrpcServer.create(grpcServerConfig(), createRouting(config)); // <1> + grpcServer.start(); // <2> + + HealthSupport health = HealthSupport.builder() + .add(grpcServer.healthChecks()) // <3> + .build(); + + Routing routing = Routing.builder() + .register(health) // <4> + .build(); + + WebServer.create(webServerConfig(), routing).start(); // <5> +---- + +<1> Create `GrpcServer` instance +<2> Start gRPC server, which will deploy all services and register default and custom health checks +<3> Add gRPC server managed health checks to `HealthSupport` instance +<4> Add `HealthSupport` to the web server routing definition +<5> Create and start web server + +All gRPC health checks will now be available via `/health` REST endpoint, in +addition to the standard gRPC `HealthService` diff --git a/docs/src/main/docs/grpc/07_metrics.adoc b/docs/src/main/docs/grpc/07_metrics.adoc new file mode 100644 index 00000000000..79f92fbcac7 --- /dev/null +++ b/docs/src/main/docs/grpc/07_metrics.adoc @@ -0,0 +1,199 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + +:pagename: grpc-server-metrics +:description: Helidon gRPC Service Metrics +:keywords: helidon, grpc, java + += Service Metrics + +Helidon gRPC Server has built-in support for metrics capture, which allows +service developers to easily enable application-level metrics for their services. + +== Enabling Metrics Capture + +By default, gRPC Server only captures two vendor-level metrics: `grpc.request.count` +and `grpc.request.meter`. These metrics provide aggregate view of requests across +all services, and serve as an indication of the overall server load. + +However, users can enable more fine grained metrics by simply configuring a built-in +`GrpcMetrics` interceptor within the routing: + +[source,java] +---- + private static GrpcRouting createRouting(Config config) { + return GrpcRouting.builder() + .intercept(GrpcMetrics.timed()) // <1> + .register(new GreetService(config)) + .register(new EchoService()) + .build(); + } +---- + +<1> Capture metrics for all methods of all services as a `timer` + +In the example above we have chosen to create and keep a `timer` metric type for +each method of each service. Alternatively, we could've chosen to use a +`counter`, `meter` or a `histogram` instead. + +== Overriding Metrics Capture + +While global metrics capture is certainly useful, it is not always sufficient. +Keeping a separate `timer` for each gRPC method may be an overkill, so the user +could decide to use a lighter-weight metric type, such as `counter` or a `meter`. + +However, she may still want to enable `histogram` or a `timer` for some services, +or even only some methods of some services. + +This can be easily accomplished by overriding the type of the captured metric at +either service or the method level: + +[source,java] +---- + private static GrpcRouting createRouting(Config config) { + return GrpcRouting.builder() + .intercept(GrpcMetrics.counted()) // <1> + .register(new MyService()) + .build(); + } + + public static class MyService implements GrpcService { + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules + .intercept(GrpcMetrics.metered()) // <2> + .unary("MyMethod", this::myMethod, + cfg -> cfg.intercept(GrpcMetrics.timer())) // <3> + } + + private void myMethod(ReqT request, StreamObserver observer) { + // do something + } + } +---- + +<1> Use `counter` for all methods of all services, unless overridden +<2> Use `meter` for all methods of `MyService` +<3> Use `timer` for `MyService::MyMethod` + +== Exposing Metrics Externally + +Collected metrics are stored in the standard Helidon Metric Registries, such as vendor and +application registry, and can be exposed via standard `/metrics` REST API. + +[source,java] +---- + Routing routing = Routing.builder() + .register(MetricsSupport.create()) // <1> + .build(); + + WebServer.create(webServerConfig(), routing) // <2> + .start() +---- + +<1> Add `MetricsSupport` instance to web server routing +<2> Create and start Helidon web server + +See <> documentation for more details. + +== Specifying Metric Meta-data + +Helidon metrics contain meta-data such as tags, a description, units etc. It is possible to +add this additional meta-data when specifying the metrics. + +=== Adding Tags + +To add tags to a metric a `Map` of key/value tags can be supplied. +For example: +[source,java] +---- +Map tagMap = new HashMap<>(); +tagMap.put("keyOne", "valueOne"); +tagMap.put("keyTwo", "valueTwo"); + +GrpcRouting routing = GrpcRouting.builder() + .intercept(GrpcMetrics.counted().tags(tagMap)) // <1> + .register(new MyService()) + .build(); +---- + +<1> the `tags()` method is used to add the `Map` of tags to the metric. + +=== Adding a Description + +A meaningful description can be added to a metric: +For example: +[source,java] +---- +GrpcRouting routing = GrpcRouting.builder() + .intercept(GrpcMetrics.counted().description("Something useful")) // <1> + .register(new MyService()) + .build(); +---- + +<1> the `description()` method is used to add the description to the metric. + +=== Adding Metric Units + +A units value can be added to the Metric: +For example: +[source,java] +---- +GrpcRouting routing = GrpcRouting.builder() + .intercept(GrpcMetrics.timed().units(MetricUnits.SECONDS)) // <1> + .register(new MyService()) + .build(); +---- + +<1> the `units()` method is used to add the metric units to the metric. +Typically the units value is one of the constants from `org.eclipse.microprofile.metrics.MetricUnits` class. + +== Overriding the Metric Name + +By default the metric name is the gRPC service name followed by a dot ('.') followed by the method name. +It is possible to supply a function that can be used to override the default behaviour. + +The function should implement the `io.helidon.grpc.metrics.GrpcMetrics.NamingFunction` interface +[source,java] +---- + @FunctionalInterface + public interface NamingFunction { + /** + * Create a metric name. + * + * @param service the service descriptor + * @param methodName the method name + * @param metricType the metric type + * @return the metric name + */ + String createName(ServiceDescriptor service, String methodName, MetricType metricType); + } +---- +This is a functional interface so lambda can be used too. + +For example: +[source,java] +---- +GrpcRouting routing = GrpcRouting.builder() + .intercept(GrpcMetrics.counted() + .nameFunction((svc, method, metric) -> "grpc." + service.name() + '.' + method) // <1> +---- +<1> the `NamingFunction` is just a lambda that returns the concatenated service name and method name +with the prefix `grpc.` So for a service "Foo", method "bar" the above example would produce a name +"grpc.Foo.bar". diff --git a/docs/src/main/docs/grpc/08_security.adoc b/docs/src/main/docs/grpc/08_security.adoc new file mode 100644 index 00000000000..c60a5a39f81 --- /dev/null +++ b/docs/src/main/docs/grpc/08_security.adoc @@ -0,0 +1,172 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + +:description: Helidon Security gRPC integration +:keywords: helidon, grpc, security + += gRPC Server Security +Security integration of the <> + +[source,xml] +.Maven Dependency +---- + + io.helidon.security.integration + helidon-security-integration-grpc + +---- + +==== Bootstrapping + +There are two steps to configure security with gRPC server: + +1. Create security instance and register it with server +2. Protect gRPC services of server with various security features + +[source,java] +.Example using builders +---- +// gRPC server's routing +GrpcRouting.builder() + // This is step 1 - register security instance with gRPC server processing + // security - instance of security either from config or from a builder + // securityDefaults - default enforcement for each service that has a security definition + .intercept(GrpcSecurity.create(security).securityDefaults(GrpcSecurity.authenticate())) + // this is step 2 - protect a service + // register and protect this service with authentication (from defaults) and role "user" + .register(greetService, GrpcSecurity.rolesAllowed("user")) + .build(); +---- + +[source,java] +.Example using builders for more fine grained method level security +---- +// create the service descriptor +ServiceDescriptor greetService = ServiceDescriptor.builder(new GreetService()) + // Add an instance of gRPC security that will apply to all methods of + // the service - in this case require the "user" role + .intercept(GrpcSecurity.rolesAllowed("user")) + // Add an instance of gRPC security that will apply to the "SetGreeting" + // method of the service - in this case require the "admin" role + .intercept("SetGreeting", GrpcSecurity.rolesAllowed("admin")) + .build(); + +// Create the gRPC server's routing +GrpcRouting.builder() + // This is step 1 - register security instance with gRPC server processing + // security - instance of security either from config or from a builder + // securityDefaults - default enforcement for each service that has a security definition + .intercept(GrpcSecurity.create(security).securityDefaults(GrpcSecurity.authenticate())) + // this is step 2 - add the service descriptor + .register(greetService) + .build(); +---- + +[source,java] +.Example using configuration +---- +GrpcRouting.builder() + // helper method to load both security and gRPC server security from configuration + .intercept(GrpcSecurity.create(config)) + // continue with gRPC server route configuration... + .register(new GreetService()) + .build(); +---- + +[source,conf] +.Example using configuration - configuration (HOCON) +---- +# This may change in the future - to align with gRPC server configuration, +# once it is supported +security + grpc-server: + # Configuration of integration with gRPC server + defaults: + authenticate: true + # Configuration security for individual services + services: + - name: "GreetService" + defaults: + roles-allowed: ["user"] + # Configuration security for individual methods of the service + methods: + - name: "SetGreeting" + roles-allowed: ["admin"] +---- + +==== Outbound security +Outbound security covers three scenarios: + +* Calling a secure gRPC service from inside a gRPC service method handler +* Calling a secure gRPC service from inside a web server method handler +* Calling a secure web endpoint from inside a gRPC service method handler + +Within each scenario credentials can be propagated if the gRPC/http method +handler is executing within a security context or credentials can be overridden +to provide a different set of credentials to use to call the outbound endpoint. + +[source,java] +.Example calling a secure gRPC service from inside a gRPC service method handler +---- +// Obtain the SecurityContext from the current gRPC call Context +SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(); + +// Create a gRPC CallCredentials that will use the current request's +// security context to configure outbound credentials +GrpcClientSecurity clientSecurity = GrpcClientSecurity.create(securityContext); + +// Create the gRPC stub using the CallCredentials +EchoServiceGrpc.EchoServiceBlockingStub stub = noCredsEchoStub.withCallCredentials(clientSecurity); +---- + +[source,java] +.Example calling a secure gRPC service from inside a web server method handler +---- +private static void propagateCredentialsWebRequest(ServerRequest req, ServerResponse res) { + try { + // Create a gRPC CallCredentials that will use the current request's + // security context to configure outbound credentials + GrpcClientSecurity clientSecurity = GrpcClientSecurity.create(req); + + // Create the gRPC stub using the CallCredentials + EchoServiceGrpc.EchoServiceBlockingStub stub = noCredsEchoStub.withCallCredentials(clientSecurity); + + String message = req.queryParams().first("message").orElse(null); + Echo.EchoResponse echoResponse = stub.echo(Echo.EchoRequest.newBuilder().setMessage(message).build()); + res.send(echoResponse.getMessage()); + } catch (StatusRuntimeException e) { + res.status(GrpcHelper.toHttpResponseStatus(e)).send(); + } catch (Throwable thrown) { + res.status(Http.ResponseStatus.create(500, thrown.getMessage())).send(); + } +} +---- + +[source,java] +.Example calling a secure web endpoint from inside a gRPC service method handler +---- +// Obtain the SecurityContext from the gRPC call Context +SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(); + +// Use the SecurityContext as normal to make a http request +Response webResponse = client.target(url) + .path("/test") + .request() + .property(ClientSecurityFeature.PROPERTY_CONTEXT, securityContext) + .get(); +---- diff --git a/docs/src/main/docs/grpc/09_marshalling.adoc b/docs/src/main/docs/grpc/09_marshalling.adoc new file mode 100644 index 00000000000..723d200b143 --- /dev/null +++ b/docs/src/main/docs/grpc/09_marshalling.adoc @@ -0,0 +1,36 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + +:pagename: grpc-server-metrics +:description: Helidon gRPC Marshalling +:keywords: helidon, grpc, java + += Marshalling + +== Default Marshalling Support + +=== Protobuf Marshalling + +=== Java Serialization Marshalling + +== Custom Marshalling + +=== Marshaller + +=== Marshaller Supplier + diff --git a/docs/src/main/docs/microprofile/01_introduction.adoc b/docs/src/main/docs/microprofile/01_introduction.adoc index 9891f01a743..ad5fc34c022 100644 --- a/docs/src/main/docs/microprofile/01_introduction.adoc +++ b/docs/src/main/docs/microprofile/01_introduction.adoc @@ -16,23 +16,35 @@ /////////////////////////////////////////////////////////////////////////////// -= Microprofile Introduction -:description: Helidon Microprofile introduction += MicroProfile Introduction +:description: Helidon MicroProfile introduction :keywords: helidon, microprofile, micro-profile -MicroProfile is a platform definition that is familiar to Java EE developers. If you have experience with JAX-RS, JSON-P, and CDI, you -may prefer to use this model. - -To extend the functionality of your MicroProfile application, you might also decide to use the Helidon core APIs, especially for -configuration and security. +MicroProfile is a collection of enterprise Java APIs that should feel familiar to +Java EE developers. MicroProfile includes existing APIs such as JAX-RS, JSON-P and +CDI, and adds additional APIs in areas such as configuration, metrics, fault +tolerance and more. == Getting Started with Helidon MicroProfile +Helidon MP {helidon-version} supports +MicroProfile {mp-version}. You can find the exact version of APIs supported on the +https://github.com/oracle/helidon/wiki/Supported-APIs[Helidon Supported APIs] +wiki page. + +Helidon provides a MicroProfile server implementation (`io.helidon.microprofile.server`) that +encapsulates the Helidon WebServer. You can either instantiate the server directly +as is done in the +<> +or use its built-in `main` as shown below. + Complete these tasks to get started with your MicroProfile application. === Maven Coordinates -Declare the following dependency in your project: +The <> page describes +how you should declare dependency management for Helidon applications. +Then declare the following dependency in your project: [source,xml] .Maven Dependency @@ -102,7 +114,7 @@ Run the main class. The server will start on port 7001 and serve your === Adding Jandex -Jandex is an indexing tool for Weld (CDI implementation) that helps speed up +Jandex is an indexing tool for Weld (the CDI implementation used by Helidon) that helps speed up the boot time of an application. To use Jandex, configure a Maven plugin that adds the index to your diff --git a/docs/src/main/docs/microprofile/04_static-content.adoc b/docs/src/main/docs/microprofile/04_static-content.adoc index df2fb1a2ac0..1dcca1a4d6e 100644 --- a/docs/src/main/docs/microprofile/04_static-content.adoc +++ b/docs/src/main/docs/microprofile/04_static-content.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ /////////////////////////////////////////////////////////////////////////////// = Serving Static Content -:description: Helidon Microprofile static content +:description: Helidon MicroProfile static content :keywords: helidon, microprofile, micro-profile You can serve static content from a location in a file system @@ -31,9 +31,9 @@ You can serve static content from a location in a file system # Location of content on file system server.static.path.location=/var/www/html # default is index.html -server.static.classpath.welcome=resource.html +server.static.path.welcome=resource.html # static content path - default is "/" -# server.static.classpath.context=/static-file +# server.static.path.context=/static-file ---- [source,properties] diff --git a/docs/src/main/docs/microprofile/07_tracing.adoc b/docs/src/main/docs/microprofile/07_tracing.adoc index 1a041faea2c..ed2e60afdb6 100644 --- a/docs/src/main/docs/microprofile/07_tracing.adoc +++ b/docs/src/main/docs/microprofile/07_tracing.adoc @@ -49,7 +49,7 @@ tracing.service=helidon-mp For additional supported properties, please see <> == Creating custom spans -Microprofile OpenTracing implementation will add support to simply +MicroProfile OpenTracing implementation will add support to simply add custom spans by annotation. Until we implement this support, you can configure custom spans as follows (in JAX-RS resources): diff --git a/docs/src/main/docs/sitegen.yaml b/docs/src/main/docs/sitegen.yaml index 87b7dc2f2a0..8ccff23f7d5 100644 --- a/docs/src/main/docs/sitegen.yaml +++ b/docs/src/main/docs/sitegen.yaml @@ -23,6 +23,7 @@ engine: plantumlconfig: "_plantuml-config.txt" javadoc-base-url: "./apidocs/index.html" helidon-version: "${project.version}" + mp-version: "1.2" guides-dir: "${project.basedir}/../examples/guides" assets: - target: "/" @@ -69,6 +70,14 @@ backend: items: - includes: - "webserver/*.adoc" + - title: "gRPC server" + pathprefix: "/grpc" + glyph: + type: "icon" + value: "swap_horiz" + items: + - includes: + - "grpc/*.adoc" - title: "Config" pathprefix: "/config" glyph: @@ -85,7 +94,7 @@ backend: items: - includes: - "security/*.adoc" - - title: "Microprofile" + - title: "MicroProfile" pathprefix: "/microprofile" glyph: type: "icon" diff --git a/docs/src/main/docs/tracing/01_tracing.adoc b/docs/src/main/docs/tracing/01_tracing.adoc index 65cb1325c56..78c55e7faac 100644 --- a/docs/src/main/docs/tracing/01_tracing.adoc +++ b/docs/src/main/docs/tracing/01_tracing.adoc @@ -22,7 +22,7 @@ == Tracing Support Helidon includes support for tracing through the `https://opentracing.io/[OpenTracing]` APIs. -Tracing is integrated with WebServer and Security. +Tracing is integrated with WebServer, gRPC Server, and Security. Support for specific tracers is abstracted. Your application can depend on the abstraction layer and provide a specific tracer implementation as a Java @@ -42,6 +42,8 @@ Declare the following dependency in your project to use the tracer abstraction: ---- +=== Configuring Tracing with WebServer + To configure tracer with WebServer: [source,java] @@ -56,6 +58,40 @@ ServerConfiguration.builder() <1> The name of the application (service) to associate with the tracing events <2> The endpoint for tracing events, specific to the tracer used, usually loaded from Config +=== Configuring Tracing with gRPC Server + +[source,java] + +.Configuring OpenTracing `Tracer` +---- +Tracer tracer = (Tracer) TracerBuilder.create("Server") + .collectorUri(URI.create("http://10.0.0.18:9411")) // <1> + .build(); +---- +<3> If using zipkin tracing system, the endpoint would be: +---- +http://10.0.0.18:9411/api/v2/spans +---- + +.Configuring Tracing Attributes +---- +TracingConfiguration tracingConfig = new TracingConfiguration.Builder() + .withStreaming() + .withVerbosity() + .withTracedAttributes(ServerRequestAttribute.CALL_ATTRIBUTES, + ServerRequestAttribute.HEADERS, + ServerRequestAttribute.METHOD_NAME) + .build(); +---- + +.Configuring gRPC Server +---- +GrpcServerConfiguration serverConfig = GrpcServerConfiguration.builder().port(0) + .tracer(tracer) + .tracingConfig(tracingConfig) + .build(); +---- + === Configuration using Helidon Config [[Tracing-config]] There is a set of common configuration options that this section describes. In addition each tracer implementation may have additional configuration options - please see the documentation of each of them. diff --git a/docs/src/main/docs/webserver/01_introduction.adoc b/docs/src/main/docs/webserver/01_introduction.adoc index 679c1f90d2b..66f6d778422 100644 --- a/docs/src/main/docs/webserver/01_introduction.adoc +++ b/docs/src/main/docs/webserver/01_introduction.adoc @@ -50,7 +50,7 @@ Here is the code for a minimalist web application that runs on a random free por == Maven Coordinates -The <> page describes how you +The <> page describes how you should declare dependency management for Helidon applications. Then declare the following dependency in your project: [source,xml,subs="verbatim,attributes"] diff --git a/examples/grpc/README.md b/examples/grpc/README.md new file mode 100644 index 00000000000..879c1c7d6d2 --- /dev/null +++ b/examples/grpc/README.md @@ -0,0 +1,4 @@ + +# Helidon SE gRPC Server Examples + + diff --git a/examples/grpc/basics/README.md b/examples/grpc/basics/README.md new file mode 100644 index 00000000000..ef8ae7ee989 --- /dev/null +++ b/examples/grpc/basics/README.md @@ -0,0 +1,16 @@ + +# Helidon gRPC Example + +A basic example gRPC server. + +## Build + +``` +mvn package +``` + +## Run + +``` +mvn exec:java +``` diff --git a/examples/grpc/basics/pom.xml b/examples/grpc/basics/pom.xml new file mode 100644 index 00000000000..fb45299efb1 --- /dev/null +++ b/examples/grpc/basics/pom.xml @@ -0,0 +1,67 @@ + + + + + 4.0.0 + + io.helidon.examples.grpc + helidon-examples-grpc-project + 1.0.4-SNAPSHOT + + helidon-examples-grpc-basics + Helidon gRPC Server Examples Basics + + + Examples of elementary use of the gRPC Server + + + + io.helidon.grpc.examples.basics.Server + + + + + io.helidon.examples.grpc + helidon-examples-grpc-common + ${project.version} + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + + + + io.helidon.grpc + helidon-grpc-client + ${project.version} + + + + io.helidon.health + helidon-health-checks + ${project.version} + + + io.helidon.bundles + helidon-bundles-config + ${project.version} + + + diff --git a/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/HealthClient.java b/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/HealthClient.java new file mode 100644 index 00000000000..27a90f814f1 --- /dev/null +++ b/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/HealthClient.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.basics; + +import io.grpc.Channel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.health.v1.HealthCheckRequest; +import io.grpc.health.v1.HealthGrpc; + +/** + * A simple gRPC health check client. + */ +public class HealthClient { + + private HealthClient() { + } + + /** + * The program entry point. + * + * @param args the program arguments + * + * @throws Exception if an error occurs + */ + public static void main(String[] args) { + Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408).usePlaintext().build(); + + HealthGrpc.HealthBlockingStub health = HealthGrpc.newBlockingStub(channel); + System.out.println(health.check(HealthCheckRequest.newBuilder().setService("GreetService").build())); + System.out.println(health.check(HealthCheckRequest.newBuilder().setService("FooService").build())); + } +} diff --git a/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/Server.java b/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/Server.java new file mode 100644 index 00000000000..9d8f88302f9 --- /dev/null +++ b/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/Server.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.basics; + +import java.util.logging.LogManager; + +import io.helidon.config.Config; +import io.helidon.grpc.examples.common.GreetService; +import io.helidon.grpc.examples.common.GreetServiceJava; +import io.helidon.grpc.examples.common.StringService; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.health.HealthSupport; +import io.helidon.health.checks.HealthChecks; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.WebServer; + +/** + * A basic example of a Helidon gRPC server. + */ +public class Server { + + private Server() { + } + + /** + * The main program entry point. + * + * @param args the program arguments + * + * @throws Exception if an error occurs + */ + public static void main(String[] args) throws Exception { + // By default this will pick up application.yaml from the classpath + Config config = Config.create(); + + // load logging configuration + LogManager.getLogManager().readConfiguration( + Server.class.getResourceAsStream("/logging.properties")); + + // Get gRPC server config from the "grpc" section of application.yaml + GrpcServerConfiguration serverConfig = + GrpcServerConfiguration.builder(config.get("grpc")).build(); + + GrpcServer grpcServer = GrpcServer.create(serverConfig, createRouting(config)); + + // Try to start the server. If successful, print some info and arrange to + // print a message at shutdown. If unsuccessful, print the exception. + grpcServer.start() + .thenAccept(s -> { + System.out.println("gRPC server is UP! http://localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("gRPC server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("Startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + + // add support for standard and gRPC health checks + HealthSupport health = HealthSupport.builder() + .add(HealthChecks.healthChecks()) + .add(grpcServer.healthChecks()) + .build(); + + // start web server with health endpoint + Routing routing = Routing.builder() + .register(health) + .build(); + + ServerConfiguration webServerConfig = ServerConfiguration.builder(config.get("webserver")).build(); + + WebServer.create(webServerConfig, routing) + .start() + .thenAccept(s -> { + System.out.println("HTTP server is UP! http://localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("HTTP server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("Startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + } + + private static GrpcRouting createRouting(Config config) { + GreetService greetService = new GreetService(config); + GreetServiceJava greetServiceJava = new GreetServiceJava(config); + + return GrpcRouting.builder() + .register(greetService) + .register(greetServiceJava) + .register(new StringService()) + .build(); + } +} diff --git a/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/package-info.java b/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/package-info.java new file mode 100644 index 00000000000..fa903037233 --- /dev/null +++ b/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.Server Main} class. + */ +package io.helidon.grpc.examples.basics; diff --git a/examples/grpc/basics/src/main/resources/application.yaml b/examples/grpc/basics/src/main/resources/application.yaml new file mode 100644 index 00000000000..2afc282df4a --- /dev/null +++ b/examples/grpc/basics/src/main/resources/application.yaml @@ -0,0 +1,26 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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" + +grpc: + name: "test.server" + port: 1408 + +webserver: + port: 8080 + bind-address: "0.0.0.0" \ No newline at end of file diff --git a/examples/grpc/basics/src/main/resources/logging.properties b/examples/grpc/basics/src/main/resources/logging.properties new file mode 100644 index 00000000000..f903114790c --- /dev/null +++ b/examples/grpc/basics/src/main/resources/logging.properties @@ -0,0 +1,37 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden 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 +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +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 +#io.netty.level=INFO diff --git a/examples/grpc/common/pom.xml b/examples/grpc/common/pom.xml new file mode 100644 index 00000000000..da988014671 --- /dev/null +++ b/examples/grpc/common/pom.xml @@ -0,0 +1,98 @@ + + + + + 4.0.0 + + io.helidon.examples.grpc + helidon-examples-grpc-project + 1.0.4-SNAPSHOT + + helidon-examples-grpc-common + Helidon gRPC Server Examples ProtoBuf Services + + + ProtoBuf generated gRPC services used in gRPC examples + + + + io.helidon.grpc.examples.common.GreetClient + + + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + + + + io.helidon.grpc + helidon-grpc-client + ${project.version} + + + + io.grpc + grpc-netty + + + io.grpc + grpc-stub + + + io.grpc + grpc-services + + + io.grpc + grpc-protobuf + + + + io.helidon.common + helidon-common + ${project.version} + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + compile + compile-custom + + + + + + + diff --git a/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/GreetClient.java b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/GreetClient.java new file mode 100644 index 00000000000..817136d247a --- /dev/null +++ b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/GreetClient.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.common; + +import java.net.URI; + +import io.helidon.grpc.client.ClientRequestAttribute; +import io.helidon.grpc.client.ClientTracingInterceptor; +import io.helidon.grpc.examples.common.Greet.GreetRequest; +import io.helidon.grpc.examples.common.Greet.SetGreetingRequest; +import io.helidon.tracing.TracerBuilder; + +import io.grpc.Channel; +import io.grpc.ClientInterceptors; +import io.grpc.ManagedChannelBuilder; +import io.opentracing.Tracer; + +/** + * A client for the {@link GreetService}. + */ +public class GreetClient { + + private GreetClient() { + } + + /** + * The program entry point. + * @param args the program arguments + * + * @throws Exception if an error occurs + */ + public static void main(String[] args) throws Exception { + Tracer tracer = (Tracer) TracerBuilder.create("Client") + .collectorUri(URI.create("http://localhost:9411/api/v2/spans")) + .build(); + + ClientTracingInterceptor tracingInterceptor = ClientTracingInterceptor.builder(tracer) + .withVerbosity().withTracedAttributes(ClientRequestAttribute.ALL_CALL_OPTIONS).build(); + + Channel channel = ClientInterceptors + .intercept(ManagedChannelBuilder.forAddress("localhost", 1408).usePlaintext().build(), tracingInterceptor); + + GreetServiceGrpc.GreetServiceBlockingStub greetSvc = GreetServiceGrpc.newBlockingStub(channel); + System.out.println(greetSvc.greet(GreetRequest.newBuilder().setName("Aleks").build())); + System.out.println(greetSvc.setGreeting(SetGreetingRequest.newBuilder().setGreeting("Ciao").build())); + System.out.println(greetSvc.greet(GreetRequest.newBuilder().setName("Aleks").build())); + + Thread.sleep(5000); + } +} diff --git a/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/GreetService.java b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/GreetService.java new file mode 100644 index 00000000000..2e801895c69 --- /dev/null +++ b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/GreetService.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.common; + +import java.util.Optional; + +import io.helidon.config.Config; +import io.helidon.grpc.examples.common.Greet.GreetRequest; +import io.helidon.grpc.examples.common.Greet.GreetResponse; +import io.helidon.grpc.examples.common.Greet.SetGreetingRequest; +import io.helidon.grpc.examples.common.Greet.SetGreetingResponse; +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; + +import io.grpc.stub.StreamObserver; +import org.eclipse.microprofile.health.HealthCheckResponse; + +/** + * An implementation of the GreetService. + */ +public class GreetService implements GrpcService { + /** + * The config value for the key {@code greeting}. + */ + private String greeting; + + /** + * Create a {@link GreetService}. + * + * @param config the service configuration + */ + public GreetService(Config config) { + this.greeting = config.get("app.greeting").asString().orElse("Ciao"); + } + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.proto(Greet.getDescriptor()) + .unary("Greet", this::greet) + .unary("SetGreeting", this::setGreeting) + .healthCheck(this::healthCheck); + } + + // ---- service methods ------------------------------------------------- + + private void greet(GreetRequest request, StreamObserver observer) { + String name = Optional.ofNullable(request.getName()).orElse("World"); + String msg = String.format("%s %s!", greeting, name); + + complete(observer, GreetResponse.newBuilder().setMessage(msg).build()); + } + + private void setGreeting(SetGreetingRequest request, StreamObserver observer) { + greeting = request.getGreeting(); + + complete(observer, SetGreetingResponse.newBuilder().setGreeting(greeting).build()); + } + + private HealthCheckResponse healthCheck() { + return HealthCheckResponse + .named(name()) + .up() + .withData("time", System.currentTimeMillis()) + .withData("greeting", greeting) + .build(); + } +} diff --git a/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/GreetServiceJava.java b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/GreetServiceJava.java new file mode 100644 index 00000000000..b03cb7e7916 --- /dev/null +++ b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/GreetServiceJava.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.common; + +import java.util.Optional; + +import io.helidon.config.Config; +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; + +import io.grpc.stub.StreamObserver; + +/** + * A plain Java implementation of the GreetService. + */ +public class GreetServiceJava + implements GrpcService { + /** + * The config value for the key {@code greeting}. + */ + private String greeting; + + /** + * Create a {@link GreetServiceJava}. + * + * @param config the service configuration + */ + public GreetServiceJava(Config config) { + this.greeting = config.get("app.greeting").asString().orElse("Ciao"); + } + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.unary("Greet", this::greet) + .unary("SetGreeting", this::setGreeting); + } + + // ---- service methods ------------------------------------------------- + + private void greet(String name, StreamObserver observer) { + name = Optional.ofNullable(name).orElse("World"); + String msg = String.format("%s %s!", greeting, name); + + complete(observer, msg); + } + + private void setGreeting(String greeting, StreamObserver observer) { + this.greeting = greeting; + + complete(observer, greeting); + } +} diff --git a/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/StringClient.java b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/StringClient.java new file mode 100644 index 00000000000..84f812ff34f --- /dev/null +++ b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/StringClient.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.common; + +import io.helidon.grpc.examples.common.Strings.StringMessage; + +import io.grpc.Channel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.stub.StreamObserver; + +/** + * A client to the {@link io.helidon.grpc.examples.common.StringService}. + */ +public class StringClient { + + private StringClient() { + } + + /** + * Program entry point. + * + * @param args the program arguments + * + * @throws Exception if an error occurs + */ + public static void main(String[] args) throws Exception { + Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408).usePlaintext().build(); + + StringServiceGrpc.StringServiceStub stub = StringServiceGrpc.newStub(channel); + stub.lower(stringMessage("Convert To Lowercase"), new PrintObserver<>()); + Thread.sleep(500L); + stub.upper(stringMessage("Convert to Uppercase"), new PrintObserver<>()); + Thread.sleep(500L); + stub.split(stringMessage("Let's split some text"), new PrintObserver<>()); + Thread.sleep(500L); + + StreamObserver sender = stub.join(new PrintObserver<>()); + sender.onNext(stringMessage("Let's")); + sender.onNext(stringMessage("join")); + sender.onNext(stringMessage("some")); + sender.onNext(stringMessage("text")); + sender.onCompleted(); + Thread.sleep(500L); + + sender = stub.echo(new PrintObserver<>()); + sender.onNext(stringMessage("Let's")); + sender.onNext(stringMessage("echo")); + sender.onNext(stringMessage("some")); + sender.onNext(stringMessage("text")); + sender.onCompleted(); + Thread.sleep(500L); + } + + private static StringMessage stringMessage(String text) { + return StringMessage.newBuilder().setText(text).build(); + } + + static class PrintObserver implements StreamObserver { + public void onNext(T value) { + System.out.println(value); + } + + public void onError(Throwable t) { + t.printStackTrace(); + } + + public void onCompleted() { + System.out.println(""); + } + } +} diff --git a/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/StringService.java b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/StringService.java new file mode 100644 index 00000000000..8a409c571fa --- /dev/null +++ b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/StringService.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.common; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.grpc.examples.common.Strings.StringMessage; +import io.helidon.grpc.server.CollectingObserver; +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; + +import io.grpc.stub.StreamObserver; + +/** + * AN implementation of the StringService. + */ +public class StringService + implements GrpcService { + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.proto(Strings.getDescriptor()) + .unary("Upper", this::upper) + .unary("Lower", this::lower) + .serverStreaming("Split", this::split) + .clientStreaming("Join", this::join) + .bidirectional("Echo", this::echo); + } + + // ---- service methods ------------------------------------------------- + + private void upper(StringMessage request, StreamObserver observer) { + complete(observer, response(request.getText().toUpperCase())); + } + + private void lower(StringMessage request, StreamObserver observer) { + complete(observer, response(request.getText().toLowerCase())); + } + + private void split(StringMessage request, StreamObserver observer) { + String[] parts = request.getText().split(" "); + stream(observer, Stream.of(parts).map(this::response)); + } + + private StreamObserver join(StreamObserver observer) { + return new CollectingObserver<>( + Collectors.joining(" "), + observer, + StringMessage::getText, + this::response); + } + + private StreamObserver echo(StreamObserver observer) { + return new StreamObserver() { + public void onNext(StringMessage value) { + observer.onNext(value); + } + + public void onError(Throwable t) { + t.printStackTrace(); + } + + public void onCompleted() { + observer.onCompleted(); + } + }; + } + + // ---- helper methods -------------------------------------------------- + + private StringMessage response(String text) { + return StringMessage.newBuilder().setText(text).build(); + } + +} diff --git a/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/package-info.java b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/package-info.java new file mode 100644 index 00000000000..777b05b9600 --- /dev/null +++ b/examples/grpc/common/src/main/java/io/helidon/grpc/examples/common/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 and ProtoBuf generated gRPC servcies used in the Helidon gROC examples. + */ +package io.helidon.grpc.examples.common; diff --git a/examples/grpc/common/src/main/proto/greet.proto b/examples/grpc/common/src/main/proto/greet.proto new file mode 100644 index 00000000000..12d21aedd6a --- /dev/null +++ b/examples/grpc/common/src/main/proto/greet.proto @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.grpc.examples.common"; + +service GreetService { + rpc Greet (GreetRequest) returns (GreetResponse) {} + rpc SetGreeting (SetGreetingRequest) returns (SetGreetingResponse) {} +} + +message GreetRequest { + string name = 1; +} + +message GreetResponse { + string message = 1; +} + +message SetGreetingRequest { + string greeting = 1; +} + +message SetGreetingResponse { + string greeting = 1; +} diff --git a/examples/grpc/common/src/main/proto/strings.proto b/examples/grpc/common/src/main/proto/strings.proto new file mode 100644 index 00000000000..aeb5800ddef --- /dev/null +++ b/examples/grpc/common/src/main/proto/strings.proto @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.grpc.examples.common"; + +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/grpc/metrics/pom.xml b/examples/grpc/metrics/pom.xml new file mode 100644 index 00000000000..f91288caa4d --- /dev/null +++ b/examples/grpc/metrics/pom.xml @@ -0,0 +1,66 @@ + + + + + 4.0.0 + + io.helidon.examples.grpc + helidon-examples-grpc-project + 1.0.4-SNAPSHOT + + helidon-examples-grpc-metrics + Helidon gRPC Server Examples Metrics + + + Examples of elementary use of the gRPC Server metrics + + + + io.helidon.grpc.examples.metrics.Server + + + + + io.helidon.examples.grpc + helidon-examples-grpc-common + ${project.version} + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + + + io.helidon.grpc + helidon-grpc-metrics + ${project.version} + + + io.helidon.bundles + helidon-bundles-config + ${project.version} + + + + io.helidon.grpc + helidon-grpc-client + ${project.version} + + + diff --git a/examples/grpc/metrics/src/main/java/io/helidon/grpc/examples/metrics/Server.java b/examples/grpc/metrics/src/main/java/io/helidon/grpc/examples/metrics/Server.java new file mode 100644 index 00000000000..bec8ffde1cf --- /dev/null +++ b/examples/grpc/metrics/src/main/java/io/helidon/grpc/examples/metrics/Server.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.metrics; + +import java.util.logging.LogManager; + +import io.helidon.config.Config; +import io.helidon.grpc.examples.common.GreetService; +import io.helidon.grpc.examples.common.StringService; +import io.helidon.grpc.metrics.GrpcMetrics; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.metrics.MetricsSupport; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.WebServer; + +/** + * A basic example of a Helidon gRPC server. + */ +public class Server { + + private Server() { + } + + /** + * The main program entry point. + * + * @param args the program arguments + * + * @throws Exception if an error occurs + */ + public static void main(String[] args) throws Exception { + // By default this will pick up application.yaml from the classpath + Config config = Config.create(); + + // load logging configuration + LogManager.getLogManager().readConfiguration( + Server.class.getResourceAsStream("/logging.properties")); + + // Get gRPC server config from the "grpc" section of application.yaml + GrpcServerConfiguration serverConfig = + GrpcServerConfiguration.builder(config.get("grpc")).build(); + + GrpcRouting grpcRouting = GrpcRouting.builder() + .intercept(GrpcMetrics.counted()) // global metrics - all service methods counted + .register(new GreetService(config)) // GreetService uses global metrics so all methods are counted + .register(new StringService(), rules -> { + // service level metrics - StringService overrides global so that its methods are timed + rules.intercept(GrpcMetrics.timed()) + // method level metrics - overrides service and global + .intercept("Upper", GrpcMetrics.histogram()); + }) + .build(); + + GrpcServer grpcServer = GrpcServer.create(serverConfig, grpcRouting); + + // Try to start the server. If successful, print some info and arrange to + // print a message at shutdown. If unsuccessful, print the exception. + grpcServer.start() + .thenAccept(s -> { + System.out.println("gRPC server is UP! http://localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("gRPC server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("Startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + + // start web server with the metrics endpoints + Routing routing = Routing.builder() + .register(MetricsSupport.create()) + .build(); + + ServerConfiguration webServerConfig = ServerConfiguration.builder(config.get("webserver")).build(); + + WebServer.create(webServerConfig, routing) + .start() + .thenAccept(s -> { + System.out.println("HTTP server is UP! http://localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("HTTP server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("Startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + } +} diff --git a/examples/grpc/metrics/src/main/java/io/helidon/grpc/examples/metrics/package-info.java b/examples/grpc/metrics/src/main/java/io/helidon/grpc/examples/metrics/package-info.java new file mode 100644 index 00000000000..342c0dcd8a6 --- /dev/null +++ b/examples/grpc/metrics/src/main/java/io/helidon/grpc/examples/metrics/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC metrics. + *

+ * Start with {@link io.helidon.grpc.examples.metrics.Server Main} class. + */ +package io.helidon.grpc.examples.metrics; diff --git a/examples/grpc/metrics/src/main/resources/application.yaml b/examples/grpc/metrics/src/main/resources/application.yaml new file mode 100644 index 00000000000..2afc282df4a --- /dev/null +++ b/examples/grpc/metrics/src/main/resources/application.yaml @@ -0,0 +1,26 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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" + +grpc: + name: "test.server" + port: 1408 + +webserver: + port: 8080 + bind-address: "0.0.0.0" \ No newline at end of file diff --git a/examples/grpc/metrics/src/main/resources/logging.properties b/examples/grpc/metrics/src/main/resources/logging.properties new file mode 100644 index 00000000000..f903114790c --- /dev/null +++ b/examples/grpc/metrics/src/main/resources/logging.properties @@ -0,0 +1,37 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden 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 +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +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 +#io.netty.level=INFO diff --git a/examples/grpc/opentracing/README.md b/examples/grpc/opentracing/README.md new file mode 100644 index 00000000000..24aa3947cb4 --- /dev/null +++ b/examples/grpc/opentracing/README.md @@ -0,0 +1,86 @@ +Opentracing gRPC Server Example Application +=========================================== + +Running locally +--------------- +Prerequisites: +1. Requirements: JDK9, Maven, Docker (optional) +2. Add following lines to `/etc/hosts` + ``` + 127.0.0.1 zipkin + ``` +3. Run Zipkin:
+ In Docker: + ``` + docker run -d -p 9411:9411 openzipkin/zipkin + ``` + or with Java 8: + ``` + wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec' + java -jar zipkin.jar + ``` + +Build and run: +``` +mvn clean install -pl examples/grpc/opentracing +mvn exec:java -pl examples/grpc/opentracing +curl "http://localhost:8080/test" +``` +Check out the traces at: ```http://zipkin:9411``` + + +Running in Minikube +------------------- + +### Preparing the infrastructure ### +Starting Minikube + +``` +% minikube start + +% kubectl version +Client Version: version.Info{Major:"1", Minor:"6", GitVersion:"v1.6.2", GitCommit:"477efc3cbe6a7effca06bd1452fa356e2201e1ee", GitTreeState:"clean", BuildDate:"2017-04-19T22:51:36Z", GoVersion:"go1.8.1", Compiler:"gc", Platform:"darwin/amd64"} +Server Version: version.Info{Major:"1", Minor:"6", GitVersion:"v1.6.0", GitCommit:"fff5156092b56e6bd60fff75aad4dc9de6b6ef37", GitTreeState:"dirty", BuildDate:"2017-04-07T20:46:46Z", GoVersion:"go1.7.3", Compiler:"gc", Platform:"linux/amd64"} + +% minikube dashboard + Waiting, endpoint for service is not ready yet... + Opening kubernetes dashboard in default browser... + +``` + +Running Zipkin in K8S +``` +% kubectl run zipkin --image=openzipkin/zipkin --port=9411 +deployment "zipkin" created + +% kubectl expose deployment zipkin --type=NodePort +service "zipkin" exposed + +% kubectl get pod +NAME READY STATUS RESTARTS AGE +zipkin-2596933303-bccnw 0/1 ContainerCreating 0 14s + +% kubectl get pod +NAME READY STATUS RESTARTS AGE +zipkin-2596933303-bccnw 1/1 Running 0 16s + +% minikube service zipkin +Opening kubernetes service default/zipkin in default browser... +``` + +Running opentracing app +``` +% eval $(minikube docker-env) +% mvn clean install -pl examples/grpc/opentracing docker:build + +% kubectl run helidon-grpc-opentracing-example --image=mic.docker.oraclecorp.com/helidon-grpc-opentracing-example:1.0.1-SNAPSHOT --port=1408 --image-pull-policy=Never +deployment "helidon-grpc-opentracing-example" created + +% kubectl expose deployment helidon-grpc-opentracing-example --type=NodePort +service "helidon-grpc-opentracing-example" exposed + +% curl $(minikube service helidon-webserver-opentracing-example --url)/test +Hello World!% +``` + + diff --git a/examples/grpc/opentracing/pom.xml b/examples/grpc/opentracing/pom.xml new file mode 100644 index 00000000000..171d095c149 --- /dev/null +++ b/examples/grpc/opentracing/pom.xml @@ -0,0 +1,123 @@ + + + + + 4.0.0 + + io.helidon.examples.grpc + helidon-examples-grpc-project + 1.0.4-SNAPSHOT + + helidon-examples-grpc-opentracing + Helidon gRPC Server Examples OpenTracing + + + Examples gRPC application using Open Tracing + + + + io.helidon.grpc.examples.opentracing.ZipkinExampleMain + + ${docker.registry}/helidon-grpc-opentracing-example:${project.version} + + mic-docker-registry-automation + mic.docker.oraclecorp.com + https://${docker.registry}/v1/ + 0.3.3 + + + + + + io.helidon.examples.grpc + helidon-examples-grpc-common + ${project.version} + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + + + io.helidon.bundles + helidon-bundles-config + ${project.version} + + + + io.helidon.grpc + helidon-grpc-client + ${project.version} + + + + io.helidon.tracing + helidon-tracing-zipkin + ${project.version} + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-docker-resources + generate-resources + + copy-resources + + + ${project.build.directory}/distribution/container + + + src/main/docker + true + + + + + + + + com.spotify + docker-maven-plugin + + ${docker.server.id} + ${docker.registry.url} + ${docker.image.name} + ${project.build.directory}/distribution/container + + ${docker.image.version} + latest + + + + / + ${project.build.directory} + ${project.build.finalName}-fat.jar + + + + + + + diff --git a/examples/grpc/opentracing/src/main/docker/Dockerfile b/examples/grpc/opentracing/src/main/docker/Dockerfile new file mode 100644 index 00000000000..87aef649fb6 --- /dev/null +++ b/examples/grpc/opentracing/src/main/docker/Dockerfile @@ -0,0 +1,23 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 java:9 + +EXPOSE 8080 +COPY ./* / +RUN rm Dockerfile +WORKDIR / +CMD if [ ! -z "$MIC_CONTAINER_MEM_QUOTA" ]; then java -Xmx${MIC_CONTAINER_MEM_QUOTA}m ${JAVA_OPTS} -jar ${artifactId}-${version}-fat.jar ; else java ${JAVA_OPTS} -jar ${artifactId}-${version}-fat.jar ; fi diff --git a/examples/grpc/opentracing/src/main/java/io/helidon/grpc/examples/opentracing/ZipkinExampleMain.java b/examples/grpc/opentracing/src/main/java/io/helidon/grpc/examples/opentracing/ZipkinExampleMain.java new file mode 100644 index 00000000000..c2e0d5269c4 --- /dev/null +++ b/examples/grpc/opentracing/src/main/java/io/helidon/grpc/examples/opentracing/ZipkinExampleMain.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.opentracing; + +import java.util.logging.LogManager; + +import io.helidon.config.Config; +import io.helidon.grpc.examples.common.GreetService; +import io.helidon.grpc.examples.common.StringService; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.grpc.server.ServerRequestAttribute; +import io.helidon.grpc.server.TracingConfiguration; +import io.helidon.tracing.TracerBuilder; + +import io.opentracing.Tracer; + +/** + * An example gRPC server with Zipkin tracing enabled. + */ +public class ZipkinExampleMain { + + private ZipkinExampleMain() { + } + + /** + * Program entry point. + * + * @param args the program command line arguments + * @throws Exception if there is a program error + */ + public static void main(String[] args) throws Exception { + // By default this will pick up application.yaml from the classpath + Config config = Config.create(); + + // load logging configuration + LogManager.getLogManager().readConfiguration( + ZipkinExampleMain.class.getResourceAsStream("/logging.properties")); + + Tracer tracer = TracerBuilder.create(config.get("tracing")).build(); + + TracingConfiguration tracingConfig = new TracingConfiguration.Builder() + .withStreaming() + .withVerbosity() + .withTracedAttributes(ServerRequestAttribute.CALL_ATTRIBUTES, + ServerRequestAttribute.HEADERS, + ServerRequestAttribute.METHOD_NAME) + .build(); + + // Get gRPC server config from the "grpc" section of application.yaml + GrpcServerConfiguration serverConfig = + GrpcServerConfiguration.builder(config.get("grpc")).tracer(tracer).tracingConfig(tracingConfig).build(); + + GrpcServer grpcServer = GrpcServer.create(serverConfig, createRouting(config)); + + // Try to start the server. If successful, print some info and arrange to + // print a message at shutdown. If unsuccessful, print the exception. + grpcServer.start() + .thenAccept(s -> { + System.out.println("gRPC server is UP! http://localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("gRPC server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("Startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + } + + private static GrpcRouting createRouting(Config config) { + return GrpcRouting.builder() + .register(new GreetService(config)) + .register(new StringService()) + .build(); + } +} diff --git a/examples/grpc/opentracing/src/main/java/io/helidon/grpc/examples/opentracing/package-info.java b/examples/grpc/opentracing/src/main/java/io/helidon/grpc/examples/opentracing/package-info.java new file mode 100644 index 00000000000..499076694fc --- /dev/null +++ b/examples/grpc/opentracing/src/main/java/io/helidon/grpc/examples/opentracing/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 of running the Helidon gRPC server with Zipkin tracing enabled. + */ +package io.helidon.grpc.examples.opentracing; diff --git a/examples/grpc/opentracing/src/main/resources/application.yaml b/examples/grpc/opentracing/src/main/resources/application.yaml new file mode 100644 index 00000000000..633ac6e3672 --- /dev/null +++ b/examples/grpc/opentracing/src/main/resources/application.yaml @@ -0,0 +1,29 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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" + +grpc: + name: "test.server" + port: 1408 + +webserver: + port: 8080 + bind-address: "0.0.0.0" + +tracing: + service: "grpc-server" \ No newline at end of file diff --git a/examples/grpc/opentracing/src/main/resources/logging.properties b/examples/grpc/opentracing/src/main/resources/logging.properties new file mode 100644 index 00000000000..f903114790c --- /dev/null +++ b/examples/grpc/opentracing/src/main/resources/logging.properties @@ -0,0 +1,37 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden 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 +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +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 +#io.netty.level=INFO diff --git a/examples/grpc/pom.xml b/examples/grpc/pom.xml new file mode 100644 index 00000000000..87d664f8490 --- /dev/null +++ b/examples/grpc/pom.xml @@ -0,0 +1,43 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 1.0.4-SNAPSHOT + + + io.helidon.examples.grpc + helidon-examples-grpc-project + Helidon gRPC Examples + pom + + + common + basics + metrics + opentracing + security + security-abac + security-outbound + + + diff --git a/examples/grpc/security-abac/README.md b/examples/grpc/security-abac/README.md new file mode 100644 index 00000000000..6c240ed4366 --- /dev/null +++ b/examples/grpc/security-abac/README.md @@ -0,0 +1,16 @@ + +# Helidon gRPC Security ABAC Example + +An example gRPC server for attribute based access control. + +## Build + +``` +mvn package +``` + +## Run + +``` +mvn exec:java +``` diff --git a/examples/grpc/security-abac/pom.xml b/examples/grpc/security-abac/pom.xml new file mode 100644 index 00000000000..648ac300c48 --- /dev/null +++ b/examples/grpc/security-abac/pom.xml @@ -0,0 +1,103 @@ + + + + + 4.0.0 + + io.helidon.examples.grpc + helidon-examples-grpc-project + 1.0.4-SNAPSHOT + + helidon-examples-grpc-security-abac + Helidon gRPC Server Examples ABAC Security + + + Examples of securing gRPC services using ABAC + + + + io.helidon.grpc.examples.security.abac.AbacServer + + + + + io.helidon.examples.grpc + helidon-examples-grpc-common + ${project.version} + + + + io.helidon.grpc + helidon-grpc-core + ${project.version} + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + + + io.helidon.security.integration + helidon-security-integration-grpc + ${project.version} + + + io.helidon.bundles + helidon-bundles-config + ${project.version} + + + io.helidon.bundles + helidon-bundles-security + ${project.version} + + + io.helidon.security.abac + helidon-security-abac-policy-el + ${project.version} + + + org.glassfish + javax.el + + + + io.helidon.grpc + helidon-grpc-client + ${project.version} + + + + io.grpc + grpc-netty + + + io.grpc + grpc-stub + + + io.grpc + grpc-services + + + io.grpc + grpc-protobuf + + + diff --git a/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AbacServer.java b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AbacServer.java new file mode 100644 index 00000000000..59164b3af1f --- /dev/null +++ b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AbacServer.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.security.abac; + +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.util.logging.LogManager; + +import io.helidon.grpc.examples.common.StringService; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.security.Security; +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.integration.grpc.GrpcSecurity; +import io.helidon.security.providers.abac.AbacProvider; + +/** + * An example of a secure gRPC server that uses + * ABAC security configured in the code below. + *

+ * This server configures in code the same rules that + * the {@link AbacServerFromConfig} class uses from + * its configuration. + */ +public class AbacServer { + + private AbacServer() { + } + + /** + * Main entry point. + * + * @param args the program arguments + * + * @throws Exception if an error occurs + */ + public static void main(String[] args) throws Exception { + LogManager.getLogManager().readConfiguration( + AbacServer.class.getResourceAsStream("/logging.properties")); + + Security security = Security.builder() + .addProvider(AtnProvider.builder().build()) // add out custom provider + .addProvider(AbacProvider.builder().build()) // add the ABAC provider + .build(); + + // Create the time validator that will be used by the ABAC security provider + TimeValidator.TimeConfig validTimes = TimeValidator.TimeConfig.builder() + .addBetween(LocalTime.of(8, 15), LocalTime.of(12, 0)) + .addBetween(LocalTime.of(12, 30), LocalTime.of(17, 30)) + .addDaysOfWeek(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY) + .build(); + + // Create the policy validator that will be used by the ABAC security provider + PolicyValidator.PolicyConfig validPolicy = PolicyValidator.PolicyConfig.builder() + .statement("${env.time.year >= 2017}") + .build(); + + // Create the scope validator that will be used by the ABAC security provider + ScopeValidator.ScopesConfig validScopes = ScopeValidator.ScopesConfig.create("calendar_read", "calendar_edit"); + + // Create the Atn config that will be used by out custom security provider + AtnProvider.AtnConfig atnConfig = AtnProvider.AtnConfig.builder() + .addAuth(AtnProvider.Auth.builder("user") + .type(SubjectType.USER) + .roles("user_role") + .scopes("calendar_read", "calendar_edit") + .build()) + .addAuth(AtnProvider.Auth.builder("service") + .type(SubjectType.SERVICE) + .roles("service_role") + .scopes("calendar_read", "calendar_edit") + .build()) + .build(); + + ServiceDescriptor stringService = ServiceDescriptor.builder(new StringService()) + .intercept("Upper", GrpcSecurity.secure() + .customObject(atnConfig) + .customObject(validScopes) + .customObject(validTimes) + .customObject(validPolicy)) + .build(); + + GrpcRouting grpcRouting = GrpcRouting.builder() + .intercept(GrpcSecurity.create(security).securityDefaults(GrpcSecurity.secure())) + .register(stringService) + .build(); + + GrpcServerConfiguration serverConfig = GrpcServerConfiguration.builder().build(); + GrpcServer grpcServer = GrpcServer.create(serverConfig, grpcRouting); + + grpcServer.start() + .thenAccept(s -> { + System.out.println("gRPC server is UP! http://localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("gRPC server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("Startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + } +} diff --git a/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AbacServerFromConfig.java b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AbacServerFromConfig.java new file mode 100644 index 00000000000..d9606568d73 --- /dev/null +++ b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AbacServerFromConfig.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.security.abac; + +import java.util.logging.LogManager; + +import io.helidon.config.Config; +import io.helidon.grpc.examples.common.StringService; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.security.Security; +import io.helidon.security.integration.grpc.GrpcSecurity; + +/** + * An example of a secure gRPC server that uses ABAC + * security configured from configuration the configuration + * file application.conf. + *

+ * This server's configuration file configures security with + * same rules that the {@link AbacServer} class builds in + * code. + */ +public class AbacServerFromConfig { + + private AbacServerFromConfig() { + } + + /** + * Main entry point. + * + * @param args the program arguments + * + * @throws Exception if an error occurs + */ + public static void main(String[] args) throws Exception { + LogManager.getLogManager().readConfiguration( + AbacServerFromConfig.class.getResourceAsStream("/logging.properties")); + + Config config = Config.create(); + + Security security = Security.create(config.get("security")); + + GrpcRouting grpcRouting = GrpcRouting.builder() + .intercept(GrpcSecurity.create(security, config.get("security"))) + .register(new StringService()) + .build(); + + GrpcServerConfiguration serverConfig = GrpcServerConfiguration.create(config.get("grpc")); + GrpcServer grpcServer = GrpcServer.create(serverConfig, grpcRouting); + + grpcServer.start() + .thenAccept(s -> { + System.out.println("gRPC server is UP! http://localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("gRPC server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("Startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + } +} diff --git a/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AtnProvider.java b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AtnProvider.java new file mode 100644 index 00000000000..0c809bc5ddd --- /dev/null +++ b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AtnProvider.java @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.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.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.helidon.common.CollectionsHelper; +import io.helidon.config.Config; +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.Subject; +import io.helidon.security.SubjectType; +import io.helidon.security.spi.AuthenticationProvider; +import io.helidon.security.spi.SynchronousProvider; + +/** + * Example authentication provider that reads annotation to create a subject. + */ +public class AtnProvider extends SynchronousProvider implements AuthenticationProvider { + + /** + * The configuration key for this provider. + */ + public static final String CONFIG_KEY = "atn"; + + private final Config config; + + private AtnProvider(Config config) { + this.config = config; + } + + @Override + protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + EndpointConfig endpointConfig = providerRequest.endpointConfig(); + Config atnConfig = endpointConfig.config(CONFIG_KEY).orElse(null); + Subject user = null; + Subject service = null; + List list; + + Optional optional = providerRequest.endpointConfig().instance(AtnConfig.class); + + if (optional.isPresent()) { + list = optional.get().auths(); + } else if (atnConfig != null && !atnConfig.isLeaf()) { + list = atnConfig.asNodeList() + .map(this::fromConfig).orElse(Collections.emptyList()); + } else { + list = fromAnnotations(endpointConfig); + } + + for (Auth authentication : list) { + if (authentication.type() == SubjectType.USER) { + user = buildSubject(authentication); + } else { + service = buildSubject(authentication); + } + } + + return AuthenticationResponse.success(user, service); + } + + private List fromConfig(List configList) { + return configList.stream() + .map(Auth::new) + .collect(Collectors.toList()); + } + + private List fromAnnotations(EndpointConfig endpointConfig) { + return endpointConfig.combineAnnotations(Authentications.class, EndpointConfig.AnnotationScope.METHOD) + .stream() + .map(Authentications::value) + .flatMap(Arrays::stream) + .map(Auth::new) + .collect(Collectors.toList()); + } + + private Subject buildSubject(Auth authentication) { + Subject.Builder subjectBuilder = Subject.builder(); + + subjectBuilder.principal(Principal.create(authentication.principal())); + + Arrays.stream(authentication.roles()) + .map(Role::create) + .forEach(subjectBuilder::addGrant); + + Arrays.stream(authentication.scopes()) + .map(scope -> Grant.builder().name(scope).type("scope").build()) + .forEach(subjectBuilder::addGrant); + + return subjectBuilder.build(); + } + + @Override + public Collection> supportedAnnotations() { + return CollectionsHelper.setOf(Authentication.class); + } + + /** + * Create a {@link AtnProvider}. + * @return a {@link AtnProvider} + */ + public static AtnProvider create() { + return builder().build(); + } + + /** + * Create a {@link AtnProvider}. + * + * @param config the configuration for the {@link AtnProvider} + * + * @return a {@link AtnProvider} + */ + public static AtnProvider create(Config config) { + return builder(config).build(); + } + + /** + * Create a {@link AtnProvider.Builder}. + * @return a {@link AtnProvider.Builder} + */ + public static Builder builder() { + return builder(null); + } + + /** + * Create a {@link AtnProvider.Builder}. + * + * @param config the configuration for the {@link AtnProvider} + * + * @return a {@link AtnProvider.Builder} + */ + public static Builder builder(Config config) { + return new Builder(config); + } + + /** + * A builder that builds {@link AtnProvider} instances. + */ + public static class Builder + implements io.helidon.common.Builder { + + private Config config; + + private Builder(Config config) { + this.config = config; + } + + /** + * Set the configuration for the {@link AtnProvider}. + * @param config the configuration for the {@link AtnProvider} + * @return this builder + */ + public Builder config(Config config) { + this.config = config; + return this; + } + + @Override + public AtnProvider build() { + return new AtnProvider(config); + } + } + + /** + * 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(); + } + + /** + * A holder for authentication settings. + */ + public static class Auth { + private String principal; + private SubjectType type = SubjectType.USER; + private String[] roles; + private String[] scopes; + + private Auth(Authentication authentication) { + principal = authentication.value(); + type = authentication.type(); + roles = authentication.roles(); + scopes = authentication.scopes(); + } + + private Auth(Config config) { + config.get("principal").ifExists(cfg -> principal = cfg.asString().get()); + config.get("type").ifExists(cfg -> type = SubjectType.valueOf(cfg.asString().get())); + config.get("roles").ifExists(cfg -> roles = cfg.asList(String.class).get().toArray(new String[0])); + config.get("scopes").ifExists(cfg -> scopes = cfg.asList(String.class).get().toArray(new String[0])); + } + + private Auth(String principal, SubjectType type, String[] roles, String[] scopes) { + this.principal = principal; + this.type = type; + this.roles = roles; + this.scopes = scopes; + } + + private String principal() { + return principal; + } + + private SubjectType type() { + return type; + } + + private String[] roles() { + return roles; + } + + private String[] scopes() { + return scopes; + } + + /** + * Obtain a builder for building {@link Auth} instances. + * + * @param principal the principal name + * + * @return a builder for building {@link Auth} instances. + */ + public static Builder builder(String principal) { + return new Auth.Builder(principal); + } + + /** + * A builder for building {@link Auth} instances. + */ + public static class Builder + implements io.helidon.common.Builder { + + private final String principal; + private SubjectType type = SubjectType.USER; + private String[] roles; + private String[] scopes; + + private Builder(String principal) { + this.principal = principal; + } + + /** + * Set the {@link SubjectType}. + * @param type the {@link SubjectType} + * @return this builder + */ + public Builder type(SubjectType type) { + this.type = type; + return this; + } + + /** + * Set the roles. + * @param roles the role names + * @return this builder + */ + public Builder roles(String... roles) { + this.roles = roles; + return this; + } + + /** + * Set the scopes. + * @param scopes the scopes names + * @return this builder + */ + public Builder scopes(String... scopes) { + this.scopes = scopes; + return this; + } + + @Override + public Auth build() { + return new Auth(principal, type, roles, scopes); + } + } + } + + /** + * The configuration for a {@link AtnProvider}. + */ + public static class AtnConfig { + private final List authData; + + private AtnConfig(List list) { + this.authData = list; + } + + /** + * Obtain the {@link List} of {@link Auth}s to use. + * + * @return the {@link List} of {@link Auth}s to use + */ + public List auths() { + return Collections.unmodifiableList(authData); + } + + /** + * Obtain a builder for building {@link AtnConfig} instances. + * + * @return a builder for building {@link AtnConfig} instances + */ + public static AtnConfig.Builder builder() { + return new Builder(); + } + + /** + * A builder for building {@link AtnConfig} instances. + */ + public static class Builder + implements io.helidon.common.Builder { + + private final List authData = new ArrayList<>(); + + /** + * Add an {@link Auth} instance. + * + * @param auth the {@link Auth} to add + * + * @return this builder + * + * @throws java.lang.NullPointerException if the {@link Auth} is null + */ + public Builder addAuth(Auth auth) { + authData.add(Objects.requireNonNull(auth)); + return this; + } + + @Override + public AtnConfig build() { + return new AtnConfig(authData); + } + } + } +} diff --git a/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AtnProviderService.java b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AtnProviderService.java new file mode 100644 index 00000000000..64eed4e13e4 --- /dev/null +++ b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AtnProviderService.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.security.abac; + +import io.helidon.config.Config; +import io.helidon.security.spi.SecurityProvider; +import io.helidon.security.spi.SecurityProviderService; + +/** + * A service provider for the {@link AtnProvider}. + */ +public class AtnProviderService + implements SecurityProviderService { + + @Override + public String providerConfigKey() { + return AtnProvider.CONFIG_KEY; + } + + @Override + public Class providerClass() { + return AtnProvider.class; + } + + @Override + public SecurityProvider providerInstance(Config config) { + return AtnProvider.create(config); + } +} diff --git a/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/SecureStringClient.java b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/SecureStringClient.java new file mode 100644 index 00000000000..7b3939342af --- /dev/null +++ b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/SecureStringClient.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.security.abac; + + +import io.helidon.grpc.examples.common.StringServiceGrpc; +import io.helidon.grpc.examples.common.Strings; + +import io.grpc.Channel; +import io.grpc.ManagedChannelBuilder; + +/** + * A {@link io.helidon.grpc.examples.common.StringService} client that optionally + * provides {@link io.grpc.CallCredentials} using basic auth. + */ +public class SecureStringClient { + + private SecureStringClient() { + } + + /** + * Program entry point. + * + * @param args program arguments + */ + public static void main(String[] args) { + Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408) + .usePlaintext() + .build(); + + StringServiceGrpc.StringServiceBlockingStub stub = StringServiceGrpc.newBlockingStub(channel); + + String text = "abcde"; + Strings.StringMessage request = Strings.StringMessage.newBuilder().setText(text).build(); + Strings.StringMessage response = stub.upper(request); + + System.out.println("Text '" + text + "' to upper is '" + response.getText() + "'"); + } +} diff --git a/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/package-info.java b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/package-info.java new file mode 100644 index 00000000000..d9150a74d88 --- /dev/null +++ b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.security.SecureServer Main} class. + */ +package io.helidon.grpc.examples.security.abac; diff --git a/examples/grpc/security-abac/src/main/resources/META-INF/services/io.helidon.security.providers.abac.spi.AbacValidator b/examples/grpc/security-abac/src/main/resources/META-INF/services/io.helidon.security.providers.abac.spi.AbacValidator new file mode 100644 index 00000000000..6a05d845ad6 --- /dev/null +++ b/examples/grpc/security-abac/src/main/resources/META-INF/services/io.helidon.security.providers.abac.spi.AbacValidator @@ -0,0 +1,19 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.security.abac.scope.ScopeValidator +io.helidon.security.abac.policy.PolicyValidator +io.helidon.security.abac.time.TimeValidator diff --git a/examples/grpc/security-abac/src/main/resources/META-INF/services/io.helidon.security.spi.SecurityProviderService b/examples/grpc/security-abac/src/main/resources/META-INF/services/io.helidon.security.spi.SecurityProviderService new file mode 100644 index 00000000000..9359c2f3df9 --- /dev/null +++ b/examples/grpc/security-abac/src/main/resources/META-INF/services/io.helidon.security.spi.SecurityProviderService @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.grpc.examples.security.abac.AtnProviderService \ No newline at end of file diff --git a/examples/grpc/security-abac/src/main/resources/application.yaml b/examples/grpc/security-abac/src/main/resources/application.yaml new file mode 100644 index 00000000000..761d97d2c31 --- /dev/null +++ b/examples/grpc/security-abac/src/main/resources/application.yaml @@ -0,0 +1,86 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +grpc: + port: 1408 + +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 + - atn: + class: "io.helidon.grpc.examples.security.abac.AtnProvider" + + grpc-server: + # Configuration of integration with grpc server + # The default configuration to apply to all services not explicitly configured below + defaults: + authenticate: true + authorize: true + services: + - name: "StringService" + methods: + - name: "Upper" + # Define our custom authenticator rules for the Upper method + atn: + - principal: "user" + type: "USER" + roles: ["user_role"] + scopes: ["calendar_read", "calendar_edit"] + - principal: "service" + type: "SERVICE" + roles: ["service_role"] + scopes: ["calendar_read", "calendar_edit"] + # Define ABAC rules for the Upper method + 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: "${env.time.year >= 2017}" + + diff --git a/examples/grpc/security-abac/src/main/resources/logging.properties b/examples/grpc/security-abac/src/main/resources/logging.properties new file mode 100644 index 00000000000..13c16d78c52 --- /dev/null +++ b/examples/grpc/security-abac/src/main/resources/logging.properties @@ -0,0 +1,21 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 +.level=INFO +AUDIT.level=FINEST diff --git a/examples/grpc/security-outbound/README.md b/examples/grpc/security-outbound/README.md new file mode 100644 index 00000000000..51b5613ada0 --- /dev/null +++ b/examples/grpc/security-outbound/README.md @@ -0,0 +1,17 @@ + +# Helidon gRPC Security ABAC Example + +An example gRPC outbound security + +## Build + +``` +mvn package +``` + +## Run + +To start the server run: +``` +mvn exec:java +``` diff --git a/examples/grpc/security-outbound/pom.xml b/examples/grpc/security-outbound/pom.xml new file mode 100644 index 00000000000..3318ac4c1cc --- /dev/null +++ b/examples/grpc/security-outbound/pom.xml @@ -0,0 +1,95 @@ + + + + + 4.0.0 + + io.helidon.examples.grpc + helidon-examples-grpc-project + 1.0.4-SNAPSHOT + + helidon-examples-grpc-security-outbound + Helidon gRPC Server Examples Outbound Security + + + Examples of outbound security when using gRPC services + + + + io.helidon.grpc.examples.security.outbound.SecureServer + + + + + io.helidon.examples.grpc + helidon-examples-grpc-common + ${project.version} + + + + io.helidon.grpc + helidon-grpc-core + ${project.version} + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + + + io.helidon.security.integration + helidon-security-integration-grpc + ${project.version} + + + io.helidon.bundles + helidon-bundles-config + ${project.version} + + + io.helidon.bundles + helidon-bundles-webserver + ${project.version} + + + org.glassfish.jersey.core + jersey-client + + + org.glassfish.jersey.inject + jersey-hk2 + + + io.helidon.security.integration + helidon-security-integration-jersey + ${project.version} + + + io.helidon.bundles + helidon-bundles-security + ${project.version} + + + + io.helidon.grpc + helidon-grpc-client + ${project.version} + + + diff --git a/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/SecureGreetClient.java b/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/SecureGreetClient.java new file mode 100644 index 00000000000..4b94278195a --- /dev/null +++ b/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/SecureGreetClient.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.security.outbound; + + +import io.helidon.config.Config; +import io.helidon.grpc.examples.common.Greet; +import io.helidon.grpc.examples.common.GreetServiceGrpc; +import io.helidon.security.Security; +import io.helidon.security.integration.grpc.GrpcClientSecurity; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; + +import io.grpc.Channel; +import io.grpc.ManagedChannelBuilder; + +/** + * A GreetService client that uses {@link io.grpc.CallCredentials} using basic auth. + */ +public class SecureGreetClient { + + private SecureGreetClient() { + } + + /** + * Program entry point. + * + * @param args program arguments + */ + public static void main(String[] args) { + Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408) + .usePlaintext() + .build(); + + Config config = Config.create(); + + // configure Helidon security and add the basic auth provider + Security security = Security.builder() + .addProvider(HttpBasicAuthProvider.create(config.get("http-basic-auth"))) + .build(); + + // create the gRPC client security call credentials + // setting the properties used by the basic auth provider for user name and password + GrpcClientSecurity clientSecurity = GrpcClientSecurity.builder(security.createContext("test.client")) + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_USER, "Bob") + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_PASSWORD, "password") + .build(); + + // create the GreetService client stub and use the GrpcClientSecurity call credentials + GreetServiceGrpc.GreetServiceBlockingStub stub = GreetServiceGrpc.newBlockingStub(channel) + .withCallCredentials(clientSecurity); + + Greet.GreetResponse greetResponse = stub.greet(Greet.GreetRequest.newBuilder().setName("Bob").build()); + + System.out.println(greetResponse.getMessage()); + + Greet.SetGreetingResponse setGreetingResponse = + stub.setGreeting(Greet.SetGreetingRequest.newBuilder().setGreeting("Merhaba").build()); + + System.out.println("Greeting set to: " + setGreetingResponse.getGreeting()); + + greetResponse = stub.greet(Greet.GreetRequest.newBuilder().setName("Bob").build()); + + System.out.println(greetResponse.getMessage()); + } +} diff --git a/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/SecureServer.java b/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/SecureServer.java new file mode 100644 index 00000000000..7fc13ad2276 --- /dev/null +++ b/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/SecureServer.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.security.outbound; + +import java.util.Optional; +import java.util.logging.LogManager; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; + +import io.helidon.config.Config; +import io.helidon.grpc.core.GrpcHelper; +import io.helidon.grpc.examples.common.Greet; +import io.helidon.grpc.examples.common.StringService; +import io.helidon.grpc.examples.common.StringServiceGrpc; +import io.helidon.grpc.examples.common.Strings; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.integration.grpc.GrpcClientSecurity; +import io.helidon.security.integration.grpc.GrpcSecurity; +import io.helidon.security.integration.jersey.ClientSecurityFeature; +import io.helidon.security.integration.webserver.WebSecurity; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; +import io.helidon.webserver.WebServer; + +import io.grpc.Channel; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.stub.StreamObserver; + +/** + * An example server that configures services with outbound security. + */ +public class SecureServer { + + private static GrpcServer grpcServer; + + private static WebServer webServer; + + private SecureServer() { + } + + /** + * Program entry point. + * + * @param args the program command line arguments + * @throws Exception if there is a program error + */ + public static void main(String[] args) throws Exception { + LogManager.getLogManager().readConfiguration( + SecureServer.class.getResourceAsStream("/logging.properties")); + + Config config = Config.create(); + + Security security = Security.builder() + .addProvider(HttpBasicAuthProvider.create(config.get("http-basic-auth"))) + .build(); + + grpcServer = createGrpcServer(config.get("grpc"), security); + webServer = createWebServer(config.get("webserver"), security); + } + + /** + * Create the gRPC server. + */ + private static GrpcServer createGrpcServer(Config config, Security security) { + + GrpcRouting grpcRouting = GrpcRouting.builder() + // Add the security interceptor with a default of allowing any authenticated user + .intercept(GrpcSecurity.create(security).securityDefaults(GrpcSecurity.authenticate())) + // add the StringService with required role "admin" + .register(new StringService(), GrpcSecurity.rolesAllowed("admin")) + // add the GreetService (picking up the default security of any authenticated user) + .register(new GreetService()) + .build(); + + GrpcServer grpcServer = GrpcServer.create(GrpcServerConfiguration.create(config), grpcRouting); + + grpcServer.start() + .thenAccept(s -> { + System.out.println("gRPC server is UP! http://localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("gRPC server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("gRPC server startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + + return grpcServer; + } + + /** + * Create the web server. + */ + private static WebServer createWebServer(Config config, Security security) { + + Routing routing = Routing.builder() + .register(WebSecurity.create(security).securityDefaults(WebSecurity.authenticate())) + .register(new RestService()) + .build(); + + WebServer webServer = WebServer.create(ServerConfiguration.create(config), routing); + + webServer.start() + .thenAccept(s -> { + System.out.println("Web server is UP! http://localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("gRPC server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("Web server startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + + return webServer; + } + + /** + * A gRPC greet service that uses outbound security to + * access a ReST API. + */ + public static class GreetService + implements GrpcService { + + /** + * The current greeting. + */ + private String greeting = "hello"; + + /** + * The JAX-RS client to use to make ReST calls. + */ + private Client client; + + private GreetService() { + client = ClientBuilder.newBuilder() + .register(new ClientSecurityFeature()) + .build(); + } + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.proto(Greet.getDescriptor()) + .unary("Greet", this::greet) + .unary("SetGreeting", this::setGreeting); + } + + /** + * This method calls a secure ReST endpoint using the caller's credentials. + * + * @param request the request + * @param observer the observer to send the response to + */ + private void greet(Greet.GreetRequest request, StreamObserver observer) { + // Obtain the greeting name from the request (default to "World". + String name = Optional.ofNullable(request.getName()).orElse("World"); + + // Obtain the security context from the current gRPC context + SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(); + + // Use the current credentials call the "lower" ReST endpoint which will call + // the "Lower" method on the secure gRPC StringService. + Response response = client.target("http://127.0.0.1:" + webServer.port()) + .path("lower") + .queryParam("value", name) + .request() + .property(ClientSecurityFeature.PROPERTY_CONTEXT, securityContext) + .get(); + + int status = response.getStatus(); + + if (status == 200) { + // Send the response to the caller of the current greeting and lower case name + String nameLower = response.readEntity(String.class); + String msg = String.format("%s %s!", greeting, nameLower); + complete(observer, Greet.GreetResponse.newBuilder().setMessage(msg).build()); + } else { + completeWithError(response, observer); + } + } + + /** + * This method calls a secure ReST endpoint overriding the caller's credentials and + * using the admin user's credentials. + * + * @param request the request + * @param observer the observer to send the response to + */ + private void setGreeting(Greet.SetGreetingRequest request, StreamObserver observer) { + // Obtain the greeting name from the request (default to "hello". + String name = Optional.ofNullable(request.getGreeting()).orElse("hello"); + + // Obtain the security context from the current gRPC context + SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(); + + // Use the admin user's credentials call the "upper" ReST endpoint which will call + // the "Upper" method on the secure gRPC StringService. + Response response = client.target("http://127.0.0.1:" + webServer.port()) + .path("upper") + .queryParam("value", name) + .request() + .property(ClientSecurityFeature.PROPERTY_CONTEXT, securityContext) + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_USER, "Ted") + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_PASSWORD, "secret") + .get(); + + if (response.getStatus() == 200) { + greeting = response.readEntity(String.class); + complete(observer, Greet.SetGreetingResponse.newBuilder().setGreeting(greeting).build()); + } else { + completeWithError(response, observer); + } + } + + private void completeWithError(Response response, StreamObserver observer) { + int status = response.getStatus(); + + if (status == Response.Status.UNAUTHORIZED.getStatusCode() + || status == Response.Status.FORBIDDEN.getStatusCode()){ + observer.onError(Status.PERMISSION_DENIED.asRuntimeException()); + } else { + observer.onError(Status.INTERNAL.withDescription(response.readEntity(String.class)).asRuntimeException()); + } + } + + @Override + public String name() { + return "GreetService"; + } + } + + /** + * A ReST service that calls the gRPC StringService to mutate String values. + */ + public static class RestService + implements Service { + + private Channel channel; + + @Override + public void update(Routing.Rules rules) { + rules.get("/lower", WebSecurity.rolesAllowed("user"), this::lower) + .get("/upper", WebSecurity.rolesAllowed("user"), this::upper); + } + + /** + * Call the gRPC StringService Lower method overriding the caller's credentials and + * using the admin user's credentials. + * + * @param req the http request + * @param res the http response + */ + private void lower(ServerRequest req, ServerResponse res) { + try { + // Create the gRPC client security credentials from the current request + // overriding with the admin user's credentials + GrpcClientSecurity clientSecurity = GrpcClientSecurity.builder(req) + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_USER, "Ted") + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_PASSWORD, "secret") + .build(); + + StringServiceGrpc.StringServiceBlockingStub stub = StringServiceGrpc.newBlockingStub(ensureChannel()) + .withCallCredentials(clientSecurity); + + String value = req.queryParams().first("value").orElse(null); + Strings.StringMessage response = stub.lower(Strings.StringMessage.newBuilder().setText(value).build()); + + res.status(200).send(response.getText()); + } catch (StatusRuntimeException e) { + res.status(GrpcHelper.toHttpResponseStatus(e.getStatus())).send(); + } + } + + /** + * Call the gRPC StringService Upper method using the current caller's credentials. + * + * @param req the http request + * @param res the http response + */ + private void upper(ServerRequest req, ServerResponse res) { + try { + // Create the gRPC client security credentials from the current request + GrpcClientSecurity clientSecurity = GrpcClientSecurity.create(req); + + StringServiceGrpc.StringServiceBlockingStub stub = StringServiceGrpc.newBlockingStub(ensureChannel()) + .withCallCredentials(clientSecurity); + + String value = req.queryParams().first("value").orElse(null); + Strings.StringMessage response = stub.upper(Strings.StringMessage.newBuilder().setText(value).build()); + + res.status(200).send(response.getText()); + } catch (StatusRuntimeException e) { + res.status(GrpcHelper.toHttpResponseStatus(e.getStatus())).send(); + } + } + + private synchronized Channel ensureChannel() { + if (channel == null) { + channel = InProcessChannelBuilder.forName(grpcServer.configuration().name()).build(); + } + return channel; + } + } +} diff --git a/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/package-info.java b/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/package-info.java new file mode 100644 index 00000000000..c498917470d --- /dev/null +++ b/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 using outbound security with gRPC services. + */ +package io.helidon.grpc.examples.security.outbound; diff --git a/examples/grpc/security-outbound/src/main/resources/application.yaml b/examples/grpc/security-outbound/src/main/resources/application.yaml new file mode 100644 index 00000000000..bfdc19d8882 --- /dev/null +++ b/examples/grpc/security-outbound/src/main/resources/application.yaml @@ -0,0 +1,34 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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" + +grpc: + name: "test.server" + port: 1408 + +webserver: + port: 8080 + +http-basic-auth: + users: + - login: "Ted" + password: "secret" + roles: ["user", "admin"] + - login: "Bob" + password: "password" + roles: ["user"] \ No newline at end of file diff --git a/examples/grpc/security-outbound/src/main/resources/logging.properties b/examples/grpc/security-outbound/src/main/resources/logging.properties new file mode 100644 index 00000000000..dcb9fcb30c9 --- /dev/null +++ b/examples/grpc/security-outbound/src/main/resources/logging.properties @@ -0,0 +1,39 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden by specific handlers and loggers +.level=INFO + +AUDIT.level=FINEST + +# Helidon Web Server has a custom log formatter that extends SimpleFormatter. +# It replaces "!thread!" with the current thread name +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +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 +#io.netty.level=INFO diff --git a/examples/grpc/security/README.md b/examples/grpc/security/README.md new file mode 100644 index 00000000000..96fea02168d --- /dev/null +++ b/examples/grpc/security/README.md @@ -0,0 +1,16 @@ + +# Helidon gRPC Security Example + +An example gRPC server using basic auth security. + +## Build + +``` +mvn package +``` + +## Run + +``` +mvn exec:java +``` diff --git a/examples/grpc/security/pom.xml b/examples/grpc/security/pom.xml new file mode 100644 index 00000000000..d46194b2491 --- /dev/null +++ b/examples/grpc/security/pom.xml @@ -0,0 +1,94 @@ + + + + + 4.0.0 + + io.helidon.examples.grpc + helidon-examples-grpc-project + 1.0.4-SNAPSHOT + + helidon-examples-grpc-security + Helidon gRPC Server Examples Security + + + Examples of securing gRPC services + + + + io.helidon.grpc.examples.security.SecureServer + + + + + io.helidon.examples.grpc + helidon-examples-grpc-common + ${project.version} + + + + io.helidon.grpc + helidon-grpc-core + ${project.version} + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + + + io.helidon.bundles + helidon-bundles-config + ${project.version} + + + io.helidon.security.integration + helidon-security-integration-grpc + ${project.version} + + + io.helidon.security.providers + helidon-security-providers-http-auth + ${project.version} + + + + io.helidon.grpc + helidon-grpc-client + ${project.version} + + + + io.grpc + grpc-netty + + + io.grpc + grpc-stub + + + io.grpc + grpc-services + + + io.grpc + grpc-protobuf + + + diff --git a/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/SecureGreetClient.java b/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/SecureGreetClient.java new file mode 100644 index 00000000000..154bc9f3ccb --- /dev/null +++ b/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/SecureGreetClient.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.security; + + +import io.helidon.config.Config; +import io.helidon.grpc.examples.common.Greet; +import io.helidon.grpc.examples.common.GreetServiceGrpc; +import io.helidon.security.Security; +import io.helidon.security.integration.grpc.GrpcClientSecurity; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; + +import io.grpc.CallCredentials; +import io.grpc.Channel; +import io.grpc.ManagedChannelBuilder; + +/** + * A {@link io.helidon.grpc.examples.common.GreetService} client that optionally + * provides {@link CallCredentials} using basic auth. + */ +public class SecureGreetClient { + + private SecureGreetClient() { + } + + /** + * Main entry point. + * + * @param args the program arguments - {@code arg[0]} is the user name + * and {@code arg[1] is the password} + */ + public static void main(String[] args) { + Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408) + .usePlaintext() + .build(); + + // Obtain the user name and password from the program arguments + String user = args.length >= 2 ? args[0] : null; + String password = args.length >= 2 ? args[1] : null; + + Config config = Config.create(); + + // configure Helidon security and add the basic auth provider + Security security = Security.builder() + .addProvider(HttpBasicAuthProvider.create(config.get("http-basic-auth"))) + .build(); + + // create the gRPC client security call credentials + // setting the properties used by the basic auth provider for user name and password + GrpcClientSecurity clientSecurity = GrpcClientSecurity.builder(security.createContext("test.client")) + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_USER, user) + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_PASSWORD, password) + .build(); + + // create the GreetService client stub and use the GrpcClientSecurity call credentials + GreetServiceGrpc.GreetServiceBlockingStub greetSvc = GreetServiceGrpc.newBlockingStub(channel) + .withCallCredentials(clientSecurity); + + greet(greetSvc); + setGreeting(greetSvc); + greet(greetSvc); + } + + private static void greet(GreetServiceGrpc.GreetServiceBlockingStub greetSvc) { + try { + Greet.GreetRequest request = Greet.GreetRequest.newBuilder().setName("Aleks").build(); + Greet.GreetResponse response = greetSvc.greet(request); + + System.out.println(response); + } catch (Exception e) { + System.err.println("Caught exception obtaining greeting: " + e.getMessage()); + } + } + + private static void setGreeting(GreetServiceGrpc.GreetServiceBlockingStub greetSvc) { + try { + Greet.SetGreetingRequest setRequest = Greet.SetGreetingRequest.newBuilder().setGreeting("Hey").build(); + Greet.SetGreetingResponse setResponse = greetSvc.setGreeting(setRequest); + + System.out.println(setResponse); + } catch (Exception e) { + System.err.println("Caught exception setting greeting: " + e.getMessage()); + } + } +} diff --git a/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/SecureServer.java b/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/SecureServer.java new file mode 100644 index 00000000000..ab519abbb6b --- /dev/null +++ b/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/SecureServer.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.security; + +import java.util.logging.LogManager; + +import io.helidon.config.Config; +import io.helidon.grpc.examples.common.GreetService; +import io.helidon.grpc.examples.common.StringService; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.security.Security; +import io.helidon.security.integration.grpc.GrpcSecurity; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; + +/** + * An example of a secure gRPC server. + */ +public class SecureServer { + + private SecureServer() { + } + + /** + * Main entry point. + * + * @param args the program arguments + * + * @throws Exception if an error occurs + */ + public static void main(String[] args) throws Exception { + LogManager.getLogManager().readConfiguration( + SecureServer.class.getResourceAsStream("/logging.properties")); + + Config config = Config.create(); + + Security security = Security.builder() + .addProvider(HttpBasicAuthProvider.create(config.get("http-basic-auth"))) + .build(); + + ServiceDescriptor greetService1 = ServiceDescriptor.builder(new GreetService(config)) + .name("GreetService") + .intercept(GrpcSecurity.rolesAllowed("user")) + .intercept("SetGreeting", GrpcSecurity.rolesAllowed("admin")) + .build(); + + GrpcRouting grpcRouting = GrpcRouting.builder() + .intercept(GrpcSecurity.create(security).securityDefaults(GrpcSecurity.authenticate())) + .register(greetService1) + .register(new StringService()) + .build(); + + GrpcServerConfiguration serverConfig = GrpcServerConfiguration.create(config.get("grpc")); + GrpcServer grpcServer = GrpcServer.create(serverConfig, grpcRouting); + + grpcServer.start() + .thenAccept(s -> { + System.out.println("gRPC server is UP! http://localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("gRPC server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("Startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + } +} diff --git a/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/SecureStringClient.java b/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/SecureStringClient.java new file mode 100644 index 00000000000..376b7db8e37 --- /dev/null +++ b/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/SecureStringClient.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.examples.security; + + +import io.helidon.config.Config; +import io.helidon.grpc.examples.common.StringServiceGrpc; +import io.helidon.grpc.examples.common.Strings; +import io.helidon.security.Security; +import io.helidon.security.integration.grpc.GrpcClientSecurity; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; + +import io.grpc.CallCredentials; +import io.grpc.Channel; +import io.grpc.ManagedChannelBuilder; + +/** + * A {@link io.helidon.grpc.examples.common.StringService} client that optionally + * provides {@link CallCredentials} using basic auth. + */ +public class SecureStringClient { + + private SecureStringClient() { + } + + /** + * Program entry point. + * + * @param args the program arguments - {@code arg[0]} is the user name + * and {@code arg[1] is the password} + */ + public static void main(String[] args) { + Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408) + .usePlaintext() + .build(); + + // Obtain the user name and password from the program arguments + String user = args.length >= 2 ? args[0] : null; + String password = args.length >= 2 ? args[1] : null; + + Config config = Config.create(); + + // configure Helidon security and add the basic auth provider + Security security = Security.builder() + .addProvider(HttpBasicAuthProvider.create(config.get("http-basic-auth"))) + .build(); + + // create the gRPC client security call credentials + // setting the properties used by the basic auth provider for user name and password + GrpcClientSecurity clientSecurity = GrpcClientSecurity.builder(security.createContext("test.client")) + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_USER, user) + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_PASSWORD, password) + .build(); + + // create the StringService client stub and use the GrpcClientSecurity call credentials + StringServiceGrpc.StringServiceBlockingStub stub = StringServiceGrpc.newBlockingStub(channel) + .withCallCredentials(clientSecurity); + + String text = "ABCDE"; + Strings.StringMessage request = Strings.StringMessage.newBuilder().setText(text).build(); + Strings.StringMessage response = stub.lower(request); + + System.out.println("Text '" + text + "' to lower is '" + response.getText() + "'"); + } +} diff --git a/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/package-info.java b/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/package-info.java new file mode 100644 index 00000000000..25aaaab44ba --- /dev/null +++ b/examples/grpc/security/src/main/java/io/helidon/grpc/examples/security/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.security.SecureServer Main} class. + */ +package io.helidon.grpc.examples.security; diff --git a/examples/grpc/security/src/main/resources/application.yaml b/examples/grpc/security/src/main/resources/application.yaml new file mode 100644 index 00000000000..bfdc19d8882 --- /dev/null +++ b/examples/grpc/security/src/main/resources/application.yaml @@ -0,0 +1,34 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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" + +grpc: + name: "test.server" + port: 1408 + +webserver: + port: 8080 + +http-basic-auth: + users: + - login: "Ted" + password: "secret" + roles: ["user", "admin"] + - login: "Bob" + password: "password" + roles: ["user"] \ No newline at end of file diff --git a/examples/grpc/security/src/main/resources/logging.properties b/examples/grpc/security/src/main/resources/logging.properties new file mode 100644 index 00000000000..dcb9fcb30c9 --- /dev/null +++ b/examples/grpc/security/src/main/resources/logging.properties @@ -0,0 +1,39 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden by specific handlers and loggers +.level=INFO + +AUDIT.level=FINEST + +# Helidon Web Server has a custom log formatter that extends SimpleFormatter. +# It replaces "!thread!" with the current thread name +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +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 +#io.netty.level=INFO diff --git a/examples/integrations/cdi/jpa/pom.xml b/examples/integrations/cdi/jpa/pom.xml new file mode 100644 index 00000000000..b6672207117 --- /dev/null +++ b/examples/integrations/cdi/jpa/pom.xml @@ -0,0 +1,245 @@ + + + + 4.0.0 + + io.helidon.examples.integrations.cdi + helidon-examples-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-examples-jpa + Helidon CDI Extensions Examples JPA + + + libs + + + + + + src/main/resources + true + + + + + com.ethlo.persistence.tools + eclipselink-maven-plugin + 2.7.1.1 + + + javax.annotation + javax.annotation-api + ${version.lib.annotation-api} + + + javax.xml.bind + jaxb-api + ${version.lib.jaxb-api} + + + + + weave + process-classes + + weave + + + + modelgen + generate-sources + + modelgen + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + + + + + maven-dependency-plugin + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${project.build.directory}/${dependenciesDirectory} + false + false + true + true + runtime + test + + + + + + maven-jar-plugin + + + + true + ${dependenciesDirectory} + io.helidon.microprofile.server.Main + + + + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + com.h2database + h2 + runtime + + + org.jboss.weld.se + weld-se-core + runtime + + + org.jboss.spec.javax.el + jboss-el-api_3.0_spec + + + org.jboss.spec.javax.interceptor + jboss-interceptors-api_1.2_spec + + + + + io.helidon.integrations.cdi + helidon-integrations-cdi-eclipselink + ${project.version} + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + ${project.version} + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-datasource-hikaricp + ${project.version} + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jpa-weld + ${project.version} + runtime + + + org.jboss + jandex + runtime + + + io.helidon.microprofile.server + helidon-microprofile-server + ${project.version} + runtime + + + org.glassfish.hk2.external + javax.inject + + + + + io.helidon.microprofile.config + helidon-microprofile-config-cdi + ${project.version} + runtime + + + org.eclipse.microprofile.config + microprofile-config-api + runtime + + + + + jakarta.persistence + jakarta.persistence-api + provided + + + javax.transaction + javax.transaction-api + provided + + + + + javax.annotation + javax.annotation-api + compile + + + javax.enterprise + cdi-api + compile + + + javax.inject + javax.inject + compile + + + javax.ws.rs + javax.ws.rs-api + compile + + + 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 00000000000..50999617f87 --- /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 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.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 00000000000..d7f9c1cc9d9 --- /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 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 javax.enterprise.context.ApplicationScoped; +import javax.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 00000000000..7965af9161e --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldResource.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.EntityNotFoundException; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceException; // for javadoc only +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.Transactional; +import javax.transaction.Transactional.TxType; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.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 = null; + // See https://tools.ietf.org/html/rfc7231#section-4.3.3; we + // track whether JPA does an insert or an update. + boolean created = false; + try { + greeting = this.entityManager.getReference(Greeting.class, firstPart); + assert greeting != null; + greeting.setSecondPart(secondPart); + } catch (final EntityNotFoundException entityNotFoundException) { + greeting = new Greeting(firstPart, secondPart); + this.entityManager.persist(greeting); + created = true; + } + 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 00000000000..51c1fb720ee --- /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 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 javax.enterprise.context.ApplicationScoped; +import javax.persistence.EntityNotFoundException; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.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 00000000000..fe314923757 --- /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 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 00000000000..89f9c163080 --- /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 00000000000..09f0967ec67 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 GREETING (FIRSTPART VARCHAR NOT NULL, SECONDPART VARCHAR NOT NULL, PRIMARY KEY (FIRSTPART))\\;INSERT INTO GREETING (FIRSTPART, SECONDPART) VALUES ('hello', 'world') +javax.sql.DataSource.test.username=sa +javax.sql.DataSource.test.password= 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 00000000000..108d28d641e --- /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/pom.xml b/examples/integrations/cdi/pom.xml index 3e066f938a3..b4e32749f3e 100644 --- a/examples/integrations/cdi/pom.xml +++ b/examples/integrations/cdi/pom.xml @@ -35,6 +35,7 @@ datasource-hikaricp datasource-hikaricp-mysql jedis + jpa oci-objectstorage diff --git a/examples/pom.xml b/examples/pom.xml index c7d66fdafd7..0f7c7b94673 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -45,6 +45,7 @@ todo-app quickstarts health + grpc diff --git a/examples/quickstarts/helidon-quickstart-mp/build.gradle b/examples/quickstarts/helidon-quickstart-mp/build.gradle index 10378f75c47..32bcad36835 100644 --- a/examples/quickstarts/helidon-quickstart-mp/build.gradle +++ b/examples/quickstarts/helidon-quickstart-mp/build.gradle @@ -33,7 +33,7 @@ ext { } repositories { - maven { url "http://repo.maven.apache.org/maven2" } + mavenCentral() mavenLocal() } diff --git a/examples/quickstarts/helidon-quickstart-se/build.gradle b/examples/quickstarts/helidon-quickstart-se/build.gradle index cfc97e3d2da..5b45588506c 100644 --- a/examples/quickstarts/helidon-quickstart-se/build.gradle +++ b/examples/quickstarts/helidon-quickstart-se/build.gradle @@ -33,7 +33,7 @@ ext { } repositories { - maven { url "http://repo.maven.apache.org/maven2" } + mavenCentral() mavenLocal() } diff --git a/grpc/client/pom.xml b/grpc/client/pom.xml new file mode 100644 index 00000000000..1a44c362fb3 --- /dev/null +++ b/grpc/client/pom.xml @@ -0,0 +1,134 @@ + + + + + 4.0.0 + + + io.helidon.grpc + helidon-grpc-project + 1.0.4-SNAPSHOT + + + helidon-grpc-client + Helidon gRPC Client + + + + io.helidon.grpc + helidon-grpc-core + ${project.version} + + + + io.helidon.tracing + helidon-tracing + ${project.version} + + + io.helidon.tracing + helidon-tracing-zipkin + ${project.version} + + + io.opentracing.contrib + opentracing-grpc + + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + javax.json + javax.json-api + test + + + org.mockito + mockito-core + test + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + test-compile + test-compile-custom + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.plugins + maven-failsafe-plugin + + false + + **/*IT.java + + + + + verify + verify + + integration-test + verify + + + + + + + diff --git a/grpc/client/src/main/java/io/helidon/grpc/client/ClientRequestAttribute.java b/grpc/client/src/main/java/io/helidon/grpc/client/ClientRequestAttribute.java new file mode 100644 index 00000000000..d9def8a2a01 --- /dev/null +++ b/grpc/client/src/main/java/io/helidon/grpc/client/ClientRequestAttribute.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.grpc.client; + +/** + * An enum of possible gRPC client call attributes to attach to + * call tracing spans. + */ +public enum ClientRequestAttribute { + /** + * Add the method type to the tracing span. + */ + METHOD_TYPE, + + /** + * Add the method name to the tracing span. + */ + METHOD_NAME, + + /** + * Add the call deadline to the tracing span. + */ + DEADLINE, + + /** + * Add the compressor type to the tracing span. + */ + COMPRESSOR, + + /** + * Add the security authority to the tracing span. + */ + AUTHORITY, + + /** + * Add the method call options to the tracing span. + */ + ALL_CALL_OPTIONS, + + /** + * Add the method call headers to the tracing span. + */ + HEADERS +} diff --git a/grpc/client/src/main/java/io/helidon/grpc/client/ClientTracingInterceptor.java b/grpc/client/src/main/java/io/helidon/grpc/client/ClientTracingInterceptor.java new file mode 100644 index 00000000000..8d84137bab1 --- /dev/null +++ b/grpc/client/src/main/java/io/helidon/grpc/client/ClientTracingInterceptor.java @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.client; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Priority; + +import io.helidon.grpc.core.ContextKeys; +import io.helidon.grpc.core.InterceptorPriorities; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.contrib.grpc.ActiveSpanSource; +import io.opentracing.contrib.grpc.OperationNameConstructor; +import io.opentracing.propagation.Format; +import io.opentracing.propagation.TextMap; + +/** + * A {@link ClientInterceptor} that captures tracing information into + * Open Tracing {@link Span}s for client calls. + */ +@Priority(InterceptorPriorities.TRACING) +public class ClientTracingInterceptor + implements ClientInterceptor { + + private final Tracer tracer; + + private final OperationNameConstructor operationNameConstructor; + + private final boolean streaming; + + private final boolean verbose; + + private final Set tracedAttributes; + + private final ActiveSpanSource activeSpanSource; + + /** + * Private constructor called by {@link Builder}. + * + * @param tracer the Open Tracing {@link Tracer} + * @param operationNameConstructor the operation name constructor + * @param streaming flag indicating whether to trace streaming calls + * @param verbose flag to indicate verbose logging to spans + * @param tracedAttributes the set of request attributes to add to the span + * @param activeSpanSource the spurce of the active span + */ + private ClientTracingInterceptor(Tracer tracer, + OperationNameConstructor operationNameConstructor, + boolean streaming, + boolean verbose, + Set tracedAttributes, + ActiveSpanSource activeSpanSource) { + this.tracer = tracer; + this.operationNameConstructor = operationNameConstructor; + this.streaming = streaming; + this.verbose = verbose; + this.tracedAttributes = tracedAttributes; + this.activeSpanSource = activeSpanSource; + } + + /** + * Use this interceptor to trace all requests made by this client channel. + * + * @param channel to be traced + * @return intercepted channel + */ + public Channel intercept(Channel channel) { + return ClientInterceptors.intercept(channel, this); + } + + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, + Channel next) { + + String operationName = operationNameConstructor.constructOperationName(method); + Span span = createSpanFromParent(activeSpanSource.getActiveSpan(), operationName); + + for (ClientRequestAttribute attr : tracedAttributes) { + switch (attr) { + case ALL_CALL_OPTIONS: + span.setTag("grpc.call_options", callOptions.toString()); + break; + case AUTHORITY: + if (callOptions.getAuthority() == null) { + span.setTag("grpc.authority", "null"); + } else { + span.setTag("grpc.authority", callOptions.getAuthority()); + } + break; + case COMPRESSOR: + if (callOptions.getCompressor() == null) { + span.setTag("grpc.compressor", "null"); + } else { + span.setTag("grpc.compressor", callOptions.getCompressor()); + } + break; + case DEADLINE: + if (callOptions.getDeadline() == null) { + span.setTag("grpc.deadline_millis", "null"); + } else { + span.setTag("grpc.deadline_millis", callOptions.getDeadline().timeRemaining(TimeUnit.MILLISECONDS)); + } + break; + case METHOD_NAME: + span.setTag("grpc.method_name", method.getFullMethodName()); + break; + case METHOD_TYPE: + if (method.getType() == null) { + span.setTag("grpc.method_type", "null"); + } else { + span.setTag("grpc.method_type", method.getType().toString()); + } + break; + case HEADERS: + break; + default: + // should not happen, but can be ignored + } + } + + return new ClientTracingListener<>(next.newCall(method, callOptions), span); + } + + private Span createSpanFromParent(Span parentSpan, String operationName) { + if (parentSpan == null) { + return tracer.buildSpan(operationName).start(); + } else { + return tracer.buildSpan(operationName).asChildOf(parentSpan).start(); + } + } + + /** + * Obtain a builder to build a {@link ClientTracingInterceptor}. + * + * @param tracer the {@link Tracer} to use + * + * @return a builder to build a {@link ClientTracingInterceptor} + */ + public static Builder builder(Tracer tracer) { + return new Builder(tracer); + } + + /** + * Builds the configuration of a ClientTracingInterceptor. + */ + public static class Builder { + + private final Tracer tracer; + + private OperationNameConstructor operationNameConstructor; + + private boolean streaming; + + private boolean verbose; + + private Set tracedAttributes; + + private ActiveSpanSource activeSpanSource; + + /** + * @param tracer to use for this intercepter + * Creates a Builder with default configuration + */ + public Builder(Tracer tracer) { + this.tracer = tracer; + operationNameConstructor = OperationNameConstructor.DEFAULT; + streaming = false; + verbose = false; + tracedAttributes = new HashSet<>(); + activeSpanSource = ActiveSpanSource.GRPC_CONTEXT; + } + + /** + * @param operationNameConstructor to name all spans created by this intercepter + * @return this Builder with configured operation name + */ + public ClientTracingInterceptor.Builder withOperationName(OperationNameConstructor operationNameConstructor) { + this.operationNameConstructor = operationNameConstructor; + return this; + } + + /** + * Logs streaming events to client spans. + * + * @return this Builder configured to log streaming events + */ + public ClientTracingInterceptor.Builder withStreaming() { + streaming = true; + return this; + } + + /** + * @param tracedAttributes to set as tags on client spans + * created by this intercepter + * @return this Builder configured to trace attributes + */ + public ClientTracingInterceptor.Builder withTracedAttributes(ClientRequestAttribute... tracedAttributes) { + this.tracedAttributes = new HashSet<>(Arrays.asList(tracedAttributes)); + return this; + } + + /** + * Logs all request life-cycle events to client spans. + * + * @return this Builder configured to be verbose + */ + public ClientTracingInterceptor.Builder withVerbosity() { + verbose = true; + return this; + } + + /** + * @param activeSpanSource that provides a method of getting the + * active span before the client call + * @return this Builder configured to start client span as children + * of the span returned by activeSpanSource.getActiveSpan() + */ + public ClientTracingInterceptor.Builder withActiveSpanSource(ActiveSpanSource activeSpanSource) { + this.activeSpanSource = activeSpanSource; + return this; + } + + /** + * @return a ClientTracingInterceptor with this Builder's configuration + */ + public ClientTracingInterceptor build() { + return new ClientTracingInterceptor(tracer, + operationNameConstructor, + streaming, + verbose, + tracedAttributes, + activeSpanSource); + } + } + + /** + * A {@link ForwardingClientCall.SimpleForwardingClientCall} that adds information + * to a tracing {@link Span} at different places in the gROC call lifecycle. + * + * @param the gRPC request type + * @param the gRPC response type + */ + private class ClientTracingListener + extends ForwardingClientCall.SimpleForwardingClientCall { + + private final Span span; + + private ClientTracingListener(ClientCall delegate, Span span) { + super(delegate); + this.span = span; + } + + @Override + public void start(Listener responseListener, final Metadata headers) { + if (verbose) { + span.log("Started call"); + } + + if (tracedAttributes.contains(ClientRequestAttribute.HEADERS)) { + // copy the headers and make sure that the AUTHORIZATION header + // is removed as we do not want auth details to appear in tracing logs + Metadata metadata = new Metadata(); + metadata.merge(headers); + metadata.removeAll(ContextKeys.AUTHORIZATION); + span.setTag("grpc.headers", metadata.toString()); + } + + tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new TextMap() { + @Override + public void put(String key, String value) { + Metadata.Key headerKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); + headers.put(headerKey, value); + } + + @Override + public Iterator> iterator() { + throw new UnsupportedOperationException( + "TextMapInjectAdapter should only be used with Tracer.inject()"); + } + }); + + Listener tracingResponseListener + = new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { + @Override + public void onHeaders(Metadata headers) { + if (verbose) { + span.log(Collections.singletonMap("Response headers received", headers.toString())); + } + delegate().onHeaders(headers); + } + + @Override + public void onMessage(RespT message) { + if (streaming || verbose) { + span.log("Response received"); + } + delegate().onMessage(message); + } + + @Override + public void onClose(Status status, Metadata trailers) { + if (verbose) { + if (status.getCode().value() == 0) { + span.log("Call closed"); + } else { + String desc = String.valueOf(status.getDescription()); + + span.log(Collections.singletonMap("Call failed", desc)); + } + } + span.finish(); + + delegate().onClose(status, trailers); + } + }; + + delegate().start(tracingResponseListener, headers); + } + + @Override + public void cancel(String message, Throwable cause) { + String errorMessage; + + errorMessage = message == null ? "Error" : message; + + if (cause == null) { + span.log(errorMessage); + } else { + span.log(Collections.singletonMap(errorMessage, cause.getMessage())); + } + + delegate().cancel(message, cause); + } + + @Override + public void halfClose() { + if (streaming) { + span.log("Finished sending messages"); + } + + delegate().halfClose(); + } + + @Override + public void sendMessage(ReqT message) { + if (streaming || verbose) { + span.log("Message sent"); + } + + delegate().sendMessage(message); + } + } +} diff --git a/grpc/client/src/main/java/io/helidon/grpc/client/package-info.java b/grpc/client/src/main/java/io/helidon/grpc/client/package-info.java new file mode 100644 index 00000000000..2a9d4c99de5 --- /dev/null +++ b/grpc/client/src/main/java/io/helidon/grpc/client/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * gRPC client API. + */ +package io.helidon.grpc.client; diff --git a/grpc/client/src/test/java/io/helidon/grpc/client/TestServer.java b/grpc/client/src/test/java/io/helidon/grpc/client/TestServer.java new file mode 100644 index 00000000000..aa1bbaecf7f --- /dev/null +++ b/grpc/client/src/test/java/io/helidon/grpc/client/TestServer.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.client; + +import java.util.logging.LogManager; + +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; + +import services.EchoService; + +/** + * A test gRPC server. + */ +public class TestServer { + /** + * Program entry point. + * + * @param args the program command line arguments + * @throws Exception if there is a program error + */ + public static void main(String[] args) throws Exception { + LogManager.getLogManager().readConfiguration(TestServer.class.getResourceAsStream("/logging.properties")); + + // Add the EchoService and enable GrpcMetrics + GrpcRouting routing = GrpcRouting.builder() + .register(new EchoService()) + .build(); + + // Run the server on port 0 so that it picks a free ephemeral port + GrpcServerConfiguration serverConfig = GrpcServerConfiguration.builder().build(); + + GrpcServer.create(serverConfig, routing) + .start() + .thenAccept(s -> { + System.out.println("gRPC server is UP and listening on localhost:" + s.port()); + s.whenShutdown().thenRun(() -> System.out.println("gRPC server is DOWN. Good bye!")); + }) + .exceptionally(t -> { + System.err.println("Startup failed: " + t.getMessage()); + t.printStackTrace(System.err); + return null; + }); + } +} diff --git a/grpc/client/src/test/java/services/EchoService.java b/grpc/client/src/test/java/services/EchoService.java new file mode 100644 index 00000000000..dd5e1c7c663 --- /dev/null +++ b/grpc/client/src/test/java/services/EchoService.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package services; + +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.grpc.client.test.Echo; + +import io.grpc.stub.StreamObserver; + +/** + * A simple test gRPC echo service. + */ +public class EchoService + implements GrpcService { + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.proto(Echo.getDescriptor()) + .unary("Echo", this::echo); + } + + /** + * Echo the message back to the caller. + * + * @param request the echo request containing the message to echo + * @param observer the call response + */ + public void echo(Echo.EchoRequest request, StreamObserver observer) { + String message = request.getMessage(); + Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build(); + complete(observer, response); + } +} diff --git a/grpc/client/src/test/proto/echo.proto b/grpc/client/src/test/proto/echo.proto new file mode 100644 index 00000000000..cf46b367fef --- /dev/null +++ b/grpc/client/src/test/proto/echo.proto @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.grpc.client.test"; + +service EchoService { + rpc Echo (EchoRequest) returns (EchoResponse) {} +} + +message EchoRequest { + string message = 1; +} + +message EchoResponse { + string message = 1; +} diff --git a/grpc/client/src/test/resources/logging.properties b/grpc/client/src/test/resources/logging.properties new file mode 100644 index 00000000000..f903114790c --- /dev/null +++ b/grpc/client/src/test/resources/logging.properties @@ -0,0 +1,37 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden 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 +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +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 +#io.netty.level=INFO diff --git a/grpc/core/pom.xml b/grpc/core/pom.xml new file mode 100644 index 00000000000..4f1e8b4d81f --- /dev/null +++ b/grpc/core/pom.xml @@ -0,0 +1,88 @@ + + + + + 4.0.0 + + + io.helidon.grpc + helidon-grpc-project + 1.0.4-SNAPSHOT + + + helidon-grpc-core + Helidon gRPC Core + + + + io.grpc + grpc-netty + + + io.grpc + grpc-stub + + + io.grpc + grpc-services + + + io.grpc + grpc-protobuf + + + + io.helidon.common + helidon-common-http + ${project.version} + + + + javax.inject + javax.inject + + + javax.annotation + javax.annotation-api + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + javax.json + javax.json-api + test + + + org.mockito + mockito-core + test + + + diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java b/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java new file mode 100644 index 00000000000..98e76b7a0d7 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.core; + +import io.grpc.Metadata; + +/** + * A collection of common gRPC {@link io.grpc.Context.Key} instances. + */ +public final class ContextKeys { + /** + * The authorization gRPC metadata header key. + */ + public static final Metadata.Key AUTHORIZATION = + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); + + /** + * Private constructor for utility class. + */ + private ContextKeys() { + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java new file mode 100644 index 00000000000..bbe598d7d83 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.core; + +import io.helidon.common.http.Http; + +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; + +/** + * Helper methods for common gRPC tasks. + */ +public final class GrpcHelper { + + /** + * Private constructor for utility class. + */ + private GrpcHelper() { + } + + /** + * Extract the gRPC service name from a method full name. + * + * @param fullMethodName the gRPC method full name + * + * @return the service name extracted from the full name + */ + public static String extractServiceName(String fullMethodName) { + int index = fullMethodName.indexOf('/'); + return index == -1 ? fullMethodName : fullMethodName.substring(0, index); + } + + /** + * Extract the name prefix from from a method full name. + *

+ * The prefix is everything upto the but not including the last + * '/' character in the full name. + * + * @param fullMethodName the gRPC method full name + * + * @return the name prefix extracted from the full name + */ + public static String extractNamePrefix(String fullMethodName) { + int index = fullMethodName.lastIndexOf('/'); + return index == -1 ? fullMethodName : fullMethodName.substring(0, index); + } + + /** + * Extract the gRPC method name from a method full name. + * + * @param fullMethodName the gRPC method full name + * + * @return the method name extracted from the full name + */ + public static String extractMethodName(String fullMethodName) { + int index = fullMethodName.lastIndexOf('/'); + return index == -1 ? fullMethodName : fullMethodName.substring(index + 1); + } + + /** + * Convert a gRPC {@link StatusException} to a {@link Http.ResponseStatus}. + * + * @param ex the gRPC {@link StatusException} to convert + * + * @return the gRPC {@link StatusException} converted to a {@link Http.ResponseStatus} + */ + public static Http.ResponseStatus toHttpResponseStatus(StatusException ex) { + return toHttpResponseStatus(ex.getStatus()); + } + + /** + * Convert a gRPC {@link StatusRuntimeException} to a {@link Http.ResponseStatus}. + * + * @param ex the gRPC {@link StatusRuntimeException} to convert + * + * @return the gRPC {@link StatusRuntimeException} converted to a {@link Http.ResponseStatus} + */ + public static Http.ResponseStatus toHttpResponseStatus(StatusRuntimeException ex) { + return toHttpResponseStatus(ex.getStatus()); + } + + /** + * Convert a gRPC {@link Status} to a {@link Http.ResponseStatus}. + * + * @param status the gRPC {@link Status} to convert + * + * @return the gRPC {@link Status} converted to a {@link Http.ResponseStatus} + */ + public static Http.ResponseStatus toHttpResponseStatus(Status status) { + Http.ResponseStatus httpStatus; + + switch (status.getCode()) { + case OK: + httpStatus = Http.ResponseStatus.create(200, status.getDescription()); + break; + case INVALID_ARGUMENT: + httpStatus = Http.ResponseStatus.create(400, status.getDescription()); + break; + case DEADLINE_EXCEEDED: + httpStatus = Http.ResponseStatus.create(408, status.getDescription()); + break; + case NOT_FOUND: + httpStatus = Http.ResponseStatus.create(404, status.getDescription()); + break; + case ALREADY_EXISTS: + httpStatus = Http.ResponseStatus.create(412, status.getDescription()); + break; + case PERMISSION_DENIED: + httpStatus = Http.ResponseStatus.create(403, status.getDescription()); + break; + case FAILED_PRECONDITION: + httpStatus = Http.ResponseStatus.create(412, status.getDescription()); + break; + case OUT_OF_RANGE: + httpStatus = Http.ResponseStatus.create(400, status.getDescription()); + break; + case UNIMPLEMENTED: + httpStatus = Http.ResponseStatus.create(501, status.getDescription()); + break; + case UNAVAILABLE: + httpStatus = Http.ResponseStatus.create(503, status.getDescription()); + break; + case UNAUTHENTICATED: + httpStatus = Http.ResponseStatus.create(401, status.getDescription()); + break; + case ABORTED: + case CANCELLED: + case DATA_LOSS: + case INTERNAL: + case RESOURCE_EXHAUSTED: + case UNKNOWN: + default: + httpStatus = Http.ResponseStatus.create(500, status.getDescription()); + } + + return httpStatus; + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java new file mode 100644 index 00000000000..fab090c477d --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.core; + +/** + * Constants that represent a priority ordering that interceptors registered with + * a gRPC service or method will be applied. + */ +public class InterceptorPriorities { + /** + * Context priority. + *

+ * Interceptors with this priority typically only perform tasks + * such as adding state to the call {@link io.grpc.Context}. + */ + public static final int CONTEXT = 1000; + + /** + * Tracing priority. + *

+ * Tracing and metrics interceptors are typically applied after any context + * interceptors so that they can trace and gather metrics on the whole call + * stack of remaining interceptors. + */ + public static final int TRACING = CONTEXT + 1; + + /** + * Security authentication priority. + */ + public static final int AUTHENTICATION = 2000; + + /** + * Security authorization priority. + */ + public static final int AUTHORIZATION = 2000; + + /** + * User-level priority. + * + * This value is also used as a default priority for application-supplied interceptors. + */ + public static final int USER = 5000; + + /** + * Cannot create instances. + */ + private InterceptorPriorities() { + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/JavaMarshaller.java b/grpc/core/src/main/java/io/helidon/grpc/core/JavaMarshaller.java new file mode 100644 index 00000000000..bbc1a84c467 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/JavaMarshaller.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.core; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import javax.inject.Named; +import javax.inject.Singleton; + +import io.grpc.MethodDescriptor; + +/** + * An implementation of a gRPC {@link MethodDescriptor.Marshaller} that + * uses Java serialization. + * + * @param the type of value to to be marshalled + */ +@Singleton +@Named("java") +public class JavaMarshaller + implements MethodDescriptor.Marshaller { + + @Override + public InputStream stream(T obj) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(out)) { + oos.writeObject(obj); + return new ByteArrayInputStream(out.toByteArray()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + @SuppressWarnings("unchecked") + public T parse(InputStream in) { + try (ObjectInputStream ois = new ObjectInputStream(in)) { + return (T) ois.readObject(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java new file mode 100644 index 00000000000..96dbc4aea9c --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.core; + +import com.google.protobuf.MessageLite; +import io.grpc.MethodDescriptor; +import io.grpc.protobuf.lite.ProtoLiteUtils; + +/** + * A supplier of {@link MethodDescriptor.Marshaller} instances for specific + * classes. + */ +@FunctionalInterface +public interface MarshallerSupplier { + + /** + * Obtain a {@link MethodDescriptor.Marshaller} for a type. + * + * @param clazz the {@link Class} of the type to obtain the {@link MethodDescriptor.Marshaller} for + * @param the type to be marshalled + * + * @return a {@link MethodDescriptor.Marshaller} for a type + */ + MethodDescriptor.Marshaller get(Class clazz); + + /** + * Obtain the default marshaller. + * + * @return the default marshaller + */ + static MarshallerSupplier defaultInstance() { + return new DefaultMarshallerSupplier(); + } + + /** + * The default {@link MarshallerSupplier}. + */ + class DefaultMarshallerSupplier + implements MarshallerSupplier { + + /** + * The singleton default {@link MethodDescriptor.Marshaller}. + */ + private static final MethodDescriptor.Marshaller JAVA_MARSHALLER = new JavaMarshaller(); + + @Override + @SuppressWarnings("unchecked") + public MethodDescriptor.Marshaller get(Class clazz) { + if (MessageLite.class.isAssignableFrom(clazz)) { + try { + java.lang.reflect.Method getDefaultInstance = clazz.getDeclaredMethod("getDefaultInstance"); + MessageLite instance = (MessageLite) getDefaultInstance.invoke(clazz); + return (MethodDescriptor.Marshaller) ProtoLiteUtils.marshaller(instance); + } catch (Exception e) { + String msg = String.format( + "Attempting to use class %s, which is not a valid Protobuf message, with a default marshaller", + clazz.getName()); + throw new IllegalArgumentException(msg); + } + } + + return JAVA_MARSHALLER; + } + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java b/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java new file mode 100644 index 00000000000..fc5a1f39be8 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Stream; + +import javax.annotation.Priority; + +import io.helidon.common.Prioritized; + +/** + * A bag of values ordered by priority. + *

+ * An element with lower priority number is more significant than an element + * with a higher priority number. + *

+ * For cases where priority is the same, elements are ordered in the order that + * they were added to the bag. + *

+ * Elements added with negative priorities are assumed to have no priority and + * will be least significant in order. + * + * @param the type of elements in the bag + */ +public class PriorityBag implements Iterable { + + private final Map> contents; + + private final List noPriorityList; + + private final int defaultPriority; + + /** + * Create a new {@link PriorityBag} where elements + * added with no priority will be last in the order. + */ + public PriorityBag() { + this(new TreeMap<>(), new ArrayList<>(), -1); + } + + /** + * Create a new {@link PriorityBag} where elements + * added with no priority will be be given a default + * priority value. + * + * @param defaultPriority the default priority value to assign + * to elements added with no priority + */ + public PriorityBag(int defaultPriority) { + this(new TreeMap<>(), new ArrayList<>(), defaultPriority); + } + + + private PriorityBag(Map> contents, List noPriorityList, int defaultPriority) { + this.contents = contents; + this.noPriorityList = noPriorityList; + this.defaultPriority = defaultPriority; + } + + /** + * Obtain a copy of this {@link PriorityBag}. + * + * @return a copy of this {@link PriorityBag} + */ + public PriorityBag copyMe() { + PriorityBag copy = new PriorityBag<>(); + copy.merge(this); + return copy; + } + + /** + * Obtain an immutable copy of this {@link PriorityBag}. + * + * @return an immutable copy of this {@link PriorityBag} + */ + public PriorityBag readOnly() { + return new PriorityBag<>(Collections.unmodifiableMap(contents), + Collections.unmodifiableList(noPriorityList), + defaultPriority); + } + + /** + * Merge a {@link PriorityBag} into this {@link PriorityBag}. + * + * @param bag the bag to merge + */ + public void merge(PriorityBag bag) { + bag.contents.forEach((priority, value) -> addAll(value, priority)); + this.noPriorityList.addAll(bag.noPriorityList); + } + + /** + * Add elements to the bag. + *

+ * If the element's class is annotated with the {@link javax.annotation.Priority} + * annotation then that value will be used to determine priority otherwise the + * default priority value will be used. + * + * @param values the elements to add + */ + public void addAll(Iterable values) { + for (T value : values) { + add(value); + } + } + + /** + * Add elements to the bag. + * + * @param values the elements to add + * @param priority the priority to assign to the elements + */ + public void addAll(Iterable values, int priority) { + for (T value : values) { + add(value, priority); + } + } + + /** + * Add an element to the bag. + *

+ * If the element's class is annotated with the {@link javax.annotation.Priority} + * annotation then that value will be used to determine priority otherwise the + * default priority value will be used. + * + * @param value the element to add + */ + public void add(T value) { + if (value != null) { + int priority; + if (value instanceof Prioritized) { + priority = ((Prioritized) value).priority(); + } else { + Priority annotation = value.getClass().getAnnotation(Priority.class); + priority = annotation == null ? defaultPriority : annotation.value(); + } + add(value, priority); + } + } + + /** + * Add an element to the bag with a specific priority. + *

+ * + * @param value the element to add + * @param priority the priority of the element + */ + public void add(T value, int priority) { + if (value != null) { + if (priority < 0) { + noPriorityList.add(value); + } else { + contents.compute(priority, (key, list) -> combine(list, value)); + } + } + } + + /** + * Obtain the contents of this {@link PriorityBag} as + * an ordered {@link Stream}. + * + * @return the contents of this {@link PriorityBag} as + * an ordered {@link Stream} + */ + public Stream stream() { + Stream stream = contents.entrySet() + .stream() + .flatMap(e -> e.getValue().stream()); + + return Stream.concat(stream, noPriorityList.stream()); + } + + @Override + public Iterator iterator() { + return stream().iterator(); + } + + private List combine(List list, T value) { + if (list == null) { + list = new ArrayList<>(); + } + list.add(value); + return list; + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java new file mode 100644 index 00000000000..553dbbe5910 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.core; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +/** + * A {@link io.grpc.stub.StreamObserver} that handles exceptions correctly. + * + * @param the type of response expected + */ +public class SafeStreamObserver + implements StreamObserver { + + /** + * Create a {@link io.helidon.grpc.core.SafeStreamObserver} that wraps + * another {@link io.grpc.stub.StreamObserver}. + * + * @param streamObserver the {@link io.grpc.stub.StreamObserver} to wrap + */ + public SafeStreamObserver(StreamObserver streamObserver) { + delegate = streamObserver; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (t == null) { + onError(Status.INVALID_ARGUMENT + .withDescription("onNext called with null. Null values are generally not allowed.") + .asRuntimeException()); + } else { + try { + delegate.onNext(t); + } catch (Throwable thrown) { + throwIfFatal(thrown); + onError(thrown); + } + } + } + + @Override + public void onError(Throwable thrown) { + try { + if (!done) { + done = true; + delegate.onError(checkNotNull(thrown)); + } else { + LOGGER.log(Level.SEVERE, checkNotNull(thrown), () -> "OnError called after StreamObserver was closed"); + } + } catch (Throwable t) { + throwIfFatal(t); + LOGGER.log(Level.SEVERE, t, () -> "Caught exception handling onError"); + } + } + + @Override + public void onCompleted() { + if (done) { + LOGGER.log(Level.WARNING, "onComplete called after StreamObserver was closed"); + } else { + try { + delegate.onCompleted(); + } catch (Throwable thrown) { + throwIfFatal(thrown); + LOGGER.log(Level.SEVERE, thrown, () -> "Caught exception handling onComplete"); + } + } + } + + private Throwable checkNotNull(Throwable thrown) { + if (thrown == null) { + thrown = Status.INVALID_ARGUMENT + .withDescription("onError called with null Throwable. Null exceptions are generally not allowed.") + .asRuntimeException(); + } + + return thrown; + } + + /** + * Throws a particular {@code Throwable} only if it belongs to a set of "fatal" error varieties. These varieties are + * as follows: + *

    + *
  • {@code VirtualMachineError}
  • + *
  • {@code ThreadDeath}
  • + *
  • {@code LinkageError}
  • + *
+ * + * @param thrown the {@code Throwable} to test and perhaps throw + */ + private static void throwIfFatal(Throwable thrown) { + if (thrown instanceof VirtualMachineError) { + throw (VirtualMachineError) thrown; + } else if (thrown instanceof ThreadDeath) { + throw (ThreadDeath) thrown; + } else if (thrown instanceof LinkageError) { + throw (LinkageError) thrown; + } + } + + /** + * Ensure that the specified {@link StreamObserver} is a safe observer. + *

+ * If the specified observer is not an instance of {@link SafeStreamObserver} then wrap + * it in a {@link SafeStreamObserver}. + * + * @param observer the {@link StreamObserver} to test + * @param the response type expected by the observer + * + * @return a safe {@link StreamObserver} + */ + public static StreamObserver ensureSafeObserver(StreamObserver observer) { + if (observer instanceof SafeStreamObserver) { + return observer; + } + + return new SafeStreamObserver<>(observer); + } + + // ----- constants ------------------------------------------------------ + + /** + * The {2link Logger} to use. + */ + private static final Logger LOGGER = Logger.getLogger(SafeStreamObserver.class.getName()); + + // ----- data members --------------------------------------------------- + + /** + * The actual StreamObserver. + */ + private StreamObserver delegate; + + /** + * Indicates a terminal state. + */ + private boolean done; +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java b/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java new file mode 100644 index 00000000000..c7629abba70 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Core classes used by both the reactive gRPC server API and gRPC client API. + */ +package io.helidon.grpc.core; diff --git a/grpc/core/src/test/java/io/helidon/grpc/core/GrpcHelperTest.java b/grpc/core/src/test/java/io/helidon/grpc/core/GrpcHelperTest.java new file mode 100644 index 00000000000..136024e324b --- /dev/null +++ b/grpc/core/src/test/java/io/helidon/grpc/core/GrpcHelperTest.java @@ -0,0 +1,666 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.core; + +import io.helidon.common.http.Http; + +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * {@link GrpcHelper} unit tests + */ +public class GrpcHelperTest { + + @Test + public void shouldExtractServiceName() { + String fullName = "Foo/1234/Bar"; + + assertThat(GrpcHelper.extractServiceName(fullName), is("Foo")); + } + + @Test + public void shouldExtractMethodName() { + String fullName = "Foo/1234/Bar"; + + assertThat(GrpcHelper.extractMethodName(fullName), is("Bar")); + } + + @Test + public void shouldExtractNamePrefix() { + String fullName = "Foo/1234/Bar"; + + assertThat(GrpcHelper.extractNamePrefix(fullName), is("Foo/1234")); + } + + @Test + public void shouldConvertAbortedStatusException() { + StatusException exception = Status.ABORTED.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertAbortedStatusExceptionWithDescription() { + StatusException exception = Status.ABORTED.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertAlreadyExistsStatusException() { + StatusException exception = Status.ALREADY_EXISTS.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Precondition Failed")); + } + + @Test + public void shouldConvertAlreadyExistsStatusExceptionWithDescription() { + StatusException exception = Status.ALREADY_EXISTS.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertOkStatusException() { + StatusException exception = Status.OK.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(200)); + assertThat(status.reasonPhrase(), is("OK")); + } + + @Test + public void shouldConvertOkStatusExceptionWithDescription() { + StatusException exception = Status.OK.withDescription("Good!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(200)); + assertThat(status.reasonPhrase(), is("Good!")); + } + + @Test + public void shouldConvertInvalidArgumentStatusException() { + StatusException exception = Status.INVALID_ARGUMENT.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Bad Request")); + } + + @Test + public void shouldConvertInvalidArgumentStatusExceptionWithDescription() { + StatusException exception = Status.INVALID_ARGUMENT.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertDeadlineExceededStatusException() { + StatusException exception = Status.DEADLINE_EXCEEDED.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(408)); + assertThat(status.reasonPhrase(), is("Request Timeout")); + } + + @Test + public void shouldConvertDeadlineExceededStatusExceptionWithDescription() { + StatusException exception = Status.DEADLINE_EXCEEDED.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(408)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertNotFoundStatusException() { + StatusException exception = Status.NOT_FOUND.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(404)); + assertThat(status.reasonPhrase(), is("Not Found")); + } + + @Test + public void shouldConvertNotFoundStatusExceptionWithDescription() { + StatusException exception = Status.NOT_FOUND.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(404)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertPermissionDeniedStatusException() { + StatusException exception = Status.PERMISSION_DENIED.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(403)); + assertThat(status.reasonPhrase(), is("Forbidden")); + } + + @Test + public void shouldConvertPermissionDeniedStatusExceptionWithDescription() { + StatusException exception = Status.PERMISSION_DENIED.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(403)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertFailedPreconditionStatusException() { + StatusException exception = Status.FAILED_PRECONDITION.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Precondition Failed")); + } + + @Test + public void shouldConvertFailedPreconditionStatusExceptionWithDescription() { + StatusException exception = Status.FAILED_PRECONDITION.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertOutOfRangeStatusException() { + StatusException exception = Status.OUT_OF_RANGE.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Bad Request")); + } + + @Test + public void shouldConvertOutOfRangeStatusExceptionWithDescription() { + StatusException exception = Status.OUT_OF_RANGE.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnimplementedStatusException() { + StatusException exception = Status.UNIMPLEMENTED.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(501)); + assertThat(status.reasonPhrase(), is("Not Implemented")); + } + + @Test + public void shouldConvertUnimplementedStatusExceptionWithDescription() { + StatusException exception = Status.UNIMPLEMENTED.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(501)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnavailableStatusException() { + StatusException exception = Status.UNAVAILABLE.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(503)); + assertThat(status.reasonPhrase(), is("Service Unavailable")); + } + + @Test + public void shouldConvertUnavailableStatusExceptionWithDescription() { + StatusException exception = Status.UNAVAILABLE.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(503)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnauthenticatedStatusException() { + StatusException exception = Status.UNAUTHENTICATED.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(401)); + assertThat(status.reasonPhrase(), is("Unauthorized")); + } + + @Test + public void shouldConvertUnauthenticatedStatusExceptionWithDescription() { + StatusException exception = Status.UNAUTHENTICATED.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(401)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertCancelledStatusException() { + StatusException exception = Status.CANCELLED.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertCancelledStatusExceptionWithDescription() { + StatusException exception = Status.CANCELLED.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertDataLossStatusException() { + StatusException exception = Status.DATA_LOSS.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertDataLossStatusExceptionWithDescription() { + StatusException exception = Status.DATA_LOSS.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertInternalStatusException() { + StatusException exception = Status.INTERNAL.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertInternalStatusExceptionWithDescription() { + StatusException exception = Status.INTERNAL.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertResourceExhaustedStatusException() { + StatusException exception = Status.RESOURCE_EXHAUSTED.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertResourceExhaustedStatusExceptionWithDescription() { + StatusException exception = Status.RESOURCE_EXHAUSTED.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnknownStatusException() { + StatusException exception = Status.UNKNOWN.asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertUnknownStatusExceptionWithDescription() { + StatusException exception = Status.UNKNOWN.withDescription("Oops!").asException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertAbortedStatusRuntimeException() { + StatusRuntimeException exception = Status.ABORTED.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertAbortedStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.ABORTED.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertAlreadyExistsStatusRuntimeException() { + StatusRuntimeException exception = Status.ALREADY_EXISTS.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Precondition Failed")); + } + + @Test + public void shouldConvertAlreadyExistsStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.ALREADY_EXISTS.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertOkStatusRuntimeException() { + StatusRuntimeException exception = Status.OK.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(200)); + assertThat(status.reasonPhrase(), is("OK")); + } + + @Test + public void shouldConvertOkStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.OK.withDescription("Good!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(200)); + assertThat(status.reasonPhrase(), is("Good!")); + } + + @Test + public void shouldConvertInvalidArgumentStatusRuntimeException() { + StatusRuntimeException exception = Status.INVALID_ARGUMENT.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Bad Request")); + } + + @Test + public void shouldConvertInvalidArgumentStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.INVALID_ARGUMENT.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertDeadlineExceededStatusRuntimeException() { + StatusRuntimeException exception = Status.DEADLINE_EXCEEDED.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(408)); + assertThat(status.reasonPhrase(), is("Request Timeout")); + } + + @Test + public void shouldConvertDeadlineExceededStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.DEADLINE_EXCEEDED.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(408)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertNotFoundStatusRuntimeException() { + StatusRuntimeException exception = Status.NOT_FOUND.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(404)); + assertThat(status.reasonPhrase(), is("Not Found")); + } + + @Test + public void shouldConvertNotFoundStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.NOT_FOUND.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(404)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertPermissionDeniedStatusRuntimeException() { + StatusRuntimeException exception = Status.PERMISSION_DENIED.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(403)); + assertThat(status.reasonPhrase(), is("Forbidden")); + } + + @Test + public void shouldConvertPermissionDeniedStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.PERMISSION_DENIED.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(403)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertFailedPreconditionStatusRuntimeException() { + StatusRuntimeException exception = Status.FAILED_PRECONDITION.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Precondition Failed")); + } + + @Test + public void shouldConvertFailedPreconditionStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.FAILED_PRECONDITION.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertOutOfRangeStatusRuntimeException() { + StatusRuntimeException exception = Status.OUT_OF_RANGE.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Bad Request")); + } + + @Test + public void shouldConvertOutOfRangeStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.OUT_OF_RANGE.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnimplementedStatusRuntimeException() { + StatusRuntimeException exception = Status.UNIMPLEMENTED.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(501)); + assertThat(status.reasonPhrase(), is("Not Implemented")); + } + + @Test + public void shouldConvertUnimplementedStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.UNIMPLEMENTED.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(501)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnavailableStatusRuntimeException() { + StatusRuntimeException exception = Status.UNAVAILABLE.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(503)); + assertThat(status.reasonPhrase(), is("Service Unavailable")); + } + + @Test + public void shouldConvertUnavailableStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.UNAVAILABLE.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(503)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnauthenticatedStatusRuntimeException() { + StatusRuntimeException exception = Status.UNAUTHENTICATED.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(401)); + assertThat(status.reasonPhrase(), is("Unauthorized")); + } + + @Test + public void shouldConvertUnauthenticatedStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.UNAUTHENTICATED.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(401)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertCancelledStatusRuntimeException() { + StatusRuntimeException exception = Status.CANCELLED.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertCancelledStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.CANCELLED.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertDataLossStatusRuntimeException() { + StatusRuntimeException exception = Status.DATA_LOSS.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertDataLossStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.DATA_LOSS.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertInternalStatusRuntimeException() { + StatusRuntimeException exception = Status.INTERNAL.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertInternalStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.INTERNAL.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertResourceExhaustedStatusRuntimeException() { + StatusRuntimeException exception = Status.RESOURCE_EXHAUSTED.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertResourceExhaustedStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.RESOURCE_EXHAUSTED.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnknownStatusRuntimeException() { + StatusRuntimeException exception = Status.UNKNOWN.asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertUnknownStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.UNKNOWN.withDescription("Oops!").asRuntimeException(); + Http.ResponseStatus status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } +} diff --git a/grpc/core/src/test/java/io/helidon/grpc/core/PriorityBagTest.java b/grpc/core/src/test/java/io/helidon/grpc/core/PriorityBagTest.java new file mode 100644 index 00000000000..f6697d0a4be --- /dev/null +++ b/grpc/core/src/test/java/io/helidon/grpc/core/PriorityBagTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.core; + +import java.util.Arrays; + +import javax.annotation.Priority; + +import io.helidon.common.Prioritized; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; + + +public class PriorityBagTest { + + @Test + public void shouldReturnElementsInOrder() { + PriorityBag bag = new PriorityBag<>(); + bag.add("Three", 3); + bag.add("Two", 2); + bag.add("One", 1); + + assertThat(bag, contains("One", "Two", "Three")); + } + + @Test + public void shouldReturnElementsInOrderWithinSamePriority() { + PriorityBag bag = new PriorityBag<>(); + bag.add("Two", 2); + bag.add("TwoToo", 2); + + assertThat(bag, contains("Two", "TwoToo")); + } + + @Test + public void shouldReturnNoPriorityElementsLast() { + PriorityBag bag = new PriorityBag<>(); + bag.add("Three", 3); + bag.add("Last"); + bag.add("One", 1); + + assertThat(bag, contains("One", "Three", "Last")); + } + + @Test + public void shouldGetPriorityFromAnnotation() { + PriorityBag bag = new PriorityBag<>(); + Value value = new Value(); + bag.add("One", 1); + bag.add("Three", 3); + bag.add(value); + + assertThat(bag, contains("One", value, "Three")); + } + + @Test + public void shouldGetPriorityFromPrioritized() { + PriorityBag bag = new PriorityBag<>(); + PrioritizedValue value = new PrioritizedValue(); + bag.add("One", 1); + bag.add("Three", 3); + bag.add(value); + + assertThat(bag, contains("One", value, "Three")); + } + + @Test + public void shouldUsePriorityFromPrioritizedOverAnnotation() { + PriorityBag bag = new PriorityBag<>(); + AnnotatedPrioritizedValue value = new AnnotatedPrioritizedValue(); + bag.add("One", 1); + bag.add("Three", 3); + bag.add(value); + + assertThat(bag, contains("One", value, "Three")); + } + + @Test + public void shouldUseDefaultPriority() { + PriorityBag bag = new PriorityBag<>(2); + bag.add("One", 1); + bag.add("Three", 3); + bag.add("Two"); + + assertThat(bag, contains("One", "Two", "Three")); + } + + @Test + public void shouldAddAll() { + PriorityBag bag = new PriorityBag<>(); + bag.addAll(Arrays.asList("One", "Two", "Three")); + + assertThat(bag, contains("One", "Two", "Three")); + } + + @Test + public void shouldAddAllWithPriority() { + PriorityBag bag = new PriorityBag<>(); + bag.add("First", 1); + bag.add("Last", 3); + bag.addAll(Arrays.asList("One", "Two", "Three"), 2); + + assertThat(bag, contains("First", "One", "Two", "Three", "Last")); + } + + @Test + public void shouldMerge() { + PriorityBag bagOne = new PriorityBag<>(); + PriorityBag bagTwo = new PriorityBag<>(); + + bagOne.add("A", 1); + bagOne.add("B", 2); + bagOne.add("C", 2); + bagOne.add("D", 3); + + bagTwo.add("E", 1); + bagTwo.add("F", 3); + bagTwo.add("G", 3); + bagTwo.add("H", 4); + + bagOne.merge(bagTwo); + + assertThat(bagOne, contains("A", "E", "B", "C", "D", "F", "G", "H")); + } + + + @Priority(2) + public static class Value { + } + + + public static class PrioritizedValue + implements Prioritized { + @Override + public int priority() { + return 2; + } + } + + + @Priority(0) + public static class AnnotatedPrioritizedValue + implements Prioritized { + @Override + public int priority() { + return 2; + } + } +} diff --git a/grpc/metrics/pom.xml b/grpc/metrics/pom.xml new file mode 100644 index 00000000000..a6d319f088c --- /dev/null +++ b/grpc/metrics/pom.xml @@ -0,0 +1,139 @@ + + + + + 4.0.0 + + + io.helidon.grpc + helidon-grpc-project + 1.0.4-SNAPSHOT + + + helidon-grpc-metrics + Helidon gRPC Metrics + + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + provided + + + io.helidon.grpc + helidon-grpc-client + ${project.version} + provided + + + + io.helidon.metrics + helidon-metrics + ${project.version} + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + javax.json + javax.json-api + test + + + org.mockito + mockito-core + test + + + org.glassfish.jersey.core + jersey-client + test + + + org.glassfish.jersey.inject + jersey-hk2 + test + + + javax.activation + javax.activation-api + test + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + test-compile + test-compile-custom + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.plugins + maven-failsafe-plugin + + false + + **/*IT.java + + + + + verify + verify + + integration-test + verify + + + + + + + diff --git a/grpc/metrics/src/main/java/io/helidon/grpc/metrics/GrpcMetrics.java b/grpc/metrics/src/main/java/io/helidon/grpc/metrics/GrpcMetrics.java new file mode 100644 index 00000000000..c42ee9a22a8 --- /dev/null +++ b/grpc/metrics/src/main/java/io/helidon/grpc/metrics/GrpcMetrics.java @@ -0,0 +1,503 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.metrics; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Priority; + +import io.helidon.grpc.core.GrpcHelper; +import io.helidon.grpc.core.InterceptorPriorities; +import io.helidon.grpc.server.MethodDescriptor; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.metrics.RegistryFactory; + +import io.grpc.Context; +import io.grpc.ForwardingServerCall; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.Histogram; +import org.eclipse.microprofile.metrics.Meter; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.MetricType; +import org.eclipse.microprofile.metrics.Timer; + +/** + * A {@link io.grpc.ServerInterceptor} that enables capturing of gRPC call metrics. + */ +@Priority(InterceptorPriorities.TRACING + 1) +public class GrpcMetrics + implements ServerInterceptor, ServiceDescriptor.Configurer, MethodDescriptor.Configurer { + + /** + * The registry of vendor metrics. + */ + private static final MetricRegistry VENDOR_REGISTRY = + RegistryFactory.getInstance().getRegistry(MetricRegistry.Type.VENDOR); + + /** + * The registry of application metrics. + */ + private static final MetricRegistry APP_REGISTRY = + RegistryFactory.getInstance().getRegistry(MetricRegistry.Type.APPLICATION); + + /** + * The context key name to use to obtain rules to use when applying metrics. + */ + private static final String KEY_STRING = GrpcMetrics.class.getName(); + + /** + * The context key to use to obtain rules to use when applying metrics. + */ + private static final Context.Key KEY = Context.keyWithDefault(KEY_STRING, new MetricsRules(MetricType.INVALID)); + + /** + * The metric rules to use. + */ + private final MetricsRules metricRule; + + /** + * Create a {@link GrpcMetrics}. + * + * @param rules the metric rules to use + */ + private GrpcMetrics(MetricsRules rules) { + this.metricRule = rules; + } + + @Override + public void configure(MethodDescriptor.Rules rules) { + rules.addContextValue(KEY, metricRule); + } + + @Override + public void configure(ServiceDescriptor.Rules rules) { + rules.addContextValue(KEY, metricRule); + } + + /** + * Set the tags to apply to the metric. + * + * @param tags the tags to apply to the metric + * @return a {@link io.helidon.grpc.metrics.GrpcMetrics} interceptor + * @see org.eclipse.microprofile.metrics.Metadata + */ + public GrpcMetrics tags(Map tags) { + return new GrpcMetrics(metricRule.tags(tags)); + } + + /** + * Set the description to apply to the metric. + * + * @param description the description to apply to the metric + * @return a {@link io.helidon.grpc.metrics.GrpcMetrics} interceptor + * @see org.eclipse.microprofile.metrics.Metadata + */ + public GrpcMetrics description(String description) { + return new GrpcMetrics(metricRule.description(description)); + } + + /** + * Set the units to apply to the metric. + * + * @param units the units to apply to the metric + * @return a {@link io.helidon.grpc.metrics.GrpcMetrics} interceptor + * @see org.eclipse.microprofile.metrics.Metadata + */ + public GrpcMetrics units(String units) { + return new GrpcMetrics(metricRule.units(units)); + } + + /** + * Set the {@link NamingFunction} to use to generate the metric name. + *

+ * The default name will be the {@code .}. + * + * @param function the function to use to create the metric name + * @return a {@link io.helidon.grpc.metrics.GrpcMetrics} interceptor + */ + public GrpcMetrics nameFunction(NamingFunction function) { + return new GrpcMetrics(metricRule.nameFunction(function)); + } + + /** + * A static factory method to create a {@link GrpcMetrics} instance + * to count gRPC method calls. + * + * @return a {@link GrpcMetrics} instance to capture call counts + */ + public static GrpcMetrics counted() { + return new GrpcMetrics(new MetricsRules(MetricType.COUNTER)); + } + + /** + * A static factory method to create a {@link GrpcMetrics} instance + * to meter gRPC method calls. + * + * @return a {@link GrpcMetrics} instance to meter gRPC calls + */ + public static GrpcMetrics metered() { + return new GrpcMetrics(new MetricsRules(MetricType.METERED)); + } + + /** + * A static factory method to create a {@link GrpcMetrics} instance + * to create a histogram of gRPC method calls. + * + * @return a {@link GrpcMetrics} instance to create a histogram of gRPC method calls + */ + public static GrpcMetrics histogram() { + return new GrpcMetrics(new MetricsRules(MetricType.HISTOGRAM)); + } + + /** + * A static factory method to create a {@link GrpcMetrics} instance + * to time gRPC method calls. + * + * @return a {@link GrpcMetrics} instance to time gRPC method calls + */ + public static GrpcMetrics timed() { + return new GrpcMetrics(new MetricsRules(MetricType.TIMER)); + } + + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { + + MetricsRules rules = Context.keyWithDefault(KEY_STRING, metricRule).get(); + MetricType type = rules.type(); + + String fullMethodName = call.getMethodDescriptor().getFullMethodName(); + String methodName = GrpcHelper.extractMethodName(fullMethodName); + ServiceDescriptor service = ServiceDescriptor.SERVICE_DESCRIPTOR_KEY.get(); + ServerCall serverCall; + + switch (type) { + case COUNTER: + serverCall = new CountedServerCall<>(APP_REGISTRY.counter(rules.metadata(service, methodName)), call); + break; + case METERED: + serverCall = new MeteredServerCall<>(APP_REGISTRY.meter(rules.metadata(service, methodName)), call); + break; + case HISTOGRAM: + serverCall = new HistogramServerCall<>(APP_REGISTRY.histogram(rules.metadata(service, methodName)), call); + break; + case TIMER: + serverCall = new TimedServerCall<>(APP_REGISTRY.timer(rules.metadata(service, methodName)), call); + break; + case GAUGE: + case INVALID: + default: + serverCall = call; + } + + serverCall = new MeteredServerCall<>(VENDOR_REGISTRY.meter("grpc.requests.meter"), serverCall); + serverCall = new CountedServerCall<>(VENDOR_REGISTRY.counter("grpc.requests.count"), serverCall); + + return next.startCall(serverCall, headers); + } + + /** + * A {@link io.grpc.ServerCall} that captures metrics for a gRPC call. + * + * @param the call request type + * @param the call response type + * @param the type of metric to capture + */ + private abstract class MetricServerCall + extends ForwardingServerCall.SimpleForwardingServerCall { + /** + * The metric to update. + */ + private final MetricT metric; + + /** + * Create a {@link TimedServerCall}. + * + * @param delegate the call to time + */ + MetricServerCall(MetricT metric, ServerCall delegate) { + super(delegate); + + this.metric = metric; + } + + /** + * Obtain the metric being tracked. + * + * @return the metric being tracked + */ + protected MetricT getMetric() { + return metric; + } + } + + /** + * A {@link GrpcMetrics.MeteredServerCall} that captures call times. + * + * @param the call request type + * @param the call response type + */ + private class TimedServerCall + extends MetricServerCall { + /** + * The method start time. + */ + private final long startNanos; + + /** + * Create a {@link TimedServerCall}. + * + * @param delegate the call to time + */ + TimedServerCall(Timer timer, ServerCall delegate) { + super(timer, delegate); + + this.startNanos = System.nanoTime(); + } + + @Override + public void close(Status status, Metadata responseHeaders) { + super.close(status, responseHeaders); + + long time = System.nanoTime() - startNanos; + getMetric().update(time, TimeUnit.NANOSECONDS); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return o != null && getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + /** + * A {@link GrpcMetrics.MeteredServerCall} that captures call counts. + * + * @param the call request type + * @param the call response type + */ + private class CountedServerCall + extends MetricServerCall { + /** + * Create a {@link CountedServerCall}. + * + * @param delegate the call to time + */ + CountedServerCall(Counter counter, ServerCall delegate) { + super(counter, delegate); + } + + @Override + public void close(Status status, Metadata responseHeaders) { + super.close(status, responseHeaders); + + getMetric().inc(); + } + } + + /** + * A {@link GrpcMetrics.MeteredServerCall} that meters gRPC calls. + * + * @param the call request type + * @param the call response type + */ + private class MeteredServerCall + extends MetricServerCall { + /** + * Create a {@link MeteredServerCall}. + * + * @param delegate the call to time + */ + MeteredServerCall(Meter meter, ServerCall delegate) { + super(meter, delegate); + } + + @Override + public void close(Status status, Metadata responseHeaders) { + super.close(status, responseHeaders); + + getMetric().mark(); + } + } + + /** + * A {@link GrpcMetrics.MeteredServerCall} that creates a histogram for gRPC calls. + * + * @param the call request type + * @param the call response type + */ + private class HistogramServerCall + extends MetricServerCall { + /** + * Create a {@link HistogramServerCall}. + * + * @param delegate the call to time + */ + HistogramServerCall(Histogram histogram, ServerCall delegate) { + super(histogram, delegate); + } + + @Override + public void close(Status status, Metadata responseHeaders) { + super.close(status, responseHeaders); + + getMetric().update(1); + } + } + + + /** + * Implemented by classes that can create a metric name. + */ + @FunctionalInterface + public interface NamingFunction { + /** + * Create a metric name. + * + * @param service the service descriptor + * @param methodName the method name + * @param metricType the metric type + * @return the metric name + */ + String createName(ServiceDescriptor service, String methodName, MetricType metricType); + } + + + /** + * An immutable holder of metrics information. + *

+ * Calls made to mutating methods return a new instance + * of {@link MetricsRules} with the mutation applied. + */ + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + static class MetricsRules { + /** + * The metric type. + */ + private MetricType type; + + /** + * The tags of the metric. + * + * @see org.eclipse.microprofile.metrics.Metadata + */ + private Optional> tags = Optional.empty(); + + /** + * The description of the metric. + * + * @see org.eclipse.microprofile.metrics.Metadata + */ + private Optional description = Optional.empty(); + + /** + * The unit of the metric. + * + * @see org.eclipse.microprofile.metrics.Metadata + * @see org.eclipse.microprofile.metrics.MetricUnits + */ + private Optional units = Optional.empty(); + + /** + * The function to use to obtain the metric name. + */ + private Optional nameFunction = Optional.empty(); + + private MetricsRules(MetricType type) { + this.type = type; + } + + private MetricsRules(MetricsRules copy) { + this.type = copy.type; + this.tags = copy.tags; + this.description = copy.description; + this.units = copy.units; + this.nameFunction = copy.nameFunction; + } + + /** + * Obtain the metric type. + * + * @return the metric type + */ + MetricType type() { + return type; + } + + /** + * Obtain the metrics metadata. + * + * @param service the service descriptor + * @param method the method name + * @return the metrics metadata + */ + org.eclipse.microprofile.metrics.Metadata metadata(ServiceDescriptor service, String method) { + String name = nameFunction.orElse(this::defaultName).createName(service, method, type); + org.eclipse.microprofile.metrics.Metadata metadata = new org.eclipse.microprofile.metrics.Metadata(name, type); + + this.tags.ifPresent(metadata::setTags); + this.description.ifPresent(metadata::setDescription); + this.units.ifPresent(metadata::setUnit); + + return metadata; + } + + private String defaultName(ServiceDescriptor service, String methodName, MetricType metricType) { + return (service.name() + "." + methodName).replaceAll("/", "."); + } + + private MetricsRules tags(Map tags) { + MetricsRules rules = new MetricsRules(this); + rules.tags = Optional.of(new HashMap<>(tags)); + return rules; + } + + private MetricsRules description(String description) { + MetricsRules rules = new MetricsRules(this); + rules.description = Optional.of(description); + return rules; + } + + private MetricsRules nameFunction(NamingFunction function) { + MetricsRules rules = new MetricsRules(this); + rules.nameFunction = Optional.of(function); + return rules; + } + + private MetricsRules units(String units) { + MetricsRules rules = new MetricsRules(this); + rules.units = Optional.of(units); + return rules; + } + } +} diff --git a/grpc/metrics/src/main/java/io/helidon/grpc/metrics/package-info.java b/grpc/metrics/src/main/java/io/helidon/grpc/metrics/package-info.java new file mode 100644 index 00000000000..61ee534ac76 --- /dev/null +++ b/grpc/metrics/src/main/java/io/helidon/grpc/metrics/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Classes to support adding metrics to gRPC calls. + */ +package io.helidon.grpc.metrics; diff --git a/grpc/metrics/src/test/java/io/helidon/grpc/metrics/GrpcMetricsInterceptorIT.java b/grpc/metrics/src/test/java/io/helidon/grpc/metrics/GrpcMetricsInterceptorIT.java new file mode 100644 index 00000000000..bc19acd61b6 --- /dev/null +++ b/grpc/metrics/src/test/java/io/helidon/grpc/metrics/GrpcMetricsInterceptorIT.java @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.metrics; + +import java.util.Map; + +import io.helidon.common.CollectionsHelper; +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.MethodDescriptor; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.metrics.MetricsSupport; +import io.helidon.metrics.RegistryFactory; +import io.helidon.webserver.Routing; + +import io.grpc.Context; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.Histogram; +import org.eclipse.microprofile.metrics.Meter; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.Timer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * A test for the {@link io.helidon.grpc.metrics.GrpcMetrics} interceptor. + *

+ * This test runs as an integration test because it causes Helidon metrics + * to be initialised which may impact other tests that rely on metrics being + * configured a specific way. + */ +@SuppressWarnings("unchecked") +public class GrpcMetricsInterceptorIT { + + private static MetricRegistry vendorRegsistry; + + private static MetricRegistry appRegistry; + + private static Meter vendorMeter; + + private static Counter vendorCounter; + + private long vendorMeterCount; + + private long vendorCount; + + @BeforeAll + static void configureMetrics() { + Routing.Rules rules = Routing.builder().get("metrics"); + MetricsSupport.create().update(rules); + + vendorRegsistry = RegistryFactory.getInstance().getRegistry(MetricRegistry.Type.VENDOR); + appRegistry = RegistryFactory.getInstance().getRegistry(MetricRegistry.Type.APPLICATION); + vendorMeter = vendorRegsistry.meter("grpc.requests.meter"); + vendorCounter = vendorRegsistry.counter("grpc.requests.count"); + } + + @BeforeEach + public void setup() { + // obtain the current counts for vendor metrics so that we can assert + // the count in each test + vendorCount = vendorCounter.getCount(); + vendorMeterCount = vendorMeter.getCount(); + } + + @Test + public void shouldUseCountedMetric() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("testCounted", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("testCounted"); + GrpcMetrics metrics = GrpcMetrics.counted(); + + ServerCall call = call(metrics, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + Counter appCounter = appRegistry.counter("Foo.testCounted"); + + assertVendorMetrics(); + assertThat(appCounter.getCount(), is(1L)); + } + + @Test + public void shouldUseHistogramMetric() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("barHistogram", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("barHistogram"); + GrpcMetrics metrics = GrpcMetrics.histogram(); + + ServerCall call = call(metrics, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + Histogram appHistogram = appRegistry.histogram("Foo.barHistogram"); + + assertVendorMetrics(); + assertThat(appHistogram.getCount(), is(1L)); + } + + @Test + public void shouldUseMeteredMetric() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("barMetered", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("barMetered"); + GrpcMetrics metrics = GrpcMetrics.metered(); + + ServerCall call = call(metrics, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + Meter appMeter = appRegistry.meter("Foo.barMetered"); + + assertVendorMetrics(); + assertThat(appMeter.getCount(), is(1L)); + } + + @Test + public void shouldUseTimerMetric() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("barTimed", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("barTimed"); + GrpcMetrics metrics = GrpcMetrics.timed(); + + ServerCall call = call(metrics, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + Timer appTimer = appRegistry.timer("Foo.barTimed"); + + assertVendorMetrics(); + assertThat(appTimer.getCount(), is(1L)); + } + + @Test + public void shouldApplyTags() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("barTags", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("barTags"); + Map tags = CollectionsHelper.mapOf("one", "t1", "two", "t2"); + GrpcMetrics metrics = GrpcMetrics.counted().tags(tags); + + ServerCall call = call(metrics, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + Counter appCounter = appRegistry.counter("Foo.barTags"); + + assertVendorMetrics(); + assertThat(appCounter.toString(), containsString("tags='{one=t1, two=t2}'")); + } + + @Test + public void shouldApplyDescription() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("barDesc", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("barDesc"); + GrpcMetrics metrics = GrpcMetrics.counted().description("foo"); + + ServerCall call = call(metrics, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + Counter appCounter = appRegistry.counter("Foo.barDesc"); + + assertVendorMetrics(); + assertThat(appCounter.toString(), containsString("description='foo'")); + } + + @Test + public void shouldApplyUnits() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("barUnits", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("barUnits"); + GrpcMetrics metrics = GrpcMetrics.counted().units(MetricUnits.BITS); + + ServerCall call = call(metrics, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + Counter appCounter = appRegistry.counter("Foo.barUnits"); + + assertVendorMetrics(); + assertThat(appCounter.toString(), containsString("unit='bits'")); + } + + @Test + public void shouldHaveCorrectNameWithDotsForSlashes() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .name("My/Service") + .unary("bar", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("bar"); + GrpcMetrics metrics = GrpcMetrics.counted(); + + ServerCall call = call(metrics, descriptor, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + Counter appCounter = appRegistry.counter("My.Service.bar"); + + assertVendorMetrics(); + assertThat(appCounter.getCount(), is(1L)); + } + + @Test + public void shouldUseNameFunction() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("barUnits", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("barUnits"); + GrpcMetrics metrics = GrpcMetrics.counted().nameFunction((svc, method, type) -> "overridden"); + + ServerCall call = call(metrics, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + Counter appCounter = appRegistry.counter("overridden"); + + assertVendorMetrics(); + assertThat(appCounter.getCount(), is(1L)); + } + + private ServerCall call(GrpcMetrics metrics, MethodDescriptor methodDescriptor) throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()).build(); + return call(metrics, descriptor, methodDescriptor); + } + + private ServerCall call(GrpcMetrics metrics, + ServiceDescriptor descriptor, + MethodDescriptor methodDescriptor) throws Exception { + Metadata headers = new Metadata(); + ServerCall call = mock(ServerCall.class); + ServerCallHandler next = mock(ServerCallHandler.class); + ServerCall.Listener listener = mock(ServerCall.Listener.class); + + when(call.getMethodDescriptor()).thenReturn(methodDescriptor.descriptor()); + when(next.startCall(any(ServerCall.class), any(Metadata.class))).thenReturn(listener); + + Context context = Context.ROOT.withValue(ServiceDescriptor.SERVICE_DESCRIPTOR_KEY, descriptor); + + ServerCall.Listener result = context.call(() -> metrics.interceptCall(call, headers, next)); + + assertThat(result, is(sameInstance(listener))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServerCall.class); + + verify(next).startCall(captor.capture(), same(headers)); + + ServerCall wrappedCall = captor.getValue(); + + assertThat(wrappedCall, is(notNullValue())); + + return wrappedCall; + } + + private void assertVendorMetrics() { + Meter meter = vendorRegsistry.meter("grpc.requests.meter"); + Counter counter = vendorRegsistry.counter("grpc.requests.count"); + + assertThat(meter.getCount(), is(vendorMeterCount + 1)); + assertThat(counter.getCount(), is(vendorCount + 1)); + } + + private GrpcService createMockService() { + GrpcService service = mock(GrpcService.class); + + when(service.name()).thenReturn("Foo"); + + return service; + } + + private void dummyUnary(String request, StreamObserver observer) { + } +} diff --git a/grpc/metrics/src/test/java/io/helidon/grpc/metrics/MetricsIT.java b/grpc/metrics/src/test/java/io/helidon/grpc/metrics/MetricsIT.java new file mode 100644 index 00000000000..08c3cfd3147 --- /dev/null +++ b/grpc/metrics/src/test/java/io/helidon/grpc/metrics/MetricsIT.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.metrics; + +import java.io.StringReader; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import javax.json.Json; +import javax.json.JsonStructure; +import javax.json.JsonValue; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; + +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.grpc.server.test.Echo; +import io.helidon.grpc.server.test.EchoServiceGrpc; +import io.helidon.metrics.MetricsSupport; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.WebServer; + +import io.grpc.Channel; +import io.grpc.ManagedChannelBuilder; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import services.EchoService; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Integration tests for gRPC server with metrics. + */ +public class MetricsIT { + + // ----- data members --------------------------------------------------- + + /** + * The Helidon {@link WebServer} to use for testing. + */ + private static WebServer webServer; + + /** + * The JAX-RS {@link Client} to use to make http requests to the {@link WebServer}. + */ + private static Client client; + + /** + * The {@link Logger} to use for logging. + */ + private static final Logger LOGGER = Logger.getLogger(MetricsIT.class.getName()); + + /** + * The Helidon {@link io.helidon.grpc.server.GrpcServer} being tested. + */ + private static GrpcServer grpcServer; + + /** + * A gRPC {@link Channel} to connect to the test gRPC server + */ + private static Channel channel; + + // ----- test lifecycle ------------------------------------------------- + + @BeforeAll + public static void setup() throws Exception { + LogManager.getLogManager().readConfiguration(MetricsIT.class.getResourceAsStream("/logging.properties")); + + // start the server at a free port + startWebServer(); + + client = ClientBuilder.newBuilder() + .register(new LoggingFeature(LOGGER, Level.WARNING, LoggingFeature.Verbosity.PAYLOAD_ANY, 500)) + .property(ClientProperties.FOLLOW_REDIRECTS, true) + .build(); + + startGrpcServer(); + + channel = ManagedChannelBuilder.forAddress("localhost", grpcServer.port()) + .usePlaintext() + .build(); + } + + @AfterAll + public static void cleanup() throws Exception { + if (webServer != null) { + webServer.shutdown() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + } + } + + // ----- test methods --------------------------------------------------- + + @Test + public void shouldPublishMetrics() { + // call the gRPC Echo service so that there should be some metrics + EchoServiceGrpc.newBlockingStub(channel).echo(Echo.EchoRequest.newBuilder().setMessage("foo").build()); + + // request the application metrics in json format from the web server + String metrics = client.target("http://localhost:" + webServer.port()) + .path("metrics/application") + .request() + .accept(MediaType.APPLICATION_JSON) + .get(String.class); + + // verify that the json response + JsonStructure json = Json.createReader(new StringReader(metrics)).read(); + JsonValue value = json.getValue("/EchoService.Echo"); + + assertThat(value, is(notNullValue())); + } + + // ----- helper methods ------------------------------------------------- + + /** + * Start the gRPC Server listening on an ephemeral port. + * + * @throws Exception in case of an error + */ + private static void startGrpcServer() throws Exception { + // Add the EchoService and enable GrpcMetrics + GrpcRouting routing = GrpcRouting.builder() + .intercept(GrpcMetrics.timed()) + .register(new EchoService(), rules -> rules.intercept(GrpcMetrics.metered()) + .intercept("Echo", + GrpcMetrics.counted())) + .build(); + + // Run the server on port 0 so that it picks a free ephemeral port + GrpcServerConfiguration serverConfig = GrpcServerConfiguration.builder().port(0).build(); + + grpcServer = GrpcServer.create(serverConfig, routing) + .start() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + + LOGGER.info("Started gRPC server at: localhost:" + grpcServer.port()); + } + + /** + * Start the Web Server listening on an ephemeral port. + * + * @throws Exception in case of an error + */ + private static void startWebServer() throws Exception { + // Add metrics to the web server routing + Routing routing = Routing.builder() + .register(MetricsSupport.create()) + .build(); + + // Run the web server on port 0 so that it picks a free ephemeral port + ServerConfiguration webServerConfig = ServerConfiguration.builder().port(0).build(); + + webServer = WebServer.create(webServerConfig, routing) + .start() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + + LOGGER.info("Started web server at: http://localhost:" + webServer.port()); + } +} diff --git a/grpc/metrics/src/test/java/services/EchoService.java b/grpc/metrics/src/test/java/services/EchoService.java new file mode 100644 index 00000000000..0d305fc4265 --- /dev/null +++ b/grpc/metrics/src/test/java/services/EchoService.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package services; + +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.grpc.server.test.Echo; + +import io.grpc.stub.StreamObserver; + +/** + * A simple test gRPC echo service. + */ +public class EchoService + implements GrpcService { + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.proto(Echo.getDescriptor()) + .unary("Echo", this::echo); + } + + /** + * Echo the message back to the caller. + * + * @param request the echo request containing the message to echo + * @param observer the call response + */ + public void echo(Echo.EchoRequest request, StreamObserver observer) { + String message = request.getMessage(); + Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build(); + complete(observer, response); + } +} diff --git a/grpc/metrics/src/test/proto/echo.proto b/grpc/metrics/src/test/proto/echo.proto new file mode 100644 index 00000000000..1b8930c1663 --- /dev/null +++ b/grpc/metrics/src/test/proto/echo.proto @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.grpc.server.test"; + +service EchoService { + rpc Echo (EchoRequest) returns (EchoResponse) {} +} + +message EchoRequest { + string message = 1; +} + +message EchoResponse { + string message = 1; +} diff --git a/grpc/metrics/src/test/resources/logging.properties b/grpc/metrics/src/test/resources/logging.properties new file mode 100644 index 00000000000..f903114790c --- /dev/null +++ b/grpc/metrics/src/test/resources/logging.properties @@ -0,0 +1,37 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden 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 +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +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 +#io.netty.level=INFO diff --git a/grpc/pom.xml b/grpc/pom.xml new file mode 100644 index 00000000000..30497264d70 --- /dev/null +++ b/grpc/pom.xml @@ -0,0 +1,43 @@ + + + + + 4.0.0 + + + helidon-project + io.helidon + 1.0.4-SNAPSHOT + + + io.helidon.grpc + helidon-grpc-project + Helidon gRPC Project + pom + + + core + server + client + metrics + + + diff --git a/grpc/server/pom.xml b/grpc/server/pom.xml new file mode 100644 index 00000000000..2844c6f90a8 --- /dev/null +++ b/grpc/server/pom.xml @@ -0,0 +1,201 @@ + + + + + 4.0.0 + + + io.helidon.grpc + helidon-grpc-project + 1.0.4-SNAPSHOT + + + helidon-grpc-server + Helidon gRPC Server + + + + io.helidon.grpc + helidon-grpc-core + ${project.version} + + + + io.helidon.common + helidon-common + ${project.version} + + + io.helidon.health + helidon-health + ${project.version} + + + io.helidon.config + helidon-config + ${project.version} + + + io.helidon.tracing + helidon-tracing + ${project.version} + + + io.opentracing.contrib + opentracing-grpc + + + io.netty + netty-tcnative-boringssl-static + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + javax.json + javax.json-api + test + + + org.glassfish + javax.json + test + + + org.mockito + mockito-core + test + + + io.reactivex.rxjava2 + rxjava + test + + + io.helidon.health + helidon-health-checks + ${project.version} + test + + + io.helidon.tracing + helidon-tracing-zipkin + ${project.version} + test + + + io.helidon.config + helidon-config-hocon + ${project.version} + test + + + io.helidon.config + helidon-config-yaml + ${project.version} + test + + + + + + + + + + + + + + + + + + io.zipkin.zipkin2 + zipkin-junit + test + + + com.oracle.bedrock + bedrock-testing-support + test + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + test-compile + test-compile-custom + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.plugins + maven-failsafe-plugin + + false + + **/*IT.java + + + + + verify + verify + + integration-test + verify + + + + + + + diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/BindableServiceImpl.java b/grpc/server/src/main/java/io/helidon/grpc/server/BindableServiceImpl.java new file mode 100644 index 00000000000..4a4e6517deb --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/BindableServiceImpl.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.helidon.grpc.core.InterceptorPriorities; +import io.helidon.grpc.core.PriorityBag; + +import io.grpc.BindableService; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; +import io.grpc.stub.StreamObserver; + +/** + * A {@link io.grpc.BindableService} implementation that creates {@link io.grpc.ServerServiceDefinition} + * from a {@link ServiceDescriptor}. + */ +class BindableServiceImpl implements BindableService { + /** + * The descriptor of this service. + */ + private final ServiceDescriptor descriptor; + + /** + * The global interceptors to apply. + */ + private final PriorityBag globalInterceptors; + + + BindableServiceImpl(ServiceDescriptor descriptor, PriorityBag interceptors) { + this.descriptor = descriptor; + this.globalInterceptors = interceptors.copyMe(); + } + + // ---- BindableService implementation ---------------------------------- + + @SuppressWarnings("unchecked") + @Override + public ServerServiceDefinition bindService() { + ServerServiceDefinition.Builder builder = ServerServiceDefinition.builder(descriptor.name()); + + descriptor.methods() + .forEach(method -> builder.addMethod(method.descriptor(), wrapCallHandler(method))); + + return builder.build(); + } + + // ---- helpers --------------------------------------------------------- + + private ServerCallHandler wrapCallHandler(MethodDescriptor method) { + ServerCallHandler handler = method.callHandler(); + + PriorityBag priorityServerInterceptors = new PriorityBag<>(InterceptorPriorities.USER); + priorityServerInterceptors.addAll(globalInterceptors); + priorityServerInterceptors.addAll(descriptor.interceptors()); + priorityServerInterceptors.addAll(method.interceptors()); + List interceptors = priorityServerInterceptors.stream().collect(Collectors.toList()); + + if (interceptors.size() > 0) { + LinkedHashSet uniqueInterceptors = new LinkedHashSet<>(interceptors.size()); + + // iterate the interceptors in reverse order so that the handler chain is in the correct order + for (int i = interceptors.size() - 1; i >= 0; i--) { + ServerInterceptor interceptor = interceptors.get(i); + if (!uniqueInterceptors.contains(interceptor)) { + uniqueInterceptors.add(interceptor); + } + } + + for (ServerInterceptor interceptor : uniqueInterceptors) { + handler = new InterceptingCallHandler<>(descriptor, interceptor, handler); + } + } + + return handler; + } + + static Supplier createSupplier(Callable callable) { + return new CallableSupplier<>(callable); + } + + static class CallableSupplier implements Supplier { + private Callable callable; + + CallableSupplier(Callable callable) { + this.callable = callable; + } + + @Override + public T get() { + try { + return callable.call(); + } catch (Exception e) { + throw new CompletionException(e.getMessage(), e); + } + } + } + + static BiConsumer completeWithResult(StreamObserver observer) { + return new CompletionAction<>(observer, true); + } + + static BiConsumer completeWithoutResult(StreamObserver observer) { + return new CompletionAction<>(observer, false); + } + + static class CompletionAction implements BiConsumer { + private StreamObserver observer; + private boolean sendResult; + + CompletionAction(StreamObserver observer, boolean sendResult) { + this.observer = observer; + this.sendResult = sendResult; + } + + @Override + @SuppressWarnings("unchecked") + public void accept(T result, Throwable error) { + if (error != null) { + observer.onError(error); + } else { + if (sendResult) { + observer.onNext((U) result); + } + observer.onCompleted(); + } + } + } + + /** + * A {@link ServerCallHandler} that wraps a {@link ServerCallHandler} with + * a {@link ServerInterceptor}. + *

+ * If the wrapped {@link ServerInterceptor} implements {@link ServiceDescriptor.Aware} + * then the {@link ServiceDescriptor.Aware#setServiceDescriptor(ServiceDescriptor)} method + * will be called before calling {@link ServerInterceptor#interceptCall(io.grpc.ServerCall, + * io.grpc.Metadata, io.grpc.ServerCallHandler)}. + * + * @param the request type + * @param the response type + */ + static final class InterceptingCallHandler implements ServerCallHandler { + private final ServiceDescriptor serviceDefinition; + private final ServerInterceptor interceptor; + private final ServerCallHandler callHandler; + + private InterceptingCallHandler(ServiceDescriptor serviceDefinition, + ServerInterceptor interceptor, + ServerCallHandler callHandler) { + this.serviceDefinition = serviceDefinition; + this.interceptor = interceptor; + this.callHandler = callHandler; + } + + @Override + public ServerCall.Listener startCall( + ServerCall call, + Metadata headers) { + if (interceptor instanceof ServiceDescriptor.Aware) { + ((ServiceDescriptor.Aware) interceptor).setServiceDescriptor(serviceDefinition); + } + return interceptor.interceptCall(call, headers, callHandler); + } + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/CollectingObserver.java b/grpc/server/src/main/java/io/helidon/grpc/server/CollectingObserver.java new file mode 100644 index 00000000000..4485cacd248 --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/CollectingObserver.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collector; + +import io.grpc.stub.StreamObserver; + +/** + * A {@link StreamObserver}. + * + * @param ToDo: Add JavaDoc + * @param ToDo: Add JavaDoc + * @param ToDo: Add JavaDoc + * @param ToDo: Add JavaDoc + * @param ToDo: Add JavaDoc + */ +public class CollectingObserver implements StreamObserver { + private final Collector collector; + private final StreamObserver responseObserver; + private final Function requestConverter; + private final Function responseConverter; + private final Consumer errorHandler; + + private final A accumulator; + + /** + * ToDo: Add JavaDoc. + * + * @param collector ToDo: Add JavaDoc + * @param responseObserver ToDo: Add JavaDoc + */ + public CollectingObserver(Collector collector, StreamObserver responseObserver) { + this(collector, responseObserver, null, null, null); + } + + /** + * ToDo: Add JavaDoc. + * + * @param collector ToDo: Add JavaDoc + * @param responseObserver ToDo: Add JavaDoc + * @param errorHandler ToDo: Add JavaDoc + */ + public CollectingObserver(Collector collector, + StreamObserver responseObserver, + Consumer errorHandler) { + this(collector, responseObserver, null, null, errorHandler); + } + + /** + * ToDo: Add JavaDoc. + * + * @param collector ToDo: Add JavaDoc + * @param responseObserver ToDo: Add JavaDoc + * @param requestConverter ToDo: Add JavaDoc + * @param responseConverter ToDo: Add JavaDoc + */ + public CollectingObserver(Collector collector, + StreamObserver responseObserver, + Function requestConverter, + Function responseConverter) { + this(collector, responseObserver, requestConverter, responseConverter, null); + } + + /** + * ToDo: Add JavaDoc. + * + * @param collector ToDo: Add JavaDoc + * @param observer ToDo: Add JavaDoc + * @param requestConverter ToDo: Add JavaDoc + * @param responseConverter ToDo: Add JavaDoc + * @param errorHandler ToDo: Add JavaDoc + */ + @SuppressWarnings("unchecked") + public CollectingObserver(Collector collector, + StreamObserver observer, + Function requestConverter, + Function responseConverter, + Consumer errorHandler) { + this.collector = Objects.requireNonNull(collector, "The collector parameter cannot be null"); + this.responseObserver = Objects.requireNonNull(observer, "The observer parameter cannot be null"); + this.requestConverter = Optional.ofNullable(requestConverter).orElse(v -> (T) v); + this.responseConverter = Optional.ofNullable(responseConverter).orElse(r -> (U) r); + this.errorHandler = Optional.ofNullable(errorHandler).orElse(t -> {}); + this.accumulator = collector.supplier().get(); + } + + @Override + public void onNext(V value) { + collector.accumulator().accept(accumulator, requestConverter.apply(value)); + } + + @Override + public void onError(Throwable t) { + errorHandler.accept(t); + } + + @Override + public void onCompleted() { + R result = collector.finisher().apply(accumulator); + responseObserver.onNext(responseConverter.apply(result)); + responseObserver.onCompleted(); + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/ConstantHealthCheck.java b/grpc/server/src/main/java/io/helidon/grpc/server/ConstantHealthCheck.java new file mode 100644 index 00000000000..d0a27aed114 --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/ConstantHealthCheck.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; + +/** + * A simple {@link HealthCheck} implementation + * that always returns the same response. + */ +public class ConstantHealthCheck + implements HealthCheck { + + private final HealthCheckResponse response; + + private ConstantHealthCheck(HealthCheckResponse response) { + this.response = response; + } + + @Override + public HealthCheckResponse call() { + return response; + } + + /** + * Obtain a {@link HealthCheck} that always returns a status of up. + * + * @param name the service name that the health check is for + * + * @return a {@link HealthCheck} that always returns a status of up + */ + public static HealthCheck up(String name) { + return new ConstantHealthCheck(HealthCheckResponse.named(name).up().build()); + } + + /** + * Obtain a {@link HealthCheck} that always returns a status of down. + * + * @param name the service name that the health check is for + * + * @return a {@link HealthCheck} that always returns a status of down + */ + public static HealthCheck down(String name) { + return new ConstantHealthCheck(HealthCheckResponse.named(name).down().build()); + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/ContextSettingServerInterceptor.java b/grpc/server/src/main/java/io/helidon/grpc/server/ContextSettingServerInterceptor.java new file mode 100644 index 00000000000..ce36895eb8e --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/ContextSettingServerInterceptor.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.Priority; + +import io.helidon.grpc.core.InterceptorPriorities; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +import static io.helidon.grpc.core.GrpcHelper.extractMethodName; + +/** + * A {@link io.grpc.ServerInterceptor} that sets values into the + * gRPC call context. + */ +@Priority(InterceptorPriorities.CONTEXT) +class ContextSettingServerInterceptor + implements ServerInterceptor, ServiceDescriptor.Aware { + + /** + * The {@link ServiceDescriptor} for the service being intercepted. + */ + private ServiceDescriptor serviceDescriptor; + + @Override + @SuppressWarnings("unchecked") + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { + + Context context = Context.current(); + String fullMethodName = call.getMethodDescriptor().getFullMethodName(); + String methodName = extractMethodName(fullMethodName); + MethodDescriptor methodDescriptor = serviceDescriptor.method(methodName); + Map, Object> contextMap = new HashMap<>(); + + // apply context keys from the service followed by the method + // so that the method can override any service keys + contextMap.putAll(serviceDescriptor.context()); + contextMap.putAll(methodDescriptor.context()); + contextMap.put(ServiceDescriptor.SERVICE_DESCRIPTOR_KEY, serviceDescriptor); + + if (!contextMap.isEmpty()) { + for (Map.Entry, Object> entry : contextMap.entrySet()) { + Context.Key key = (Context.Key) entry.getKey(); + context = context.withValue(key, entry.getValue()); + } + } + + return Contexts.interceptCall(context, call, headers, next); + } + + @Override + public void setServiceDescriptor(ServiceDescriptor descriptor) { + this.serviceDescriptor = descriptor; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return o != null && getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/GrpcRouting.java b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcRouting.java new file mode 100644 index 00000000000..313dd96e5f7 --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcRouting.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.helidon.grpc.core.InterceptorPriorities; +import io.helidon.grpc.core.PriorityBag; + +import io.grpc.BindableService; +import io.grpc.ServerInterceptor; + +/** + * GrpcRouting represents the composition of gRPC services with interceptors and routing rules. + * + * It is together with {@link GrpcServerConfiguration.Builder} a cornerstone of the {@link GrpcServer}. + */ +public interface GrpcRouting { + + /** + * Obtain a {@link List} of the {@link ServiceDescriptor} instances + * contained in this {@link GrpcRouting}. + * + * @return a {@link List} of the {@link ServiceDescriptor} instances + * contained in this {@link GrpcRouting} + */ + List services(); + + /** + * Obtain a {@link List} of the global {@link io.grpc.ServerInterceptor interceptors} + * that should be applied to all services. + * + * @return a {@link List} of the global {@link io.grpc.ServerInterceptor interceptors} + * that should be applied to all services + */ + PriorityBag interceptors(); + + /** + * Obtain a GrpcRouting builder. + * + * @return a GrpcRouting builder + */ + static Builder builder() { + return new Builder(); + } + + /** + * Creates new {@link GrpcServer} instance with provided configuration and this routing. + * + * @param configuration a gRPC server configuration + * @return new {@link GrpcServer} instance + * @throws IllegalStateException if none SPI implementation found + */ + default GrpcServer createServer(GrpcServerConfiguration configuration) { + return GrpcServer.create(configuration, this); + } + + /** + * Creates new {@link GrpcServer} instance with this routing and default configuration. + * + * @return new {@link GrpcServer} instance + * @throws IllegalStateException if none SPI implementation found + */ + default GrpcServer createServer() { + return GrpcServer.create(this); + } + + /** + * A builder that can build {@link GrpcRouting} instances. + */ + final class Builder implements io.helidon.common.Builder { + + /** + * The {@link List} of the {@link ServiceDescriptor} instances + * to add to the {@link GrpcRouting}. + */ + private List services = new ArrayList<>(); + + /** + * The {@link List} of the global {@link io.grpc.ServerInterceptor}s that should be + * applied to all services. + */ + private PriorityBag interceptors = new PriorityBag<>(InterceptorPriorities.USER); + + /** + * Add one or more global {@link ServerInterceptor} instances that will intercept calls + * to all services in the {@link GrpcRouting} built by this builder. + *

+ * If the added interceptors are annotated with the {@link javax.annotation.Priority} + * annotation then that value will be used to assign a priority to use when applying + * the interceptor otherwise a priority of {@link InterceptorPriorities#USER} will + * be used. + * + * @param interceptors one or more global {@link ServerInterceptor}s + * @return this builder to allow fluent method chaining + */ + public Builder intercept(ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + /** + * Add one or more global {@link ServerInterceptor} instances that will intercept calls + * to all services in the {@link GrpcRouting} built by this builder. + *

+ * The added interceptors will be applied using the specified priority. + * + * @param priority the priority to assign to the interceptors + * @param interceptors one or more global {@link ServerInterceptor}s + * @return this builder to allow fluent method chaining + */ + public Builder intercept(int priority, ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), priority); + return this; + } + + /** + * Add a {@link GrpcService} with the {@link GrpcRouting} to be built by this builder. + * + * @param service the {@link GrpcService} to register + * @return this builder to allow fluent method chaining + */ + public Builder register(GrpcService service) { + return register(service, null); + } + + /** + * Add a {@link GrpcService} with the {@link GrpcRouting} to be built by this builder. + * + * @param service the {@link GrpcService} to register + * @param configurer an optional configurer that can update the {@link ServiceDescriptor} + * for the registered service + * @return this builder to allow fluent method chaining + */ + public Builder register(GrpcService service, ServiceDescriptor.Configurer configurer) { + return register(ServiceDescriptor.builder(service), configurer); + } + + /** + * Add a {@link BindableService} with the {@link GrpcRouting} to be built by this builder. + * + * @param service the {@link BindableService} to register + * @return this builder to allow fluent method chaining + */ + public Builder register(BindableService service) { + return register(service, null); + } + + /** + * Add a {@link BindableService} with the {@link GrpcRouting} to be built by this builder. + * + * @param service the {@link BindableService} to register + * @param configurer an optional configurer that can update the {@link ServiceDescriptor} + * for the registered service + * @return this builder to allow fluent method chaining + */ + public Builder register(BindableService service, ServiceDescriptor.Configurer configurer) { + return register(ServiceDescriptor.builder(service), configurer); + } + + /** + * Register a {@link ServiceDescriptor} with the {@link GrpcRouting} to be built by this builder. + * + * @param service the {@link ServiceDescriptor} to register + * @return this builder to allow fluent method chaining + */ + public Builder register(ServiceDescriptor service) { + services.add(service); + return this; + } + + /** + * Builds a new {@link GrpcRouting}. + * + * @return a new {@link GrpcRouting} instance + */ + public GrpcRouting build() { + return new GrpcRoutingImpl(services, interceptors); + } + + // ---- helpers ----------------------------------------------------- + + private Builder register(ServiceDescriptor.Builder builder, + ServiceDescriptor.Configurer configurer) { + if (configurer != null) { + configurer.configure(builder); + } + + interceptors.stream() + .filter(interceptor -> ServiceDescriptor.Configurer.class.isAssignableFrom(interceptor.getClass())) + .map(ServiceDescriptor.Configurer.class::cast) + .forEach(interceptor -> interceptor.configure(builder)); + + services.add(builder.build()); + return this; + } + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/GrpcRoutingImpl.java b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcRoutingImpl.java new file mode 100644 index 00000000000..95c0e1ff4b8 --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcRoutingImpl.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.helidon.grpc.core.PriorityBag; + +import io.grpc.ServerInterceptor; + +/** + * An implementation of {@link io.helidon.grpc.server.GrpcRouting}. + */ +public class GrpcRoutingImpl + implements GrpcRouting { + + /** + * The {@link List} of registered {@link ServiceDescriptor} instances. + */ + private List services; + + /** + * The {@link List} of the global {@link io.grpc.ServerInterceptor}s that should + * be applied to all services. + */ + private PriorityBag interceptors; + + /** + * Create a {@link GrpcRoutingImpl}. + * + * @param services the {@link List} of registered {@link ServiceDescriptor} instances + * @param interceptors the {@link List} of the global {@link io.grpc.ServerInterceptor}s that should + * be applied to all services + */ + GrpcRoutingImpl(List services, PriorityBag interceptors) { + this.services = new ArrayList<>(Objects.requireNonNull(services)); + this.interceptors = interceptors.copyMe(); + } + + @Override + public List services() { + return services; + } + + @Override + public PriorityBag interceptors() { + return interceptors.readOnly(); + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServer.java b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServer.java new file mode 100644 index 00000000000..11cba062f31 --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServer.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; + +import io.helidon.grpc.core.PriorityBag; + +import io.grpc.ServerInterceptor; +import io.opentracing.Tracer; +import org.eclipse.microprofile.health.HealthCheck; + +/** + * Represents a immutably configured gRPC server. + *

+ * Provides a basic lifecycle and monitoring API. + *

+ * Instance can be created from {@link GrpcRouting} and optionally from {@link + * GrpcServerConfiguration} using {@link #create(GrpcRouting)}, {@link + * #create(GrpcServerConfiguration, GrpcRouting)} or {@link #builder(GrpcRouting)} methods + * and their builder enabled overloads. + */ +public interface GrpcServer { + /** + * Gets effective server configuration. + * + * @return Server configuration + */ + GrpcServerConfiguration configuration(); + + /** + * Starts the server. Has no effect if server is running. + * + * @return a completion stage of starting tryProcess + */ + CompletionStage start(); + + /** + * Completion stage is completed when server is shut down. + * + * @return a completion stage of the server + */ + CompletionStage whenShutdown(); + + /** + * Attempt to gracefully shutdown server. It is possible to use returned + * {@link CompletionStage} to react. + *

+ * RequestMethod can be called periodically. + * + * @return to react on finished shutdown tryProcess + * @see #start() + */ + CompletionStage shutdown(); + + /** + * Return an array of health checks for this server. + * + * @return an array of {@link HealthCheck} instances for this server + */ + HealthCheck[] healthChecks(); + + /** + * Obtain the deployed services. + * + * @return an immutable {@link Map} of deployed {@link ServiceDescriptor}s + * keyed by service name + */ + Map services(); + + /** + * Returns {@code true} if the server is currently running. A running server + * in the stopping phase returns {@code true} until it is fully stopped. + * + * @return {@code true} if server is running + */ + boolean isRunning(); + + /** + * Returns a port number the default server socket is bound to and is + * listening on; or {@code -1} if unknown or not active. + *

+ * Only supported only when server is running. + * + * @return a listen port; or {@code -1} if unknown or the default server + * socket is not active + */ + int port(); + + /** + * Creates a new instance from a provided configuration and a GrpcRouting. + * + * @param configurationBuilder a server configuration builder that will be + * built as a first step of this method + * execution; may be {@code null} + * @param routing a GrpcRouting instance + * + * @return a new gRPC server instance + * + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'GrpcRouting' parameter is {@code null} + */ + static GrpcServer create(Supplier configurationBuilder, GrpcRouting routing) { + return create(configurationBuilder != null + ? configurationBuilder.get() + : null, routing); + } + + /** + * Creates new instance form provided configuration and GrpcRouting. + * + * @param configurationBuilder a server configuration builder that will be + * built as a first step of this method + * execution; may be {@code null} + * @param routingBuilder a GrpcRouting builder that will be built as a + * second step of this method execution + * + * @return a new gRPC server instance + * + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'routingBuilder' parameter is {@code + * null} + */ + static GrpcServer create(Supplier configurationBuilder, + Supplier routingBuilder) { + Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); + return create(configurationBuilder != null + ? configurationBuilder.get() + : null, routingBuilder.get()); + } + + /** + * Creates new instance form provided configuration and GrpcRouting. + * + * @param configuration a server configuration instance + * @param routingBuilder a GrpcRouting builder that will be built as a second + * step of this method execution + * @return a new gRPC server instance + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'routingBuilder' parameter is {@code + * null} + */ + static GrpcServer create( + GrpcServerConfiguration configuration, + Supplier routingBuilder) { + Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); + return create(configuration, routingBuilder.get()); + } + + /** + * Creates new instance form provided GrpcRouting and default configuration. + * + * @param routing a GrpcRouting instance + * @return a new gRPC server instance + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'routing' parameter is {@code null} + */ + static GrpcServer create(GrpcRouting routing) { + return create((GrpcServerConfiguration) null, routing); + } + + /** + * Creates new instance form provided configuration and GrpcRouting. + * + * @param configuration a server configuration instance + * @param routing a GrpcRouting instance + * @return a new gRPC server instance + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'GrpcRouting' parameter is {@code null} + */ + static GrpcServer create(GrpcServerConfiguration configuration, GrpcRouting routing) { + Objects.requireNonNull(routing, "Parameter 'routing' is null!"); + + return builder(routing) + .config(configuration) + .build(); + } + + /** + * Creates new instance form provided GrpcRouting and default configuration. + * + * @param routingBuilder a GrpcRouting builder instance that will be built as a + * first step of this method execution + * @return a new gRPC server instance + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'GrpcRouting' parameter is {@code null} + */ + static GrpcServer create(Supplier routingBuilder) { + Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); + return create(routingBuilder.get()); + } + + /** + * Creates a builder of the {@link GrpcServer}. + * + * @param routingBuilder the GrpcRouting builder; must not be {@code null} + * @return the builder + */ + static Builder builder(Supplier routingBuilder) { + Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); + return builder(routingBuilder.get()); + } + + /** + * Creates a builder of the {@link GrpcServer}. + * + * @param routing the GrpcRouting; must not be {@code null} + * @return the builder + */ + static Builder builder(GrpcRouting routing) { + return new Builder(GrpcServerConfiguration.create(), routing); + } + + /** + * GrpcServer builder class provides a convenient way to timed a + * GrpcServer instance. + */ + final class Builder + implements io.helidon.common.Builder { + + private final GrpcRouting routing; + + private GrpcServerConfiguration configuration; + + private Builder(GrpcServerConfiguration configuration, GrpcRouting routing) { + Objects.requireNonNull(configuration, "Parameter 'configuration' must not be null!"); + Objects.requireNonNull(routing, "Parameter 'routing' must not be null!"); + + this.configuration = configuration; + this.routing = routing; + } + + /** + * Set a configuration of the {@link GrpcServer}. + * + * @param configuration the configuration + * @return an updated builder + */ + public Builder config(GrpcServerConfiguration configuration) { + this.configuration = configuration != null ? configuration : GrpcServerConfiguration.create(); + return this; + } + + /** + * Set a configuration of the {@link GrpcServer}. + * + * @param configurationBuilder the configuration builder + * @return an updated builder + */ + public Builder config(Supplier configurationBuilder) { + this.configuration = configurationBuilder != null + ? configurationBuilder.get() + : GrpcServerConfiguration.create(); + return this; + } + + /** + * Builds the {@link GrpcServer} instance as configured by this builder + * and its parameters. + * + * @return a ready to use {@link GrpcServer} + */ + @Override + public GrpcServer build() { + PriorityBag interceptors = new PriorityBag<>(); + GrpcServerImpl server = new GrpcServerImpl(configuration); + + interceptors.add(new ContextSettingServerInterceptor()); + + Tracer tracer = configuration.tracer(); + if (tracer != null) { + interceptors.add(new GrpcTracing(tracer, configuration.tracingConfig())); + } + + // add the global interceptors from the routing AFTER the tracing interceptor + // so that all of those interceptors are included in the trace timings + interceptors.merge(routing.interceptors()); + + for (ServiceDescriptor service : routing.services()) { + server.deploy(service, interceptors); + } + + return server; + } + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServerBasicConfig.java b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServerBasicConfig.java new file mode 100644 index 00000000000..526ce1fa34f --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServerBasicConfig.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; + +/** + * Configuration class for the {@link GrpcServer} implementations. + */ +public class GrpcServerBasicConfig + implements GrpcServerConfiguration { + + private final String name; + + private final int port; + + private final boolean nativeTransport; + + private final Tracer tracer; + + private final TracingConfiguration tracingConfig; + + private final int workers; + + private final SslConfiguration sslConfig; + + /** + * Construct {@link GrpcServerBasicConfig} instance. + * + * @param name the server name + * @param port the port to listen on + * @param workers a count of threads in a pool used to tryProcess HTTP requests + * @param nativeTransport {@code true} to enable native transport for + * the server + * @param tracer the tracer to use + * @param tracingConfig the tracing configuration + * @param sslConfig the SSL configuration + */ + public GrpcServerBasicConfig(String name, + int port, + int workers, + boolean nativeTransport, + Tracer tracer, + TracingConfiguration tracingConfig, + SslConfiguration sslConfig) { + + this.name = name == null || name.trim().isEmpty() ? DEFAULT_NAME : name.trim(); + this.port = port <= 0 ? 0 : port; + this.nativeTransport = nativeTransport; + this.tracer = tracer == null ? GlobalTracer.get() : tracer; + this.tracingConfig = tracingConfig == null ? new TracingConfiguration.Builder().build() : tracingConfig; + this.workers = workers > 0 ? workers : DEFAULT_WORKER_COUNT; + this.sslConfig = sslConfig; + } + + // ---- accessors --------------------------------------------------- + + /** + * Get the server name. + * + * @return the server name + */ + @Override + public String name() { + return name; + } + + /** + * Get the server port. + * + * @return the server port + */ + @Override + public int port() { + return port; + } + + /** + * Determine whether use native transport if possible. + *

+ * If native transport support is enabled, gRPC server will use epoll on + * Linux, or kqueue on OS X. Otherwise, the standard NIO transport will + * be used. + * + * @return {@code true} if native transport should be used + */ + @Override + public boolean useNativeTransport() { + return nativeTransport; + } + + + @Override + public Tracer tracer() { + return tracer; + } + + @Override + public TracingConfiguration tracingConfig() { + return tracingConfig; + } + + @Override + public int workers() { + return workers; + } + + @Override + public SslConfiguration sslConfig() { + return sslConfig; + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServerConfiguration.java b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServerConfiguration.java new file mode 100644 index 00000000000..743e03b45cb --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServerConfiguration.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.grpc.server; + +import java.util.function.Supplier; + +import io.helidon.config.Config; + +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; + +/** + * The configuration for a gRPC server. + */ +public interface GrpcServerConfiguration { + /** + * The default server name. + */ + String DEFAULT_NAME = "grpc.server"; + + /** + * The default grpc port. + */ + int DEFAULT_PORT = 1408; + + /** + * The default number of worker threads that will be used if not explicitly set. + */ + int DEFAULT_WORKER_COUNT = Runtime.getRuntime().availableProcessors(); + + /** + * Get the server name. + * + * @return the server name + */ + String name(); + + /** + * Get the server port. + * + * @return the server port + */ + int port(); + + /** + * Determine whether use native transport if possible. + *

+ * If native transport support is enabled, gRPC server will use epoll on + * Linux, or kqueue on OS X. Otherwise, the standard NIO transport will + * be used. + * + * @return {@code true} if native transport should be used + */ + boolean useNativeTransport(); + + /** + * Returns an opentracing.io tracer. Default is {@link GlobalTracer}. + * + * @return a tracer to use - never {@code null} (defaulting to {@link GlobalTracer} + */ + Tracer tracer(); + + /** + * Returns tracing configuration. + * + * @return a tracing configuration. + */ + TracingConfiguration tracingConfig(); + + /** + * Returns a count of threads in s pool used to tryProcess gRPC requests. + *

+ * Default value is {@code CPU_COUNT * 2}. + * + * @return a workers count + */ + int workers(); + + /** + * Returns a SslConfiguration to use with the server socket. If not {@code null} then + * the server enforces an SSL communication. + * + * @return a SSL context to use + */ + SslConfiguration sslConfig(); + + /** + * Creates new instance with default values for all configuration properties. + * + * @return a new instance + */ + static GrpcServerConfiguration create() { + return builder().build(); + } + + /** + * Creates new instance with values from external configuration. + * + * @param config the externalized configuration + * @return a new instance + */ + static GrpcServerConfiguration create(Config config) { + return builder(config).build(); + } + + /** + * Creates new instance of a {@link Builder server configuration builder}. + * + * @return a new builder instance + */ + static GrpcServerConfiguration.Builder builder() { + return new Builder(); + } + + /** + * Creates new instance of a {@link Builder server configuration builder} with defaults from external configuration source. + * + * @param config the externalized configuration + * @return a new builder instance + */ + static Builder builder(Config config) { + return new Builder().config(config); + } + + /** + * A {@link GrpcServerConfiguration} builder. + */ + final class Builder implements io.helidon.common.Builder { + private String name = DEFAULT_NAME; + + private int port = DEFAULT_PORT; + + private boolean useNativeTransport; + + private Tracer tracer; + + private TracingConfiguration tracingConfig; + + private int workers; + + private SslConfiguration sslConfig = null; + + private Builder() { + } + + public GrpcServerConfiguration.Builder config(Config config) { + if (config == null) { + return this; + } + + name = config.get("name").asString().orElse(DEFAULT_NAME); + port = config.get("port").asInt().orElse(DEFAULT_PORT); + useNativeTransport = config.get("native").asBoolean().orElse(false); + config.get("workers").asInt().ifPresent(this::workersCount); + + return this; + } + + /** + * Set the name of the gRPC server. + *

+ * Configuration key: {@code name} + * + * @param name the name of the gRPC server + * + * @return an updated builder + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets server port. If port is {@code 0} or less then any available ephemeral port will be used. + *

+ * Configuration key: {@code port} + * + * @param port the server port + * @return an updated builder + */ + public Builder port(int port) { + this.port = port < 0 ? 0 : port; + return this; + } + + /** + * Sets an opentracing.io tracer. (Default is {@link GlobalTracer}.) + * + * @param tracer a tracer to set + * @return an updated builder + */ + public Builder tracer(Tracer tracer) { + this.tracer = tracer; + return this; + } + + /** + * Sets an opentracing.io tracer. (Default is {@link GlobalTracer}.) + * + * @param tracerBuilder a tracer builder to set; will be built as a first step of this method execution + * @return updated builder + */ + public Builder tracer(Supplier tracerBuilder) { + this.tracer = tracerBuilder != null ? tracerBuilder.get() : null; + return this; + } + + /** + * Set trace configuration. + * + * @param tracingConfig the tracing configuration to set + * @return an updated builder + */ + public Builder tracingConfig(TracingConfiguration tracingConfig) { + this.tracingConfig = tracingConfig; + return this; + } + + /** + * Sets a count of threads in pool used to tryProcess HTTP requests. + * Default value is {@code CPU_COUNT * 2}. + *

+ * Configuration key: {@code workers} + * + * @param workers a workers count + * @return an updated builder + */ + public Builder workersCount(int workers) { + this.workers = workers; + return this; + } + + /** + * Configures SslConfiguration to use with the server socket. If not {@code null} then + * the server enforces an SSL communication. + * + * @param sslConfig a SSL context to use + * @return this builder + */ + public Builder sslConfig(SslConfiguration sslConfig) { + this.sslConfig = sslConfig; + return this; + } + + @Override + public GrpcServerConfiguration build() { + return new GrpcServerBasicConfig(name, + port, + workers, + useNativeTransport, + tracer, + tracingConfig, + sslConfig); + } + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServerImpl.java b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServerImpl.java new file mode 100644 index 00000000000..6ea5dd739ed --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcServerImpl.java @@ -0,0 +1,424 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLContext; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.pki.KeyConfig; +import io.helidon.grpc.core.PriorityBag; + +import io.grpc.BindableService; +import io.grpc.HandlerRegistry; +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.Server; +import io.grpc.ServerInterceptor; +import io.grpc.ServerMethodDefinition; +import io.grpc.ServerServiceDefinition; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.util.MutableHandlerRegistry; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.JdkSslContext; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import org.eclipse.microprofile.health.HealthCheck; + +import static java.lang.String.format; + +/** + * A gRPC Server implementation. + */ +public class GrpcServerImpl implements GrpcServer { + + private CompletableFuture startFuture = new CompletableFuture<>(); + + private CompletableFuture shutdownFuture = new CompletableFuture<>(); + + /** + * The {@link Logger} to use. + */ + private static final Logger LOGGER = Logger.getLogger(GrpcServerImpl.class.getName()); + + /** + * Configuration values. + */ + private GrpcServerConfiguration config; + + /** + * The TCP-based gRPC server. + */ + private Server server; + + /** + * The in-process gRPC server. + */ + private Server inProcessServer; + + /** + * The health status manager. + */ + private HealthServiceImpl healthService = new HealthServiceImpl(); + + /** + * The {@link HandlerRegistry} to register services. + */ + private final MutableHandlerRegistry handlerRegistry = new MutableHandlerRegistry(); + + /** + * The map of service class name to {@link ServerServiceDefinition}. + */ + private Map mapServices = new ConcurrentHashMap<>(); + + /** + * The map of service names to {@link io.helidon.grpc.server.ServiceDescriptor ServiceDescriptors}. + */ + private Map services = new ConcurrentHashMap<>(); + + // ---- constructors ---------------------------------------------------- + + /** + * Create a {@link GrpcServerImpl}. + * + * @param config the configuration for this server + */ + GrpcServerImpl(GrpcServerConfiguration config) { + this.config = config; + } + + // ---- GrpcServer interface -------------------------------------------- + + @Override + public CompletionStage start() { + String sName = config.name(); + int port = config.port(); + boolean tls = false; + SslConfiguration sslConfig = config.sslConfig(); + SslContext sslContext = null; + + try { + if (sslConfig != null) { + if (sslConfig.isJdkSSL()) { + SSLContext sslCtx = SSLContextBuilder.create(KeyConfig.pemBuilder() + .key(Resource.create(sslConfig.getTLSKey())) + .certChain(Resource.create(sslConfig.getTLSCerts())) + .build()).build(); + sslContext = new JdkSslContext(sslCtx, false, ClientAuth.NONE); + + } else { + sslContext = sslContextBuilder(sslConfig).build(); + } + } + + NettyServerBuilder builder = sslContext == null + ? NettyServerBuilder.forPort(port) + : NettyServerBuilder.forPort(port).sslContext(sslContext); + + HandlerRegistry handlerRegistry = this.handlerRegistry; + + server = configureNetty(builder) + .directExecutor() + .addService(healthService) + .fallbackHandlerRegistry(handlerRegistry) + .build() + .start(); + + inProcessServer = InProcessServerBuilder + .forName(sName) + .addService(healthService) + .fallbackHandlerRegistry(handlerRegistry) + .build() + .start(); + + LOGGER.log(Level.INFO, + () -> format("gRPC server [%s]: listening on port %d (TLS=%s)", sName, server.getPort(), tls)); + + Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown)); + startFuture.complete(this); + } catch (Throwable e) { + LOGGER.log(Level.SEVERE, format("gRPC server [%s]: failed to start on port %d (TLS=%s)", sName, port, tls), e); + startFuture.completeExceptionally(e); + } + return startFuture; + } + + @Override + public CompletionStage shutdown() { + String name = config.name(); + + try { + if (server != null) { + server.shutdown(); + inProcessServer.shutdown(); + server.awaitTermination(); + inProcessServer.awaitTermination(); + + LOGGER.log(Level.INFO, () -> format("gRPC server [%s]: server stopped", name)); + server = null; + inProcessServer = null; + + shutdownFuture.complete(this); + } + } catch (Throwable e) { + LOGGER.log(Level.SEVERE, format("gRPC server [%s]: server failed to shut down", name), e); + shutdownFuture.completeExceptionally(e); + } + + return shutdownFuture; + } + + @Override + public GrpcServerConfiguration configuration() { + return config; + } + + @Override + public CompletionStage whenShutdown() { + return shutdownFuture; + } + + @Override + public boolean isRunning() { + return server != null && !(server.isShutdown() || server.isTerminated()); + } + + @Override + public int port() { + return isRunning() ? server.getPort() : -1; + } + + @Override + public HealthCheck[] healthChecks() { + return healthService.healthChecks().toArray(new HealthCheck[0]); + } + + @Override + public Map services() { + return Collections.unmodifiableMap(services); + } + + // ---- helper methods -------------------------------------------------- + + private NettyServerBuilder configureNetty(NettyServerBuilder builder) { + int workersCount = config.workers(); + + Class channelType = null; + EventLoopGroup boss = null; + EventLoopGroup workers = null; + + // ToDo: add back native transport support, so the check bellow makes sense + // boolean useNative = config.useNativeTransport(); + + if (channelType == null) { + LOGGER.log(Level.FINE, () -> "Using NIO transport"); + channelType = NioServerSocketChannel.class; + boss = new NioEventLoopGroup(1); + workers = workersCount <= 0 ? new NioEventLoopGroup() : new NioEventLoopGroup(workersCount); + } + + return builder + .channelType(channelType) + .bossEventLoopGroup(boss) + .workerEventLoopGroup(workers); + } + + /** + * Deploy the specified {@link ServiceDescriptor service} to this {@link GrpcServer}. + * + * @param serviceDescriptor the service to deploy + * @param globalInterceptors the global {@link io.grpc.ServerInterceptor}s to wrap all services with + * @throws NullPointerException if {@code serviceDescriptor} is {@code null} + */ + public void deploy(ServiceDescriptor serviceDescriptor, PriorityBag globalInterceptors) { + Objects.requireNonNull(serviceDescriptor); + + String serverName = config.name(); + BindableService service = serviceDescriptor.bindableService(globalInterceptors); + ServerServiceDefinition ssd = service.bindService(); + String serviceName = ssd.getServiceDescriptor().getName(); + + services.put(serviceDescriptor.name(), serviceDescriptor); + handlerRegistry.addService(ssd); + mapServices.put(service.getClass().getName(), ssd); + healthService.add(serviceName, serviceDescriptor.healthCheck()); + + LOGGER.info(() -> format("gRPC server [%s]: registered service [%s]", + serverName, serviceName)); + + Iterator methods = ssd.getMethods() + .stream() + .map(ServerMethodDefinition::getMethodDescriptor) + .map(MethodDescriptor::getFullMethodName) + .sorted() + .iterator(); + + if (methods.hasNext()) { + LOGGER.info(() -> format("gRPC server [%s]: with methods [%s]", + serverName, + methods.next())); + } + while (methods.hasNext()) { + LOGGER.info(() -> format("gRPC server [%s]: [%s]", + serverName, + methods.next())); + } + } + + /** + * Undeploy the specified {@link BindableService} from this {@link GrpcServerImpl}. + * + * @param service the service to undeploy + * @param sName the gRPC server name + * @throws NullPointerException if {@code service} is {@code null} + */ + public void undeploy(BindableService service, String sName) { + Objects.requireNonNull(service); + + String serviceClassName = service.getClass().getName(); + ServerServiceDefinition ssd = mapServices.get(serviceClassName); + if (null == ssd) { + return; + } + + handlerRegistry.removeService(ssd); + mapServices.remove(serviceClassName); + + LOGGER.info(() -> format("gRPC server [%s]: unregistered service [%s]", + sName, + ssd.getServiceDescriptor().getName())); + } + + /** + * Obtain an immutable {@link List} of registered {@link ServerServiceDefinition}s. + * + * @return an immutable {@link List} of registered {@link ServerServiceDefinition}s + */ + public List getServices() { + return Collections.unmodifiableList(handlerRegistry.getServices()); + } + + /** + * @return a new in-process {@link ManagedChannel} for interacting with + * the services managed by this {@link GrpcServerImpl}. + */ + public ManagedChannel createInProcessChannel() { + return InProcessChannelBuilder.forName(config.name()).build(); + } + + /** + * Return an instance of SslContextBuilder from the specified SslConfig. + * + * @param sslConfig the ssl configuration + * @return an instance of SslContextBuilder + */ + protected SslContextBuilder sslContextBuilder(SslConfiguration sslConfig) { + String sCertFile = sslConfig.getTLSCerts(); + String sKeyFile = sslConfig.getTLSKey(); + String sClientCertFile = sslConfig.getTLSClientCerts(); + + if (sCertFile == null || sCertFile.isEmpty()) { + throw new IllegalStateException("gRPC server is configured to use TLS but cert file is not set"); + } + + if (sKeyFile == null || sKeyFile.isEmpty()) { + throw new IllegalStateException("gRPC server is configured to use TLS but key file is not set"); + } + + File fileCerts = new File(sCertFile); + File fileKey = new File(sKeyFile); + X509Certificate[] aX509Certificates; + + if (!fileCerts.exists() || !fileCerts.isFile()) { + throw new IllegalStateException("gRPC server is configured to use TLS but certs file " + + sCertFile + " either does not exist or is not a file"); + } + + if (!fileKey.exists() || !fileKey.isFile()) { + throw new IllegalStateException("gRPC server is configured to use TLS but key file " + + sKeyFile + " either does not exist or is not a file"); + } + + if (sClientCertFile != null) { + File fileClientCerts = new File(sClientCertFile); + + if (!fileClientCerts.exists() || !fileClientCerts.isFile()) { + throw new IllegalStateException("gRPC server is configured to use TLS but client cert file " + + sClientCertFile + " either does not exist or is not a file"); + } + + try { + aX509Certificates = loadX509Cert(fileClientCerts); + } catch (Exception e) { + throw new IllegalStateException("gRPC server is configured to use TLS but failed to load trusted CA files"); + } + + } else { + aX509Certificates = new X509Certificate[0]; + } + + SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(fileCerts, fileKey); + + if (aX509Certificates.length > 0) { + sslContextBuilder.trustManager(aX509Certificates) + .clientAuth(ClientAuth.REQUIRE); + } else { + sslContextBuilder.clientAuth(ClientAuth.OPTIONAL); + } + + return GrpcSslContexts.configure(sslContextBuilder, SslProvider.OPENSSL); + } + + private static X509Certificate[] loadX509Cert(File... aFile) + throws CertificateException, IOException { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate[] aCerts = new X509Certificate[aFile.length]; + + for (int i = 0; i < aFile.length; i++) { + try (InputStream in = new FileInputStream(aFile[i])) { + aCerts[i] = (X509Certificate) cf.generateCertificate(in); + } + } + + return aCerts; + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/GrpcService.java b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcService.java new file mode 100644 index 00000000000..b933a94589e --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcService.java @@ -0,0 +1,394 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import io.helidon.grpc.core.SafeStreamObserver; + +import io.grpc.stub.StreamObserver; + +import static io.helidon.grpc.server.BindableServiceImpl.completeWithResult; +import static io.helidon.grpc.server.BindableServiceImpl.completeWithoutResult; +import static io.helidon.grpc.server.BindableServiceImpl.createSupplier; + +/** + * A Helidon gRPC service. + */ +public interface GrpcService { + + /** + * Update service configuration. + * + * @param rules configuration to update + */ + void update(ServiceDescriptor.Rules rules); + + /** + * Obtain the name of this service. + *

+ * The default implementation returns the implementation class's {@link Class#getSimpleName()}. + * + * @return the name of this service + */ + default String name() { + return getClass().getSimpleName(); + } + + // ---- convenience methods --------------------------------------------- + + /** + * Complete a gRPC request. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * specified value then calling {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to complete + * @param value the value to use when calling {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + default void complete(StreamObserver observer, T value) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + safe.onNext(value); + safe.onCompleted(); + } + + /** + * Complete a gRPC request based on the result of a {@link CompletionStage}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link CompletionStage} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param future the {@link CompletionStage} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + default void complete(StreamObserver observer, CompletionStage future) { + future.whenComplete(completeWithResult(observer)); + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link CompletionStage}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link CompletionStage} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called. + *

+ * The execution will take place asynchronously on the fork-join thread pool. + * + * @param observer the {@link StreamObserver} to complete + * @param future the {@link CompletionStage} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + default void completeAsync(StreamObserver observer, CompletionStage future) { + future.whenCompleteAsync(completeWithResult(observer)); + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link CompletionStage}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link CompletionStage} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param future the {@link CompletionStage} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param executor the {@link java.util.concurrent.Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + default void completeAsync(StreamObserver observer, CompletionStage future, Executor executor) { + future.whenCompleteAsync(completeWithResult(observer), executor); + } + + /** + * Complete a gRPC request based on the result of a {@link Callable}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link Callable} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param callable the {@link Callable} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + default void complete(StreamObserver observer, Callable callable) { + try { + observer.onNext(callable.call()); + observer.onCompleted(); + } catch (Throwable t) { + observer.onError(t); + } + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link Callable}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link Callable} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + *

+ * The execution will take place asynchronously on the fork-join thread pool. + * + * @param observer the {@link StreamObserver} to complete + * @param callable the {@link Callable} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + default void completeAsync(StreamObserver observer, Callable callable) { + completeAsync(observer, CompletableFuture.supplyAsync(createSupplier(callable))); + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link Callable}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link Callable} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param callable the {@link Callable} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param executor the {@link java.util.concurrent.Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + default void completeAsync(StreamObserver observer, Callable callable, Executor executor) { + completeAsync(observer, CompletableFuture.supplyAsync(createSupplier(callable), executor)); + } + + /** + * Execute a {@link Runnable} task and on completion of the task complete the gRPC request by + * calling {@link StreamObserver#onNext(Object)} using the specified result and then call + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param task the {@link Runnable} to execute + * @param result the result to pass to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + default void complete(StreamObserver observer, Runnable task, T result) { + complete(observer, Executors.callable(task, result)); + } + + /** + * Asynchronously execute a {@link Runnable} task and on completion of the task complete the gRPC + * request by calling {@link StreamObserver#onNext(Object)} using the specified result and then + * call {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + *

+ * The task and and request completion will be executed on the fork-join thread pool. + * + * @param observer the {@link StreamObserver} to complete + * @param task the {@link Runnable} to execute + * @param result the result to pass to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + default void completeAsync(StreamObserver observer, Runnable task, T result) { + completeAsync(observer, Executors.callable(task, result)); + } + + /** + * Asynchronously execute a {@link Runnable} task and on completion of the task complete the gRPC + * request by calling {@link StreamObserver#onNext(Object)} using the specified result and then + * call {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param task the {@link Runnable} to execute + * @param result the result to pass to {@link StreamObserver#onNext(Object)} + * @param executor the {@link java.util.concurrent.Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + default void completeAsync(StreamObserver observer, Runnable task, T result, Executor executor) { + completeAsync(observer, Executors.callable(task, result), executor); + } + + /** + * Send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until the + * {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param stream the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + default void stream(StreamObserver observer, Stream stream) { + stream(observer, () -> stream); + } + + /** + * Asynchronously send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until + * the {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param stream the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param executor the {@link java.util.concurrent.Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + default void streamAsync(StreamObserver observer, Stream stream, Executor executor) { + executor.execute(() -> stream(observer, () -> stream)); + } + + /** + * Send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until the + * {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param supplier the {@link Supplier} of the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + default void stream(StreamObserver observer, Supplier> supplier) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + Throwable thrown = null; + + try { + supplier.get().forEach(safe::onNext); + } catch (Throwable t) { + thrown = t; + } + + if (thrown == null) { + safe.onCompleted(); + } else { + safe.onError(thrown); + } + } + + /** + * Asynchronously send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method + * until the {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param supplier the {@link Supplier} of the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param executor the {@link java.util.concurrent.Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + default void streamAsync(StreamObserver observer, Supplier> supplier, Executor executor) { + executor.execute(() -> stream(observer, supplier)); + } + + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until + * the {@link CompletionStage} completes then call {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called instead of {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes + * @param stage the {@link CompletionStage} to await completion of + * @param they type of the request result + * + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + // todo: a bit of a chicken or egg when used with Coherence streaming methods, isn't it? + default Consumer stream(StreamObserver observer, CompletionStage stage) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + stage.whenComplete(completeWithoutResult(safe)); + return safe::onNext; + } + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until + * the {@link CompletionStage} completes then asynchronously call {@link StreamObserver#onCompleted()} using the + * fork-join thread pool. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called instead of {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes + * @param stage the {@link CompletionStage} to await completion of + * @param they type of the request result + * + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + default Consumer streamAsync(StreamObserver observer, CompletionStage stage) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + stage.whenCompleteAsync(completeWithoutResult(safe)); + return value -> CompletableFuture.runAsync(() -> safe.onNext(value)); + } + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until + * the {@link CompletionStage} completes then asynchronously call {@link StreamObserver#onCompleted()} using the executor + * thread. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called instead of {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes + * @param stage the {@link CompletionStage} to await completion of + * @param executor the {@link java.util.concurrent.Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + * + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + default Consumer streamAsync(StreamObserver observer, CompletionStage stage, Executor executor) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + stage.whenCompleteAsync(completeWithoutResult(safe), executor); + return value -> CompletableFuture.runAsync(() -> safe.onNext(value), executor); + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/GrpcTracing.java b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcTracing.java new file mode 100644 index 00000000000..799bf173692 --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/GrpcTracing.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Priority; + +import io.helidon.grpc.core.ContextKeys; +import io.helidon.grpc.core.InterceptorPriorities; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import io.opentracing.contrib.grpc.OpenTracingContextKey; +import io.opentracing.contrib.grpc.OperationNameConstructor; +import io.opentracing.propagation.Format; +import io.opentracing.propagation.TextMapExtractAdapter; + +/** + * A {@link ServerInterceptor} that adds tracing to gRPC service calls. + */ +@Priority(InterceptorPriorities.TRACING) +public class GrpcTracing + implements ServerInterceptor { + /** + * public constructor. + * + * @param tracer the Open Tracing {@link Tracer} + * @param tracingConfig the tracing configuration + */ + public GrpcTracing(Tracer tracer, TracingConfiguration tracingConfig) { + this.tracer = tracer; + operationNameConstructor = tracingConfig.operationNameConstructor(); + streaming = tracingConfig.isStreaming(); + verbose = tracingConfig.isVerbose(); + tracedAttributes = tracingConfig.tracedAttributes(); + } + + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { + Map headerMap = new HashMap<>(); + + for (String key : headers.keys()) { + if (!key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { + String value = headers.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)); + headerMap.put(key, value); + } + } + + String operationName = operationNameConstructor.constructOperationName(call.getMethodDescriptor()); + Span span = getSpanFromHeaders(headerMap, operationName); + + if (tracedAttributes.contains(ServerRequestAttribute.ALL)) { + span.setTag("grpc.method_type", call.getMethodDescriptor().getType().toString()); + span.setTag("grpc.method_name", call.getMethodDescriptor().getFullMethodName()); + span.setTag("grpc.call_attributes", call.getAttributes().toString()); + addMetadata(headers, span); + } else { + for (ServerRequestAttribute attr : tracedAttributes) { + switch (attr) { + case METHOD_TYPE: + span.setTag("grpc.method_type", call.getMethodDescriptor().getType().toString()); + break; + case METHOD_NAME: + span.setTag("grpc.method_name", call.getMethodDescriptor().getFullMethodName()); + break; + case CALL_ATTRIBUTES: + span.setTag("grpc.call_attributes", call.getAttributes().toString()); + break; + case HEADERS: + addMetadata(headers, span); + break; + default: + // ignored - should never happen + } + } + } + + Context ctxWithSpan = Context.current().withValue(OpenTracingContextKey.getKey(), span); + ServerCall.Listener listenerWithContext = Contexts.interceptCall(ctxWithSpan, call, headers, next); + + return new TracingListener<>(listenerWithContext, span); + } + + private void addMetadata(Metadata headers, Span span) { + // copy the headers and make sure that the AUTHORIZATION header + // is removed as we do not want auth details to appear in tracing logs + Metadata metadata = new Metadata(); + + metadata.merge(headers); + metadata.removeAll(ContextKeys.AUTHORIZATION); + + span.setTag("grpc.headers", metadata.toString()); + } + + private Span getSpanFromHeaders(Map headers, String operationName) { + Span span; + + try { + SpanContext parentSpanCtx = tracer.extract(Format.Builtin.HTTP_HEADERS, + new TextMapExtractAdapter(headers)); + if (parentSpanCtx == null) { + span = tracer.buildSpan(operationName) + .start(); + } else { + span = tracer.buildSpan(operationName) + .asChildOf(parentSpanCtx) + .start(); + } + } catch (IllegalArgumentException iae) { + span = tracer.buildSpan(operationName) + .withTag("Error", "Extract failed and an IllegalArgumentException was thrown") + .start(); + } + + return span; + } + + /** + * A {@link ServerCall.Listener} to apply details to a tracing {@link Span} at various points + * in a call lifecycle. + * + * @param the type of the request + */ + private class TracingListener + extends ForwardingServerCallListener.SimpleForwardingServerCallListener { + + private final Span span; + + private TracingListener(ServerCall.Listener delegate, Span span) { + super(delegate); + this.span = span; + } + + @Override + public void onMessage(ReqT message) { + if (streaming || verbose) { + span.log(Collections.singletonMap("Message received", message)); + } + + delegate().onMessage(message); + } + + @Override + public void onHalfClose() { + if (streaming) { + span.log("Client finished sending messages"); + } + + delegate().onHalfClose(); + } + + @Override + public void onCancel() { + span.log("Call cancelled"); + + try { + delegate().onCancel(); + } finally { + span.finish(); + } + } + + @Override + public void onComplete() { + if (verbose) { + span.log("Call completed"); + } + + try { + delegate().onComplete(); + } finally { + span.finish(); + } + } + } + + /** + * The Open Tracing {@link Tracer}. + */ + private final Tracer tracer; + + /** + * A flag indicating whether to log streaming. + */ + private final OperationNameConstructor operationNameConstructor; + + /** + * + */ + private final boolean streaming; + + /** + * A flag indicating verbose logging. + */ + private final boolean verbose; + + /** + * The set of attributes to log in spans. + */ + private final Set tracedAttributes; +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/HealthServiceImpl.java b/grpc/server/src/main/java/io/helidon/grpc/server/HealthServiceImpl.java new file mode 100644 index 00000000000..144176eb32b --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/HealthServiceImpl.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.grpc.Status; +import io.grpc.health.v1.HealthCheckRequest; +import io.grpc.health.v1.HealthCheckResponse; +import io.grpc.health.v1.HealthGrpc; +import io.grpc.services.HealthStatusManager; +import io.grpc.stub.StreamObserver; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse.State; + +/** + * An implementation of the {@link HealthGrpc} service. + */ +class HealthServiceImpl + extends HealthGrpc.HealthImplBase { + + /** + * A map of {@link HealthCheck}s keyed by service name. + */ + private final Map mapHealthChecks = new ConcurrentHashMap<>(); + + /** + * Create a {@link HealthServiceImpl}. + */ + HealthServiceImpl() { + // register the empty service name to represent the global health check + // see: https://github.com/grpc/grpc/blob/master/doc/health-checking.md + mapHealthChecks.put(HealthStatusManager.SERVICE_NAME_ALL_SERVICES, + ConstantHealthCheck.up(HealthStatusManager.SERVICE_NAME_ALL_SERVICES)); + } + + /** + * Add a {@link HealthCheck}. + * @param name the name of the service that the health check is for + * @param healthCheck the {@link HealthCheck} implementation + */ + void add(String name, HealthCheck healthCheck) { + mapHealthChecks.put(name, healthCheck); + } + + /** + * Obtain the collection of registered {@link HealthCheck}s. + * + * @return the collection of registered {@link HealthCheck}s + */ + Collection healthChecks() { + return mapHealthChecks.values(); + } + + @Override + public void check(HealthCheckRequest request, StreamObserver responseObserver) { + String service = request.getService(); + HealthCheck check = mapHealthChecks.get(service); + + if (check == null) { + // If no health check is registered for the requested service then respond with a not found error. + // See method comments: + // https://github.com/grpc/grpc-java/blob/7df2d5feebf8bc5ecfcea3edba290db500382dcf/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java#L149 + String message = "Service '" + service + "' does not exist or does not have a registered health check"; + responseObserver.onError(Status.NOT_FOUND.withDescription(message).asException()); + } else { + responseObserver.onNext(toHealthCheckResponse(check.call())); + responseObserver.onCompleted(); + } + } + + private HealthCheckResponse toHealthCheckResponse(HealthCheckResponse.ServingStatus status) { + return HealthCheckResponse.newBuilder().setStatus(status).build(); + } + + private HealthCheckResponse toHealthCheckResponse(org.eclipse.microprofile.health.HealthCheckResponse response) { + return response.getState().equals(State.UP) + ? toHealthCheckResponse(HealthCheckResponse.ServingStatus.SERVING) + : toHealthCheckResponse(HealthCheckResponse.ServingStatus.NOT_SERVING); + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/MethodDescriptor.java b/grpc/server/src/main/java/io/helidon/grpc/server/MethodDescriptor.java new file mode 100644 index 00000000000..bf9b815a180 --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/MethodDescriptor.java @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import io.helidon.grpc.core.InterceptorPriorities; +import io.helidon.grpc.core.MarshallerSupplier; +import io.helidon.grpc.core.PriorityBag; + +import io.grpc.Context; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +/** + * Encapsulates all metadata necessary to define a gRPC method. + * + * @param request type + * @param response type + */ +public class MethodDescriptor { + private final String name; + private final io.grpc.MethodDescriptor descriptor; + private final ServerCallHandler callHandler; + private final Map context; + private final PriorityBag interceptors; + + private MethodDescriptor(String name, + io.grpc.MethodDescriptor descriptor, + ServerCallHandler callHandler, + Map context, + PriorityBag interceptors) { + this.name = name; + this.descriptor = descriptor; + this.callHandler = callHandler; + this.context = context; + this.interceptors = interceptors.copyMe(); + } + + /** + * Return the name of the method. + * @return method name + */ + public String name() { + return name; + } + + /** + * Return gRPC method descriptor. + * @return gRPC method descriptor + */ + public io.grpc.MethodDescriptor descriptor() { + return descriptor; + } + + /** + * Return the call handler. + * @return call handler + */ + public ServerCallHandler callHandler() { + return callHandler; + } + + /** + * Obtain the {@link Map} of {@link Context.Key}s and values to add to the + * call context when this method is invoked. + * + * @return an unmodifiable {@link Map} of {@link Context.Key}s and values to + * add to the call context when this method is invoked + */ + public Map context() { + return Collections.unmodifiableMap(context); + } + + /** + * Obtain the {@link io.grpc.ServerInterceptor}s to use for this method. + * + * @return the {@link io.grpc.ServerInterceptor}s to use for this method + */ + public PriorityBag interceptors() { + return interceptors.readOnly(); + } + + static Builder builder(String serviceName, + String name, + io.grpc.MethodDescriptor.Builder descriptor, + ServerCallHandler callHandler) { + return new Builder<>(serviceName, name, descriptor, callHandler); + } + + static MethodDescriptor create(String serviceName, + String name, + io.grpc.MethodDescriptor.Builder descriptor, + ServerCallHandler callHandler) { + return builder(serviceName, name, descriptor, callHandler).build(); + } + + @Override + public String toString() { + String fullName = descriptor == null ? null : descriptor.getFullMethodName(); + return "MethodDescriptor(" + + "name='" + name + '\'' + + "fullName='" + fullName + "\')"; + } + + /** + * Method configuration API. + * + * @param request type + * @param response type + */ + public interface Rules { + /** + * Add a {@link Context.Key} and value to be added to the call {@link io.grpc.Context} + * when this method is invoked. + * + * @param key the {@link Context.Key} to add + * @param value the value to map to the {@link Context.Key} + * @param the type of the {@link Context.Key} and value + * + * @return this {@link MethodDescriptor.Rules} instance for fluent call chaining + * + * @throws java.lang.NullPointerException if the key parameter is null + */ + + Rules addContextValue(Context.Key key, T value); + + /** + * Register one or more {@link io.grpc.ServerInterceptor interceptors} for the method. + * + * @param interceptors one or more {@link ServerInterceptor}s to register + * @return this builder to allow fluent method chaining + */ + Rules intercept(ServerInterceptor... interceptors); + + /** + * Register one or more {@link io.grpc.ServerInterceptor interceptors} for the method. + *

+ * The added interceptors will be applied using the specified priority. + * + * @param priority the priority to assign to the interceptors + * @param interceptors one or more {@link ServerInterceptor}s to register + * @return this builder to allow fluent method chaining + */ + Rules intercept(int priority, ServerInterceptor... interceptors); + + /** + * Register the {@link MarshallerSupplier} for the method. + *

+ * If not set the default {@link MarshallerSupplier} from the service will be used. + * + * @param marshallerSupplier the {@link MarshallerSupplier} for the service + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); + + /** + * Set the request type. + *

+ * Setting the request type is optional as it is used to obtain the + * correct marshaller so if the marshaller supplier being used is type + * agnostic, such as Java serialization then whereas some marshallers + * such as Protocol Buffers require a type. + * + * @param requestType the type of the request message + * @param the type of the request message + * + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance + * for fluent call chaining + */ + Rules requestType(Class requestType); + + /** + * Set the response type. + *

+ * Setting the response type is optional as it is used to obtain the + * correct marshaller so if the marshaller supplier being used is type + * agnostic, such as Java serialization then whereas some marshallers + * such as Protocol Buffers require a type. + * + * @param responseType the type of the request message + * @param the type of the request message + * + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance + * for fluent call chaining + */ + Rules responseType(Class responseType); + } + + /** + * An interface implemented by classes that can configure + * a {@link MethodDescriptor.Rules}. + * + * @param request type + * @param response type + */ + @FunctionalInterface + public interface Configurer { + /** + * Apply extra configuration to a {@link MethodDescriptor.Rules}. + * + * @param rules the {@link MethodDescriptor.Rules} to configure + */ + void configure(MethodDescriptor.Rules rules); + } + + /** + * {@link MethodDescriptor} builder implementation. + * + * @param request type + * @param response type + */ + static final class Builder + implements Rules, io.helidon.common.Builder> { + private final String name; + private final io.grpc.MethodDescriptor.Builder descriptor; + private final ServerCallHandler callHandler; + + private final PriorityBag interceptors = new PriorityBag<>(InterceptorPriorities.USER); + + private final Map context = new HashMap<>(); + + private Class requestType; + + private Class responseType; + + private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.defaultInstance(); + private MarshallerSupplier marshallerSupplier; + + Builder(String serviceName, + String name, + io.grpc.MethodDescriptor.Builder descriptor, + ServerCallHandler callHandler) { + + this.name = name; + this.callHandler = callHandler; + this.descriptor = descriptor.setFullMethodName(serviceName + "/" + name); + } + + Builder fullname(String name) { + descriptor.setFullMethodName(name); + return this; + } + + @Override + public Builder marshallerSupplier(MarshallerSupplier supplier) { + this.marshallerSupplier = supplier; + return this; + } + + Builder defaultMarshallerSupplier(MarshallerSupplier supplier) { + if (supplier == null) { + this.defaultMarshallerSupplier = MarshallerSupplier.defaultInstance(); + } else { + this.defaultMarshallerSupplier = supplier; + } + return this; + } + + @Override + public Builder addContextValue(Context.Key key, T value) { + context.put(Objects.requireNonNull(key, "The context key cannot be null"), value); + return this; + } + + @Override + public Builder intercept(ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + processInterceptors(interceptors); + return this; + } + + @Override + public Rules intercept(int priority, ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), priority); + processInterceptors(interceptors); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public Builder requestType(Class requestType) { + this.requestType = requestType; + return (Builder) this; + } + + @Override + @SuppressWarnings("unchecked") + public Builder responseType(Class responseType) { + this.responseType = responseType; + return (Builder) this; + } + + @Override + @SuppressWarnings("unchecked") + public MethodDescriptor build() { + MarshallerSupplier supplier = this.marshallerSupplier; + + if (supplier == null) { + supplier = defaultMarshallerSupplier; + } + + if (requestType != null) { + descriptor.setRequestMarshaller((io.grpc.MethodDescriptor.Marshaller) supplier.get(requestType)); + } + + if (responseType != null) { + descriptor.setResponseMarshaller((io.grpc.MethodDescriptor.Marshaller) supplier.get(responseType)); + } + + return new MethodDescriptor<>(name, + descriptor.build(), + callHandler, + context, + interceptors); + } + + @SuppressWarnings("unchecked") + private void processInterceptors(ServerInterceptor... interceptors) { + // If any interceptors implement MethodDescriptor.Configurer allow them to apply further configuration + Arrays.stream(interceptors) + .filter(interceptor -> MethodDescriptor.Configurer.class.isAssignableFrom(interceptor.getClass())) + .map(MethodDescriptor.Configurer.class::cast) + .forEach(interceptor -> interceptor.configure(this)); + } + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/RpcMethod.java b/grpc/server/src/main/java/io/helidon/grpc/server/RpcMethod.java new file mode 100644 index 00000000000..a46178cd55b --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/RpcMethod.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.grpc.MethodDescriptor.MethodType; + +/** + * An annotation to mark a class as representing a gRPC service + * or a method as a gRPC service method. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RpcMethod { + /** + * Obtain the name of the service or method. + * + * @return name of the service or method + */ + String name() default ""; + + /** + * Obtain the gRPC method type. + * + * @return the gRPC method type + */ + MethodType type(); +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/RpcService.java b/grpc/server/src/main/java/io/helidon/grpc/server/RpcService.java new file mode 100644 index 00000000000..38ae0457706 --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/RpcService.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used to mark a class as representing a gRPC service. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface RpcService { + /** + * Obtain the service name. + * + * @return the service name + */ + String name() default ""; + + /** + * Obtain the service version. + * + * @return the service version + */ + int version() default 0; +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/SSLContextBuilder.java b/grpc/server/src/main/java/io/helidon/grpc/server/SSLContextBuilder.java new file mode 100644 index 00000000000..d7795133289 --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/SSLContextBuilder.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.Random; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.TrustManagerFactory; + +import io.helidon.common.Builder; +import io.helidon.common.CollectionsHelper; +import io.helidon.common.pki.KeyConfig; +import io.helidon.config.Config; + +/** + * Builder for configuring a new SslContext for creation. + */ +public final class SSLContextBuilder implements Builder { + + private static final String PROTOCOL = "TLS"; + private static final Random RANDOM = new Random(); + + private KeyConfig privateKeyConfig; + private KeyConfig trustConfig; + private long sessionCacheSize; + private long sessionTimeout; + + private SSLContextBuilder() { + } + + /** + * Creates a builder of the {@link javax.net.ssl.SSLContext}. + * + * @param privateKeyConfig the required private key configuration parameter + * @return this builder + */ + public static SSLContextBuilder create(KeyConfig privateKeyConfig) { + return new SSLContextBuilder().privateKeyConfig(privateKeyConfig); + } + + /** + * Creates {@link javax.net.ssl.SSLContext} from the provided configuration. + * + * @param sslConfig the ssl configuration + * @return a built {@link javax.net.ssl.SSLContext} + * @throws IllegalStateException in case of a problem; will wrap either an instance of {@link java.io.IOException} or + * a {@link java.security.GeneralSecurityException} + */ + public static SSLContext create(Config sslConfig) { + return new SSLContextBuilder().privateKeyConfig(KeyConfig.create(sslConfig.get("private-key"))) + .sessionCacheSize(sslConfig.get("session-cache-size").asInt().orElse(0)) + .sessionTimeout(sslConfig.get("session-timeout").asInt().orElse(0)) + .trustConfig(KeyConfig.create(sslConfig.get("trust"))) + .build(); + } + + private SSLContextBuilder privateKeyConfig(KeyConfig privateKeyConfig) { + this.privateKeyConfig = privateKeyConfig; + return this; + } + + /** + * Set the trust key configuration to be used to validate certificates. + * + * @param trustConfig the trust configuration + * @return an updated builder + */ + public SSLContextBuilder trustConfig(KeyConfig trustConfig) { + this.trustConfig = trustConfig; + return this; + } + + /** + * Set the size of the cache used for storing SSL session objects. {@code 0} to use the + * default value. + * + * @param sessionCacheSize the session cache size + * @return an updated builder + */ + public SSLContextBuilder sessionCacheSize(long sessionCacheSize) { + this.sessionCacheSize = sessionCacheSize; + return this; + } + + /** + * Set the timeout for the cached SSL session objects, in seconds. {@code 0} to use the + * default value. + * + * @param sessionTimeout the session timeout + * @return an updated builder + */ + public SSLContextBuilder sessionTimeout(long sessionTimeout) { + this.sessionTimeout = sessionTimeout; + return this; + } + + /** + * Create new {@code {@link javax.net.ssl.SSLContext}} instance with configured settings. + * + * @return the SSL Context built instance + * @throws IllegalStateException in case of a problem; will wrap either an instance of {@link java.io.IOException} or + * a {@link java.security.GeneralSecurityException} + */ + public SSLContext build() { + Objects.requireNonNull(privateKeyConfig, "The private key config must be set!"); + + try { + return newSSLContext(privateKeyConfig, trustConfig, sessionCacheSize, sessionTimeout); + } catch (IOException | GeneralSecurityException e) { + throw new IllegalStateException("Building of the SSLContext of unsuccessful!", e); + } + } + + private static SSLContext newSSLContext(KeyConfig privateKeyConfig, + KeyConfig trustConfig, + long sessionCacheSize, + long sessionTimeout) + throws IOException, GeneralSecurityException { + KeyManagerFactory kmf = buildKmf(privateKeyConfig); + TrustManagerFactory tmf = buildTmf(trustConfig); + + // Initialize the SSLContext to work with our key managers. + SSLContext ctx = SSLContext.getInstance(PROTOCOL); + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + SSLSessionContext sessCtx = ctx.getServerSessionContext(); + if (sessionCacheSize > 0) { + sessCtx.setSessionCacheSize((int) Math.min(sessionCacheSize, Integer.MAX_VALUE)); + } + if (sessionTimeout > 0) { + sessCtx.setSessionTimeout((int) Math.min(sessionTimeout, Integer.MAX_VALUE)); + } + return ctx; + } + + private static KeyManagerFactory buildKmf(KeyConfig privateKeyConfig) throws IOException, GeneralSecurityException { + String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); + if (algorithm == null) { + algorithm = "SunX509"; + } + + byte[] passwordBytes = new byte[64]; + RANDOM.nextBytes(passwordBytes); + char[] password = Base64.getEncoder().encodeToString(passwordBytes).toCharArray(); + + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + ks.setKeyEntry("key", + privateKeyConfig.privateKey().orElseThrow(() -> new RuntimeException("Private key not available")), + password, + privateKeyConfig.certChain().toArray(new Certificate[0])); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); + kmf.init(ks, password); + + return kmf; + } + + private static TrustManagerFactory buildTmf(KeyConfig trustConfig) + throws IOException, GeneralSecurityException { + List certs; + + if (trustConfig == null) { + certs = CollectionsHelper.listOf(); + } else { + certs = trustConfig.certs(); + } + + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + + int i = 1; + for (X509Certificate cert : certs) { + ks.setCertificateEntry(String.valueOf(i), cert); + i++; + } + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + return tmf; + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/ServerRequestAttribute.java b/grpc/server/src/main/java/io/helidon/grpc/server/ServerRequestAttribute.java new file mode 100644 index 00000000000..98079dcb0be --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/ServerRequestAttribute.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.grpc.server; + +/** + * An enum representing different types of gRPC request + * attribute that can be added to tracing logs. + */ +public enum ServerRequestAttribute { + /** + * Log the request headers. + */ + HEADERS, + + /** + * Log the method type. + */ + METHOD_TYPE, + + /** + * log the method name. + */ + METHOD_NAME, + + /** + * log the call attributes. + */ + CALL_ATTRIBUTES, + + /** + * log all attributes. + */ + ALL +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/ServiceDescriptor.java b/grpc/server/src/main/java/io/helidon/grpc/server/ServiceDescriptor.java new file mode 100644 index 00000000000..205ef818a1d --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/ServiceDescriptor.java @@ -0,0 +1,736 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import io.helidon.grpc.core.InterceptorPriorities; +import io.helidon.grpc.core.MarshallerSupplier; +import io.helidon.grpc.core.PriorityBag; + +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import io.grpc.BindableService; +import io.grpc.Context; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerMethodDefinition; +import io.grpc.ServerServiceDefinition; +import io.grpc.stub.ServerCalls; +import org.eclipse.microprofile.health.HealthCheck; + +import static io.helidon.grpc.core.GrpcHelper.extractMethodName; + +/** + * Encapsulates all metadata necessary to create and deploy a gRPC service. + */ +public class ServiceDescriptor { + /** + * The {@link io.grpc.Context.Key} to use to obtain the {@link io.grpc.ServiceDescriptor}. + */ + public static final Context.Key SERVICE_DESCRIPTOR_KEY = + Context.key("Helidon.ServiceDescriptor"); + + private final String name; + private final Map methods; + private final PriorityBag interceptors; + private final Map, Object> context; + private final HealthCheck healthCheck; + + private ServiceDescriptor(String name, + Map methods, + PriorityBag interceptors, + Map, Object> context, + HealthCheck healthCheck) { + this.name = Objects.requireNonNull(name); + this.methods = methods; + this.context = Collections.unmodifiableMap(context); + this.healthCheck = healthCheck; + this.interceptors = interceptors.copyMe(); + } + + /** + * Return service name. + * @return service name + */ + public String name() { + return name; + } + + /** + * Return {@link io.helidon.grpc.server.MethodDescriptor} for a specified method name. + * + * @param name method name + * @return method descriptor for the specified name + */ + public MethodDescriptor method(String name) { + return methods.get(name); + } + + /** + * Return service methods. + * @return service methods + */ + public Collection methods() { + return Collections.unmodifiableCollection(methods.values()); + } + + /** + * Return service interceptors. + * @return service interceptors + */ + public PriorityBag interceptors() { + return interceptors.readOnly(); + } + + /** + * Return context map. + * @return context map + */ + public Map, Object> context() { + return context; + } + + /** + * Return a {@link org.eclipse.microprofile.health.HealthCheck} for this service. + * @return a health check + */ + public HealthCheck healthCheck() { + return healthCheck; + } + + BindableService bindableService(PriorityBag interceptors) { + return new BindableServiceImpl(this, interceptors); + } + + @Override + public String toString() { + return "ServiceDescriptor(name='" + name + '\'' + ')'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServiceDescriptor that = (ServiceDescriptor) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + /** + * Create a {@link Builder}. + * @param serviceClass the {@link Class} representing the service + * @param name the name of the service + * @return a {@link Builder} + */ + public static Builder builder(Class serviceClass, String name) { + return new Builder(serviceClass, name); + } + + /** + * Create a {@link Builder}. + * @param service the {@link GrpcService} to use to initialise the builder + * @return a {@link Builder} + */ + public static Builder builder(GrpcService service) { + return new Builder(service); + } + + /** + * Create a {@link Builder}. + * @param service the {@link BindableService} to use to initialise the builder + * @return a {@link Builder} + */ + public static Builder builder(BindableService service) { + return new Builder(service); + } + + // ---- inner interface: Config ----------------------------------------- + + /** + * Fluent configuration interface for the {@link ServiceDescriptor}. + */ + public interface Rules { + /** + * Set the name for the service. + * + * @param name the service name + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + * @throws java.lang.NullPointerException if the name is null + * @throws java.lang.IllegalArgumentException if the name is a blank String + */ + Rules name(String name); + + /** + * Obtain the name fo the service this configuration configures. + * @return the name fo the service this configuration configures + */ + String name(); + + /** + * Register the proto for the service. + * + * @param proto the service proto + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules proto(Descriptors.FileDescriptor proto); + + /** + * Register the {@link MarshallerSupplier} for the service. + * + * @param marshallerSupplier the {@link MarshallerSupplier} for the service + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); + + /** + * Add one or more {@link ServerInterceptor} instances that will intercept calls + * to this service. + *

+ * If the added interceptors are annotated with the {@link javax.annotation.Priority} + * annotation then that value will be used to assign a priority to use when applying + * the interceptor otherwise a priority of {@link InterceptorPriorities#USER} will + * be used. + * + * @param interceptors one or more {@link ServerInterceptor}s to add + * @return this builder to allow fluent method chaining + */ + Rules intercept(ServerInterceptor... interceptors); + + /** + * Add one or more {@link ServerInterceptor} instances that will intercept calls + * to this service. + *

+ * The added interceptors will be applied using the specified priority. + * + * @param priority the priority to assign to the interceptors + * @param interceptors one or more {@link ServerInterceptor}s to add + * @return this builder to allow fluent method chaining + */ + Rules intercept(int priority, ServerInterceptor... interceptors); + + /** + * Register one or more {@link io.grpc.ServerInterceptor interceptors} for a named method of the service. + *

+ * If the added interceptors are annotated with the {@link javax.annotation.Priority} + * annotation then that value will be used to assign a priority to use when applying + * the interceptor otherwise a priority of {@link InterceptorPriorities#USER} will + * be used. + * + * @param methodName the name of the method to intercept + * @param interceptors the interceptor(s) to register + * + * @return this {@link Rules} instance for fluent call chaining + * + * @throws IllegalArgumentException if no method exists for the specified name + */ + Rules intercept(String methodName, ServerInterceptor... interceptors); + + /** + * Register one or more {@link io.grpc.ServerInterceptor interceptors} for a named method of the service. + *

+ * The added interceptors will be applied using the specified priority. + * + * @param methodName the name of the method to intercept + * @param priority the priority to assign to the interceptors + * @param interceptors the interceptor(s) to register + * + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + * + * @throws IllegalArgumentException if no method exists for the specified name + */ + Rules intercept(String methodName, int priority, ServerInterceptor... interceptors); + + /** + * Add value to the {@link io.grpc.Context} for the service. + * + * @param key the key for the context value + * @param value the value to add + * @param the type of the value + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules addContextValue(Context.Key key, V value); + + /** + * Register unary method for the service. + * + * @param name the name of the method + * @param method the unary method to register + * @param the method request type + * @param the method response type + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules unary(String name, ServerCalls.UnaryMethod method); + + /** + * Register unary method for the service. + * + * @param name the name of the method + * @param method the unary method to register + * @param configurer the method configurer + * @param the method request type + * @param the method response type + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules unary(String name, + ServerCalls.UnaryMethod method, + MethodDescriptor.Configurer configurer); + + /** + * Register server streaming method for the service. + * + * @param name the name of the method + * @param method the server streaming method to register + * @param the method request type + * @param the method response type + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules serverStreaming(String name, ServerCalls.ServerStreamingMethod method); + + /** + * Register server streaming method for the service. + * + * @param name the name of the method + * @param method the server streaming method to register + * @param configurer the method configurer + * @param the method request type + * @param the method response type + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules serverStreaming(String name, + ServerCalls.ServerStreamingMethod method, + MethodDescriptor.Configurer configurer); + + /** + * Register client streaming method for the service. + * + * @param name the name of the method + * @param method the client streaming method to register + * @param the method request type + * @param the method response type + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules clientStreaming(String name, ServerCalls.ClientStreamingMethod method); + + /** + * Register client streaming method for the service. + * + * @param name the name of the method + * @param method the client streaming method to register + * @param configurer the method configurer + * @param the method request type + * @param the method response type + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules clientStreaming(String name, + ServerCalls.ClientStreamingMethod method, + MethodDescriptor.Configurer configurer); + + /** + * Register bi-directional streaming method for the service. + * + * @param name the name of the method + * @param method the bi-directional streaming method to register + * @param the method request type + * @param the method response type + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules bidirectional(String name, ServerCalls.BidiStreamingMethod method); + + /** + * Register bi-directional streaming method for the service. + * + * @param name the name of the method + * @param method the bi-directional streaming method to register + * @param configurer the method configurer + * @param the method request type + * @param the method response type + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules bidirectional(String name, + ServerCalls.BidiStreamingMethod method, + MethodDescriptor.Configurer configurer); + + /** + * Register the service {@link HealthCheck}. + * + * @param healthCheck the service {@link HealthCheck} + * @return this {@link io.helidon.grpc.server.ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules healthCheck(HealthCheck healthCheck); + } + + // ---- inner class: Configurer ----------------------------------------- + + /** + * An interface implemented by classs that can configure + * a {@link ServiceDescriptor.Rules}. + */ + @FunctionalInterface + public interface Configurer { + /** + * Apply extra configuration to a {@link ServiceDescriptor.Rules}. + * + * @param rules the {@link ServiceDescriptor.Rules} to configure + */ + void configure(Rules rules); + } + + // ---- inner class: Aware ---------------------------------------------- + + /** + * Allows users to specify that they would like to have access to a + * {@link io.helidon.grpc.server.ServiceDescriptor} within their {@link io.grpc.ServerInterceptor} + * implementation. + */ + public interface Aware { + /** + * Set service descriptor. + * @param descriptor service descriptor instance + */ + void setServiceDescriptor(ServiceDescriptor descriptor); + } + + // ---- inner class: Builder -------------------------------------------- + + /** + * A {@link ServiceDescriptor} builder. + */ + public static final class Builder implements Rules, io.helidon.common.Builder { + private final Class serviceClass; + + private String name; + private Descriptors.FileDescriptor proto; + private MarshallerSupplier marshallerSupplier = MarshallerSupplier.defaultInstance(); + private Map methodBuilders = new LinkedHashMap<>(); + private PriorityBag interceptors = new PriorityBag<>(InterceptorPriorities.USER); + private Map, Object> context = new HashMap<>(); + private HealthCheck healthCheck; + + Builder(Class serviceClass, String name) { + this.name = name == null || name.trim().isEmpty() ? serviceClass.getSimpleName() : name.trim(); + this.serviceClass = serviceClass; + this.healthCheck = ConstantHealthCheck.up(name); + } + + Builder(GrpcService service) { + this.name = service.name(); + this.serviceClass = service.getClass(); + this.healthCheck = ConstantHealthCheck.up(name); + + service.update(this); + } + + @SuppressWarnings("unchecked") + Builder(BindableService service) { + ServerServiceDefinition def = service.bindService(); + + this.name = def.getServiceDescriptor().getName(); + this.serviceClass = service.getClass(); + this.healthCheck = ConstantHealthCheck.up(name); + + for (ServerMethodDefinition smd : def.getMethods()) { + io.grpc.MethodDescriptor md = smd.getMethodDescriptor(); + ServerCallHandler handler = smd.getServerCallHandler(); + String methodName = extractMethodName(md.getFullMethodName()); + MethodDescriptor.Builder descriptor = MethodDescriptor.builder(this.name, methodName, md.toBuilder(), handler) + .marshallerSupplier(marshallerSupplier); + + methodBuilders.put(methodName, descriptor); + } + } + + @Override + public String name() { + return name; + } + + @Override + public Builder name(String name) { + if (name == null) { + throw new NullPointerException("name cannot be null"); + } + + if (name.trim().isEmpty()) { + throw new IllegalArgumentException("name cannot be blank"); + } + + this.name = name.trim(); + for (Map.Entry entry : methodBuilders.entrySet()) { + entry.getValue().fullname(name + "/" + entry.getKey()); + } + return this; + } + + @Override + public Builder proto(Descriptors.FileDescriptor proto) { + this.proto = proto; + return this; + } + + @Override + public Builder marshallerSupplier(MarshallerSupplier marshallerSupplier) { + this.marshallerSupplier = marshallerSupplier; + return this; + } + + @Override + public Builder unary(String name, ServerCalls.UnaryMethod method) { + return unary(name, method, null); + } + + @Override + public Builder unary(String name, + ServerCalls.UnaryMethod method, + MethodDescriptor.Configurer configurer) { + methodBuilders.put(name, createMethodDescriptor(name, + io.grpc.MethodDescriptor.MethodType.UNARY, + ServerCalls.asyncUnaryCall(method), + configurer)); + return this; + } + + @Override + public Builder serverStreaming(String name, ServerCalls.ServerStreamingMethod method) { + return serverStreaming(name, method, null); + } + + @Override + public Builder serverStreaming(String name, + ServerCalls.ServerStreamingMethod method, + MethodDescriptor.Configurer configurer) { + + methodBuilders.put(name, createMethodDescriptor(name, + io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING, + ServerCalls.asyncServerStreamingCall(method), + configurer)); + return this; + } + + @Override + public Builder clientStreaming(String name, ServerCalls.ClientStreamingMethod method) { + return clientStreaming(name, method, null); + } + + @Override + public Builder clientStreaming(String name, + ServerCalls.ClientStreamingMethod method, + MethodDescriptor.Configurer configurer) { + + methodBuilders.put(name, createMethodDescriptor(name, + io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING, + ServerCalls.asyncClientStreamingCall(method), + configurer)); + return this; + } + + @Override + public Builder bidirectional(String name, ServerCalls.BidiStreamingMethod method) { + return bidirectional(name, method, null); + } + + @Override + public Builder bidirectional(String name, + ServerCalls.BidiStreamingMethod method, + MethodDescriptor.Configurer configurer) { + + methodBuilders.put(name, createMethodDescriptor(name, + io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING, + ServerCalls.asyncBidiStreamingCall(method), + configurer)); + return this; + } + + @Override + public Builder intercept(ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + @Override + public Builder intercept(int priority, ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), priority); + return this; + } + + @Override + public Builder intercept(String methodName, ServerInterceptor... interceptors) { + MethodDescriptor.Builder method = methodBuilders.get(methodName); + + if (method == null) { + throw new IllegalArgumentException("No method exists with name '" + methodName + "'"); + } + + method.intercept(interceptors); + + return this; + } + + @Override + public Builder intercept(String methodName, int priority, ServerInterceptor... interceptors) { + MethodDescriptor.Builder method = methodBuilders.get(methodName); + + if (method == null) { + throw new IllegalArgumentException("No method exists with name '" + methodName + "'"); + } + + method.intercept(priority, interceptors); + + return this; + } + + @Override + public Builder addContextValue(Context.Key key, V value) { + context.put(key, value); + return this; + } + + @Override + public Builder healthCheck(HealthCheck healthCheck) { + this.healthCheck = healthCheck; + return this; + } + + @Override + public ServiceDescriptor build() { + Map methods = new LinkedHashMap<>(); + + for (Map.Entry entry : methodBuilders.entrySet()) { + methods.put(entry.getKey(), entry.getValue().build()); + } + + return new ServiceDescriptor(name, methods, interceptors, context, healthCheck); + } + + @Override + public String toString() { + return "ServiceDescriptor.Builder(name='" + name + '\'' + ')'; + } + + // ---- helpers ----------------------------------------------------- + + private MethodDescriptor.Builder createMethodDescriptor( + String methodName, + io.grpc.MethodDescriptor.MethodType methodType, + ServerCallHandler callHandler, + MethodDescriptor.Configurer configurer) { + + io.grpc.MethodDescriptor.Builder grpcDesc = io.grpc.MethodDescriptor.newBuilder() + .setFullMethodName(io.grpc.MethodDescriptor.generateFullMethodName(this.name, methodName)) + .setType(methodType) + .setSampledToLocalTracing(true); + + Class requestType = getTypeFromMethodDescriptor(methodName, true); + Class responseType = getTypeFromMethodDescriptor(methodName, false); + + MethodDescriptor.Builder builder = MethodDescriptor.builder(this.name, methodName, grpcDesc, callHandler) + .defaultMarshallerSupplier(marshallerSupplier) + .requestType(requestType) + .responseType(responseType); + + if (configurer != null) { + configurer.configure(builder); + } + + return builder; + } + + @SuppressWarnings("unchecked") + private Class getTypeFromMethodDescriptor(String methodName, boolean fInput) { + // if the proto is not present, assume that we are not using + // protobuf for marshalling and that whichever marshaller is used + // doesn't need type information (basically, that the serialized + // stream is self-describing) + if (proto == null) { + return (Class) Object.class; + } + + // todo: add error handling here, and fail fast with a more + // todo: meaningful exception (and message) than a NPE + // todo: if the service or the method cannot be found + Descriptors.ServiceDescriptor svc = proto.findServiceByName(name); + Descriptors.MethodDescriptor mtd = svc.findMethodByName(methodName); + Descriptors.Descriptor type = fInput ? mtd.getInputType() : mtd.getOutputType(); + + String pkg = getPackageName(); + String outerClass = getOuterClassName(); + + // make sure that any nested protobuf class names are converted + // into a proper Java binary class name + String className = pkg + "." + outerClass + type.getFullName().replace('.', '$'); + + // the assumption here is that the protobuf generated classes can always + // be loaded by the same class loader that loaded the service class, + // as the service implementation is bound to depend on them + try { + return (Class) serviceClass.getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private String getPackageName() { + String pkg = proto.getOptions().getJavaPackage(); + return "".equals(pkg) ? proto.getPackage() : pkg; + } + + private String getOuterClassName() { + DescriptorProtos.FileOptions options = proto.getOptions(); + if (options.getJavaMultipleFiles()) { + // there is no outer class -- each message will have its own top-level class + return ""; + } + + String outerClass = options.getJavaOuterClassname(); + if ("".equals(outerClass)) { + outerClass = getOuterClassFromFileName(proto.getName()); + } + + // append $ in order to timed a proper binary name for the nested message class + return outerClass + "$"; + } + + private String getOuterClassFromFileName(String name) { + // strip .proto extension + name = name.substring(0, name.lastIndexOf(".proto")); + + String[] words = name.split("_"); + StringBuilder sb = new StringBuilder(name.length()); + + for (String word : words) { + sb.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1)); + } + + return sb.toString(); + } + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/SslConfiguration.java b/grpc/server/src/main/java/io/helidon/grpc/server/SslConfiguration.java new file mode 100644 index 00000000000..2dd7cd38a6d --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/SslConfiguration.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import io.helidon.config.Config; + +/** + * SSL configuration details. + */ +public class SslConfiguration { + private boolean jdkSSL; + private String tlsCert; + private String tlsKey; + private String tlsCaCert; + + /** + * Create a new instance. + * + * @param jdkSSL flag specifying whether to use JDK SSL implementation + * @param tlsCert the TLS certificate file + * @param tlsKey the TLS key file + * @param tlsCaCert the TLS CA file + */ + private SslConfiguration(boolean jdkSSL, String tlsCert, String tlsKey, String tlsCaCert) { + this.jdkSSL = jdkSSL; + this.tlsCert = tlsCert; + this.tlsKey = tlsKey; + this.tlsCaCert = tlsCaCert; + } + + /** + * Return true if JDK SSL implementation should be used. + * + * @return {@code true} if JDK SSL implementation should be used; + * {@code false} otherwise + */ + public boolean isJdkSSL() { + return jdkSSL; + } + + /** + * Return TLS certs file. + * + * @return the TLS certs file + */ + public String getTLSCerts() { + return tlsCert; + } + + /** + * Return the TLS key file. + * + * @return the location of the TLS key file to use + */ + public String getTLSKey() { + return tlsKey; + } + + /** + * Return the TLS CA certs file. + * + * @return the TLS CA certs file + */ + public String getTLSClientCerts() { + return tlsCaCert; + } + + /** + * Return an instance of builder. + * + * @return an instance of builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Return an instance of builder based on the specified external config. + * + * @param config external config + * @return an instance of builder + */ + public static Builder builder(Config config) { + return new Builder(config); + } + + /** + * Create an instance of sslConfig from external configuration source. + * + * @param config external config + * @return an instance of sslconfig + */ + public static SslConfiguration create(Config config) { + return builder(config).build(); + } + + /** + * Builds the configuration for ssl. + */ + static class Builder implements io.helidon.common.Builder { + private boolean jdkSSL; + private String tlsCert = null; + private String tlsKey = null; + private String tlsCaCert = null; + + private Builder() { + } + + private Builder(Config config) { + if (config == null) { + return; + } + + Path path = Paths.get(config.get("path").asString().orElse("")); + + String tlsCert = config.get("tlsCert").asString().orElse(null); + if (tlsCert != null) { + this.tlsCert = path.resolve(tlsCert).toAbsolutePath().toString(); + } + + String tlsKey = config.get("tlsKey").asString().orElse(null); + if (tlsKey != null) { + this.tlsKey = path.resolve(tlsKey).toAbsolutePath().toString(); + } + + String tlsCaCert = config.get("tlsCaCert").asString().orElse(null); + if (tlsCaCert != null) { + this.tlsCaCert = path.resolve(tlsCaCert).toAbsolutePath().toString(); + } + + this.jdkSSL = config.get("jdkSSL").asBoolean().orElse(false); + } + + /** + * Sets the type of SSL implementation to be used. + */ + public Builder jdkSSL(boolean jdkSSL) { + this.jdkSSL = jdkSSL; + return this; + } + + /** + * Sets the tls certificate file. + */ + public Builder tlsCert(String tlsCert) { + this.tlsCert = tlsCert; + return this; + } + + /** + * Sets the tls key file. + */ + public Builder tlsKey(String tlsKey) { + this.tlsKey = tlsKey; + return this; + } + + /** + * Sets the tls CA file. + */ + public Builder tlsCaCert(String tlsCaCert) { + this.tlsCaCert = tlsCaCert; + return this; + } + + @Override + public SslConfiguration build() { + return new SslConfiguration(jdkSSL, tlsCert, tlsKey, tlsCaCert); + } + } +} + + diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/TracingConfiguration.java b/grpc/server/src/main/java/io/helidon/grpc/server/TracingConfiguration.java new file mode 100644 index 00000000000..69e0b32cede --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/TracingConfiguration.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import io.opentracing.contrib.grpc.OperationNameConstructor; + +/** + * The configuration for gRPC tracing. + */ +public class TracingConfiguration { + /** + * A flag indicating whether to log streaming. + */ + private OperationNameConstructor operationNameConstructor; + + /** + * A flag indicating verbose logging. + */ + private boolean streaming; + + /** + * A flag indicating verbose logging. + */ + private boolean verbose; + + /** + * The set of attributes to log in spans. + */ + private Set tracedAttributes; + + /** + * Private constructor called by the {@link Builder}. + * + * @param operationNameConstructor the operation name constructor + * @param streaming flag indicating whether to log streaming + * @param verbose flag indicating verbose logging + * @param tracedAttributes the set of attributes to log in spans + */ + private TracingConfiguration(OperationNameConstructor operationNameConstructor, + Set tracedAttributes, + boolean streaming, + boolean verbose) { + this.operationNameConstructor = operationNameConstructor; + this.tracedAttributes = tracedAttributes; + this.streaming = streaming; + this.verbose = verbose; + } + + /** + * @return the configured verbose. + */ + public boolean isVerbose() { + return verbose; + } + + /** + * @return the configured streaming. + */ + public boolean isStreaming() { + return streaming; + } + + /** + * @return the set of configured tracedAttributes. + */ + public Set tracedAttributes() { + return tracedAttributes; + } + + /** + * @return the configured operationNameConstructor. + */ + public OperationNameConstructor operationNameConstructor() { + return operationNameConstructor; + } + + /** + * Builds the configuration of a tracer. + */ + public static class Builder { + /** + * Creates a Builder with default configuration. + */ + public Builder() { + operationNameConstructor = OperationNameConstructor.DEFAULT; + streaming = false; + verbose = false; + tracedAttributes = Collections.emptySet(); + } + + /** + * @param operationNameConstructor for all spans + * @return this Builder with configured operation name + */ + public Builder withOperationName(OperationNameConstructor operationNameConstructor) { + this.operationNameConstructor = operationNameConstructor; + return this; + } + + /** + * @param attributes to set as tags on server spans + * @return this Builder configured to trace request + * attributes + */ + public Builder withTracedAttributes(ServerRequestAttribute... attributes) { + tracedAttributes = new HashSet<>(Arrays.asList(attributes)); + return this; + } + + /** + * Logs streaming events to server spans. + * + * @return this Builder configured to log streaming events + */ + public Builder withStreaming() { + streaming = true; + return this; + } + + /** + * Logs all request life-cycle events to server spans. + * + * @return this Builder configured to be verbose + */ + public Builder withVerbosity() { + verbose = true; + return this; + } + + /** + * @return a TracingConfiguration with this Builder's configuration + */ + public TracingConfiguration build() { + return new TracingConfiguration(operationNameConstructor, tracedAttributes, streaming, verbose); + } + + /** + * A flag indicating whether to log streaming. + */ + private OperationNameConstructor operationNameConstructor; + + /** + * A flag indicating verbose logging. + */ + private boolean streaming; + + /** + * A flag indicating verbose logging. + */ + private boolean verbose; + + /** + * The set of attributes to log in spans. + */ + private Set tracedAttributes; + } +} diff --git a/grpc/server/src/main/java/io/helidon/grpc/server/package-info.java b/grpc/server/src/main/java/io/helidon/grpc/server/package-info.java new file mode 100644 index 00000000000..eb6744769bf --- /dev/null +++ b/grpc/server/src/main/java/io/helidon/grpc/server/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 gRPC server API. + */ +package io.helidon.grpc.server; diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/BindableServiceImplTest.java b/grpc/server/src/test/java/io/helidon/grpc/server/BindableServiceImplTest.java new file mode 100644 index 00000000000..3b5a9c476ea --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/BindableServiceImplTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import io.helidon.common.CollectionsHelper; +import io.helidon.grpc.core.InterceptorPriorities; +import io.helidon.grpc.core.PriorityBag; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerMethodDefinition; +import io.grpc.ServerServiceDefinition; +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class BindableServiceImplTest { + + /** + * Assert that even though a specific interceptor may be added + * more than once at different levels (global, service, method) + * that it only actually gets added once. + */ + @Test + public void shouldNotAddDuplicateInterceptors() { + ServerInterceptor interceptorOne = spy(new InterceptorStub()); + ServerInterceptor interceptorTwo = spy(new InterceptorStub()); + ServerInterceptor interceptorThree = spy(new InterceptorStub()); + ServerInterceptor interceptorFour = spy(new InterceptorStub()); + ServerInterceptor interceptorFive = spy(new InterceptorStub()); + ServerInterceptor interceptorSix = spy(new InterceptorStub()); + + PriorityBag global = new PriorityBag<>(InterceptorPriorities.USER); + global.addAll(CollectionsHelper.listOf(interceptorOne, interceptorTwo, interceptorThree)); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(new Service()) + .intercept(interceptorTwo) + .intercept(interceptorFour) + .intercept(interceptorFive) + .unary("foo", this::unary, rules -> rules.intercept(interceptorThree, interceptorFour, interceptorSix)) + .build(); + + BindableServiceImpl bindableService = new BindableServiceImpl(descriptor, global); + ServerServiceDefinition definition = bindableService.bindService(); + ServerMethodDefinition method = definition.getMethod("Service/foo"); + ServerCallHandler callHandler = method.getServerCallHandler(); + Metadata headers = new Metadata(); + + ServerCall call = mock(ServerCall.class); + when(call.getMethodDescriptor()).thenReturn(method.getMethodDescriptor()); + + callHandler.startCall(call, headers); + + verify(interceptorOne, times(1)).interceptCall(same(call), same(headers), any(ServerCallHandler.class)); + verify(interceptorTwo, times(1)).interceptCall(same(call), same(headers), any(ServerCallHandler.class)); + verify(interceptorThree, times(1)).interceptCall(same(call), same(headers), any(ServerCallHandler.class)); + verify(interceptorFour, times(1)).interceptCall(same(call), same(headers), any(ServerCallHandler.class)); + verify(interceptorFive, times(1)).interceptCall(same(call), same(headers), any(ServerCallHandler.class)); + verify(interceptorSix, times(1)).interceptCall(same(call), same(headers), any(ServerCallHandler.class)); + } + + + public void unary(String request, StreamObserver response) { + } + + public static class Service + implements GrpcService { + @Override + public void update(ServiceDescriptor.Rules rules) { + } + } + + public static class InterceptorStub + implements ServerInterceptor { + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { + return next == null ? null : next.startCall(call, headers); + } + } +} diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/ConstantHealthCheckTest.java b/grpc/server/src/test/java/io/helidon/grpc/server/ConstantHealthCheckTest.java new file mode 100644 index 00000000000..f3c27857893 --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/ConstantHealthCheckTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + + +public class ConstantHealthCheckTest { + @Test + public void shouldBeUp() { + HealthCheck check = ConstantHealthCheck.up("foo"); + HealthCheckResponse response = check.call(); + + assertThat(response.getName(), is("foo")); + assertThat(response.getState(), is(HealthCheckResponse.State.UP)); + assertThat(response.getData(), is(notNullValue())); + } + + @Test + public void shouldBeDown() { + HealthCheck check = ConstantHealthCheck.down("foo"); + HealthCheckResponse response = check.call(); + + assertThat(response.getName(), is("foo")); + assertThat(response.getState(), is(HealthCheckResponse.State.DOWN)); + assertThat(response.getData(), is(notNullValue())); + } +} diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/ContextSettingServerInterceptorTest.java b/grpc/server/src/test/java/io/helidon/grpc/server/ContextSettingServerInterceptorTest.java new file mode 100644 index 00000000000..2adbb92585c --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/ContextSettingServerInterceptorTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import io.grpc.Context; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * {@link ContextSettingServerInterceptor} unit tests. + */ +@SuppressWarnings("unchecked") +public class ContextSettingServerInterceptorTest { + + @Test + public void shouldAddServiceDescriptor() { + ServiceDescriptor serviceDescriptor = ServiceDescriptor.builder(createMockService()) + .unary("test", this::dummyUnary) + .build(); + + ContextSettingServerInterceptor interceptor = new ContextSettingServerInterceptor(); + + Metadata headers = new Metadata(); + ServerCall call = mock(ServerCall.class); + ServerCallHandler next = mock(ServerCallHandler.class); + ListenerStub listener = new ListenerStub(); + + when(call.getMethodDescriptor()).thenReturn(serviceDescriptor.method("test").descriptor()); + when(next.startCall(any(ServerCall.class), any(Metadata.class))).thenReturn(listener); + + interceptor.setServiceDescriptor(serviceDescriptor); + ServerCall.Listener result = interceptor.interceptCall(call, headers, next); + + result.onMessage("testing..."); + + Context contextCall = listener.getContext(); + ServiceDescriptor descriptor = ServiceDescriptor.SERVICE_DESCRIPTOR_KEY.get(contextCall); + + assertThat(descriptor, is(sameInstance(serviceDescriptor))); + } + + @Test + public void shouldAddServiceContext() { + Context.Key key = Context.key("test-service-key"); + ServiceDescriptor serviceDescriptor = ServiceDescriptor.builder(createMockService()) + .addContextValue(key, "test-service-value") + .unary("test", this::dummyUnary) + .build(); + + ContextSettingServerInterceptor interceptor = new ContextSettingServerInterceptor(); + + Metadata headers = new Metadata(); + ServerCall call = mock(ServerCall.class); + ServerCallHandler next = mock(ServerCallHandler.class); + ListenerStub listener = new ListenerStub(); + + when(call.getMethodDescriptor()).thenReturn(serviceDescriptor.method("test").descriptor()); + when(next.startCall(any(ServerCall.class), any(Metadata.class))).thenReturn(listener); + + interceptor.setServiceDescriptor(serviceDescriptor); + ServerCall.Listener result = interceptor.interceptCall(call, headers, next); + + Context currentContext = Context.current(); + + result.onMessage("testing..."); + + Context contextCall = listener.getContext(); + + assertThat(contextCall, is(not(sameInstance(currentContext)))); + + assertThat(key.get(contextCall), is("test-service-value")); + } + + @Test + public void shouldAddServiceAndMethodContext() { + Context.Key key1 = Context.key("test-service-key"); + Context.Key key2 = Context.key("test-service-key"); + ServiceDescriptor serviceDescriptor = ServiceDescriptor.builder(createMockService()) + .addContextValue(key1, "test-service-value") + .unary("test", this::dummyUnary, cfg -> cfg.addContextValue(key2, "test-method-value")) + .build(); + + ContextSettingServerInterceptor interceptor = new ContextSettingServerInterceptor(); + + Metadata headers = new Metadata(); + ServerCall call = mock(ServerCall.class); + ServerCallHandler next = mock(ServerCallHandler.class); + ListenerStub listener = new ListenerStub(); + + when(call.getMethodDescriptor()).thenReturn(serviceDescriptor.method("test").descriptor()); + when(next.startCall(any(ServerCall.class), any(Metadata.class))).thenReturn(listener); + + interceptor.setServiceDescriptor(serviceDescriptor); + ServerCall.Listener result = interceptor.interceptCall(call, headers, next); + + Context currentContext = Context.current(); + + result.onMessage("testing..."); + + Context contextCall = listener.getContext(); + + assertThat(contextCall, is(not(sameInstance(currentContext)))); + + assertThat(key1.get(contextCall), is("test-service-value")); + assertThat(key2.get(contextCall), is("test-method-value")); + } + + @Test + public void shouldAddOverrideServiceContextKeyWithMethodContextKey() { + Context.Key key = Context.key("test-service-key"); + ServiceDescriptor serviceDescriptor = ServiceDescriptor.builder(createMockService()) + .addContextValue(key, "test-service-value") + .unary("test", this::dummyUnary, cfg -> cfg.addContextValue(key, "test-method-value")) + .build(); + + ContextSettingServerInterceptor interceptor = new ContextSettingServerInterceptor(); + + Metadata headers = new Metadata(); + ServerCall call = mock(ServerCall.class); + ServerCallHandler next = mock(ServerCallHandler.class); + ListenerStub listener = new ListenerStub(); + + when(call.getMethodDescriptor()).thenReturn(serviceDescriptor.method("test").descriptor()); + when(next.startCall(any(ServerCall.class), any(Metadata.class))).thenReturn(listener); + + interceptor.setServiceDescriptor(serviceDescriptor); + ServerCall.Listener result = interceptor.interceptCall(call, headers, next); + + Context currentContext = Context.current(); + + result.onMessage("testing..."); + + Context contextCall = listener.getContext(); + + assertThat(contextCall, is(not(sameInstance(currentContext)))); + + assertThat(key.get(contextCall), is("test-method-value")); + } + + + private GrpcService createMockService() { + GrpcService service = mock(GrpcService.class); + + when(service.name()).thenReturn("foo"); + + return service; + } + + private void dummyUnary(String request, StreamObserver observer) { + } + + private class ListenerStub + extends ServerCall.Listener { + + private Context context; + + @Override + public void onMessage(String message) { + context = Context.current(); + } + + public Context getContext() { + return context; + } + } +} diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/GrpcServerConfigurationTest.java b/grpc/server/src/test/java/io/helidon/grpc/server/GrpcServerConfigurationTest.java new file mode 100644 index 00000000000..4e1707f0cce --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/GrpcServerConfigurationTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.function.Supplier; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * {@link GrpcServerConfiguration} unit tests. + */ +public class GrpcServerConfigurationTest { + + @Test + public void shouldHaveDefault() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .build(); + + assertThat(configuration.name(), is(GrpcServerConfiguration.DEFAULT_NAME)); + } + + @Test + public void shouldSetName() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .name(" foo ") + .build(); + + assertThat(configuration.name(), is("foo")); + } + + @Test + public void shouldNotSetNullName() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .name(null) + .build(); + + assertThat(configuration.name(), is(GrpcServerConfiguration.DEFAULT_NAME)); + } + + @Test + public void shouldNotSetBlankName() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .name(" \t ") + .build(); + + assertThat(configuration.name(), is(GrpcServerConfiguration.DEFAULT_NAME)); + } + + @Test + public void shouldHaveDefaultPort() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .build(); + + assertThat(configuration.port(), is(GrpcServerConfiguration.DEFAULT_PORT)); + } + + @Test + public void shouldSetPort() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .port(19) + .build(); + + assertThat(configuration.port(), is(19)); + } + + @Test + public void shouldSetNegativePortToZero() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .port(-1) + .build(); + + assertThat(configuration.port(), is(0)); + } + + @Test + public void shouldHaveDefaultTracer() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .build(); + + assertThat(configuration.tracer(), is(sameInstance(GlobalTracer.get()))); + } + + @Test + public void shouldSetTracer() { + Tracer tracer = mock(Tracer.class); + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .tracer(tracer) + .build(); + + assertThat(configuration.tracer(), is(sameInstance(tracer))); + } + + @Test + public void shouldNotSetNullTracer() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .tracer((Tracer) null) + .build(); + + assertThat(configuration.tracer(), is(sameInstance(GlobalTracer.get()))); + } + + @Test + public void shouldSetTracerSupplier() { + Tracer tracer = mock(Tracer.class); + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .tracer(() -> tracer) + .build(); + + assertThat(configuration.tracer(), is(sameInstance(tracer))); + } + + @Test + public void shouldNotSetNullTracerSupplier() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .tracer((Supplier) null) + .build(); + + assertThat(configuration.tracer(), is(sameInstance(GlobalTracer.get()))); + } + + @Test + public void shouldNotSetNullTracerFromSupplier() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .tracer(() -> null) + .build(); + + assertThat(configuration.tracer(), is(sameInstance(GlobalTracer.get()))); + } + + @Test + public void shouldHaveDefaultTracingConfig() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .build(); + + assertThat(configuration.tracingConfig(), is(notNullValue())); + } + + @Test + public void shouldSetTracingConfiguration() { + TracingConfiguration tracingConfig = mock(TracingConfiguration.class); + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .tracingConfig(tracingConfig) + .build(); + + assertThat(configuration.tracingConfig(), is(sameInstance(tracingConfig))); + } + + @Test + public void shouldNotSetNullTracingConfiguration() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .tracingConfig(null) + .build(); + + assertThat(configuration.tracingConfig(), is(notNullValue())); + } + + @Test + public void shouldHaveDefaultWorkerCount() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .build(); + + assertThat(configuration.workers(), is(GrpcServerConfiguration.DEFAULT_WORKER_COUNT)); + } + + @Test + public void shouldSetWorkerCount() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .workersCount(100) + .build(); + + assertThat(configuration.workers(), is(100)); + } + + @Test + public void shouldNotSetNegativeWorkerCount() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .workersCount(-1) + .build(); + + assertThat(configuration.workers(), is(GrpcServerConfiguration.DEFAULT_WORKER_COUNT)); + } + + @Test + public void shouldNotSetZeroWorkerCount() { + GrpcServerConfiguration configuration = GrpcServerConfiguration.builder() + .workersCount(0) + .build(); + + assertThat(configuration.workers(), is(GrpcServerConfiguration.DEFAULT_WORKER_COUNT)); + } + + @Test + public void shouldBuildFromConfig() { + Config config = Config.builder().sources(ConfigSources.classpath("config1.conf")).build(); + GrpcServerConfiguration serverConfig = config.get("grpcserver").as(GrpcServerConfiguration::create).get(); + + assertThat(serverConfig.name(), is("foo")); + assertThat(serverConfig.port(), is(19)); + assertThat(serverConfig.useNativeTransport(), is(true)); + assertThat(serverConfig.workers(), is(51)); + } +} diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/GrpcServiceTest.java b/grpc/server/src/test/java/io/helidon/grpc/server/GrpcServiceTest.java new file mode 100644 index 00000000000..709e041737e --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/GrpcServiceTest.java @@ -0,0 +1,520 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * {@link GrpcService} unit tests. + */ +public class GrpcServiceTest { + + private static final Executor EXECUTOR = Executors.newSingleThreadExecutor(); + + @Test + public void shouldHaveDefaultName() { + GrpcService service = new GrpcServiceStub(); + + assertThat(service.name(), is(GrpcServiceStub.class.getSimpleName())); + } + + @Test + public void shouldCompleteCall() { + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.complete(observer, "foo"); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertValueCount(1) + .assertValue("foo") + .assertComplete(); + } + + @Test + public void shouldCompleteCallUsingCompletionStage() { + CompletionStage stage = CompletableFuture.completedFuture("foo"); + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.complete(observer, stage); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertValueCount(1) + .assertValue("foo") + .assertComplete(); + } + + @Test + public void shouldCompleteCallUsingExceptionalCompletionStage() { + CompletableFuture future = new CompletableFuture<>(); + RuntimeException error = new RuntimeException("Oops!"); + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + future.completeExceptionally(error); + + service.complete(observer, future); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(error) + .assertValueCount(0) + .assertNotComplete(); + } + + @Test + public void shouldCompleteCallUsingAsyncCompletionStage() { + CompletionStage stage = CompletableFuture.completedFuture("foo"); + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.completeAsync(observer, stage); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertValueCount(1) + .assertValue("foo") + .assertComplete(); + } + + @Test + public void shouldCompleteCallUsingExceptionalAsyncCompletionStage() { + CompletableFuture future = new CompletableFuture<>(); + RuntimeException error = new RuntimeException("Oops!"); + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + future.completeExceptionally(error); + + service.completeAsync(observer, future); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(error) + .assertValueCount(0) + .assertNotComplete(); + } + + @Test + public void shouldCompleteCallUsingAsyncCompletionStageAndExecutor() { + CompletionStage stage = CompletableFuture.completedFuture("foo"); + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.completeAsync(observer, stage, EXECUTOR); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertValueCount(1) + .assertValue("foo") + .assertComplete(); + } + + @Test + public void shouldCompleteCallUsingExceptionalAsyncCompletionStageAndExecutor() { + CompletableFuture future = new CompletableFuture<>(); + RuntimeException error = new RuntimeException("Oops!"); + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + future.completeExceptionally(error); + + service.completeAsync(observer, future, EXECUTOR); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(error) + .assertValueCount(0) + .assertNotComplete(); + } + + @Test + public void shouldCompleteCallUsingCallable() { + Callable callable = () -> "foo"; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.complete(observer, callable); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertValueCount(1) + .assertValue("foo") + .assertComplete(); + } + + @Test + public void shouldCompleteCallUsingExceptionalCallable() { + RuntimeException error = new RuntimeException("Oops!"); + Callable callable = () -> { throw error; }; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.complete(observer, callable); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(error) + .assertValueCount(0) + .assertNotComplete(); + } + + @Test + public void shouldCompleteCallUsingAsyncCallable() { + Callable callable = () -> "foo"; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.completeAsync(observer, callable); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertValueCount(1) + .assertValue("foo") + .assertComplete(); + } + + @Test + public void shouldCompleteCallUsingExceptionalAsyncCallable() { + RuntimeException error = new RuntimeException("Oops!"); + Callable callable = () -> { throw error; }; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.completeAsync(observer, callable); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(e -> Objects.equals(e.getCause(), error)) + .assertValueCount(0) + .assertNotComplete(); + } + + @Test + public void shouldCompleteCallUsingAsyncCallableAndExecutor() { + Callable callable = () -> "foo"; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.completeAsync(observer, callable, EXECUTOR); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertValueCount(1) + .assertValue("foo") + .assertComplete(); + } + + @Test + public void shouldCompleteCallUsingExceptionalAsyncCallableAndExecutor() { + RuntimeException error = new RuntimeException("Oops!"); + Callable callable = () -> { throw error; }; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.completeAsync(observer, callable, EXECUTOR); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(e -> Objects.equals(e.getCause(), error)) + .assertValueCount(0) + .assertNotComplete(); + } + + @Test + public void shouldCompleteCallUsingRunnable() { + Runnable runnable = () -> {}; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.complete(observer, runnable, "foo"); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertValueCount(1) + .assertValue("foo") + .assertComplete(); + } + + @Test + public void shouldCompleteCallUsingExceptionalRunnable() { + RuntimeException error = new RuntimeException("Oops!"); + Runnable runnable = () -> { throw error; }; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.complete(observer, runnable, "foo"); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(error) + .assertValueCount(0) + .assertNotComplete(); + } + + @Test + public void shouldCompleteCallUsingAsyncRunnable() { + Runnable callable = () -> {}; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.completeAsync(observer, callable, "foo"); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertValueCount(1) + .assertValue("foo") + .assertComplete(); + } + + @Test + public void shouldCompleteCallUsingExceptionalAsyncRunnable() { + RuntimeException error = new RuntimeException("Oops!"); + Runnable runnable = () -> { throw error; }; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.completeAsync(observer, runnable, "foo"); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(e -> Objects.equals(e.getCause(), error)) + .assertValueCount(0) + .assertNotComplete(); + } + + @Test + public void shouldCompleteCallUsingAsyncRunnableAndExecutor() { + Runnable runnable = () -> {}; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.completeAsync(observer, runnable, "foo", EXECUTOR); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertValueCount(1) + .assertValue("foo") + .assertComplete(); + } + + @Test + public void shouldCompleteCallUsingExceptionalAsyncRunnableAndExecutor() { + RuntimeException error = new RuntimeException("Oops!"); + Runnable runnable = () -> { throw error; }; + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + service.completeAsync(observer, runnable, "foo", EXECUTOR); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(e -> Objects.equals(e.getCause(), error)) + .assertValueCount(0) + .assertNotComplete(); + } + + + @Test + public void shouldStream() { + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + List list = Arrays.asList("One", "Two", "Three"); + + service.stream(observer, list.stream()); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertComplete() + .assertValueCount(3) + .assertValues("One", "Two", "Three"); + } + + @Test + public void shouldStreamAsync() { + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + List list = Arrays.asList("One", "Two", "Three"); + + service.streamAsync(observer, list.stream(), EXECUTOR); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertComplete() + .assertValueCount(3) + .assertValues("One", "Two", "Three"); + } + + @Test + public void shouldStreamFromSupplier() { + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + List list = Arrays.asList("One", "Two", "Three"); + + service.stream(observer, list::stream); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertComplete() + .assertValueCount(3) + .assertValues("One", "Two", "Three"); + } + + @Test + public void shouldStreamAsyncFromSupplier() { + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + List list = Arrays.asList("One", "Two", "Three"); + + service.streamAsync(observer, list::stream, EXECUTOR); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertComplete() + .assertValueCount(3) + .assertValues("One", "Two", "Three"); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldStreamFromSupplierHandlingError() { + RuntimeException error = new RuntimeException("Oops!"); + Supplier> supplier = mock(Supplier.class); + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + + when(supplier.get()).thenThrow(error); + + service.stream(observer, supplier); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(e -> Objects.equals(e, error)) + .assertValueCount(0) + .assertNotComplete(); + } + + @Test + public void shouldStreamUsingCompletionStage() { + CompletableFuture future = new CompletableFuture<>(); + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + Consumer consumer = service.stream(observer, future); + + consumer.accept("One"); + consumer.accept("Two"); + consumer.accept("Three"); + future.complete(null); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertComplete() + .assertValueCount(3) + .assertValues("One", "Two", "Three"); + } + + @Test + public void shouldStreamAsyncUsingCompletionStage() { + CompletableFuture future = new CompletableFuture<>(); + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + Consumer consumer = service.streamAsync(observer, future); + + consumer.accept("One"); + consumer.accept("Two"); + consumer.accept("Three"); + + observer.awaitCount(3); + + future.complete(null); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertComplete() + .assertValueCount(3); + + assertThat(observer.values(), containsInAnyOrder("One", "Two", "Three")); + } + + @Test + public void shouldStreamAsyncWithExecutorUsingCompletionStage() { + CompletableFuture future = new CompletableFuture<>(); + TestStreamObserver observer = new TestStreamObserver<>(); + GrpcService service = new GrpcServiceStub(); + Consumer consumer = service.streamAsync(observer, future, EXECUTOR); + + consumer.accept("One"); + consumer.accept("Two"); + consumer.accept("Three"); + observer.awaitCount(3); + + future.complete(null); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertNoErrors() + .assertComplete() + .assertValueCount(3); + + assertThat(observer.values(), containsInAnyOrder("One", "Two", "Three")); + } + + + private class GrpcServiceStub + implements GrpcService { + @Override + public void update(ServiceDescriptor.Rules rules) { + } + } +} diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/HealthServiceImplTest.java b/grpc/server/src/test/java/io/helidon/grpc/server/HealthServiceImplTest.java new file mode 100644 index 00000000000..c2eb32d927a --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/HealthServiceImplTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.List; + +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import io.grpc.health.v1.HealthCheckRequest; +import io.grpc.health.v1.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheck; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + + +public class HealthServiceImplTest { + @Test + public void shouldRequestCheckForUpService() { + HealthServiceImpl healthService = new HealthServiceImpl(); + String serviceName = "foo"; + HealthCheck check = ConstantHealthCheck.up(serviceName); + HealthCheckRequest request = HealthCheckRequest.newBuilder().setService(serviceName).build(); + TestStreamObserver observer = new TestStreamObserver<>(); + + healthService.add(serviceName, check); + + healthService.check(request, observer); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertComplete() + .assertNoErrors() + .assertValueCount(1); + + List responses = observer.values(); + assertThat(responses.size(), is(1)); + HealthCheckResponse response = responses.get(0); + assertThat(response.getStatus(), is(HealthCheckResponse.ServingStatus.SERVING)); + } + + @Test + public void shouldRequestCheckForDownService() { + HealthServiceImpl healthService = new HealthServiceImpl(); + String serviceName = "foo"; + HealthCheck check = ConstantHealthCheck.down(serviceName); + HealthCheckRequest request = HealthCheckRequest.newBuilder().setService(serviceName).build(); + TestStreamObserver observer = new TestStreamObserver<>(); + + healthService.add(serviceName, check); + + healthService.check(request, observer); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertComplete() + .assertNoErrors() + .assertValueCount(1); + + List responses = observer.values(); + assertThat(responses.size(), is(1)); + HealthCheckResponse response = responses.get(0); + assertThat(response.getStatus(), is(HealthCheckResponse.ServingStatus.NOT_SERVING)); + } + + @Test + public void shouldRequestCheckForGlobalService() { + HealthServiceImpl healthService = new HealthServiceImpl(); + String serviceName = ""; + HealthCheck check = ConstantHealthCheck.up(serviceName); + HealthCheckRequest request = HealthCheckRequest.newBuilder().setService(serviceName).build(); + TestStreamObserver observer = new TestStreamObserver<>(); + + healthService.add(serviceName, check); + + healthService.check(request, observer); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertComplete() + .assertNoErrors() + .assertValueCount(1); + + List responses = observer.values(); + assertThat(responses.size(), is(1)); + HealthCheckResponse response = responses.get(0); + assertThat(response.getStatus(), is(HealthCheckResponse.ServingStatus.SERVING)); + } + + @Test + public void shouldRequestCheckWithoutServiceName() { + HealthServiceImpl healthService = new HealthServiceImpl(); + String serviceName = ""; + HealthCheck check = ConstantHealthCheck.up(serviceName); + HealthCheckRequest request = HealthCheckRequest.newBuilder().build(); + TestStreamObserver observer = new TestStreamObserver<>(); + + healthService.add(serviceName, check); + + healthService.check(request, observer); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertComplete() + .assertNoErrors() + .assertValueCount(1); + + List responses = observer.values(); + assertThat(responses.size(), is(1)); + HealthCheckResponse response = responses.get(0); + assertThat(response.getStatus(), is(HealthCheckResponse.ServingStatus.SERVING)); + } + + @Test + public void shouldRequestCheckForUnknownService() { + HealthServiceImpl healthService = new HealthServiceImpl(); + String serviceName = "unknown"; + HealthCheckRequest request = HealthCheckRequest.newBuilder().setService(serviceName).build(); + TestStreamObserver observer = new TestStreamObserver<>(); + + healthService.check(request, observer); + + assertThat(observer.awaitTerminalEvent(), is(true)); + + observer.assertError(this::isNotFoundError); + } + + private boolean isNotFoundError(Throwable thrown) { + if (thrown instanceof StatusException) { + return ((StatusException) thrown).getStatus().getCode().equals(Status.NOT_FOUND.getCode()); + } else if (thrown instanceof StatusRuntimeException) { + return ((StatusRuntimeException) thrown).getStatus().getCode().equals(Status.NOT_FOUND.getCode()); + } else { + return false; + } + } +} diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/MethodDescriptorTest.java b/grpc/server/src/test/java/io/helidon/grpc/server/MethodDescriptorTest.java new file mode 100644 index 00000000000..c6d63804fc9 --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/MethodDescriptorTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import io.helidon.grpc.server.test.EchoServiceGrpc; + +import io.grpc.Context; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyIterable.emptyIterable; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.mockito.Mockito.mock; + +/** + * {@link MethodDescriptor} unit tests. + */ +@SuppressWarnings("unchecked") +public class MethodDescriptorTest { + @Test + public void shouldCreateMethodDescriptor() { + ServerCallHandler handler = mock(ServerCallHandler.class); + io.grpc.MethodDescriptor grpcDescriptor = EchoServiceGrpc.getServiceDescriptor() + .getMethods() + .stream() + .filter(md -> md.getFullMethodName().equals("EchoService/Echo")) + .findFirst() + .orElseThrow(() -> new AssertionError("Could not find echo method")); + + MethodDescriptor descriptor = MethodDescriptor.create("EchoService","foo", grpcDescriptor.toBuilder(), handler); + + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + assertThat(descriptor.callHandler(), is(sameInstance(handler))); + assertThat(descriptor.context(), is(notNullValue())); + assertThat(descriptor.context().size(), is(0)); + + io.grpc.MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("EchoService/foo")); + } + + @Test + public void shouldBuildMethodDescriptorWithContextValue() { + ServerCallHandler handler = mock(ServerCallHandler.class); + io.grpc.MethodDescriptor grpcDescriptor = EchoServiceGrpc.getServiceDescriptor() + .getMethods() + .stream() + .filter(md -> md.getFullMethodName().equals("EchoService/Echo")) + .findFirst() + .orElseThrow(() -> new AssertionError("Could not find echo method")); + + Context.Key key = Context.key("test"); + MethodDescriptor descriptor = MethodDescriptor + .builder("EchoService", "foo", grpcDescriptor.toBuilder(), handler) + .addContextValue(key, "test-value") + .build(); + + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + assertThat(descriptor.callHandler(), is(sameInstance(handler))); + assertThat(descriptor.context(), is(notNullValue())); + assertThat(descriptor.context().size(), is(1)); + assertThat(descriptor.context().get(key), is("test-value")); + } + + @Test + public void shouldAddZeroInterceptors() { + ServerCallHandler handler = mock(ServerCallHandler.class); + io.grpc.MethodDescriptor grpcDescriptor = EchoServiceGrpc.getServiceDescriptor() + .getMethods() + .stream() + .filter(md -> md.getFullMethodName().equals("EchoService/Echo")) + .findFirst() + .orElseThrow(() -> new AssertionError("Could not find echo method")); + + MethodDescriptor descriptor = MethodDescriptor + .builder("EchoService", "foo", grpcDescriptor.toBuilder(), handler) + .intercept() + .build(); + + assertThat(descriptor.interceptors(), is(emptyIterable())); + } + + @Test + public void shouldAddOneInterceptor() { + ServerInterceptor interceptor = mock(ServerInterceptor.class); + ServerCallHandler handler = mock(ServerCallHandler.class); + io.grpc.MethodDescriptor grpcDescriptor = EchoServiceGrpc.getServiceDescriptor() + .getMethods() + .stream() + .filter(md -> md.getFullMethodName().equals("EchoService/Echo")) + .findFirst() + .orElseThrow(() -> new AssertionError("Could not find echo method")); + + MethodDescriptor descriptor = MethodDescriptor + .builder("EchoService", "foo", grpcDescriptor.toBuilder(), handler) + .intercept(interceptor) + .build(); + + assertThat(descriptor.interceptors(), contains(interceptor)); + } + + @Test + public void shouldAddMultipleInterceptors() { + ServerInterceptor interceptor1 = mock(ServerInterceptor.class); + ServerInterceptor interceptor2 = mock(ServerInterceptor.class); + ServerInterceptor interceptor3 = mock(ServerInterceptor.class); + ServerCallHandler handler = mock(ServerCallHandler.class); + io.grpc.MethodDescriptor grpcDescriptor = EchoServiceGrpc.getServiceDescriptor() + .getMethods() + .stream() + .filter(md -> md.getFullMethodName().equals("EchoService/Echo")) + .findFirst() + .orElseThrow(() -> new AssertionError("Could not find echo method")); + + MethodDescriptor descriptor = MethodDescriptor + .builder("EchoService", "foo", grpcDescriptor.toBuilder(), handler) + .intercept(interceptor1, interceptor2) + .intercept(interceptor3) + .build(); + + assertThat(descriptor.interceptors(), contains(interceptor1, interceptor2, interceptor3)); + } + + @Test + public void shouldSetName() { + ServerCallHandler handler = mock(ServerCallHandler.class); + io.grpc.MethodDescriptor grpcDescriptor = EchoServiceGrpc.getServiceDescriptor() + .getMethods() + .stream() + .filter(md -> md.getFullMethodName().equals("EchoService/Echo")) + .findFirst() + .orElseThrow(() -> new AssertionError("Could not find echo method")); + + MethodDescriptor descriptor = MethodDescriptor + .builder("EchoService", "foo", grpcDescriptor.toBuilder(), handler) + .fullname("Test/bar") + .build(); + + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + assertThat(descriptor.descriptor().getFullMethodName(), is("Test/bar")); + } +} diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/ServiceDescriptorTest.java b/grpc/server/src/test/java/io/helidon/grpc/server/ServiceDescriptorTest.java new file mode 100644 index 00000000000..70c502dad17 --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/ServiceDescriptorTest.java @@ -0,0 +1,585 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +import io.helidon.grpc.core.JavaMarshaller; +import io.helidon.grpc.core.MarshallerSupplier; +import io.helidon.grpc.core.PriorityBag; +import io.helidon.grpc.server.test.Echo; +import io.helidon.grpc.server.test.EchoServiceGrpc; + +import com.google.protobuf.Descriptors; +import io.grpc.BindableService; +import io.grpc.Context; +import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; +import io.grpc.stub.StreamObserver; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyIterable.emptyIterable; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.hamcrest.collection.IsMapContaining.hasEntry; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link ServiceDescriptor} unit tests. + */ +public class ServiceDescriptorTest { + + @Test + public void shouldHaveCorrectName() { + GrpcService service = createMockService(); + ServiceDescriptor descriptor = ServiceDescriptor.builder(service) + .build(); + + assertThat(descriptor.name(), is(service.name())); + } + + @Test + public void shouldHaveZeroContextValuesByDefault() { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .build(); + + Map, Object> map = descriptor.context(); + assertThat(map, is(notNullValue())); + assertThat(map.isEmpty(), is(true)); + } + + @Test + public void shouldAddContextValue() { + Context.Key key = Context.key("test"); + String value = "test-value"; + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .addContextValue(key, value) + .build(); + + Map, Object> map = descriptor.context(); + assertThat(map, is(notNullValue())); + assertThat(map.size(), is(1)); + assertThat(map, hasEntry(key, value)); + } + + @Test + public void shouldAddMultipleContextValues() { + Context.Key key1 = Context.key("test-1"); + String value1 = "test-value-1"; + Context.Key key2 = Context.key("test-2"); + String value2 = "test-value-2"; + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .addContextValue(key1, value1) + .addContextValue(key2, value2) + .build(); + + Map, Object> map = descriptor.context(); + assertThat(map, is(notNullValue())); + assertThat(map.size(), is(2)); + assertThat(map, hasEntry(key1, value1)); + assertThat(map, hasEntry(key2, value2)); + } + + @Test + public void shouldHaveDefaultHealthCheck() { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .build(); + + assertThat(descriptor.healthCheck(), is(notNullValue())); + + HealthCheckResponse response = descriptor.healthCheck().call(); + + assertThat(response.getName(), is(notNullValue())); + assertThat(response.getState(), is(HealthCheckResponse.State.UP)); + } + + @Test + public void shouldHaveSpecifiedHealthCheck() { + HealthCheck healthCheck = mock(HealthCheck.class); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .healthCheck(healthCheck) + .build(); + + assertThat(descriptor.healthCheck(), is(sameInstance(healthCheck))); + } + + @Test + public void shouldAddZeroInterceptors() { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .intercept() + .build(); + + assertThat(descriptor.interceptors(), is(emptyIterable())); + } + + @Test + public void shouldAddOneInterceptor() { + ServerInterceptor interceptor = mock(ServerInterceptor.class); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .intercept(interceptor) + .build(); + + assertThat(descriptor.interceptors(), contains(interceptor)); + } + + @Test + public void shouldAddMultipleInterceptors() { + ServerInterceptor interceptor1 = mock(ServerInterceptor.class); + ServerInterceptor interceptor2 = mock(ServerInterceptor.class); + ServerInterceptor interceptor3 = mock(ServerInterceptor.class); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .intercept(interceptor1, interceptor2) + .intercept(interceptor3) + .build(); + + assertThat(descriptor.interceptors(), contains(interceptor1, interceptor2, interceptor3)); + } + + @Test + public void shouldHaveZeroMethods() { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()).build(); + + assertThat(descriptor.methods(), is(emptyIterable())); + } + + @Test + public void shouldAddBidirectionalMethod() { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .bidirectional("methodOne", this::dummyBiDi) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(descriptor.methods().size(), is(1)); + assertThat(descriptor.methods(), contains(methodDescriptor)); + + assertThat(methodDescriptor.name(), is("methodOne")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + io.grpc.MethodDescriptor grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)); + assertThat(grpcDescriptor.getFullMethodName(), is("foo/methodOne")); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(JavaMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(JavaMarshaller.class))); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldAddBidirectionalMethodWithConfigurer() { + MethodDescriptor.Configurer configurer = mock(MethodDescriptor.Configurer.class); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .bidirectional("methodOne", this::dummyBiDi, configurer) + .build(); + + verify(configurer).configure(notNull()); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(descriptor.methods().size(), is(1)); + assertThat(descriptor.methods(), contains(methodDescriptor)); + + assertThat(methodDescriptor.name(), is("methodOne")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + io.grpc.MethodDescriptor grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)); + assertThat(grpcDescriptor.getFullMethodName(), is("foo/methodOne")); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(JavaMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(JavaMarshaller.class))); + } + + @Test + public void shouldAddClientStreaminglMethod() { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .clientStreaming("methodOne", this::dummyClientStreaming) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(descriptor.methods().size(), is(1)); + assertThat(descriptor.methods(), contains(methodDescriptor)); + + assertThat(methodDescriptor.name(), is("methodOne")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + io.grpc.MethodDescriptor grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING)); + assertThat(grpcDescriptor.getFullMethodName(), is("foo/methodOne")); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(JavaMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(JavaMarshaller.class))); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldAddClientStreamingMethodWithConfigurer() { + MethodDescriptor.Configurer configurer = mock(MethodDescriptor.Configurer.class); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .clientStreaming("methodOne", this::dummyClientStreaming, configurer) + .build(); + + verify(configurer).configure(notNull()); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(descriptor.methods().size(), is(1)); + assertThat(descriptor.methods(), contains(methodDescriptor)); + + assertThat(methodDescriptor.name(), is("methodOne")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + io.grpc.MethodDescriptor grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING)); + assertThat(grpcDescriptor.getFullMethodName(), is("foo/methodOne")); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(JavaMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(JavaMarshaller.class))); + } + + @Test + public void shouldAddServerStreaminglMethod() { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .serverStreaming("methodOne", this::dummyServerStreaming) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(descriptor.methods().size(), is(1)); + assertThat(descriptor.methods(), contains(methodDescriptor)); + + assertThat(methodDescriptor.name(), is("methodOne")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + io.grpc.MethodDescriptor grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)); + assertThat(grpcDescriptor.getFullMethodName(), is("foo/methodOne")); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(JavaMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(JavaMarshaller.class))); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldAddServerStreamingMethodWithConfigurer() { + MethodDescriptor.Configurer configurer = mock(MethodDescriptor.Configurer.class); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .serverStreaming("methodOne", this::dummyServerStreaming, configurer) + .build(); + + verify(configurer).configure(notNull()); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(descriptor.methods().size(), is(1)); + assertThat(descriptor.methods(), contains(methodDescriptor)); + + assertThat(methodDescriptor.name(), is("methodOne")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + io.grpc.MethodDescriptor grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)); + assertThat(grpcDescriptor.getFullMethodName(), is("foo/methodOne")); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(JavaMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(JavaMarshaller.class))); + } + + @Test + public void shouldAddUnaryMethod() { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("methodOne", this::dummyServerStreaming) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(descriptor.methods().size(), is(1)); + assertThat(descriptor.methods(), contains(methodDescriptor)); + + assertThat(methodDescriptor.name(), is("methodOne")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + io.grpc.MethodDescriptor grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.UNARY)); + assertThat(grpcDescriptor.getFullMethodName(), is("foo/methodOne")); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(JavaMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(JavaMarshaller.class))); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldAddUnaryMethodWithConfigurer() { + MethodDescriptor.Configurer configurer = mock(MethodDescriptor.Configurer.class); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("methodOne", this::dummyServerStreaming, configurer) + .build(); + + verify(configurer).configure(notNull()); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(descriptor.methods().size(), is(1)); + assertThat(descriptor.methods(), contains(methodDescriptor)); + + assertThat(methodDescriptor.name(), is("methodOne")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + io.grpc.MethodDescriptor grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.UNARY)); + assertThat(grpcDescriptor.getFullMethodName(), is("foo/methodOne")); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(JavaMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(JavaMarshaller.class))); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldAddZeroMethodLevelInterceptors() { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("methodOne", this::dummyServerStreaming) + .intercept("methodOne") + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.interceptors(), is(emptyIterable())); + } + + @Test + public void shouldAddOneMethodLevelInterceptor() { + ServerInterceptor interceptor = mock(ServerInterceptor.class); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("methodOne", this::dummyServerStreaming) + .intercept("methodOne", interceptor) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.interceptors(), contains(interceptor)); + } + + @Test + public void shouldAddMultipleMethodLevelInterceptors() { + ServerInterceptor interceptor1 = mock(ServerInterceptor.class); + ServerInterceptor interceptor2 = mock(ServerInterceptor.class); + ServerInterceptor interceptor3 = mock(ServerInterceptor.class); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("methodOne", this::dummyServerStreaming) + .intercept("methodOne", interceptor1, interceptor2) + .intercept("methodOne", interceptor3) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("methodOne"); + + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.interceptors(), contains(interceptor1, interceptor2, interceptor3)); + } + + @Test + public void shouldAddMethodLevelInterceptorsToDifferentMethods() { + ServerInterceptor interceptor1 = mock(ServerInterceptor.class); + ServerInterceptor interceptor2 = mock(ServerInterceptor.class); + ServerInterceptor interceptor3 = mock(ServerInterceptor.class); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("methodOne", this::dummyServerStreaming) + .intercept("methodOne", interceptor1, interceptor2) + .unary("methodTwo", this::dummyServerStreaming) + .intercept("methodTwo", interceptor3) + .build(); + + MethodDescriptor methodDescriptor1 = descriptor.method("methodOne"); + MethodDescriptor methodDescriptor2 = descriptor.method("methodTwo"); + + assertThat(methodDescriptor1, is(notNullValue())); + assertThat(methodDescriptor1.interceptors(), contains(interceptor1, interceptor2)); + assertThat(methodDescriptor2, is(notNullValue())); + assertThat(methodDescriptor2.interceptors(), contains(interceptor3)); + } + + @Test + public void shouldSetMarshaller() { + io.grpc.MethodDescriptor.Marshaller marshaller = mock(io.grpc.MethodDescriptor.Marshaller.class); + MarshallerSupplier supplier = new MarshallerSupplier() { + @Override + @SuppressWarnings("unchecked") + public io.grpc.MethodDescriptor.Marshaller get(Class clazz) { + return marshaller; + } + }; + + ServiceDescriptor descriptor = ServiceDescriptor + .builder(createMockService()) + .marshallerSupplier(supplier) + .unary("bar", this::dummyUnary) + .build(); + + io.grpc.MethodDescriptor methodDescriptor = descriptor.bindableService(new PriorityBag<>()) + .bindService() + .getMethod("foo/bar") + .getMethodDescriptor(); + + assertThat(methodDescriptor.getResponseMarshaller(), is(sameInstance(marshaller))); + assertThat(methodDescriptor.getRequestMarshaller(), is(sameInstance(marshaller))); + } + + @Test + public void shouldBuildFromBindableService() { + BindableService service = new EchoStub(); + ServerServiceDefinition definition = service.bindService(); + io.grpc.ServiceDescriptor grpcDescriptor = definition.getServiceDescriptor(); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(service).build(); + + assertThat(descriptor.name(), is(grpcDescriptor.getName())); + + BindableService bindableService = descriptor.bindableService(new PriorityBag<>()); + assertThat(bindableService, is(notNullValue())); + + ServerServiceDefinition ssd = bindableService.bindService(); + assertThat(ssd, is(notNullValue())); + + io.grpc.ServiceDescriptor actualDescriptor = ssd.getServiceDescriptor(); + assertThat(actualDescriptor, is(notNullValue())); + assertThat(actualDescriptor.getName(), is(grpcDescriptor.getName())); + + Map> methods = grpcDescriptor.getMethods() + .stream() + .collect(Collectors.toMap(io.grpc.MethodDescriptor::getFullMethodName, m -> m)); + + Collection> methodsActual = actualDescriptor.getMethods(); + + for (io.grpc.MethodDescriptor method : methodsActual) { + assertThat(method.toString(), is(methods.get(method.getFullMethodName()).toString())); + } + } + + @Test + public void shouldOverrideServiceName() { + BindableService service = new EchoStub(); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(service) + .name("Foo") + .build(); + + assertThat(descriptor.name(), is("Foo")); + + BindableService bindableService = descriptor.bindableService(new PriorityBag<>()); + assertThat(bindableService, is(notNullValue())); + + ServerServiceDefinition ssd = bindableService.bindService(); + assertThat(ssd, is(notNullValue())); + + io.grpc.ServiceDescriptor actualDescriptor = ssd.getServiceDescriptor(); + assertThat(actualDescriptor, is(notNullValue())); + assertThat(actualDescriptor.getName(), is("Foo")); + + Collection> methods = actualDescriptor.getMethods(); + + for (io.grpc.MethodDescriptor method : methods) { + assertThat(method.getFullMethodName().startsWith("Foo/"), is(true)); + } + } + + @Test + public void shouldBuildFromProtoFile() { + GrpcService service = mock(GrpcService.class); + + when(service.name()).thenReturn("EchoService"); + + Descriptors.FileDescriptor protoDescriptor = Echo.getDescriptor(); + + ServiceDescriptor descriptor = ServiceDescriptor.builder(service) + .proto(protoDescriptor) + .unary("Echo", this::dummyUnary) + .build(); + + assertThat(descriptor.name(), is("EchoService")); + + BindableService bindableService = descriptor.bindableService(new PriorityBag<>()); + assertThat(bindableService, is(notNullValue())); + + ServerServiceDefinition ssd = bindableService.bindService(); + assertThat(ssd, is(notNullValue())); + + io.grpc.ServiceDescriptor actualDescriptor = ssd.getServiceDescriptor(); + assertThat(actualDescriptor, is(notNullValue())); + assertThat(actualDescriptor.getName(), is("EchoService")); + } + + + private StreamObserver dummyBiDi(StreamObserver observer) { + return null; + } + + private StreamObserver dummyClientStreaming(StreamObserver observer) { + return null; + } + + private void dummyServerStreaming(String request, StreamObserver observer) { + } + + private void dummyUnary(String request, StreamObserver observer) { + } + + private GrpcService createMockService() { + GrpcService service = mock(GrpcService.class); + + when(service.name()).thenReturn("foo"); + + return service; + } + + private class EchoStub + extends EchoServiceGrpc.EchoServiceImplBase { + + } +} diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/SslIT.java b/grpc/server/src/test/java/io/helidon/grpc/server/SslIT.java new file mode 100644 index 00000000000..3b0a9bc2cfb --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/SslIT.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import java.io.File; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.net.ssl.SSLException; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.grpc.server.test.Echo; +import io.helidon.grpc.server.test.EchoServiceGrpc; + +import com.oracle.bedrock.runtime.LocalPlatform; +import com.oracle.bedrock.runtime.network.AvailablePortIterator; + +import io.grpc.Channel; +import io.grpc.ManagedChannel; +import io.grpc.StatusRuntimeException; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NegotiationType; +import io.grpc.netty.NettyChannelBuilder; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; + +import org.junit.AfterClass; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import services.EchoService; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for gRPC server with SSL connections + */ +public class SslIT { + + // ----- data members --------------------------------------------------- + + /** + * The {@link java.util.logging.Logger} to use for logging. + */ + private static final Logger LOGGER = Logger.getLogger(SslIT.class.getName()); + + /** + * The Helidon {@link GrpcServer} being tested. + */ + private static GrpcServer grpcServer_1WaySSL; + + /** + * The Helidon {@link GrpcServer} being tested. + */ + private static GrpcServer grpcServer_2WaySSL; + + /** + * The Helidon {@link GrpcServer} being tested. + */ + private static GrpcServer grpcServer_2WaySSLConfig; + + /** + * Port used for 1waySSL + */ + private static int port1WaySSL; + + /** + * Port used for 2waySSL + */ + private static int port2WaySSL; + + /** + * Port used for 2waySSL using config-ssl.conf + */ + private static int port2WaySSLConfig; + + private static final String CLIENT_CERT = "clientCert.pem"; + private static final String CLIENT_KEY = "clientKey.pem"; + private static final String CA_CERT = "ca.pem"; + private static final String SERVER_CERT = "serverCert.pem"; + private static final String SERVER_KEY = "serverKey.pem"; + + private static String tlsCert; + private static String tlsKey; + private static String tlsCaCert; + private static String tlsClientKey; + private static String tlsClientCert; + + private static String filePath; + + // ----- test lifecycle ------------------------------------------------- + + @BeforeAll + public static void setup() throws Exception { + LogManager.getLogManager().readConfiguration(SslIT.class.getResourceAsStream("/logging.properties")); + File resourcesDirectory = new File("src/test/resources/ssl"); + filePath = resourcesDirectory.getAbsolutePath(); + tlsCert = getFile(SERVER_CERT); + tlsKey = getFile(SERVER_KEY); + tlsCaCert = getFile(CA_CERT); + tlsClientCert = getFile(CLIENT_CERT); + tlsClientKey = getFile(CLIENT_KEY); + + AvailablePortIterator ports = LocalPlatform.get().getAvailablePorts(); + + port1WaySSL = ports.next(); + port2WaySSL = ports.next(); + port2WaySSLConfig = ports.next(); + + grpcServer_1WaySSL = startGrpcServer(port1WaySSL, false /*mutual*/, false /*useConfig*/); + grpcServer_2WaySSL = startGrpcServer(port2WaySSL, true /*mutual*/, false /*useConfig*/); + grpcServer_2WaySSLConfig = startGrpcServer(port2WaySSLConfig, true/*mutual*/, true /*useConfig*/); + } + + @AfterClass + public static void cleanup() throws Exception + { + CompletableFuture[] futures = + Stream.of(grpcServer_1WaySSL, grpcServer_2WaySSL, grpcServer_2WaySSLConfig) + .map(server -> server.shutdown().toCompletableFuture()) + .toArray(CompletableFuture[]::new); + + CompletableFuture.allOf(futures).get(10, TimeUnit.SECONDS); + } + + // ----- test methods --------------------------------------------------- + + @Test + public void shouldConnectWithoutClientCertsFor1Way() throws Exception { + // client do not have to provide certs for 1way ssl + SslContext sslContext = clientSslContext(tlsCaCert, null, null); + + Channel channel = NettyChannelBuilder.forAddress("localhost", grpcServer_1WaySSL.port()) + .negotiationType(NegotiationType.TLS) + .sslContext(sslContext) + .build(); + + // call the gRPC Echo service suggestion + Echo.EchoResponse response = EchoServiceGrpc.newBlockingStub(channel).echo(Echo.EchoRequest.newBuilder().setMessage("foo").build()); + assertThat(response.getMessage(), is("foo")); + + ((ManagedChannel) channel).shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + @Test + public void shouldNotConnectWithoutCAFor1Way() throws Exception { + // client do not have to provide certs for 1way ssl + SslContext sslContext = clientSslContext(null, null, null); + + Channel channel = NettyChannelBuilder.forAddress("localhost", grpcServer_1WaySSL.port()) + .negotiationType(NegotiationType.TLS) + .sslContext(sslContext) + .build(); + + // call the gRPC Echo service should throw + Assertions.assertThrows(StatusRuntimeException.class, + ()->EchoServiceGrpc.newBlockingStub(channel).echo(Echo.EchoRequest.newBuilder().setMessage("foo").build())); + + ((ManagedChannel) channel).shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + @Test + public void shouldConnectWithClientCertsFor2Way() throws Exception { + SslContext sslContext = clientSslContext(tlsCaCert, tlsClientCert, tlsClientKey); + + Channel channel = NettyChannelBuilder.forAddress("localhost", grpcServer_2WaySSL.port()) + .negotiationType(NegotiationType.TLS) + .sslContext(sslContext) + .build(); + + // call the gRPC Echo service + Echo.EchoResponse response = EchoServiceGrpc.newBlockingStub(channel).echo(Echo.EchoRequest.newBuilder().setMessage("foo").build()); + assertThat(response.getMessage(), is("foo")); + + ((ManagedChannel) channel).shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + @Test + public void shouldNotConnectWithoutCAFor2Way() throws Exception { + SslContext sslContext = clientSslContext(null, tlsClientCert, tlsClientKey); + + Channel channel = NettyChannelBuilder.forAddress("localhost", grpcServer_2WaySSL.port()) + .negotiationType(NegotiationType.TLS) + .sslContext(sslContext) + .build(); + + // call the gRPC Echo service should throw + Assertions.assertThrows(StatusRuntimeException.class, + ()->EchoServiceGrpc.newBlockingStub(channel).echo(Echo.EchoRequest.newBuilder().setMessage("foo").build())); + + ((ManagedChannel) channel).shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + @Test + public void shouldNotConnectWithoutClientCertFor2Way() throws Exception { + SslContext sslContext = clientSslContext(tlsCaCert, null, tlsClientKey); + + Channel channel = NettyChannelBuilder.forAddress("localhost", grpcServer_2WaySSL.port()) + .negotiationType(NegotiationType.TLS) + .sslContext(sslContext) + .build(); + + // call the gRPC Echo service should throw + Assertions.assertThrows(StatusRuntimeException.class, + ()->EchoServiceGrpc.newBlockingStub(channel).echo(Echo.EchoRequest.newBuilder().setMessage("foo").build())); + + ((ManagedChannel) channel).shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + @Test + public void shouldConnectWithClientCertsFor2WayUseConfig() throws Exception{ + SslContext sslContext = clientSslContext(tlsCaCert, tlsClientCert, tlsClientKey); + + Channel channel = NettyChannelBuilder.forAddress("localhost", grpcServer_2WaySSLConfig.port()) + .negotiationType(NegotiationType.TLS) + .sslContext(sslContext) + .build(); + + // call the gRPC Echo service + Echo.EchoResponse response = EchoServiceGrpc.newBlockingStub(channel).echo(Echo.EchoRequest.newBuilder().setMessage("foo").build()); + assertThat(response.getMessage(), is("foo")); + + ((ManagedChannel) channel).shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + @Test + public void shouldNotConnectWithoutClientCertFor2WayUseConfig() throws Exception { + SslContext sslContext = clientSslContext(tlsCaCert, null, tlsClientKey); + + Channel channel = NettyChannelBuilder.forAddress("localhost", grpcServer_2WaySSLConfig.port()) + .negotiationType(NegotiationType.TLS) + .sslContext(sslContext) + .build(); + + // call the gRPC Echo service should throw + Assertions.assertThrows(StatusRuntimeException.class, + ()->EchoServiceGrpc.newBlockingStub(channel).echo(Echo.EchoRequest.newBuilder().setMessage("foo").build())); + + ((ManagedChannel) channel).shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + // ----- helper methods ------------------------------------------------- + + private static SslContext clientSslContext(String trustCertCollectionFilePath, + String clientCertChainFilePath, + String clientPrivateKeyFilePath) throws SSLException { + SslContextBuilder builder = GrpcSslContexts.forClient(); + if (trustCertCollectionFilePath != null) { + builder.trustManager(new File(trustCertCollectionFilePath)); + } + + if (clientCertChainFilePath != null && clientPrivateKeyFilePath != null) { + builder.keyManager(new File(clientCertChainFilePath), new File(clientPrivateKeyFilePath)); + } + return builder.build(); + } + + /** + * Start the gRPC Server listening on the specified nPort. + * + * @throws Exception in case of an error + */ + private static GrpcServer startGrpcServer(int nPort, boolean mutual, boolean useConfig ) throws Exception { + SslConfiguration sslConfig; + String name = "grpc.server"; + if (useConfig) { + name = name + 1; + Config config = Config.builder().sources(ConfigSources.classpath("config-ssl.conf")).build(); + sslConfig = config.get("grpcserver.ssl").as(SslConfiguration::create).get(); + } else if (mutual) { + name = name + 2; + sslConfig = SslConfiguration.builder() + .jdkSSL(false) + .tlsCert(tlsCert) + .tlsKey(tlsKey) + .tlsCaCert(tlsCaCert) + .build(); + } else { + name = name + 3; + sslConfig = SslConfiguration.builder() + .jdkSSL(false) + .tlsCert(tlsCert) + .tlsKey(tlsKey) + .build(); + } + // Add the EchoService + GrpcRouting routing = GrpcRouting.builder() + .register(new EchoService()) + .build(); + + GrpcServerConfiguration serverConfig = GrpcServerConfiguration.builder().name(name).port(nPort).sslConfig(sslConfig).build(); + + GrpcServer grpcServer = GrpcServer.create(serverConfig, routing) + .start() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + + + LOGGER.info("Started gRPC server at: localhost:" + grpcServer.port()); + + return grpcServer; + } + + private static String getFile(String fileName){ + return filePath + "/" + fileName; + } +} diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/TestStreamObserver.java b/grpc/server/src/test/java/io/helidon/grpc/server/TestStreamObserver.java new file mode 100644 index 00000000000..66e6bc55fa7 --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/TestStreamObserver.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import io.grpc.stub.StreamObserver; +import io.reactivex.Observer; +import io.reactivex.observers.TestObserver; + +/** + * A test {@link io.grpc.stub.StreamObserver} that is based on the + * RxJava {@link io.reactivex.observers.TestObserver} that allows tests + * to easily assert state of the observer. + */ +public class TestStreamObserver + extends TestObserver + implements StreamObserver { + + public TestStreamObserver() { + onSubscribe(new TestObserver<>()); + } + + public TestStreamObserver(Observer actual) { + super(actual); + onSubscribe(new TestObserver<>()); + } + + @Override + public void onCompleted() { + onComplete(); + } +} diff --git a/grpc/server/src/test/java/io/helidon/grpc/server/TracingIT.java b/grpc/server/src/test/java/io/helidon/grpc/server/TracingIT.java new file mode 100644 index 00000000000..30ceef8098f --- /dev/null +++ b/grpc/server/src/test/java/io/helidon/grpc/server/TracingIT.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.grpc.server; + +import com.oracle.bedrock.testsupport.deferred.Eventually; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import io.helidon.grpc.server.test.Echo; +import io.helidon.grpc.server.test.EchoServiceGrpc; +import io.helidon.tracing.TracerBuilder; + +import io.grpc.Channel; +import io.grpc.ManagedChannelBuilder; +import io.opentracing.Tracer; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import services.EchoService; +import zipkin2.Span; +import zipkin2.junit.ZipkinRule; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static com.oracle.bedrock.deferred.DeferredHelper.invoking; + +/** + * Tests for gRPC server with tracing. + */ +public class TracingIT { + + // ----- data members --------------------------------------------------- + + /** + * The {@link java.util.logging.Logger} to use for logging. + */ + private static final Logger LOGGER = Logger.getLogger(TracingIT.class.getName()); + + /** + * The Helidon {@link io.helidon.grpc.server.GrpcServer} being tested. + */ + private static GrpcServer grpcServer; + + /** + * A gRPC {@link io.grpc.Channel} to connect to the test gRPC server + */ + private static Channel channel; + + /** + * ZipkinRule to start a Zipkin server + */ + public static ZipkinRule zipkin = new ZipkinRule(); + + + // ----- test lifecycle ------------------------------------------------- + + @BeforeAll + public static void setup() throws Exception { + LogManager.getLogManager().readConfiguration(TracingIT.class.getResourceAsStream("/logging.properties")); + + //start zipkin server on an ephemeral port + zipkin.start(0); + + startGrpcServer(); + + channel = ManagedChannelBuilder.forAddress("localhost", grpcServer.port()) + .usePlaintext() + .build(); + } + + @AfterAll + public static void cleanup() throws Exception { + zipkin.shutdown(); + } + + // ----- test methods --------------------------------------------------- + + @Test + public void shouldTraceMethodNameAndHeaders() throws Exception { + // call the gRPC Echo service so that there should be tracing span sent to zipkin server + EchoServiceGrpc.newBlockingStub(channel).echo(Echo.EchoRequest.newBuilder().setMessage("foo").build()); + + Eventually.assertThat(invoking(this).getSpanCount(), is(1)); + + List> listTraces = zipkin.getTraces(); + assertThat(listTraces, is(notNullValue())); + + String sTraces = listTraces.toString(); + + assertThat("The traces should include method name", sTraces.contains("grpc.method_name")); + assertThat("The traces should include Echo method", sTraces.contains("EchoService/Echo")); + + assertThat("Tha traces should include headers", sTraces.contains("grpc.headers")); + assertThat("Tha traces should include attributes", sTraces.contains("grpc.call_attributes")); + } + + // ----- helper methods ------------------------------------------------- + + /** + * Start the gRPC Server listening on an ephemeral port. + * + * @throws Exception in case of an error + */ + private static void startGrpcServer() throws Exception { + // Add the EchoService + GrpcRouting routing = GrpcRouting.builder() + .register(new EchoService()) + .build(); + // Enable tracing + Tracer tracer = (Tracer) TracerBuilder.create("Server") + .collectorUri(URI.create(zipkin.httpUrl() + "/api/v2/spans")) + .build(); + + TracingConfiguration tracingConfig = new TracingConfiguration.Builder() + .withStreaming() + .withVerbosity() + .withTracedAttributes(ServerRequestAttribute.CALL_ATTRIBUTES, + ServerRequestAttribute.HEADERS, + ServerRequestAttribute.METHOD_NAME) + .build(); + + // Run the server on port 0 so that it picks a free ephemeral port + GrpcServerConfiguration serverConfig = GrpcServerConfiguration.builder().port(0).tracer(tracer).tracingConfig(tracingConfig).build(); + + grpcServer = GrpcServer.create(serverConfig, routing) + .start() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + + LOGGER.info("Started gRPC server at: localhost:" + grpcServer.port()); + } + + /** + * Return the span count collect. + */ + public int getSpanCount() { + return zipkin.collectorMetrics().spans(); + } +} diff --git a/grpc/server/src/test/java/services/EchoService.java b/grpc/server/src/test/java/services/EchoService.java new file mode 100644 index 00000000000..0d305fc4265 --- /dev/null +++ b/grpc/server/src/test/java/services/EchoService.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package services; + +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.grpc.server.test.Echo; + +import io.grpc.stub.StreamObserver; + +/** + * A simple test gRPC echo service. + */ +public class EchoService + implements GrpcService { + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.proto(Echo.getDescriptor()) + .unary("Echo", this::echo); + } + + /** + * Echo the message back to the caller. + * + * @param request the echo request containing the message to echo + * @param observer the call response + */ + public void echo(Echo.EchoRequest request, StreamObserver observer) { + String message = request.getMessage(); + Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build(); + complete(observer, response); + } +} diff --git a/grpc/server/src/test/proto/echo.proto b/grpc/server/src/test/proto/echo.proto new file mode 100644 index 00000000000..1b8930c1663 --- /dev/null +++ b/grpc/server/src/test/proto/echo.proto @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.grpc.server.test"; + +service EchoService { + rpc Echo (EchoRequest) returns (EchoResponse) {} +} + +message EchoRequest { + string message = 1; +} + +message EchoResponse { + string message = 1; +} diff --git a/grpc/server/src/test/resources/config-ssl.conf b/grpc/server/src/test/resources/config-ssl.conf new file mode 100644 index 00000000000..5009f29e7c9 --- /dev/null +++ b/grpc/server/src/test/resources/config-ssl.conf @@ -0,0 +1,26 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +grpcserver { + ssl: { + tlsCert: serverCert.pem, + tlsKey: serverKey.pem, + tlsCaCert: ca.pem, + jdkSSL: false + path: "src/test/resources/ssl" + } + } \ No newline at end of file diff --git a/grpc/server/src/test/resources/config1.conf b/grpc/server/src/test/resources/config1.conf new file mode 100644 index 00000000000..f3cadf1353a --- /dev/null +++ b/grpc/server/src/test/resources/config1.conf @@ -0,0 +1,24 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +grpcserver { + name: "foo" + port: 19, + bind-address: "127.0.0.1", + native: true + workers: 51 +} diff --git a/grpc/server/src/test/resources/logging.properties b/grpc/server/src/test/resources/logging.properties new file mode 100644 index 00000000000..f903114790c --- /dev/null +++ b/grpc/server/src/test/resources/logging.properties @@ -0,0 +1,37 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden 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 +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +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 +#io.netty.level=INFO diff --git a/grpc/server/src/test/resources/ssl/ca.pem b/grpc/server/src/test/resources/ssl/ca.pem new file mode 100644 index 00000000000..fe872b835d3 --- /dev/null +++ b/grpc/server/src/test/resources/ssl/ca.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFZDCCA0wCCQDcF3EaDha/QjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExDzAN +BgNVBAoMBk9yYWNsZTEVMBMGA1UECwwMSGVsaWRvbi1nUlBDMRIwEAYDVQQDDAls +b2NhbGhvc3QwHhcNMTkwMzI4MTg0NTA0WhcNMjAwMzI3MTg0NTA0WjB0MQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLU2FudGEgQ2xh +cmExDzANBgNVBAoMBk9yYWNsZTEVMBMGA1UECwwMSGVsaWRvbi1nUlBDMRIwEAYD +VQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC1 +x5GX12M9tP/W8VVwytr95wOtqNfZ46bIMtJAF3JoCWTug54TOeW3RRGnVpdXt1ww +AxQffgZdLVm/AE88MbEhrRkUlEkzktyd6fUr2XSYZ/NlzvNDAhftfoz25o8Olww6 +oScD0CFmw4vlVo9203BmLMjZzaX/srDLylRhiLaq3pgcKS82oVesh/qCzQXcWX09 +EuRKJX4ktG5kcPqfYxuZLCn2Wf/JsLmxRwxGe0ux4dpyZ2F+Ehmfe7ukMA8eehJH +H6IdN08A9Pd7o/koJuhBYkDIP9JymdNhJNod7kjiYxOEcmxRR9v3/vQR6ZF92fwA +frO7K0/Rg5meXw8VviDJryHEVYODXSOvfpLmCrjZ3esWkj//R9oybtTWYrDxkva9 +4fYmedXX4c3FWxtNljtz5thS6TXg2u8IDHvh0xcN0JPkfiw+Kql0+8/JOGw1JChQ +9G8SnsKcg+hRccd823tLAiUNbznY+ZOyyVwX70bZu1Uvj8KTRiBRGzfCv8QTAHbX +YYYlaPV3WDiDKpRQy5zE/FQKFqZ2Avtab/09tyd3kc05JJW0xqhN7tgOF1WunQEh +ciDGlywSFhPxU7K2w/YptQW8HEFoMIKXnd0c1ERhwNuCbcnPeUwvdeTT+IiiDtdo +kqYDudhnc+kjCsnTkrl06bzvmFxV/ETIovAxAiY9wwIDAQABMA0GCSqGSIb3DQEB +CwUAA4ICAQCpvl8JII4wo02jcDA0eaL9XBj7w0D1yPEtnwjbszpVqVHPNHiZ/239 +DEk07eEDQxF0sH9sQlZzidAVtLSqtWncfkcKruI1Hn4HtR/CyF6bqHqby3Y4P4PJ +kGuHqYo6V9d0I+AHUZdqV/d9rU0CB8jeff1WJi+jirvQnYHlImPd09tCLuyH6cqI +lT1yKzXPT9IyqLmVmxhpLxI4Zysfz5bKAkeKBMW1qXl4XiN5pq6hN5x3uX9zo5dA +rpL9LU/+DxVcrb6PZCJpAtmRsVwbRmQz3lTSa1NOgCeW1mzb3Fljevg1uQ3nuy29 +34/1vX9d9pz1eFaGOf/eECz1cNsvQjCTPuhEMaU4HVIKC3BTiGeiQoT1sdgd8OdA +nGVSVOuydcL/UQOCm/DJb6X7wAaOQj5JL9FvhPNEEnf5zYzX0Tc8KHIyg/wRkTGv +BgtbTIg51fGyHpReKTBQkfExG09yjfHvJHIBw8PYn81IM3ViueqpPoiH91XPl87h +bwHd0fjd77vfysKi/owYkJxNn5xaVLaVaeZegkXw/b7jy0oGJaUiKspfi8RBTvZa +PfALLqpgcjBllAzThEbH7/rAiSU6JQwkuaFgZpg00r7cZ85nr88j+p4sm9ou/o8U +EwmKA60B42To6yE8Hb6byBSXf2rZeWCEyZUyv2LTW1HST4SeUWVM/A== +-----END CERTIFICATE----- diff --git a/grpc/server/src/test/resources/ssl/clientCert.pem b/grpc/server/src/test/resources/ssl/clientCert.pem new file mode 100644 index 00000000000..c15fb6c17c8 --- /dev/null +++ b/grpc/server/src/test/resources/ssl/clientCert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE/DCCAuQCAQEwDQYJKoZIhvcNAQEFBQAwdDELMAkGA1UEBhMCVVMxEzARBgNV +BAgMCkNhbGlmb3JuaWExFDASBgNVBAcMC1NhbnRhIENsYXJhMQ8wDQYDVQQKDAZP +cmFjbGUxFTATBgNVBAsMDEhlbGlkb24tZ1JQQzESMBAGA1UEAwwJbG9jYWxob3N0 +MB4XDTE5MDMyODE4NDUzMFoXDTIwMDMyNzE4NDUzMFowFDESMBAGA1UEAwwJbG9j +YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp5Vyo+dklM4V +HwD4LIvRiwYfH/dgNNLjEBmK5k7tJRVXTbFf0xR9wo65bUzpHcOvxxXjjEMV6gk+ +yLqggWGFx1B64i434HFl+yVRhD0LP/KSQRVv+qHVOAxscEMz4TNdKbGGnju3HNhG +cvU24chn0QK9QWGTBs+f+GanJk/USsinfIB3xElleu5miXmXFyHkHzU98uRBg3Hx +l1tv90OpVB+RwgwXYRvSk8woGZ1IRuAqE2DZzzSEiXlrcFXAovGuCpayfRJyQBlG +RsiYq6xkceSrMNdY6BMFw1D/CGcPGlVb3KBhaEo3U7WFntOfzZ9suEvn4SrzgDCQ +Oyrl/sWehHmDrZyWk3B3PcrQI4tzG1iimjt1keCbNj7LrBHpLB3vJsH8gUXpDHqG +6AQS4VktR41rSknT/fjrPqMrJclCBZi24Z/JJWrF62dpdacZbpkxBD4Hzy+i+sLs +RbOOh9V4dgdGBIvyrJDfd54r43i1mfMo3hEMnT0PBB9miun2au0AbxZPaj0iMMp6 +0IZ0SyVNdMZEVapaxJDiQUd2PR9hXs/3K5VGLaPhc+RH9rDs+5cyQzhxlOkm+Z2Q +pBJADuYmuCLnFZrlH+cGSL1F0TX0Y+Fy+i4hdnd/LLET5t+sLLuJ/L1Xy/y32vbh +FKZUVKsJsN3rIAL7MUpz9/6OI2sU5NkCAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA +YBeVjOWa72wI947ATWsB12SDS1jPXJCh3lzI5q8Zzt1DqJ1VIbPK2z2oIUg1ilQ9 +zY7kafa+8g5qIjjyA1YaL//wH/Eaw8LYqmc2EAWZoBNkRog5pTpY1D6dWtucjVvq +g8YH+iKM294DDODBM01IqtDTvJPZQpi5x7xkv1JLAA6p4bgsoWNt8M3usHW9MTYW +ZTTabmQA+6UBbYMq3nIqwY3Golw4U2htI/mj5dkUkeGc43e1ZhVNz80vavlZbB1A +iBfifnfH6hTf4JYaU+HoxoTCMU72yK//VBLxhqAICjnFKWbwJPe2E+n2b1A6jmqV +j0AWNEHFC4GFH7W/0IRaENXw7OZTEypxM+LGOXr8dZnn6wmFrdZrcEa0GhLQKQkX +hNOvWYtXOVSq6DxB+Z7y8B7DB9CLcy2A1g2WXJKjnjMX3EQU0CBn6eatGTKL7nDp +5SU6IWKfXgowNJM8biFz7T0hSdmYhisN7UOAat8+DOz7CkapHDUFp0WWeg9GqsXi +En5/tsOqQXY9+aNQ4U9bJ1UT6xUZ3Nznv4nOfJ126jFTG5VAwMU4lhXrCw+AersI ++ITqOiL8pARlhRIgWqgdO5vhhes2VYoa8Gi155UK9xKa26yochVkyPrO7q1DNLEb +mpPKfTcMNGazZlx2uSvT65PQRhnnupJijTDD25yZoFQ= +-----END CERTIFICATE----- diff --git a/grpc/server/src/test/resources/ssl/clientKey.pem b/grpc/server/src/test/resources/ssl/clientKey.pem new file mode 100644 index 00000000000..0c1063efe82 --- /dev/null +++ b/grpc/server/src/test/resources/ssl/clientKey.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCnlXKj52SUzhUf +APgsi9GLBh8f92A00uMQGYrmTu0lFVdNsV/TFH3CjrltTOkdw6/HFeOMQxXqCT7I +uqCBYYXHUHriLjfgcWX7JVGEPQs/8pJBFW/6odU4DGxwQzPhM10psYaeO7cc2EZy +9TbhyGfRAr1BYZMGz5/4ZqcmT9RKyKd8gHfESWV67maJeZcXIeQfNT3y5EGDcfGX +W2/3Q6lUH5HCDBdhG9KTzCgZnUhG4CoTYNnPNISJeWtwVcCi8a4KlrJ9EnJAGUZG +yJirrGRx5Ksw11joEwXDUP8IZw8aVVvcoGFoSjdTtYWe05/Nn2y4S+fhKvOAMJA7 +KuX+xZ6EeYOtnJaTcHc9ytAji3MbWKKaO3WR4Js2PsusEeksHe8mwfyBRekMeobo +BBLhWS1HjWtKSdP9+Os+oyslyUIFmLbhn8klasXrZ2l1pxlumTEEPgfPL6L6wuxF +s46H1Xh2B0YEi/KskN93nivjeLWZ8yjeEQydPQ8EH2aK6fZq7QBvFk9qPSIwynrQ +hnRLJU10xkRVqlrEkOJBR3Y9H2Fez/crlUYto+Fz5Ef2sOz7lzJDOHGU6Sb5nZCk +EkAO5ia4IucVmuUf5wZIvUXRNfRj4XL6LiF2d38ssRPm36wsu4n8vVfL/Lfa9uEU +plRUqwmw3esgAvsxSnP3/o4jaxTk2QIDAQABAoICAGoJiixjsaTmcT7Y4i9tmP0a +tn8Hi/xgpAP/5X+s2SjpxAAJB9UL1jdRD8s1KxwGQib6lqydljQr/WoNjJnRF1Dy +f462J+0AU8MB7f56ka+40QoVgnFYDQ4V7ldqbl1EnuJOzhs5NbgusQT0W/GpWOdL +kGzaTKm2nkYSzhjT0kQviqqgryeoOWn7hy3dndipPSryCWyzOTDu3CslVEdkrLPp +9dSOygxMCoYyW/05JA6UsIrqcWdGl8ZU6cJ+wBFaUwGoMPAqx9f/JhSLwO5/el8G +jbutxzexMahQi87JRykmcaW8ZWYT78YryYyh4y+8fJJwlRxIjSLDl7+YeUcnzBCI +ltcCIV272HCF+lhYSa76ug2sGAUXdE5G5ANOfMcBu2siRaWlEnLuYdc5XjpphrFr +XUYZWK+vckQh5bLZ+KvP83tmPn0i2S7WyntD6r1gb2VobSU+92TWyoTB0MeauNpA +uoiaZHDkdEgc9m702/WAfv5peOnbyys3vHYJSm+d+Xqm+0lMW01tgbxf/kMQ2y5e +fI/JaHC9nj5+eSQrrLl8OHati4QQ6nV6ZQi3iolag5b2krKh95cihopVvV+6MWsb +9QhjADQLOHbIm1jypldrDBLNP4gDf4h81Ux6obL+/yenEVXsZtam/3G1gwrjihvC +K3KYuiwX73tQiHDjt0BxAoIBAQDTUo1QSNpGVwR0f4d32D9kUSS1Wh+6v/tDlmO4 +CIj9LaPIwnctLO9I3xH+YdHADnAiL2+86ymx9dNTlfz/Rci91BKOksRoJIdHngbh +/Opd2/PYNYE9ZWlo574QfE91UyRqOo786f4ydkygIFXZ0EUL1h4vwnA05TrpiQsU +0b33c9qruG/kj2yqv7ggeTB1gqHUV5ONLYOx0gJTVmRJ8TesiDjuR+2MhiX0TFz/ +yaqE+BlvK76UCqYA/UoVztQ6s99JxAQDx5FSZS0IaApjS1X0Ug8R8B73pXj/DHgI +NFFXA4EtcCelPa8rsup6K5B2B/+IZvqDE+he7Pz8atYIkTdDAoIBAQDLA5466roY +boaKIRv5UXlTNbm9SYqyWuk5q04dLgSnGX101rjDatjP1sTXHDutj2Caxv5wXvt9 +chm4xEU2qleeg55V+QskUrTZI0FxMI9TCJN/dmijteB7SLyaXpbVCwnkoIbv+Z9T +JKn8Dc0DjeBlTsGBx02fjeA1AMBFvmnKJ8z8pCmi/xx5IJmC4Yb/tkiZs2FzVPKa +qwaMdluIXQhjjQ5rnanHHHGwTo1XbiIY44D3akcay4Wad15Bos4eZBJ5kkMzJsxe +LS72BRqrpg2+rswWLYGoWdZ8lKxOCLmhzUm3aKLMyuBE1VyXWaID1dD3Cvg10UcU +ZeVAyZTGASuzAoIBAQCo4cVrjv1Zz3sQ+0YxIdHvguWaRS8sWPceGZcR4UhRRWMq +Mzd5E/sbOqKvsE8ndKJqv4xF544H2dSMbUamX4fBJTW7WVG7hM3UD1eizTQj1H84 +n/HsNhqaqvF7TU1XDx4AA1J4jywF+gLcyVqy2r4DapfvRS3VKAj/z/TaK5DsrFrq +d1h8yZ5nixZEZXWzw7U+9MpHvkMMHjCbl2RIr4z1xlvxGeyfLCoy5cBA71LmNzxk +WwpQV345NjM54HQ9gbiFxDANMTVTKuZlXHhIyFEmE1pV1Jd7BHQBKF7Y9661omhe +/s3AQd63RqIbBqvJmDTANsQncwZ2rmCz/VGVwS/vAoIBAQCjuQ5Bq41bKL+d5CfA +QMGcpqZdjlKplgGi/ZEonTUpYbg4ovYsux2ROD5U0LMcPJYHHNaT3zXorD0sMjUz +adrxvmnSId+ncn/g4brkQupDZ5Y0ZFTtOj+IjVd1EzWHKLjYxdEs9V5VWiswWBzv +cdr3OLV8KTRjqbKYbDujw3VjkAeT8/yUNDNNSEolMCaEUQhp1C2SKn7g126Xltgo +uAaM6nQnu+Xw6vKQ4jjAutQKyGb4oaAobtwXf47nVhnujnwLyTYiqXaFuOC3jdQ9 +e9R35FY4yIhZMeJ1ZB01bXnvkDSUEnHAwRhuyrc6ZBHyD9zPbuxP0rAFNkvVnPw7 +VzRbAoIBAFnytzhO1hSyExdG8XJzZWHwNt48sncq/SGIt3r7wW7x5jigU1agwyLc +ikaxjItdd1bt8XgGxiqKveKsVZ/Z/ZCdvS9h+l7xPWLRAVesQR3Pugynxz7bMyBo +3RWbPU2sxIxNEs5mGeqLS7qCOSFj/uX61H8yhjh4Wk15jPC96l+NTAZdTLhO5ko3 +m5xfJyly37Wl3L31F/EBwJDXbciQ73FfPIyYRtVThf0q/Xdt4IRsxpLnL/hC9EMk +tzGSVVlF/85cBUGjEow4eKpVcGvNUHZefOSSWXKiabZCqMZ6ucUjk29DU4IcLNZr +51XCua1iLx7dqivIxXwIbraXa7bE4Ew= +-----END PRIVATE KEY----- diff --git a/grpc/server/src/test/resources/ssl/serverCert.pem b/grpc/server/src/test/resources/ssl/serverCert.pem new file mode 100644 index 00000000000..c81d2dee4d3 --- /dev/null +++ b/grpc/server/src/test/resources/ssl/serverCert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE/DCCAuQCAQEwDQYJKoZIhvcNAQEFBQAwdDELMAkGA1UEBhMCVVMxEzARBgNV +BAgMCkNhbGlmb3JuaWExFDASBgNVBAcMC1NhbnRhIENsYXJhMQ8wDQYDVQQKDAZP +cmFjbGUxFTATBgNVBAsMDEhlbGlkb24tZ1JQQzESMBAGA1UEAwwJbG9jYWxob3N0 +MB4XDTE5MDMyODE4NDUyMVoXDTIwMDMyNzE4NDUyMVowFDESMBAGA1UEAwwJbG9j +YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA77mb+N4hpxyR +tYw2fxySrKAnk4HkMkanT9PMLRiuDKzaR+LS/xyTGm8CBY6146/aICExR6tF2GVt +H4R/RkHMma8MHrAkP9M4FSnDejs4JXjsaQW50P03Ja7keRAznpl/yYfMTVY1xuzR +NRoIuTQAizzsnaSqAk50E8S6Ok7XusqaVfW48ZZr8pasGHHzawp21Qo9pqME8JDm +TDZLQxM132VghzqTwdhPTk3k6LtKSKwBPuTkZYsu6UmyEUDJ+BYolqHKbbxIp8Ey +0WhyHbmeOPJMBz18iLIwnWpH83ajkHVtvwa3uxV4Cylv120J0VNQ5n+gYZbAj4mN +gOCbPwG0qN40CVZDKHlRuHQEJUOddTpqGqdc4N4/TOtZbAo1gJgCBSNgDoHvzwJ6 +RcNR2VF+79ydq8cMYAA3OKuOXsn2PUu/ceKeq9wekyVMwfZiOC/+HmlcWKPcmV1f +TXLpJV8L2lMj1RQL0ppK2viTN6dEracCXbu3vazDQSe8H7kQsyqLFXKq6/VkjcDV +D57uhMGIscVxXlLQWi6wIAu11LnjoT8Qk1RLDTcDjziN9xqd1H00k5dS0JKvp2vT +z43lFYcWmeLv4fJr7ruMKGp7oMjkVDO3F1x4ar6QtapHAgu8wq8Hf7VR3yqrbPYT +ezSwXp1kLDL4Vw5o0AlXqbRni9YgVFkCAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA +OmgIh3aic4uZl8w/NYVVpGNpydZW1BC75N+m6oCByZ7Q60IOBHZXQWwhYpQOTFEW +hDcJMXjpAnBRpEqYyZa0JzoN8IA7miOL7hTKyF9sqtxpfoBjq5OfvLXhuR/e/5Ca +27ydxonYlXADEz7TEiMFvbVngo+avvvWlevdYxL5iGYDNPmqxO6zGApPaSFO6uRb +GGT0UDwMDvR7ENk2B5yfLMqW0AiJQcz2dffsDCSjrLGYoCmhSLUX2CyLLvyrnPDE +BSr/exF31POfxCLK3b5rZmlO9c4WEe5JjM2d3lqjRmcz/bYnapqoBe1hF/KjvEjg +2erYxkSvypRk8/9LIf9KxWb/OoUR8sI0iFsNbzTlXpu2aXiIkZCY81g4mAx8BjOG +v2FwhmvhXyWkRVjCidaPnRG5KeaL4JYoFOwlGEDYRZRdtEo6/x3FeclFRH71muvF +uWFikNg8+1G2Umu/Dc5Q/l7OJPEBSLh5+VAxZgrXrlD4D10/LXv8DQs9CxQYFcd3 +Akj0NFM/IoTuNEgKXownTlNX2GW4npNDMaH/aOZO5nyPZi0FcEpkH8WCObBfyRRx +5ylFM1R/LgJrEkHCYUh7xpJkZe5AAScTUa7D299LFOgb+7raU3GqyfoxghVUS7DL +AhNYMDM6SgZAMBobjLb8EW1ecvmTiYH84qiC2rA3Wo4= +-----END CERTIFICATE----- diff --git a/grpc/server/src/test/resources/ssl/serverKey.pem b/grpc/server/src/test/resources/ssl/serverKey.pem new file mode 100644 index 00000000000..28b09fc2398 --- /dev/null +++ b/grpc/server/src/test/resources/ssl/serverKey.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDvuZv43iGnHJG1 +jDZ/HJKsoCeTgeQyRqdP08wtGK4MrNpH4tL/HJMabwIFjrXjr9ogITFHq0XYZW0f +hH9GQcyZrwwesCQ/0zgVKcN6OzgleOxpBbnQ/TclruR5EDOemX/Jh8xNVjXG7NE1 +Ggi5NACLPOydpKoCTnQTxLo6Tte6yppV9bjxlmvylqwYcfNrCnbVCj2mowTwkOZM +NktDEzXfZWCHOpPB2E9OTeTou0pIrAE+5ORliy7pSbIRQMn4FiiWocptvEinwTLR +aHIduZ448kwHPXyIsjCdakfzdqOQdW2/Bre7FXgLKW/XbQnRU1Dmf6BhlsCPiY2A +4Js/AbSo3jQJVkMoeVG4dAQlQ511Omoap1zg3j9M61lsCjWAmAIFI2AOge/PAnpF +w1HZUX7v3J2rxwxgADc4q45eyfY9S79x4p6r3B6TJUzB9mI4L/4eaVxYo9yZXV9N +cuklXwvaUyPVFAvSmkra+JM3p0StpwJdu7e9rMNBJ7wfuRCzKosVcqrr9WSNwNUP +nu6EwYixxXFeUtBaLrAgC7XUueOhPxCTVEsNNwOPOI33Gp3UfTSTl1LQkq+na9PP +jeUVhxaZ4u/h8mvuu4woanugyORUM7cXXHhqvpC1qkcCC7zCrwd/tVHfKqts9hN7 +NLBenWQsMvhXDmjQCVeptGeL1iBUWQIDAQABAoICAQCm/CCke37vnBv3XWL8qheV +fb3zpzAgwUx8c6ku5JkgpSsrldmp8OqbKCY9XspefrkvVk+wf6KYeRZJNQ8/DkiJ +6QBFRCcL+oJ/Y2cR9rxGhfQPt/hWlnvamPf3SIzcdkooRJrQaDPvjDiyh575+zpB +wz+DFBeEoceDma6rPQ5SxBw7sn2Qc1pgUgowwPdxYYmjHo4RTE79j8GL5uoIIBKP +uVtC90/DYHzlq6a/Yu78rivz9pcOEKStxjLuh7XoRO8i9jI+vD/T8uUGL34GLK0F +N+ozxOXBhnYMwOK7ubF3yASZYwGKpQrIo6WPZxnGoQWiedZrBArAMW3LDym0+ugu +u9qLIT1IpYDa8C7QZJKcyuImMBHTBHLRnCmmlUv6NEoKpREvE30+xBfcLk++ibTx +sCV3u4WPP2DLJAnvPsLoNNZDlNkmZGCjDyqPRP58SCiwA2AwQUzjQ5Ef0tKab/el +cKdMihccyznleZK6jF051Km5ioQZc36sHK+0FHN/BhfQTedYbsuPou8ySfLNoH7Y ++0qhSiU64IhidMCrALzxV3gIxQ3fuFqiCcof/BydahnEgw2U/5GJ33q02Ss9FNfO +sZksMxCaAguJeWnfnwPgSPIzPGkSeUfRgsSfNlQUHKMURFyaTtPswm0gbD9/2GXd +OMN6Ch8t+jthnb1q3mc4cQKCAQEA/Wx7qV2pzJPeToUOmOPz3Vnrp8jAg0/M3S1S +QuybUzVihaJuXTamSWR01qggbvDac8lH13F3NuZJ39IGXu/2T7HN/SZ8DO5ujoB2 +bD/GvSHngmbdRJFMSRCjYYnBsRI+Jc/0DOWxZs9Za22DgPUo9N3gZVYBrQq8/oSw +IZxQnon86rOW8rrwmBQ3lwIlyk7DkmHg+gAuYEbP8xWcasA16d0Iox17hLCTSd9k +Mz2rdtq1QALoi6KMGwxAGna67AChPdWrPFDNlBiVfalcg+KNTDTrTr7kk8yJW7mA +lFFK/DEsEXMtK2RFLPyQvA6d6XCAwwqEWPNgLkpY0YRdjDMgrQKCAQEA8il57RHD +XY6jTWWmBBcEsc7dLJvgNkubfocnxpYT+WyNW0XaqYwFUEz3iG+Tftil2R/N45vE +wzE2XmDY925LvcyCYjz0yk8j46HTNfDixEj8vmQnOCsAuLJZ8catY/O8iDyPlB5s +3tAESFJyhu8DZIcpo+Twe/c2l6goq1vkp6g15osEKCllRPplJNYgmaFLsmjNHO45 +FjkRwQD9J/rKY0CVhpsZ6dmp3yAFKSD/P6Ih4Fk4duaP476aWPTSMk4uM9CrReJw +slSMgpSQc58Z4Lb6t/xi1363a8NKNqYECNHp+Ha3z10pn3VHUR1HsB+Uoe30Ao2T +6rlEhT5CObJ73QKCAQAk1QCAFBnant8fgF0nwXGvCSOj5hx8uTtoJe8fxO2qQUcJ +bxslNw0h+5Szogncteq6C1YCrP4Ln5q7CfZdE/kzif8LPO2on3q4AVm0z+wdjvDr +1fFwxsFUmTN1uKTLDIC11Cx/N176N0aiw27qbjc+p4S2lbJ2qOng//3tVJ+u+xUk +M2fL7XmMUNvneatuZNYtz0X4b9BNs90DUm/CYB811ycrNdy+4z707J/OhrcF9jGD +lSihKsW8JKKodfsi09CbgRN7F69ZR91qPxQwItwwaDyO3FWyWXbZOvPCGrKQRsFF +4/x7ov4e1LJgcqtmwzAjqMKlfvHKqImSIe/KsZ+dAoIBAQDce7MNjHzKdBJvZ7tu +Qd7+XZC3Cv1rsg9Tdo9amk55bMKPddyPPnOgF1PRH3W1ql/mfrShJbRS3D3aVGd3 +cS75EnksJL/ka4YLvc1mcXqmqKWmW93xoAW6JHkMT46TUNo5WJqJi+IoRFGB379y +56+5Ge9p0Aq9c+p6zpqo2rQniln5HF2ei4Iu+aSZk3R/LIKVYCJQ0v2bJD7r8ZCN +lCQtliIOOq2vY4TtKGRKfEaqk14wvEtSxr3/6Az0LYQsVU5v0u/+Yxuk4qJESivz +ecq/9Rn15o/AERnDvj2V4DYQKb/Tf5P/85zwGIHkcrpQu5BClzryECG7NxASGQuH +KODpAoIBAQCoK8YZmkAMSrYpMltIupE7OgHIU60glKnBY3xOqkK7ut18M8AMITEW +8SlYKx1PPi8TAmQA5NWfKOrxjRomMaVat2VPPnX5Q6EebYqShAXbprbITxumRMW/ +/j4CzUu8q0IHDkeZ9Yi3tAYm3KQm2anEL92v+Gn/0HKNDQIyrWeUxHRXyDjH5pYZ +ZJxsaqUKnB9TS8aoXT4FahymDxkmbx/IQGYIesFE8dgSpVLukmCouYzAMPoTrjtP +6FMY3daxuSypS7gLR8aMCvAqn/f8BfURWSvxjoyQ5NF1k6LLygkD9fheZOr51W0u +U2AVsEYJOGQvHr0rVQF4Gwp5/GF8xUXW +-----END PRIVATE KEY----- diff --git a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/HikariCPBackedDataSourceExtension.java b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/HikariCPBackedDataSourceExtension.java index 923e35b7711..fba2c465b96 100644 --- a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/HikariCPBackedDataSourceExtension.java +++ b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/HikariCPBackedDataSourceExtension.java @@ -239,7 +239,7 @@ private static Entry toProperties(final DataSourceDefinition assert name != null; final String value = propertyString.substring(equalsIndex + 1); assert value != null; - properties.setProperty("dataSource." + name, value); + properties.setProperty("dataSource." + name.trim(), value.trim()); } } @@ -327,34 +327,34 @@ private static Entry toProperties(final DataSourceDefinition final String databaseName = dsd.databaseName(); assert databaseName != null; if (!databaseName.isEmpty()) { - properties.setProperty("datasource.databaseName", databaseName); + properties.setProperty("dataSource.databaseName", databaseName); } // description -> dataSource.description (standard DataSource property) final String description = dsd.description(); assert description != null; if (!description.isEmpty()) { - properties.setProperty("datasource.description", description); + properties.setProperty("dataSource.description", description); } // portNumber -> dataSource.portNumber (standard DataSource property) final int portNumber = dsd.portNumber(); if (portNumber >= 0) { - properties.setProperty("datasource.portNumber", String.valueOf(portNumber)); + properties.setProperty("dataSource.portNumber", String.valueOf(portNumber)); } // serverName -> dataSource.serverName (standard DataSource property) final String serverName = dsd.serverName(); assert serverName != null; if (!serverName.isEmpty()) { - properties.setProperty("datasource.serverName", serverName); + properties.setProperty("dataSource.serverName", serverName); } // url -> dataSource.url (standard DataSource property) final String url = dsd.url(); assert url != null; if (!url.isEmpty()) { - properties.setProperty("datasource.url", url); + properties.setProperty("dataSource.url", url); } return returnValue; diff --git a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/config/package-info.java b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/config/package-info.java index e8ca5483138..f394401b458 100644 --- a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/config/package-info.java +++ b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/config/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,15 @@ */ /** - * Provides classes and interfaces marrying MicroProfile Config - * constructs and {@link + * Provides classes and interfaces marrying MicroProfile Config constructs and {@link * io.helidon.service.configuration.api.ServiceConfiguration} - * constructs for the Hikari Connection Pool. + * constructs for the Hikari Connection Pool. + * + * @see + * io.helidon.integrations.datasource.hikaricp.cdi.config.HikariCP */ package io.helidon.integrations.datasource.hikaricp.cdi.config; diff --git a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/package-info.java b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/package-info.java index f20f96ab4f9..5b744133f25 100644 --- a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/package-info.java +++ b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,10 @@ /** * CDI integration for the Hikari - * connection pool. + * href="https://github.com/brettwooldridge/HikariCP/blob/HikariCP-2.7.8/README.md#-hikaricpits-fasterhikari-hikal%C4%93-origin-japanese-light-ray" + * target="_parent">Hikari connection pool. + * + * @see + * io.helidon.integrations.datasource.hikaricp.cdi.HikariCPBackedDataSourceExtension */ package io.helidon.integrations.datasource.hikaricp.cdi; diff --git a/integrations/cdi/eclipselink-cdi/README.adoc b/integrations/cdi/eclipselink-cdi/README.adoc new file mode 100644 index 00000000000..c5162e4fa9a --- /dev/null +++ b/integrations/cdi/eclipselink-cdi/README.adoc @@ -0,0 +1,21 @@ += Helidon Eclipselink CDI Integration + +The Helidon Eclipselink Integration project contains a +https://www.eclipse.org/eclipselink/api/2.7/org/eclipse/persistence/platform/server/ServerPlatform.html[`ServerPlatform`] +implementation for use with Helidon's link:../jpa-cdi[JPA CDI integration]. + +IMPORTANT: Please note that this feature is currently experimental and + not suitable for production use. + += Usage + +Ensure that this library is on your application's runtime classpath, +and that you are actually using Eclipselink as your JPA provider. +Then ensure that the following vendor-specific property is present in +your `META-INF/persistence.xml` resource: + +[source,xml] +---- + +---- + diff --git a/integrations/cdi/eclipselink-cdi/pom.xml b/integrations/cdi/eclipselink-cdi/pom.xml new file mode 100644 index 00000000000..ae5d6e6363a --- /dev/null +++ b/integrations/cdi/eclipselink-cdi/pom.xml @@ -0,0 +1,94 @@ + + + + 4.0.0 + + io.helidon.integrations.cdi + helidon-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-cdi-eclipselink + Helidon CDI Integrations Eclipselink + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.slf4j + slf4j-simple + test + + + + + org.jboss + jandex + runtime + + + + + javax.enterprise + cdi-api + provided + + + javax.transaction + javax.transaction-api + provided + + + + + org.eclipse.persistence + org.eclipse.persistence.jpa + compile + + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + process-classes + + + + + + diff --git a/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/CDISEPlatform.java b/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/CDISEPlatform.java new file mode 100644 index 00000000000..3d6d06f152e --- /dev/null +++ b/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/CDISEPlatform.java @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.eclipselink; + +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.Objects; +import java.util.concurrent.Executor; + +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.spi.CDI; +import javax.enterprise.util.AnnotationLiteral; +import javax.inject.Qualifier; +import javax.management.MBeanServer; +import javax.transaction.TransactionManager; + +import org.eclipse.persistence.platform.server.JMXServerPlatformBase; +import org.eclipse.persistence.platform.server.ServerPlatformBase; // for javadoc only +import org.eclipse.persistence.sessions.DatabaseSession; +import org.eclipse.persistence.sessions.JNDIConnector; +import org.eclipse.persistence.transaction.JTATransactionController; + +/** + * A {@link JMXServerPlatformBase} that arranges things such that CDI, + * not JNDI, will be used to acquire a {@link TransactionManager} and + * {@link MBeanServer}. + * + *

Most users will not use this class directly, but will supply its + * fully-qualified name as the value of the {@code + * eclipselink.target-server} Eclipselink JPA extension property + * in a {@code + * META-INF/persistence.xml} file.

+ * + *

For example:

+ * + *
<property name="eclipselink.target-server"
+ *          value="io.helidon.integrations.cdi.eclipselink.CDISEPlatform"/>
+ * + * @see #getExternalTransactionControllerClass() + */ +public class CDISEPlatform extends JMXServerPlatformBase { + + + /* + * Constructors. + */ + + + /** + * Creates a {@link CDISEPlatform}. + * + * @param session the {@link DatabaseSession} this platform will + * wrap; must not be {@code null} + * + * @see JMXServerPlatformBase#JMXServerPlatformBase(DatabaseSession) + */ + public CDISEPlatform(final DatabaseSession session) { + super(session); + } + + + /* + * Instance methods. + */ + + + /** + * Sets the name of the platform. + * + *

The format of the platform name is subject to change without + * notice.

+ * + * @see #getServerNameAndVersion() + */ + @Override + protected void initializeServerNameAndVersion() { + this.serverNameAndVersion = this.getClass().getSimpleName(); + } + + /** + * Uses CDI to find a relevant {@link MBeanServer}, caches it, and + * returns it. + * + *

This method may return {@code null}.

+ * + *

Overrides of this method may return {@code null}.

+ * + *

If there is no such {@link MBeanServer} then the {@link + * MBeanServer} found and cached by the {@linkplain + * JMXServerPlatformBase#getMBeanServer() superclass + * implementation of this method} is returned instead.

+ * + * @return an {@link MBeanServer}, or {@code null} + */ + @Override + public MBeanServer getMBeanServer() { + if (this.mBeanServer == null) { + final CDI cdi = CDI.current(); + if (cdi != null) { + Instance instance = cdi.select(MBeanServer.class, Eclipselink.Literal.INSTANCE); + assert instance != null; + if (instance.isUnsatisfied()) { + instance = cdi.select(MBeanServer.class); + } + if (!instance.isUnsatisfied()) { + final MBeanServer mBeanServer = instance.get(); + assert mBeanServer != null; + this.mBeanServer = mBeanServer; + } + } + } + return super.getMBeanServer(); + } + + /** + * Uses CDI to find a relevant {@link Executor} whose {@link + * Executor#execute(Runnable)} method will be used to submit the + * supplied {@link Runnable}. + * + *

If there is no such {@link Executor}, then the {@linkplain + * JMXServerPlatformBase#launchContainerRunnable(Runnable) + * superclass implementation of this method} is used instead.

+ * + * @param runnable the {@link Runnable} to launch; should not be + * {@code null} + * + * @see JMXServerPlatformBase#launchContainerRunnable(Runnable) + */ + @Override + public void launchContainerRunnable(final Runnable runnable) { + if (runnable == null) { + super.launchContainerRunnable(null); + } else { + final CDI cdi = CDI.current(); + if (cdi == null) { + super.launchContainerRunnable(runnable); + } else { + Instance executorInstance = cdi.select(Executor.class, Eclipselink.Literal.INSTANCE); + assert executorInstance != null; + if (executorInstance.isUnsatisfied()) { + executorInstance = cdi.select(Executor.class); + } + assert executorInstance != null; + final Executor executor; + if (executorInstance.isUnsatisfied()) { + executor = null; + } else { + executor = executorInstance.get(); + } + if (executor != null) { + executor.execute(runnable); + } else { + super.launchContainerRunnable(runnable); + } + } + } + } + + /** + * Overrides the {@link + * ServerPlatformBase#initializeExternalTransactionController()} + * method to {@linkplain #disableJTA() disable JTA} if there is no + * {@link TransactionManager} bean present in CDI before invoking + * the {@linkplain + * ServerPlatformBase#initializeExternalTransactionController() + * superclass implementation}. + * + * @see ServerPlatformBase#initializeExternalTransactionController() + */ + @Override + public void initializeExternalTransactionController() { + final CDI cdi = CDI.current(); + if (cdi == null || cdi.select(TransactionManager.class).isUnsatisfied()) { + this.disableJTA(); + } + super.initializeExternalTransactionController(); + } + + /** + * Returns a non-{@code null} {@link Class} that extends {@link + * org.eclipse.persistence.transaction.AbstractTransactionController}, + * namely {@link TransactionController}. + * + * @return a non-{@code null} {@link Class} that extends {@link + * org.eclipse.persistence.transaction.AbstractTransactionController} + * + * @see + * org.eclipse.persistence.transaction.AbstractTransactionController + * + * @see TransactionController + */ + @Override + public Class getExternalTransactionControllerClass() { + if (this.externalTransactionControllerClass == null) { + this.externalTransactionControllerClass = TransactionController.class; + } + return this.externalTransactionControllerClass; + } + + /** + * Returns {@link JNDIConnector#UNDEFINED_LOOKUP} when invoked. + * + * @return {@link JNDIConnector#UNDEFINED_LOOKUP} + */ + @Override + public final int getJNDIConnectorLookupType() { + return JNDIConnector.UNDEFINED_LOOKUP; + } + + + /* + * Inner and nested classes. + */ + + + /** + * A {@link JTATransactionController} whose {@link + * #acquireTransactionManager()} method uses CDI, not JNDI, to + * return a {@link TransactionManager} instance. + * + * @see #acquireTransactionManager() + * + * @see JTATransactionController + * + * @see CDISEPlatform#getExternalTransactionControllerClass() + */ + public static class TransactionController extends JTATransactionController { + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link TransactionController}. + */ + public TransactionController() { + super(); + } + + + /* + * Instance methods. + */ + + + /** + * Returns a non-{@code null} {@link TransactionManager}. + * + *

This method never returns {@code null}.

+ * + * @return a non-{@code null} {@link TransactionManager} + * + * @exception NullPointerException if in exceedingly rare + * specification-violating cases the return value of {@link + * CDI#current()} is {@code null}, or if the {@link + * Instance#get()} method returns {@code null} + * + * @exception RuntimeException if the {@link Instance#get()} + * method encounters an error providing a {@link + * TransactionManager} + * + * @see JTATransactionController#acquireTransactionManager() + */ + @Override + protected TransactionManager acquireTransactionManager() { + return Objects.requireNonNull(CDI.current().select(TransactionManager.class).get()); + } + + } + + /** + * A {@link Qualifier} used to designate various things as being + * related to Eclipselink in some way. + * + *

The typical end user will apply this annotation to an + * implementation of {@link Executor} if she wants that particular + * {@link Executor} used by the {@link + * CDISEPlatform#launchContainerRunnable(Runnable)} method.

+ * + *

The {@link Eclipselink} qualifier may also be used to + * annotate an implementation of {@link MBeanServer} for use by + * the {@link CDISEPlatform#getMBeanServer()} method.

+ * + * @see CDISEPlatform#launchContainerRunnable(Runnable) + * + * @see CDISEPlatform#getMBeanServer() + */ + @Documented + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) + public @interface Eclipselink { + + /** + * An {@link AnnotationLiteral} that implements {@link + * Eclipselink}. + */ + class Literal extends AnnotationLiteral implements Eclipselink { + + /** + * The single instance of the {@link Literal} class. + */ + public static final Eclipselink INSTANCE = new Literal(); + + /** + * The version of this class for Java serialization + * purposes. + */ + private static final long serialVersionUID = 1L; + + } + + } + +} diff --git a/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/package-info.java b/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/package-info.java new file mode 100644 index 00000000000..44f3ad8774c --- /dev/null +++ b/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 for working with Eclipselink in CDI. + * + * @see io.helidon.integrations.cdi.eclipselink.CDISEPlatform + */ +package io.helidon.integrations.cdi.eclipselink; diff --git a/integrations/cdi/jpa-cdi/README.adoc b/integrations/cdi/jpa-cdi/README.adoc new file mode 100644 index 00000000000..e0c3dc8face --- /dev/null +++ b/integrations/cdi/jpa-cdi/README.adoc @@ -0,0 +1,30 @@ += Helidon JPA CDI Integration + +The Helidon JPA CDI Integration project performs the +provider-independent work of integrating JPA into standalone CDI +applications (including those based on Helidon MicroProfile). It is +one of several projects that together make up overall JPA support for +standalone CDI applications. + +To function properly, this project also requires: + +* a CDI-provider-specific counterpart, such as the `jpa-weld` project + found elsewhere in this git repository +* a JPA provider implementation, such as Eclipselink +* a JPA-provider-specific library to assist the JPA provider in + determining what kind of environment it is running in, such as the + `eclipselink-cdi` project found elsewhere in this git repository +* a library capable of integrating `DataSource`s into CDI, such as the + `datasource-hikaricp` project found elsewhere in this git repository +* a suitable JDBC-compliant database driver library + +IMPORTANT: Please note that this feature is currently experimental and + not suitable for production use. + +== Installation + +Ensure that the Helidon JPA CDI Integration project and its runtime +dependencies are present on your application's runtime classpath. + +Please see the `examples/integrations/cdi/jpa` project found elsewhere +in this git repository for a working `pom.xml` file. diff --git a/integrations/cdi/jpa-cdi/pom.xml b/integrations/cdi/jpa-cdi/pom.xml new file mode 100644 index 00000000000..d6c6d3cb3ea --- /dev/null +++ b/integrations/cdi/jpa-cdi/pom.xml @@ -0,0 +1,159 @@ + + + + 4.0.0 + + io.helidon.integrations.cdi + helidon-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-cdi-jpa + Helidon CDI Integrations JPA + + + -syntax + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.slf4j + slf4j-simple + test + + + + + org.jboss + jandex + runtime + + + + + jakarta.persistence + jakarta.persistence-api + provided + + + javax.annotation + javax.annotation-api + provided + + + javax.enterprise + cdi-api + provided + + + javax.transaction + javax.transaction-api + provided + + + + + + + org.jvnet.jaxb2.maven2 + maven-jaxb2-plugin + + + Generate persistence.xml Java objects + + generate + + + io.helidon.integrations.cdi.jpa.jaxb + true + + + + jakarta.persistence + jakarta.persistence-api + javax/persistence/persistence_2_2.xsd + + + + false + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + process-classes + + + + + + + + + xmlbind + + [9,) + false + + + + javax.xml.bind + jaxb-api + compile + + + javax.activation + javax.activation-api + runtime + + + com.sun.xml.bind + jaxb-core + runtime + + + com.sun.xml.bind + jaxb-impl + runtime + + + + + diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/BeanManagerBackedDataSourceProvider.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/BeanManagerBackedDataSourceProvider.java new file mode 100644 index 00000000000..8d87466f5be --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/BeanManagerBackedDataSourceProvider.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.jpa; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.literal.NamedLiteral; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.inject.Inject; +import javax.sql.DataSource; + +/** + * A {@link PersistenceUnitInfoBean.DataSourceProvider} implementation + * that uses a {@link BeanManager} to look up relevant {@link + * DataSource}s. + * + * @see PersistenceUnitInfoBean.DataSourceProvider + */ +@ApplicationScoped +class BeanManagerBackedDataSourceProvider implements PersistenceUnitInfoBean.DataSourceProvider { + + + /* + * Instance fields. + */ + + + /** + * The {@link BeanManager} to use to look up relevant {@link + * DataSource}s. + * + *

This field may be {@code null} in which case the {@link + * #getDataSource(boolean, boolean, String)} method will throw an + * {@link IllegalStateException}.

+ * + * @see #BeanManagerBackedDataSourceProvider(BeanManager) + */ + private final BeanManager beanManager; + + + /* + * Constructors. + */ + + + /** + * Creates a new nonfunctional {@link + * BeanManagerBackedDataSourceProvider}. + * + *

This constructor exists only to conform to section + * 3.15 of the CDI specification and for no other purpose.

+ * + * @deprecated Please use the {@link + * #BeanManagerBackedDataSourceProvider(BeanManager)} constructor + * instead. + */ + @Deprecated + BeanManagerBackedDataSourceProvider() { + this(null); + } + + /** + * Creates a new {@link BeanManagerBackedDataSourceProvider}. + * + * @param beanManager the {@link BeanManager} to use; may be + * {@code null}, but shouldn't, and if so the {@link + * #getDataSource(boolean, boolean, String)} method will throw an + * {@link IllegalStateException} + */ + @Inject + BeanManagerBackedDataSourceProvider(final BeanManager beanManager) { + super(); + this.beanManager = beanManager; + } + + + /* + * Instance methods. + */ + + + /** + * Supplies a {@link DataSource} according to rules defined by the + * JPA specification and portions of the Java EE specification. + * + *

Implementations of this method are permitted to return + * {@code null}.

+ * + * @param jta if {@code true}, the {@link DataSource} that is + * returned may be enrolled in JTA-compliant transactions; this + * implementation ignores this parameter + * + * @param useDefaultJta if {@code true}, and if the {@code jta} + * parameter value is {@code true}, the supplied {@code + * dataSourceName} may be ignored and a default {@link DataSource} + * eligible for enrolling in JTA-compliant transactions will be + * returned if possible + * + * @param dataSourceName the name of the {@link DataSource} to + * return; may be {@code null} + * + * @return an appropriate {@link DataSource}, or {@code null} + * + * @see PersistenceUnitInfoBean#getJtaDataSource() + * + * @see PersistenceUnitInfoBean#getNonJtaDataSource() + * + * @exception IllegalStateException if this {@link + * BeanManagerBackedDataSourceProvider} was created with a {@code + * null} {@link BeanManager} + */ + @Override + public DataSource getDataSource(final boolean jta, + final boolean useDefaultJta, + final String dataSourceName) { + if (this.beanManager == null) { + throw new IllegalStateException("beanManager == null"); + } + final Bean bean; + if (dataSourceName == null) { + if (useDefaultJta) { + bean = this.beanManager.resolve(this.beanManager.getBeans(DataSource.class)); + } else { + bean = null; + } + } else { + bean = this.beanManager.resolve(this.beanManager.getBeans(DataSource.class, NamedLiteral.of(dataSourceName))); + } + final DataSource returnValue; + if (bean == null) { + returnValue = null; + } else { + returnValue = + (DataSource) this.beanManager.getReference(bean, + DataSource.class, + this.beanManager.createCreationalContext(bean)); + } + return returnValue; + } +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java new file mode 100644 index 00000000000..0382d7583ba --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java @@ -0,0 +1,803 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.jpa; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Priority; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.CreationException; +import javax.enterprise.inject.Vetoed; +import javax.enterprise.inject.literal.NamedLiteral; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.AnnotatedType; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.ProcessAnnotatedType; +import javax.enterprise.inject.spi.WithAnnotations; +import javax.inject.Singleton; +import javax.persistence.Converter; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.MappedSuperclass; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceProperty; +import javax.persistence.PersistenceUnit; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import javax.persistence.spi.PersistenceUnitInfo; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import io.helidon.integrations.cdi.jpa.PersistenceUnitInfoBean.DataSourceProvider; +import io.helidon.integrations.cdi.jpa.jaxb.Persistence; + +import static javax.interceptor.Interceptor.Priority.LIBRARY_AFTER; + +/** + * A {@linkplain Extension portable extension} normally instantiated + * by the Java {@linkplain java.util.ServiceLoader service provider + * infrastructure} that integrates the provider-independent parts of + * JPA into CDI. + * + * @see PersistenceUnitInfoBean + */ +public class JpaExtension implements Extension { + + + /* + * Static fields. + */ + + + /** + * The {@link Logger} for use by all instances of this class. + * + *

This field is never {@code null}.

+ */ + private static final Logger LOGGER = Logger.getLogger(JpaExtension.class.getName(), "messages"); + + + /* + * Instance fields. + */ + + + /** + * A {@link Map} of {@link PersistenceUnitInfoBean} instances that + * were created by the {@link + * #gatherImplicitPersistenceUnits(ProcessAnnotatedType, + * BeanManager)} observer method, indexed by the names of + * persistence units. + * + *

This field is never {@code null}.

+ * + *

The contents of this field are used only when no explicit + * {@link PersistenceUnitInfo} beans are otherwise available in + * the container.

+ * + * @see #gatherImplicitPersistenceUnits(ProcessAnnotatedType, BeanManager) + * + * @see #afterBeanDiscovery(AfterBeanDiscovery, BeanManager) + */ + private final Map implicitPersistenceUnits; + + /** + * A {@link Map} of {@link Set}s of {@link Class}es whose keys are + * persistence unit names and whose values are {@link Set}s of + * {@link Class}es discovered by CDI (and hence consist of + * unlisted classes in the sense that they might not be found in + * any {@link PersistenceUnitInfo}). + * + *

Such {@link Class}es, of course, might not have been weaved + * appropriately by the relevant {@link PersistenceProvider}.

+ * + *

This field is never {@code null}.

+ */ + private final Map>> unlistedManagedClassesByPersistenceUnitNames; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link JpaExtension}. + */ + public JpaExtension() { + super(); + this.unlistedManagedClassesByPersistenceUnitNames = new HashMap<>(); + this.implicitPersistenceUnits = new HashMap<>(); + } + + + /* + * Instance methods. + */ + + + /** + * Looks for type-level {@link PersistenceContext} annotations + * that have at least one {@link PersistenceProperty} annotation + * {@linkplain PersistenceContext#properties() associated with} + * them and uses them to define persistence units, potentially + * preventing the need for {@code META-INF/persistence.xml} + * processing. + * + * @param event the {@link ProcessAnnotatedType} event occurring; + * may be {@code null} in which case no action will be taken + * + * @param beanManager the {@link BeanManager} in effect; may be + * {@code null} in which case no action will be taken + * + * @see PersistenceContext + * + * @see PersistenceProperty + * + * @see PersistenceUnitInfoBean + */ + private void gatherImplicitPersistenceUnits(@Observes + @WithAnnotations({ + PersistenceContext.class // yes, PersistenceContext, not PersistenceUnit + }) + final ProcessAnnotatedType event, + final BeanManager beanManager) { + final String cn = JpaExtension.class.getName(); + final String mn = "gatherImplicitPersistenceUnits"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, beanManager}); + } + + if (event != null && beanManager != null) { + final AnnotatedType annotatedType = event.getAnnotatedType(); + if (annotatedType != null + && !annotatedType.isAnnotationPresent(Vetoed.class)) { + final Set persistenceContexts = + annotatedType.getAnnotations(PersistenceContext.class); + if (persistenceContexts != null && !persistenceContexts.isEmpty()) { + for (final PersistenceContext persistenceContext : persistenceContexts) { + assert persistenceContext != null; + final PersistenceProperty[] persistenceProperties = persistenceContext.properties(); + if (persistenceProperties != null && persistenceProperties.length > 0) { + final String persistenceUnitName = persistenceContext.unitName(); + assert persistenceUnitName != null; + PersistenceUnitInfoBean persistenceUnit = this.implicitPersistenceUnits.get(persistenceUnitName); + if (persistenceUnit == null) { + final String jtaDataSourceName; + if (persistenceUnitName.isEmpty()) { + jtaDataSourceName = null; + } else { + jtaDataSourceName = persistenceUnitName; + } + final Class javaClass = annotatedType.getJavaClass(); + assert javaClass != null; + URL persistenceUnitRoot = null; + final ProtectionDomain pd = javaClass.getProtectionDomain(); + if (pd != null) { + final CodeSource cs = pd.getCodeSource(); + if (cs != null) { + persistenceUnitRoot = cs.getLocation(); + } + } + final Properties properties = new Properties(); + for (final PersistenceProperty persistenceProperty : persistenceProperties) { + assert persistenceProperty != null; + final String persistencePropertyName = persistenceProperty.name(); + assert persistencePropertyName != null; + if (!persistencePropertyName.isEmpty()) { + properties.setProperty(persistencePropertyName, persistenceProperty.value()); + } + } + persistenceUnit = + new PersistenceUnitInfoBean(persistenceUnitName, + persistenceUnitRoot, + null, + new BeanManagerBackedDataSourceProvider(beanManager), + properties); + this.implicitPersistenceUnits.put(persistenceUnitName, persistenceUnit); + } + } + } + } + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Tracks {@linkplain Converter converters}, {@linkplain Entity + * entities}, {@linkplain Embeddable embeddables} and {@linkplain + * MappedSuperclass mapped superclasses} that were auto-discovered + * by CDI bean discovery, and makes sure that they are not + * actually CDI beans, since according to the JPA specification + * they cannot be. + * + *

This method also keeps track of these classes as potential + * "unlisted classes" to be used by a {@linkplain + * PersistenceUnitInfo persistence unit} if its {@linkplain + * PersistenceUnitInfo#excludeUnlistedClasses()} method returns + * {@code false}.

+ * + * @param event the event describing the {@link AnnotatedType} + * being processed; may be {@code null} in which case no action + * will be taken + * + * @see Converter + * + * @see Embeddable + * + * @see Entity + * + * @see MappedSuperclass + * + * @see PersistenceUnitInfo#excludeUnlistedClasses() + */ + private void discoverManagedClasses(@Observes + @WithAnnotations({ + Converter.class, + Embeddable.class, + Entity.class, + MappedSuperclass.class + }) + final ProcessAnnotatedType event) { + final String cn = JpaExtension.class.getName(); + final String mn = "discoverManagedClasses"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, event); + } + + if (event != null) { + final AnnotatedType annotatedType = event.getAnnotatedType(); + if (annotatedType != null && !annotatedType.isAnnotationPresent(Vetoed.class)) { + this.assignManagedClassToPersistenceUnit(annotatedType.getAnnotations(PersistenceContext.class), + annotatedType.getAnnotations(PersistenceUnit.class), + annotatedType.getJavaClass()); + event.veto(); // managed classes can't be beans + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Given {@link Set}s of {@link PersistenceContext} and {@link + * PersistenceUnit} annotations that will be used for their {@code + * unitName} elements only, associates the supplied {@link Class} + * with the persistence units implied by the annotations. + * + * @param persistenceContexts a {@link Set} of {@link + * PersistenceContext}s whose {@link PersistenceContext#unitName() + * unitName} elements identify persistence units; may be {@code + * null} or {@linkplain Collection#isEmpty() empty} + * + * @param persistenceUnits a {@link Set} of {@link + * PersistenceUnit}s whose {@link PersistenceUnit#unitName() + * unitName} elements identify persistence units; may be {@code + * null} or {@linkplain Collection#isEmpty() empty} + * + * @param c the {@link Class} to associate; may be {@code null} in + * which case no action will be taken + * + * @see PersistenceContext + * + * @see PersistenceUnit + */ + private void assignManagedClassToPersistenceUnit(final Set persistenceContexts, + final Set persistenceUnits, + final Class c) { + final String cn = JpaExtension.class.getName(); + final String mn = "assignManagedClassToPersistenceUnit"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {persistenceContexts, persistenceUnits, c}); + } + + if (c != null) { + boolean processed = false; + if (persistenceContexts != null && !persistenceContexts.isEmpty()) { + for (final PersistenceContext persistenceContext : persistenceContexts) { + if (persistenceContext != null) { + final String name = persistenceContext.unitName(); // yes, unitName + assert name != null; + if (!name.isEmpty()) { + processed = true; + addUnlistedManagedClass(name, c); + } + } + } + } + if (persistenceUnits != null && !persistenceUnits.isEmpty()) { + for (final PersistenceUnit persistenceUnit : persistenceUnits) { + if (persistenceUnit != null) { + final String name = persistenceUnit.unitName(); // yes, unitName + assert name != null; + if (!name.isEmpty()) { + processed = true; + addUnlistedManagedClass(name, c); + } + } + } + } + if (!processed) { + addUnlistedManagedClass("", c); + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Given a {@link Class} and a name of a persistence unit, + * associates the {@link Class} with that persistence unit as a + * member of its list of governed classes. + * + * @param name the name of the persistence unit in question; may + * be {@code null} in which case the empty string ({@code ""}) + * will be used instead + * + * @param managedClass the {@link Class} to associate; may be + * {@code null} in which case no action will be taken + * + * @see PersistenceUnitInfo#getManagedClassNames() + */ + private void addUnlistedManagedClass(String name, final Class managedClass) { + final String cn = JpaExtension.class.getName(); + final String mn = "addUnlistedManagedClass"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {name, managedClass}); + } + + if (managedClass != null) { + if (name == null) { + name = ""; + } + if (!name.isEmpty()) { + Set> unlistedManagedClasses = this.unlistedManagedClassesByPersistenceUnitNames.get(name); + if (unlistedManagedClasses == null) { + unlistedManagedClasses = new HashSet<>(); + this.unlistedManagedClassesByPersistenceUnitNames.put(name, unlistedManagedClasses); + } + unlistedManagedClasses.add(managedClass); + } + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Converts {@code META-INF/persistence.xml} resources into {@link + * PersistenceUnitInfo} objects and takes into account any other + * {@link PersistenceUnitInfo} objects that already exist and + * ensures that all of them are registered as CDI beans. + * + *

This allows other CDI-provider-specific mechanisms to use + * these {@link PersistenceUnitInfo} beans as inputs for creating + * {@link EntityManager} instances.

+ * + * @param event the {@link AfterBeanDiscovery} event describing + * the fact that bean discovery has been performed; must not be + * {@code null} + * + * @param beanManager the {@link BeanManager} currently in effect; + * must not be {@code null} + * + * @exception IOException if an input or output error occurs, + * typically because a {@code META-INF/persistence.xml} resource + * was found but could not be loaded for some reason + * + * @exception JAXBException if there was a problem {@linkplain + * Unmarshaller#unmarshal(Reader) unmarshalling} a {@code + * META-INF/persistence.xml} resource + * + * @exception NullPointerException if either {@code event} or + * {@code beanManager} is {@code null} + * + * @exception ReflectiveOperationException if reflection failed + * + * @exception XMLStreamException if there was a problem setting up + * JAXB + * + * @see PersistenceUnitInfo + */ + private void addPersistenceUnitInfoBeans(@Observes @Priority(LIBRARY_AFTER) + final AfterBeanDiscovery event, + final BeanManager beanManager) + throws IOException, JAXBException, ReflectiveOperationException, XMLStreamException { + final String cn = JpaExtension.class.getName(); + final String mn = "addPersistenceUnitInfoBeans"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, beanManager}); + } + + Objects.requireNonNull(event); + Objects.requireNonNull(beanManager); + + final Collection providers = addPersistenceProviderBeans(event); + + // Should we consider type-level @PersistenceContext + // definitions of persistence units ("implicits")? + boolean processImplicits = true; + + // Collect all pre-existing PersistenceUnitInfo beans + // (i.e. supplied by the end user) and make sure their + // associated PersistenceProviders are beanified. (Almost + // always this Set will be empty.) + final Set> preexistingPersistenceUnitInfoBeans = + beanManager.getBeans(PersistenceUnitInfo.class, Any.Literal.INSTANCE); + if (preexistingPersistenceUnitInfoBeans != null && !preexistingPersistenceUnitInfoBeans.isEmpty()) { + processImplicits = false; + maybeAddPersistenceProviderBeans(event, beanManager, preexistingPersistenceUnitInfoBeans, providers); + } + + // Next, and most commonly, load all META-INF/persistence.xml + // resources with JAXB, and turn them into PersistenceUnitInfo + // instances, and add beans for all of them as well as their + // associated PersistenceProviders (if applicable). + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + assert classLoader != null; + final Enumeration urls = classLoader.getResources("META-INF/persistence.xml"); + if (urls != null && urls.hasMoreElements()) { + processImplicits = false; + this.processPersistenceXmls(event, beanManager, classLoader, urls, providers); + } + + // If we did not find any PersistenceUnitInfo instances via + // any other means, only then look at those defined "implicitly", + // i.e. via type-level @PersistenceContext annotations. + if (processImplicits) { + this.processImplicitPersistenceUnits(event, providers); + } + + this.unlistedManagedClassesByPersistenceUnitNames.clear(); + this.implicitPersistenceUnits.clear(); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + private static Collection addPersistenceProviderBeans(final AfterBeanDiscovery event) { + final String cn = JpaExtension.class.getName(); + final String mn = "addPersistenceProviderBeans"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, event); + } + + Objects.requireNonNull(event); + final PersistenceProviderResolver resolver = PersistenceProviderResolverHolder.getPersistenceProviderResolver(); + event.addBean() + .types(PersistenceProviderResolver.class) + .scope(Singleton.class) + .createWith(cc -> resolver); + final Collection providers = resolver.getPersistenceProviders(); + assert providers != null; + for (final PersistenceProvider provider : providers) { + event.addBean() + .addTransitiveTypeClosure(provider.getClass()) + .scope(Singleton.class) + .createWith(cc -> provider); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, providers); + } + return providers; + } + + private void processImplicitPersistenceUnits(final AfterBeanDiscovery event, + final Collection providers) + throws ReflectiveOperationException { + final String cn = JpaExtension.class.getName(); + final String mn = "processImplicitPersistenceUnits"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, providers}); + } + + Objects.requireNonNull(event); + for (final PersistenceUnitInfoBean persistenceUnitInfoBean : this.implicitPersistenceUnits.values()) { + assert persistenceUnitInfoBean != null; + final String persistenceUnitName = persistenceUnitInfoBean.getPersistenceUnitName(); + if (!persistenceUnitInfoBean.excludeUnlistedClasses()) { + final Collection> unlistedManagedClasses = + this.unlistedManagedClassesByPersistenceUnitNames.get(persistenceUnitName); + if (unlistedManagedClasses != null && !unlistedManagedClasses.isEmpty()) { + for (final Class unlistedManagedClass : unlistedManagedClasses) { + assert unlistedManagedClass != null; + persistenceUnitInfoBean.addManagedClassName(unlistedManagedClass.getName()); + } + } + } + event.addBean() + .types(Collections.singleton(PersistenceUnitInfo.class)) + .scope(Singleton.class) + .addQualifiers(NamedLiteral.of(persistenceUnitName == null ? "" : persistenceUnitName)) + .createWith(cc -> persistenceUnitInfoBean); + maybeAddPersistenceProviderBean(event, persistenceUnitInfoBean, providers); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + private void processPersistenceXmls(final AfterBeanDiscovery event, + final BeanManager beanManager, + final ClassLoader classLoader, + final Enumeration urls, + final Collection providers) + throws IOException, JAXBException, ReflectiveOperationException, XMLStreamException { + final String cn = JpaExtension.class.getName(); + final String mn = "processPersistenceXmls"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, beanManager, classLoader, urls, providers}); + } + + Objects.requireNonNull(event); + if (urls != null && urls.hasMoreElements()) { + final Supplier tempClassLoaderSupplier; + if (classLoader instanceof URLClassLoader) { + tempClassLoaderSupplier = () -> new URLClassLoader(((URLClassLoader) classLoader).getURLs()); + } else { + tempClassLoaderSupplier = () -> classLoader; + } + // We use StAX for XML loading because it is the same + // strategy used by CDI implementations. If the end user + // wants to customize the StAX implementation then we want + // that customization to apply here as well. + // + // Note that XMLInputFactory is NOT deprecated in JDK 8: + // https://docs.oracle.com/javase/8/docs/api/javax/xml/stream/XMLInputFactory.html#newFactory-- + // ...but IS deprecated in JDK 9: + // https://docs.oracle.com/javase/9/docs/api/javax/xml/stream/XMLInputFactory.html#newFactory-- + // ...with an incorrect claim that it was deprecated since + // JDK 1.7. In JDK 7 it actually was *not* deprecated: + // https://docs.oracle.com/javase/7/docs/api/javax/xml/stream/XMLInputFactory.html#newFactory() + // ...and now in JDK 10 it is NO LONGER deprecated: + // https://docs.oracle.com/javase/10/docs/api/javax/xml/stream/XMLInputFactory.html#newFactory() + // ...nor in JDK 11: + // https://docs.oracle.com/en/java/javase/11/docs/api/java.xml/javax/xml/stream/XMLInputFactory.html#newFactory() + // ...nor in JDK 12: + // https://docs.oracle.com/en/java/javase/12/docs/api/java.xml/javax/xml/stream/XMLInputFactory.html#newFactory() + // So we suppress deprecation warnings since deprecation + // in JDK 9 appears to have been a mistake. + @SuppressWarnings("deprecation") + final XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); + assert xmlInputFactory != null; + final Unmarshaller unmarshaller = + JAXBContext.newInstance(Persistence.class.getPackage().getName()).createUnmarshaller(); + assert unmarshaller != null; + final DataSourceProvider dataSourceProvider = new BeanManagerBackedDataSourceProvider(beanManager); + while (urls.hasMoreElements()) { + final URL url = urls.nextElement(); + assert url != null; + Collection persistenceUnitInfos = null; + try (InputStream inputStream = new BufferedInputStream(url.openStream())) { + final XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(inputStream); + assert reader != null; + persistenceUnitInfos = + PersistenceUnitInfoBean.fromPersistence((Persistence) unmarshaller.unmarshal(reader), + classLoader, + tempClassLoaderSupplier, + new URL(url, ".."), // e.g. META-INF/.. + this.unlistedManagedClassesByPersistenceUnitNames, + dataSourceProvider); + } + if (persistenceUnitInfos != null && !persistenceUnitInfos.isEmpty()) { + for (final PersistenceUnitInfo persistenceUnitInfo : persistenceUnitInfos) { + final String persistenceUnitName = persistenceUnitInfo.getPersistenceUnitName(); + event.addBean() + .types(Collections.singleton(PersistenceUnitInfo.class)) + .scope(Singleton.class) + .addQualifiers(NamedLiteral.of(persistenceUnitName == null ? "" : persistenceUnitName)) + .createWith(cc -> persistenceUnitInfo); + maybeAddPersistenceProviderBean(event, persistenceUnitInfo, providers); + } + } + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + private static void maybeAddPersistenceProviderBeans(final AfterBeanDiscovery event, + final BeanManager beanManager, + final Set> preexistingPersistenceUnitInfoBeans, + final Collection providers) + throws ReflectiveOperationException { + final String cn = JpaExtension.class.getName(); + final String mn = "maybeAddPersistenceProviderBeans"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, beanManager, preexistingPersistenceUnitInfoBeans, providers}); + } + + Objects.requireNonNull(event); + Objects.requireNonNull(beanManager); + Objects.requireNonNull(preexistingPersistenceUnitInfoBeans); + for (final Bean bean : preexistingPersistenceUnitInfoBeans) { + if (bean != null) { + assert bean.getTypes().contains(PersistenceUnitInfo.class); + @SuppressWarnings("unchecked") + final Bean preexistingPersistenceUnitInfoBean = (Bean) bean; + // We use Contextual#create() directly to create a + // PersistenceUnitInfo contextual instance (normally + // for this use case in CDI you would acquire a + // contextual reference via + // BeanManager#getReference(), but it is too early in + // the (spec-defined) lifecycle to do that here). We + // also deliberately do not use + // Context#get(Contextual, CreationalContext), since + // that might "install" the instance so acquired in + // whatever Context/scope it is defined in and we just + // need it transiently. + // + // Getting a contextual instance this way, via + // Contextual#create(), is normally frowned upon, + // since it bypasses CDI's Context mechansims and + // proxying and interception features (it is the + // foundation upon which they are built), but here we + // need the instance only for the return values of + // getPersistenceProviderClassName() and + // getClassLoader(). We then destroy the instance + // immediately so that everything behaves as though + // this contextual instance acquired by shady means + // never existed. + final CreationalContext cc = + beanManager.createCreationalContext(preexistingPersistenceUnitInfoBean); + final PersistenceUnitInfo pui = preexistingPersistenceUnitInfoBean.create(cc); + assert pui != null; + try { + maybeAddPersistenceProviderBean(event, pui, providers); + } finally { + preexistingPersistenceUnitInfoBean.destroy(pui, cc); + // Contextual#destroy() *should* release the + // CreationalContext, but it is an idempotent call + // and many Bean authors forget to do this. + cc.release(); + } + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Given a {@link PersistenceUnitInfo} and a {@link Collection} of + * {@link PersistenceProvider} instances representing already + * "beanified" {@link PersistenceProvider}s, adds a CDI bean for + * the {@linkplain + * PersistenceUnitInfo#getPersistenceProviderClassName() + * persistence provider referenced by the supplied + * PersistenceUnitInfo} if appropriate. + * + * @param event the {@link AfterBeanDiscovery} event that will do + * the actual bean addition; must not be {@code null} + * + * @param persistenceUnitInfo the {@link PersistenceUnitInfo} + * whose {@linkplain + * PersistenceUnitInfo#getPersistenceProviderClassName() + * associated persistence provider} will be beanified; must not be + * {@code null} + * + * @param providers a {@link Collection} of {@link + * PersistenceProvider} instances that represent {@link + * PersistenceProvider}s that have already had beans added for + * them; may be {@code null} + * + * @exception NullPointerException if {@code event} or {@code + * persistenceUnitInfo} is {@code null} + * + * @exception ReflectiveOperationException if an error occurs + * during reflection + */ + private static void maybeAddPersistenceProviderBean(final AfterBeanDiscovery event, + final PersistenceUnitInfo persistenceUnitInfo, + final Collection providers) + throws ReflectiveOperationException { + final String cn = JpaExtension.class.getName(); + final String mn = "maybeAddPersistenceProviderBean"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, persistenceUnitInfo, providers}); + } + + Objects.requireNonNull(event); + Objects.requireNonNull(persistenceUnitInfo); + final String providerClassName = persistenceUnitInfo.getPersistenceProviderClassName(); + if (providerClassName != null) { + + boolean add = true; + + if (providers != null && !providers.isEmpty()) { + for (final PersistenceProvider provider : providers) { + if (provider != null && provider.getClass().getName().equals(providerClassName)) { + add = false; + break; + } + } + } + + if (add) { + // The PersistenceProvider class in question is not one we + // already loaded. Add a bean for it too. + + final String persistenceUnitName = persistenceUnitInfo.getPersistenceUnitName(); + event.addBean() + .types(PersistenceProvider.class) + .scope(Singleton.class) + .addQualifiers(NamedLiteral.of(persistenceUnitName == null ? "" : persistenceUnitName)) + .createWith(cc -> { + try { + ClassLoader classLoader = persistenceUnitInfo.getClassLoader(); + if (classLoader == null) { + classLoader = Thread.currentThread().getContextClassLoader(); + } + assert classLoader != null; + @SuppressWarnings("unchecked") + final Class c = + (Class) Class.forName(providerClassName, true, classLoader); + return c.getDeclaredConstructor().newInstance(); + } catch (final ReflectiveOperationException reflectiveOperationException) { + throw new CreationException(reflectiveOperationException.getMessage(), + reflectiveOperationException); + } + }); + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceUnitInfoBean.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceUnitInfoBean.java new file mode 100644 index 00000000000..ebf8252b0ca --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceUnitInfoBean.java @@ -0,0 +1,871 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.jpa; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javax.persistence.SharedCacheMode; +import javax.persistence.ValidationMode; +import javax.persistence.spi.ClassTransformer; +import javax.persistence.spi.PersistenceProvider; // for javadoc only +import javax.persistence.spi.PersistenceUnitInfo; +import javax.persistence.spi.PersistenceUnitTransactionType; +import javax.sql.DataSource; + +import io.helidon.integrations.cdi.jpa.jaxb.Persistence; +import io.helidon.integrations.cdi.jpa.jaxb.Persistence.PersistenceUnit; +import io.helidon.integrations.cdi.jpa.jaxb.PersistenceUnitCachingType; +import io.helidon.integrations.cdi.jpa.jaxb.PersistenceUnitValidationModeType; + +/** + * A {@link PersistenceUnitInfo} implementation that can be + * constructed by hand. + * + * @see PersistenceUnitInfo + */ +public class PersistenceUnitInfoBean implements PersistenceUnitInfo { + + + /* + * Instance fields. + */ + + + private final ClassLoader classLoader; + + private final ClassLoader originalClassLoader; + + private final boolean excludeUnlistedClasses; + + private final List jarFileUrls; + + private final Set managedClassNames; + + private final List managedClassNamesView; + + private final List mappingFileNames; + + private final String jtaDataSourceName; + + private final String nonJtaDataSourceName; + + private final DataSourceProvider dataSourceProvider; + + private final String persistenceProviderClassName; + + private final String persistenceUnitName; + + private final URL persistenceUnitRootUrl; + + private final String persistenceXMLSchemaVersion; + + private final Properties properties; + + private final SharedCacheMode sharedCacheMode; + + private final Consumer classTransformerConsumer; + + private final Supplier tempClassLoaderSupplier; + + private final PersistenceUnitTransactionType transactionType; + + private final ValidationMode validationMode; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link PersistenceUnitInfoBean} using as many + * defaults as reasonably possible. + * + * @param persistenceUnitName the name of the persistence unit + * this {@link PersistenceUnitInfoBean} represents; must not be + * {@code null} + * + * @param persistenceUnitRootUrl the {@link URL} identifying the + * root of the persistence unit this {@link + * PersistenceUnitInfoBean} represents; must not be {@code null} + * + * @param managedClassNames a {@link Collection} of + * fully-qualified class names identifying JPA-managed classes + * (such as entity classes, mapped superclasses and the like); may + * be {@code null}. The {@link Collection} is copied and no + * reference to it is retained. + * + * @param dataSourceProvider a {@link DataSourceProvider} capable + * of supplying {@link DataSource} instances; must not be {@code + * null} + * + * @param properties a {@link Properties} object representing the + * properties of the persistence unit represented by this {@link + * PersistenceUnitInfoBean}; may be {@code null}. A reference is + * retained to this object. + * + * @exception NullPointerException if {@code persistenceUnitName}, + * {@code persistenceUnitRootUrl} or {@code dataSourceProvider} is + * {@code null} + * + * @see #PersistenceUnitInfoBean(String, URL, String, String, + * ClassLoader, Supplier, Consumer, boolean, Collection, + * Collection, Collection, String, String, DataSourceProvider, + * Properties, SharedCacheMode, PersistenceUnitTransactionType, + * ValidationMode) + */ + public PersistenceUnitInfoBean(final String persistenceUnitName, + final URL persistenceUnitRootUrl, + final Collection managedClassNames, + final DataSourceProvider dataSourceProvider, + final Properties properties) { + this(persistenceUnitName, + persistenceUnitRootUrl, + null, + null, + Thread.currentThread().getContextClassLoader(), + null, + null, + managedClassNames != null && !managedClassNames.isEmpty(), + null, + managedClassNames, + null, + persistenceUnitName, + null, + dataSourceProvider, + properties, + SharedCacheMode.UNSPECIFIED, + PersistenceUnitTransactionType.JTA, + ValidationMode.AUTO); + } + + /** + * Creates a new {@link PersistenceUnitInfoBean}. + * + * @param persistenceUnitName the name of the persistence unit + * this {@link PersistenceUnitInfoBean} represents; must not be + * {@code null} + * + * @param persistenceUnitRootUrl the {@link URL} identifying the + * root of the persistence unit this {@link + * PersistenceUnitInfoBean} represents; must not be {@code null} + * + * @param persistenceXMLSchemaVersion a {@link String} + * representation of the version of JPA being supported; may be + * {@code null} in which case "{@code 2.2}" will be used instead + * + * @param persistenceProviderClassName the fully-qualified class + * name of a {@link PersistenceProvider} implementation; may be + * {@code null} in which case a default will be used + * + * @param classLoader a {@link ClassLoader} to be returned by the + * {@link #getClassLoader()} method; may be {@code null} + * + * @param tempClassLoaderSupplier a {@link Supplier} of {@link + * ClassLoader} instances to be used by the {@link + * #getNewTempClassLoader()} method; may be {@code null} + * + * @param classTransformerConsumer a {@link Consumer} of any + * {@link ClassTransformer}s that may be added via a JPA + * provider's invocation of the {@link + * #addTransformer(ClassTransformer)} method; may be {@code null} + * in which case no action will be taken + * + * @param excludeUnlistedClasses if {@code true}, then any + * automatically discovered managed classes not explicitly + * contained in {@code managedClassNames} will be excluded from + * consideration + * + * @param jarFileUrls a {@link Collection} of {@link URL}s + * identifying {@code .jar} files containing managed classes; may + * be {@code null}. The {@link Collection} is copied and no + * reference to it is retained. + * + * @param managedClassNames a {@link Collection} of + * fully-qualified class names identifying JPA-managed classes + * (such as entity classes, mapped superclasses and the like); may + * be {@code null}. The {@link Collection} is copied and no + * reference to it is retained. + * + * @param mappingFileNames a {@link Collection} of classpath + * resource names identifying JPA mapping files; may be {@code + * null}. The {@link Collection} is copied and no reference to it + * is retained. + * + * @param jtaDataSourceName the name of a data source that may be + * enrolled in JTA-compliant transactions; may be {@code null} + * + * @param nonJtaDataSourceName the name of a data source that + * should not be enrolled in JTA-compliant transactions; may be + * {@code null} + * + * @param dataSourceProvider a {@link DataSourceProvider} capable + * of supplying {@link DataSource} instances; must not be {@code + * null} + * + * @param properties a {@link Properties} object representing the + * properties of the persistence unit represented by this {@link + * PersistenceUnitInfoBean}; may be {@code null}. A reference is + * retained to this object. + * + * @param sharedCacheMode the {@link SharedCacheMode} this {@link + * PersistenceUnitInfoBean} will use; may be {@code null} in which + * case {@link SharedCacheMode#UNSPECIFIED} will be used instead + * + * @param transactionType the {@link + * PersistenceUnitTransactionType} this {@link + * PersistenceUnitInfoBean} will use; may be {@code null} in which + * case {@link PersistenceUnitTransactionType#JTA} will be used + * instead + * + * @param validationMode the {@link ValidationMode} this {@link + * PersistenceUnitInfoBean} will use; may be {@code null} in which + * case {@link ValidationMode#AUTO} will be used instead + * + * @exception NullPointerException if {@code persistenceUnitName}, + * {@code persistenceUnitRootUrl} or {@code dataSourceProvider} is + * {@code null} + * + * @see #getPersistenceUnitName() + * + * @see #getPersistenceUnitRootUrl() + * + * @see #getPersistenceXMLSchemaVersion() + * + * @see #getPersistenceProviderClassName() + * + * @see #getClassLoader() + * + * @see #getNewTempClassLoader() + * + * @see #excludeUnlistedClasses() + * + * @see #getJarFileUrls() + * + * @see #getManagedClassNames() + * + * @see #getMappingFileNames() + * + * @see #getJtaDataSource() + * + * @see #getNonJtaDataSource() + * + * @see #getProperties() + * + * @see #getSharedCacheMode() + * + * @see #getTransactionType() + * + * @see #getValidationMode() + */ + public PersistenceUnitInfoBean(final String persistenceUnitName, + final URL persistenceUnitRootUrl, + final String persistenceXMLSchemaVersion, + final String persistenceProviderClassName, + final ClassLoader classLoader, + final Supplier tempClassLoaderSupplier, + final Consumer classTransformerConsumer, + final boolean excludeUnlistedClasses, + final Collection jarFileUrls, + final Collection managedClassNames, + final Collection mappingFileNames, + final String jtaDataSourceName, + final String nonJtaDataSourceName, + final DataSourceProvider dataSourceProvider, + final Properties properties, + final SharedCacheMode sharedCacheMode, + final PersistenceUnitTransactionType transactionType, + final ValidationMode validationMode) { + super(); + Objects.requireNonNull(persistenceUnitName); + Objects.requireNonNull(persistenceUnitRootUrl); + Objects.requireNonNull(dataSourceProvider); + Objects.requireNonNull(transactionType); + + this.persistenceUnitName = persistenceUnitName; + this.persistenceUnitRootUrl = persistenceUnitRootUrl; + this.persistenceProviderClassName = persistenceProviderClassName; + this.persistenceXMLSchemaVersion = persistenceXMLSchemaVersion == null ? "2.2" : persistenceXMLSchemaVersion; + this.originalClassLoader = classLoader; + this.classLoader = classLoader; + this.tempClassLoaderSupplier = tempClassLoaderSupplier; + this.classTransformerConsumer = classTransformerConsumer; + this.excludeUnlistedClasses = excludeUnlistedClasses; + + if (jarFileUrls == null || jarFileUrls.isEmpty()) { + this.jarFileUrls = Collections.emptyList(); + } else { + this.jarFileUrls = Collections.unmodifiableList(new ArrayList<>(jarFileUrls)); + } + + if (managedClassNames == null || managedClassNames.isEmpty()) { + this.managedClassNames = new LinkedHashSet<>(); + } else { + this.managedClassNames = new LinkedHashSet<>(managedClassNames); + } + this.managedClassNamesView = new AbstractList() { + @Override + public boolean isEmpty() { + return PersistenceUnitInfoBean.this.managedClassNames.isEmpty(); + } + @Override + public int size() { + return PersistenceUnitInfoBean.this.managedClassNames.size(); + } + @Override + public Iterator iterator() { + return PersistenceUnitInfoBean.this.managedClassNames.iterator(); + } + @Override + public String get(final int index) { + final Iterator iterator = this.iterator(); + assert iterator != null; + for (int i = 0; i < index; i++) { + iterator.next(); + } + return iterator.next(); + } + }; + + if (mappingFileNames == null || mappingFileNames.isEmpty()) { + this.mappingFileNames = Collections.emptyList(); + } else { + this.mappingFileNames = Collections.unmodifiableList(new ArrayList<>(mappingFileNames)); + } + + if (properties == null) { + this.properties = new Properties(); + } else { + this.properties = properties; + } + + if (jtaDataSourceName == null || jtaDataSourceName.isEmpty()) { + this.jtaDataSourceName = null; + } else { + this.jtaDataSourceName = jtaDataSourceName; + } + this.nonJtaDataSourceName = nonJtaDataSourceName; + this.dataSourceProvider = dataSourceProvider; + + if (sharedCacheMode == null) { + this.sharedCacheMode = SharedCacheMode.UNSPECIFIED; + } else { + this.sharedCacheMode = sharedCacheMode; + } + this.transactionType = transactionType; + if (validationMode == null) { + this.validationMode = ValidationMode.AUTO; + } else { + this.validationMode = validationMode; + } + } + + + /* + * Instance methods. + */ + + + boolean addManagedClassName(final String className) { + return className != null && this.managedClassNames.add(className); + } + + @Override + public List getJarFileUrls() { + return this.jarFileUrls; + } + + @Override + public URL getPersistenceUnitRootUrl() { + return this.persistenceUnitRootUrl; + } + + @Override + public List getManagedClassNames() { + return this.managedClassNamesView; + } + + @Override + public boolean excludeUnlistedClasses() { + return this.excludeUnlistedClasses; + } + + @Override + public SharedCacheMode getSharedCacheMode() { + return this.sharedCacheMode; + } + + @Override + public ValidationMode getValidationMode() { + return this.validationMode; + } + + @Override + public Properties getProperties() { + return this.properties; + } + + @Override + public ClassLoader getClassLoader() { + return this.classLoader; + } + + @Override + public String getPersistenceXMLSchemaVersion() { + return this.persistenceXMLSchemaVersion; + } + + @Override + public ClassLoader getNewTempClassLoader() { + ClassLoader cl = null; + if (this.tempClassLoaderSupplier != null) { + cl = this.tempClassLoaderSupplier.get(); + } + if (cl == null) { + cl = this.originalClassLoader; + if (cl == null) { + cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = this.getClass().getClassLoader(); + } + } + } + return cl; + } + + @Override + public void addTransformer(final ClassTransformer classTransformer) { + if (this.classTransformerConsumer != null) { + this.classTransformerConsumer.accept(classTransformer); + } + } + + @Override + public String getPersistenceUnitName() { + return this.persistenceUnitName; + } + + @Override + public String getPersistenceProviderClassName() { + return this.persistenceProviderClassName; + } + + @Override + public PersistenceUnitTransactionType getTransactionType() { + return this.transactionType; + } + + @Override + public final DataSource getJtaDataSource() { + return this.dataSourceProvider.getDataSource(true, this.nonJtaDataSourceName == null, this.jtaDataSourceName); + } + + @Override + public final DataSource getNonJtaDataSource() { + return this.dataSourceProvider.getDataSource(false, false, this.nonJtaDataSourceName); + } + + @Override + public List getMappingFileNames() { + return this.mappingFileNames; + } + + /** + * Given a {@link Persistence} (a Java object representation of a + * {@code META-INF/persistence.xml} resource), a {@link URL} + * representing the root of all persistence units, a {@link Map} + * of unlisted managed classes (entity classes, mapped + * superclasses and so on) indexed by persistence unit name, and a + * {@link DataSourceProvider} that can provide {@link DataSource} + * instances, returns a {@link Collection} of {@link + * PersistenceUnitInfoBean} instances representing all the + * persistence units in play. + * + *

This method never returns {@code null}.

+ * + * @param persistence a {@link Persistence} containing bootstrap + * information from which persistence units and their + * configuration may be deduced; may be {@code null} in which case + * an {@linkplain Collection#isEmpty() empty} {@link Collection} + * will be returned + * + * @param classLoader a {@link ClassLoader} that the resulting + * {@link PersistenceUnitInfoBean} instances will use; may be + * {@code null} + * + * @param tempClassLoaderSupplier a {@link Supplier} of a {@link + * ClassLoader} that will be used to implement the {@link + * PersistenceUnitInfo#getNewTempClassLoader()} method; may be + * {@code null} + * + * @param rootUrl the {@link URL} representing the root of all + * persistence units; must not be {@code null} + * + * @param unlistedClasses a {@link Map} of managed classes indexed + * by persistence unit name whose values might not be explicitly + * listed in a {@link PersistenceUnit}; may be {@code null} + * + * @param dataSourceProvider a {@link DataSourceProvider}; must + * not be {@code null} + * + * @return a non-{@code null} {@link Collection} of {@link + * PersistenceUnitInfoBean} instances + * + * @exception MalformedURLException if a {@link URL} could not be + * constructed + * + * @exception NullPointerException if {@code rootUrl} or {@code + * dataSourceProvider} is {@code null} + * + * @see #fromPersistenceUnit(Persistence.PersistenceUnit, + * ClassLoader, Supplier, URL, Map, DataSourceProvider) + * + * @see PersistenceUnitInfo + */ + public static final Collection + fromPersistence(final Persistence persistence, + final ClassLoader classLoader, + final Supplier tempClassLoaderSupplier, + final URL rootUrl, + Map>> unlistedClasses, + final DataSourceProvider dataSourceProvider) + throws MalformedURLException { + Objects.requireNonNull(rootUrl); + if (unlistedClasses == null) { + unlistedClasses = Collections.emptyMap(); + } + final Collection returnValue; + if (persistence == null) { + returnValue = Collections.emptySet(); + } else { + final Collection persistenceUnits = persistence.getPersistenceUnit(); + if (persistenceUnits == null || persistenceUnits.isEmpty()) { + returnValue = Collections.emptySet(); + } else { + returnValue = new ArrayList<>(); + for (final PersistenceUnit persistenceUnit : persistenceUnits) { + assert persistenceUnit != null; + returnValue.add(fromPersistenceUnit(persistenceUnit, + classLoader, + tempClassLoaderSupplier, + rootUrl, + unlistedClasses, + dataSourceProvider)); + } + } + } + return returnValue; + } + + /** + * Given a {@link PersistenceUnit} (a Java object representation + * of a {@code } element in a {@code + * META-INF/persistence.xml} resource), a {@link URL} representing + * the persistence unit's root, a {@link Map} of unlisted managed + * classes (entity classes, mapped superclasses and so on) indexed + * by persistence unit name, and a {@link DataSourceProvider} that + * can supply {@link DataSource} instances, returns a {@link + * PersistenceUnitInfoBean} representing the persistence unit in + * question. + * + *

This method never returns {@code null}.

+ * + *

This method calls the {@link + * #fromPersistenceUnit(Persistence.PersistenceUnit, ClassLoader, + * Supplier, URL, Map, DataSourceProvider)} method using the + * return value of the {@link Thread#getContextClassLoader()} + * method as the {@link ClassLoader}.

+ * + * @param persistenceUnit a {@link PersistenceUnit}; must not be + * {@code null} + * + * @param rootUrl the {@link URL} representing the root of the + * persistence unit; must not be {@code null} + * + * @param unlistedClasses a {@link Map} of managed classes indexed + * by persistence unit name whose values might not be explicitly + * listed in the supplied {@link PersistenceUnit}; may be {@code + * null} + * + * @param dataSourceProvider a {@link DataSourceProvider}; must not + * be {@code null} + * + * @return a non-{@code null} {@link PersistenceUnitInfoBean} + * + * @exception MalformedURLException if a {@link URL} could not be + * constructed + * + * @exception NullPointerException if {@code persistenceUnit}, + * {@code rootUrl} or {@code dataSourceProvider} is {@code null} + * + * @see #fromPersistenceUnit(Persistence.PersistenceUnit, + * ClassLoader, Supplier, URL, Map, DataSourceProvider) + * + * @see PersistenceUnit + * + * @see PersistenceUnitInfo + */ + public static final PersistenceUnitInfoBean + fromPersistenceUnit(final PersistenceUnit persistenceUnit, + final URL rootUrl, + final Map>> unlistedClasses, + final DataSourceProvider dataSourceProvider) + throws MalformedURLException { + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + return fromPersistenceUnit(persistenceUnit, + classLoader, + () -> classLoader, + rootUrl, + unlistedClasses, + dataSourceProvider); + } + + /** + * Given a {@link PersistenceUnit} (a Java object representation + * of a {@code } element in a {@code + * META-INF/persistence.xml} resource), a {@link ClassLoader} for + * loading JPA classes and resources, a {@link Supplier} of {@link + * ClassLoader} instances for helping to implement the {@link + * PersistenceUnitInfo#getNewTempClassLoader()} method, a {@link + * URL} representing the persistence unit's root, a {@link Map} of + * unlisted managed classes (entity classes, mapped superclasses + * and so on) indexed by persistence unit name, and a {@link + * DataSourceProvider} that can provide {@link DataSource} + * instances, returns a {@link PersistenceUnitInfoBean} + * representing the persistence unit in question. + * + *

This method never returns {@code null}.

+ * + * @param persistenceUnit a {@link PersistenceUnit}; must not be + * {@code null} + * + * @param classLoader a {@link ClassLoader} that the resulting + * {@link PersistenceUnitInfoBean} will use; may be {@code null} + * + * @param tempClassLoaderSupplier a {@link Supplier} of a {@link + * ClassLoader} that will be used to implement the {@link + * PersistenceUnitInfo#getNewTempClassLoader()} method; may be + * {@code null} + * + * @param rootUrl the {@link URL} representing the root of the + * persistence unit; must not be {@code null} + * + * @param unlistedClasses a {@link Map} of managed classes indexed + * by persistence unit name whose values might not be explicitly + * listed in the supplied {@link PersistenceUnit}; may be {@code + * null} + * + * @param dataSourceProvider a {@link DataSourceProvider}; must + * not be {@code null} + * + * @return a non-{@code null} {@link PersistenceUnitInfoBean} + * + * @exception MalformedURLException if a {@link URL} could not be + * constructed + * + * @exception NullPointerException if {@code persistenceUnit} or + * {@code rootUrl} is {@code null} + * + * @see PersistenceUnit + * + * @see PersistenceUnitInfo + */ + public static final PersistenceUnitInfoBean + fromPersistenceUnit(final PersistenceUnit persistenceUnit, + final ClassLoader classLoader, + Supplier tempClassLoaderSupplier, + final URL rootUrl, + final Map>> unlistedClasses, + final DataSourceProvider dataSourceProvider) + throws MalformedURLException { + Objects.requireNonNull(persistenceUnit); + Objects.requireNonNull(rootUrl); + Objects.requireNonNull(dataSourceProvider); + + final Collection jarFiles = persistenceUnit.getJarFile(); + final List jarFileUrls = new ArrayList<>(); + for (final String jarFile : jarFiles) { + if (jarFile != null) { + jarFileUrls.add(createJarFileURL(rootUrl, jarFile)); + } + } + + final Collection mappingFiles = persistenceUnit.getMappingFile(); + + final Properties properties = new Properties(); + final PersistenceUnit.Properties persistenceUnitProperties = persistenceUnit.getProperties(); + if (persistenceUnitProperties != null) { + final Collection propertyInstances = + persistenceUnitProperties.getProperty(); + if (propertyInstances != null && !propertyInstances.isEmpty()) { + for (final PersistenceUnit.Properties.Property property : propertyInstances) { + assert property != null; + properties.setProperty(property.getName(), property.getValue()); + } + } + } + + final Collection managedClasses = persistenceUnit.getClazz(); + assert managedClasses != null; + String name = persistenceUnit.getName(); + if (name == null) { + name = ""; + } + final Boolean excludeUnlistedClasses = persistenceUnit.isExcludeUnlistedClasses(); + if (!Boolean.TRUE.equals(excludeUnlistedClasses)) { + if (unlistedClasses != null && !unlistedClasses.isEmpty()) { + Collection> myUnlistedClasses = unlistedClasses.get(name); + if (myUnlistedClasses != null && !myUnlistedClasses.isEmpty()) { + for (final Class unlistedClass : myUnlistedClasses) { + if (unlistedClass != null) { + managedClasses.add(unlistedClass.getName()); + } + } + } + // Also add "default" ones + if (!name.isEmpty()) { + myUnlistedClasses = unlistedClasses.get(""); + if (myUnlistedClasses != null && !myUnlistedClasses.isEmpty()) { + for (final Class unlistedClass : myUnlistedClasses) { + if (unlistedClass != null) { + managedClasses.add(unlistedClass.getName()); + } + } + } + } + } + } + + final SharedCacheMode sharedCacheMode; + final PersistenceUnitCachingType persistenceUnitCachingType = persistenceUnit.getSharedCacheMode(); + if (persistenceUnitCachingType == null) { + sharedCacheMode = SharedCacheMode.UNSPECIFIED; + } else { + sharedCacheMode = SharedCacheMode.valueOf(persistenceUnitCachingType.name()); + } + + final PersistenceUnitTransactionType transactionType; + final io.helidon.integrations.cdi.jpa.jaxb.PersistenceUnitTransactionType persistenceUnitTransactionType = + persistenceUnit.getTransactionType(); + if (persistenceUnitTransactionType == null) { + transactionType = PersistenceUnitTransactionType.JTA; // I guess + } else { + transactionType = PersistenceUnitTransactionType.valueOf(persistenceUnitTransactionType.name()); + } + + final ValidationMode validationMode; + final PersistenceUnitValidationModeType validationModeType = persistenceUnit.getValidationMode(); + if (validationModeType == null) { + validationMode = ValidationMode.AUTO; + } else { + validationMode = ValidationMode.valueOf(validationModeType.name()); + } + + if (tempClassLoaderSupplier == null) { + if (classLoader instanceof URLClassLoader) { + tempClassLoaderSupplier = () -> new URLClassLoader(((URLClassLoader) classLoader).getURLs()); + } else { + tempClassLoaderSupplier = () -> classLoader; + } + } + + final PersistenceUnitInfoBean returnValue = + new PersistenceUnitInfoBean(name, + rootUrl, + "2.2", + persistenceUnit.getProvider(), + classLoader, + tempClassLoaderSupplier, + null, // no consuming of ClassTransformer for now + excludeUnlistedClasses == null ? true : excludeUnlistedClasses, + jarFileUrls, + managedClasses, + mappingFiles, + persistenceUnit.getJtaDataSource(), + persistenceUnit.getNonJtaDataSource(), + dataSourceProvider, + properties, + sharedCacheMode, + transactionType, + validationMode); + return returnValue; + } + + private static URL createJarFileURL(final URL persistenceUnitRootUrl, final String jarFileUrlString) + throws MalformedURLException { + Objects.requireNonNull(persistenceUnitRootUrl); + Objects.requireNonNull(jarFileUrlString); + // Revisit: probably won't work if persistenceUnitRootUrl is, say, a jar URL + final URL returnValue = new URL(persistenceUnitRootUrl, jarFileUrlString); + return returnValue; + } + + + /** + * A {@linkplain FunctionalInterface functional interface} + * indicating that its implementations can supply {@link + * DataSource}s. + * + * @see #getDataSource(boolean, boolean, String) + */ + @FunctionalInterface + public interface DataSourceProvider { + + /** + * Supplies a {@link DataSource}. + * + *

Implementations of this method are permitted to return + * {@code null}.

+ * + * @param jta if {@code true}, the {@link DataSource} that is + * returned may be enrolled in JTA-compliant transactions + * + * @param useDefaultJta if {@code true}, and if the {@code + * jta} parameter value is {@code true}, the supplied {@code + * dataSourceName} may be ignored and a default {@link + * DataSource} eligible for enrolling in JTA-compliant + * transactions will be returned if possible + * + * @param dataSourceName the name of the {@link DataSource} to + * return; may be {@code null}; ignored if both {@code jta} + * and {@code useDefaultJta} are {@code true} + * + * @return an appropriate {@link DataSource}, or {@code null} + * + * @see PersistenceUnitInfoBean#getJtaDataSource() + * + * @see PersistenceUnitInfoBean#getNonJtaDataSource() + */ + DataSource getDataSource(boolean jta, boolean useDefaultJta, String dataSourceName); + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/package-info.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/package-info.java new file mode 100644 index 00000000000..95831872e8b --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 that integrate the + * provider-independent parts of JPA into CDI. + * + * @see io.helidon.integrations.cdi.jpa.JpaExtension + * + * @see io.helidon.integrations.cdi.jpa.PersistenceUnitInfoBean + */ +package io.helidon.integrations.cdi.jpa; diff --git a/integrations/cdi/jpa-cdi/src/main/javadoc/io/helidon/integrations/cdi/jpa/jaxb/package.html b/integrations/cdi/jpa-cdi/src/main/javadoc/io/helidon/integrations/cdi/jpa/jaxb/package.html new file mode 100644 index 00000000000..5c361b80409 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/javadoc/io/helidon/integrations/cdi/jpa/jaxb/package.html @@ -0,0 +1,30 @@ + + + Provides classes and interfaces generated from + the JPA 2.2 {@code persistence} XML schema; other + documentation in this package is generated indirectly by + the {@code xjc} tool, which is solely responsible + for its contents and omissions. + + @author Laird Nelson + + @see io.helidon.integrations.cdi.jpa.jaxb.Persistence + diff --git a/integrations/cdi/jpa-cdi/src/main/resources/META-INF/beans.xml b/integrations/cdi/jpa-cdi/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..89f9c163080 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/integrations/cdi/jpa-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/integrations/cdi/jpa-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000000..caae586014f --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.integrations.cdi.jpa.JpaExtension diff --git a/integrations/cdi/jpa-cdi/src/main/resources/messages.properties b/integrations/cdi/jpa-cdi/src/main/resources/messages.properties new file mode 100644 index 00000000000..fdb1c0f5f01 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/resources/messages.properties @@ -0,0 +1,15 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# diff --git a/integrations/cdi/jpa-weld/README.adoc b/integrations/cdi/jpa-weld/README.adoc new file mode 100644 index 00000000000..0c6f4f4d7ed --- /dev/null +++ b/integrations/cdi/jpa-weld/README.adoc @@ -0,0 +1,30 @@ += Helidon JPA Weld Integration + +The Helidon JPA Weld Integration project performs the +CDI-provider-specific work of integrating JPA into standalone CDI +applications (including those based on Helidon MicroProfile). It is +one of several projects that together make up overall JPA support for +standalone CDI applications. + +To function properly, this project also requires: + +* a CDI-provider-agnostic counterpart, such as the `jpa-cdi` project + found elsewhere in this git repository +* a JPA provider implementation, such as Eclipselink +* a JPA-provider-specific library to assist the JPA provider in + determining what kind of environment it is running in, such as the + `eclipselink-cdi` project found elsewhere in this git repository +* a library capable of integrating `DataSource`s into CDI, such as the + `datasource-hikaricp` project found elsewhere in this git repository +* a suitable JDBC-compliant database driver library + +IMPORTANT: Please note that this feature is currently experimental and + not suitable for production use. + +== Installation + +Ensure that the Helidon JPA Weld Integration project and its runtime +dependencies are present on your application's runtime classpath. + +Please see the `examples/integrations/cdi/jpa` project found elsewhere +in this git repository for a working `pom.xml` file. diff --git a/integrations/cdi/jpa-weld/pom.xml b/integrations/cdi/jpa-weld/pom.xml new file mode 100644 index 00000000000..200c5ebff82 --- /dev/null +++ b/integrations/cdi/jpa-weld/pom.xml @@ -0,0 +1,162 @@ + + + + 4.0.0 + + io.helidon.integrations.cdi + helidon-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-cdi-jpa-weld + Helidon CDI Integrations JPA Weld + + + package + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.jboss.weld.se + weld-se-core + test + + + org.slf4j + slf4j-jdk14 + test + + + com.h2database + h2 + test + + + ${project.groupId} + helidon-integrations-cdi-jta-weld + ${project.version} + test + + + ${project.groupId} + helidon-integrations-cdi-datasource-hikaricp + ${project.version} + test + + + ${project.groupId} + helidon-integrations-cdi-eclipselink + ${project.version} + test + + + io.helidon.microprofile.config + helidon-microprofile-config-cdi + ${project.version} + test + + + + + ${project.groupId} + helidon-integrations-cdi-jpa + ${project.version} + runtime + + + org.jboss + jandex + runtime + + + + + javax.annotation + javax.annotation-api + provided + + + javax.enterprise + cdi-api + provided + + + jakarta.persistence + jakarta.persistence-api + provided + + + javax.transaction + javax.transaction-api + provided + + + javax.validation + validation-api + provided + + + + + org.jboss.weld + weld-spi + compile + + + + + + + maven-surefire-plugin + + + ${basedir}/src/test/java/logging.properties + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + process-classes + + + + + + + diff --git a/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/TransactionObserver.java b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/TransactionObserver.java new file mode 100644 index 00000000000..5469b85c762 --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/TransactionObserver.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.jpa.weld; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Destroyed; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Observes; +import javax.transaction.TransactionScoped; + +/** + * A bean housing an observer method that alerts a {@link + * WeldJpaInjectionServices} instance when a JTA transaction is + * available. + * + *

Design Notes

+ * + *

This class is excluded by this bean archive's {@code + * META-INF/beans.xml} resource if the {@link TransactionScoped + * javax.transaction.TransactionScoped} class is not available. This + * has the effect of fully decoupling the rest of this bean archive + * (most notably the {@link WeldJpaInjectionServices} class) from + * transactional concerns if a JTA implementation is not present at + * runtime.

+ * + * @see WeldJpaInjectionServices + * + * @see Initialized + * + * @see TransactionScoped + */ +@ApplicationScoped +final class TransactionObserver { + + + /* + * Static fields. + */ + private static final Logger LOGGER = Logger.getLogger(TransactionObserver.class.getName(), "messages"); + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link TransactionObserver}. + */ + private TransactionObserver() { + super(); + final String cn = TransactionObserver.class.getName(); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, ""); + LOGGER.exiting(cn, ""); + } + } + + + /* + * Static methods. + */ + + + /** + * Observes the {@linkplain Initialized initialization} of the + * {@link TransactionScoped} scope and calls the {@link + * WeldJpaInjectionServices#jtaTransactionBegun()} method on + * the supplied {@link WeldJpaInjectionServices} instance. + * + * @param event the opaque event that represents the + * initialization of the {@link TransactionScoped} scope; may be + * {@code null}; ignored + * + * @param services the {@link WeldJpaInjectionServices} to + * notify; may be {@code null} in which case no action will be + * taken + */ + private static void jtaTransactionBegun(@Observes @Initialized(TransactionScoped.class) final Object event, + final WeldJpaInjectionServices services) { + final String cn = TransactionObserver.class.getName(); + final String mn = "jtaTransactionBegun"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, services}); + } + + if (services != null) { + services.jtaTransactionBegun(); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Observes the {@linkplain Destroyed destruction} of the + * {@link TransactionScoped} scope and calls the {@link + * WeldJpaInjectionServices#jtaTransactionEnded()} method on + * the supplied {@link WeldJpaInjectionServices} instance. + * + * @param event the opaque event that represents the + * destruction of the {@link TransactionScoped} scope; may be + * {@code null}; ignored + * + * @param services the {@link WeldJpaInjectionServices} to + * notify; may be {@code null} in which case no action will be + * taken + */ + private static void jtaTransactionEnded(@Observes @Destroyed(TransactionScoped.class) final Object event, + final WeldJpaInjectionServices services) { + final String cn = TransactionObserver.class.getName(); + final String mn = "jtaTransactionEnded"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, services}); + } + + if (services != null) { + services.jtaTransactionEnded(); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + +} diff --git a/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServices.java b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServices.java new file mode 100644 index 00000000000..e57285e3e3c --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServices.java @@ -0,0 +1,1121 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.jpa.weld; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.literal.NamedLiteral; +import javax.enterprise.inject.spi.Annotated; +import javax.enterprise.inject.spi.AnnotatedField; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.CDI; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Named; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceException; +import javax.persistence.PersistenceUnit; +import javax.persistence.SynchronizationType; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceUnitInfo; + +import org.jboss.weld.injection.spi.JpaInjectionServices; +import org.jboss.weld.injection.spi.ResourceReference; +import org.jboss.weld.injection.spi.ResourceReferenceFactory; +import org.jboss.weld.manager.api.ExecutorServices; +import org.jboss.weld.manager.api.WeldManager; + +import static javax.persistence.spi.PersistenceUnitTransactionType.JTA; +import static javax.persistence.spi.PersistenceUnitTransactionType.RESOURCE_LOCAL; + +/** + * A {@link JpaInjectionServices} implementation that integrates JPA + * functionality into Weld-based CDI environments. + * + * @see JpaInjectionServices + */ +final class WeldJpaInjectionServices implements JpaInjectionServices { + + + /* + * Static fields. + */ + + + /* + * Weld instantiates this class exactly three (!) times during + * normal execution (see https://issues.jboss.org/browse/WELD-2563 + * for details). Only one of those instances (the first) is + * actually used to produce EntityManagers and + * EntityManagerFactories; the other two are discarded. The + * static instance and underway fields ensure that truly only one + * instance processes all incoming calls, and that it is the one + * that is actually tracked and stored by Weld itself in the + * return value of the WeldManager#getServices() method. + * + * See the underway() method as well. + */ + + /** + * The single officially sanctioned instance of this class. + * + *

This field may be {@code null}.

+ * + * @see WELD-2563 + * + * @see #getInstance() + */ + private static volatile WeldJpaInjectionServices instance; + + /** + * Whether a "business" method of this class has been invoked or + * not. + * + * @see WELD-2563 + */ + private static volatile boolean underway; + + /** + * The {@link Logger} for use by all instances of this class. + * + *

This field is never {@code null}.

+ */ + private static final Logger LOGGER = Logger.getLogger(WeldJpaInjectionServices.class.getName(), "messages"); + + + /* + * Instance fields. + */ + + + /** + * A {@link Set} of {@link EntityManager}s that have been created + * as container-managed {@link EntityManager}s, i.e. not + * application-managed ones. + * + *

This field is never {@code null}.

+ * + *

The {@link Set} assigned to this field is safe for + * concurrent usage by multiple threads.

+ */ + private final Set containerManagedEntityManagers; + + /** + * A {@link Map} of {@link EntityManagerFactory} instances indexed + * by names of persistence units. + * + *

This field is never {@code null}.

+ * + *

Synchronization on this field is required for concurrent + * thread access.

+ */ + // @GuardedBy("this") + private volatile Map emfs; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link WeldJpaInjectionServices}. + * + *

Oddly, the fact that this constructor is {@code private} + * does not prevent Weld from loading it as a service. This is an + * unexpected bonus as nothing about this class should be {@code + * public}.

+ */ + private WeldJpaInjectionServices() { + super(); + synchronized (WeldJpaInjectionServices.class) { + // See https://issues.jboss.org/browse/WELD-2563. Make sure + // only the first instance is "kept" as it's the one tracked by + // WeldManager's ServiceRegistry. The others are discarded. + if (instance == null) { + assert !underway; + instance = this; + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.logp(Level.WARNING, + WeldJpaInjectionServices.class.getName(), + "", + "experimental"); + } + } else if (underway) { + throw new IllegalStateException(); + } + } + this.containerManagedEntityManagers = ConcurrentHashMap.newKeySet(); + } + + /** + * Records the fact that a significant method has been invoked. + * + * @see WELD-2563 + */ + private static synchronized void underway() { + underway = true; + } + + /** + * Returns the only instance of this class. + * + *

This method never returns {@code null}.

+ * + * @return the same non-{@code null} {@link WeldJpaInjectionServices} + * when invoked + * + * @see WELD-2563 + */ + static synchronized WeldJpaInjectionServices getInstance() { + return instance; + } + + /** + * Called by the ({@code private}) {@code TransactionObserver} + * class when a JTA transaction is begun. + * + *

The Narayana CDI integration this class is often deployed + * with will fire such events. These events serve as an + * indication that a call to {@link + * javax.transaction.TransactionManager#begin()} has been + * made.

+ * + *

{@link EntityManager}s created by this class will have their + * {@link EntityManager#joinTransaction()} methods called if the + * supplied object is non-{@code null}.

+ */ + void jtaTransactionBegun() { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "jtaTransactionBegun"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + assert this == instance; + for (final EntityManager containerManagedEntityManager : this.containerManagedEntityManagers) { + assert containerManagedEntityManager != null; + final Map properties = containerManagedEntityManager.getProperties(); + final Object synchronizationType; + if (properties == null) { + synchronizationType = null; + } else { + synchronizationType = properties.get(SynchronizationType.class.getName()); + } + if (SynchronizationType.SYNCHRONIZED.equals(synchronizationType)) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, cn, mn, "{0} joining transaction", containerManagedEntityManager); + } + containerManagedEntityManager.joinTransaction(); + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Called by the ({@code private}) {@code TransactionObserver} + * class when a JTA transaction has ended, either successfully or + * unsuccessfully. + * + *

The Narayana CDI integration this class is often deployed + * with will fire such events. These events serve as an + * indication that a call to {@link + * javax.transaction.TransactionManager#commit()} or {@link + * javax.transaction.TransactionManager#rollback()} has been + * made.

+ */ + void jtaTransactionEnded() { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "jtaTransactionEnded"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + // This method is reserved for future use. + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Returns a {@link ResourceReferenceFactory} whose {@link + * ResourceReferenceFactory#createResource()} method will be + * invoked appropriately by Weld later. + * + *

This method never returns {@code null}.

+ * + * @param injectionPoint the {@link InjectionPoint} annotated with + * {@link PersistenceContext}; must not be {@code null} + * + * @return a non-{@code null} {@link ResourceReferenceFactory} + * whose {@link ResourceReferenceFactory#createResource()} method + * will create {@link EntityManager} instances + * + * @exception NullPointerException if {@code injectionPoint} is + * {@code null} + * + * @see ResourceReferenceFactory#createResource() + */ + @Override + public ResourceReferenceFactory registerPersistenceContextInjectionPoint(final InjectionPoint injectionPoint) { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "registerPersistenceContextInjectionPoint"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, injectionPoint); + } + underway(); + assert this == instance; + final ResourceReferenceFactory returnValue; + Objects.requireNonNull(injectionPoint); + final Annotated annotatedMember = injectionPoint.getAnnotated(); + assert annotatedMember != null; + final PersistenceContext persistenceContextAnnotation = annotatedMember.getAnnotation(PersistenceContext.class); + if (persistenceContextAnnotation == null) { + throw new IllegalArgumentException("injectionPoint.getAnnotated().getAnnotation(PersistenceContext.class) == null"); + } + final String name; + final String n = persistenceContextAnnotation.unitName(); + if (n.isEmpty()) { + if (annotatedMember instanceof AnnotatedField) { + name = ((AnnotatedField) annotatedMember).getJavaMember().getName(); + } else { + name = n; + } + } else { + name = n; + } + final SynchronizationType synchronizationType = persistenceContextAnnotation.synchronization(); + assert synchronizationType != null; + synchronized (this) { + if (this.emfs == null) { + this.emfs = new ConcurrentHashMap<>(); + } + } + returnValue = () -> new EntityManagerResourceReference(name, synchronizationType); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Returns a {@link ResourceReferenceFactory} whose {@link + * ResourceReferenceFactory#createResource()} method will be + * invoked appropriately by Weld later. + * + *

This method never returns {@code null}.

+ * + * @param ip the {@link InjectionPoint} annotated with {@link + * PersistenceUnit}; must not be {@code null} + * + * @return a non-{@code null} {@link ResourceReferenceFactory} + * whose {@link ResourceReferenceFactory#createResource()} method + * will create {@link EntityManagerFactory} instances + * + * @exception NullPointerException if {@code ip} is + * {@code null} + * + * @see ResourceReferenceFactory#createResource() + */ + @Override + public ResourceReferenceFactory registerPersistenceUnitInjectionPoint(final InjectionPoint ip) { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "registerPersistenceUnitInjectionPoint"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, ip); + } + + underway(); + assert this == instance; + final ResourceReferenceFactory returnValue; + Objects.requireNonNull(ip); + final Annotated annotatedMember = ip.getAnnotated(); + assert annotatedMember != null; + final PersistenceUnit persistenceUnitAnnotation = annotatedMember.getAnnotation(PersistenceUnit.class); + if (persistenceUnitAnnotation == null) { + throw new IllegalArgumentException("ip.getAnnotated().getAnnotation(PersistenceUnit.class) == null"); + } + final String name; + final String n = persistenceUnitAnnotation.unitName(); + if (n.isEmpty()) { + if (annotatedMember instanceof AnnotatedField) { + name = ((AnnotatedField) annotatedMember).getJavaMember().getName(); + } else { + name = n; + } + } else { + name = n; + } + synchronized (this) { + if (this.emfs == null) { + this.emfs = new ConcurrentHashMap<>(); + } + } + returnValue = () -> new EntityManagerFactoryResourceReference(this.emfs, name); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Invoked by Weld automatically to clean up any resources held by + * this class. + */ + @Override + public void cleanup() { + // cleanup() can get invoked multiple times at will by Weld. + // Specifically, the same Service instance can be stored in + // multiple BeanManagerImpls, and each one can call its + // cleanup() method, so it must be idempotent. + // + // See + // https://github.com/weld/core/blob/06fcaf4a6f625f101be5804208c1eb3a32884773/impl/src/main/java/org/jboss/weld/Container.java#L143-L145 + // and + // https://github.com/weld/core/blob/06fcaf4a6f625f101be5804208c1eb3a32884773/impl/src/main/java/org/jboss/weld/manager/BeanManagerImpl.java#L1173. + if (underway) { + assert this == instance; + + // this.containerManagedEntityManagers should be empty + // already. If for some reason it is not, we just clear() + // it (rather than, say, calling em.close() on each + // element). This is for two reasons: one, we're being + // cleaned up so the whole container is going down anyway. + // Two, we're going to close() all EntityManagerFactories + // which should also do the job since an + // EntityManagerFactory's EntityManagers are supposed to + // be closed when the EntityManagerFactory is closed. + this.containerManagedEntityManagers.clear(); + + final Map emfs = this.emfs; + if (emfs != null && !emfs.isEmpty()) { + final Collection values = emfs.values(); + assert values != null; + assert !values.isEmpty(); + final Iterator iterator = values.iterator(); + assert iterator != null; + assert iterator.hasNext(); + while (iterator.hasNext()) { + final EntityManagerFactory emf = iterator.next(); + assert emf != null; + if (emf.isOpen()) { + emf.close(); + } + iterator.remove(); + } + } + } + assert this.containerManagedEntityManagers.isEmpty(); + assert this.emfs == null || this.emfs.isEmpty(); + synchronized (WeldJpaInjectionServices.class) { + underway = false; + instance = null; + } + } + + /** + * Calls the {@link + * #registerPersistenceContextInjectionPoint(InjectionPoint)} + * method and invokes {@link ResourceReference#getInstance()} on + * its return value and returns the result. + * + *

This method never returns {@code null}.

+ * + * @param injectionPoint an {@link InjectionPoint} annotated with + * {@link PersistenceContext}; must not be {@code null} + * + * @return a non-{@code null} {@link EntityManager} + * + * @see #registerPersistenceContextInjectionPoint(InjectionPoint) + * + * @deprecated See the documentation for the {@link + * JpaInjectionServices#resolvePersistenceContext(InjectionPoint)} + * method. + */ + @Deprecated + @Override + public EntityManager resolvePersistenceContext(final InjectionPoint injectionPoint) { + return this.registerPersistenceContextInjectionPoint(injectionPoint).createResource().getInstance(); + } + + /** + * Calls the {@link + * #registerPersistenceUnitInjectionPoint(InjectionPoint)} method + * and invokes {@link ResourceReference#getInstance()} on its + * return value and returns the result. + * + *

This method never returns {@code null}.

+ * + * @param injectionPoint an {@link InjectionPoint} annotated with + * {@link PersistenceUnit}; must not be {@code null} + * + * @return a non-{@code null} {@link EntityManagerFactory} + * + * @see #registerPersistenceUnitInjectionPoint(InjectionPoint) + * + * @deprecated See the documentation for the {@link + * JpaInjectionServices#resolvePersistenceUnit(InjectionPoint)} + * method. + */ + @Deprecated + @Override + public EntityManagerFactory resolvePersistenceUnit(final InjectionPoint injectionPoint) { + return this.registerPersistenceUnitInjectionPoint(injectionPoint).createResource().getInstance(); + } + + + /* + * Static methods. + */ + + + /** + * Returns a {@link PersistenceProvider} for the supplied {@link + * PersistenceUnitInfo}. + * + *

This method never returns {@code null}.

+ * + * @param persistenceUnitInfo the {@link PersistenceUnitInfo} in + * question; must not be {@code null} + * + * @return a non-{@code null} {@link PersistenceProvider} + * + * @exception NullPointerException if {@code persistenceUnitInfo} + * was {@code null} + * + * @exception + * javax.enterprise.inject.UnsatisfiedResolutionException if no + * {@link PersistenceProvider} could be found + * + * @exception javax.enterprise.inject.AmbiguousResolutionException + * if there were many possible {@link PersistenceProvider}s that + * could be returned + * + * @exception ReflectiveOperationException if there was a + * reflection-related error + */ + private static PersistenceProvider getPersistenceProvider(final PersistenceUnitInfo persistenceUnitInfo) + throws ReflectiveOperationException { + final String providerClassName = Objects.requireNonNull(persistenceUnitInfo).getPersistenceProviderClassName(); + final PersistenceProvider persistenceProvider; + final CDI cdi = CDI.current(); + assert cdi != null; + if (providerClassName == null) { + persistenceProvider = cdi.select(PersistenceProvider.class).get(); + } else { + persistenceProvider = + (PersistenceProvider) cdi.select(Class.forName(providerClassName, + true, + Thread.currentThread().getContextClassLoader())).get(); + } + return persistenceProvider; + } + + /** + * Given the name of a persistence unit, uses a {@link + * BeanManager} internally to locate a {@link PersistenceUnitInfo} + * qualified with a {@link Named} annotation that {@linkplain + * Named#value() has the same name} as the supplied {@code name}, + * and returns it. + * + *

This method never returns {@code null}.

+ * + *

If there is only one {@link PersistenceUnitInfo} present in + * the CDI container, then it will be returned by this method when + * it is invoked, regardless of the value of the {@code name} + * parameter.

+ * + * @param name the name of the {@link PersistenceUnitInfo} to + * return; may be effectively ignored in some cases; must not be + * {@code null} + * + * @return a non-{@code null} {@link PersistenceUnitInfo}, which + * may not have the same name as that which was requested if it + * was the only such {@link PersistenceUnitInfo} in the CDI + * container + * + * @exception NullPointerException if {@code name} is {@code null} + * + * @exception javax.enterprise.inject.AmbiguousResolutionException + * if there somehow was more than one {@link PersistenceUnitInfo} + * available + * + * @exception + * javax.enterprise.inject.UnsatisfiedResolutionException if there + * were no {@link PersistenceUnitInfo} instances available + */ + private static PersistenceUnitInfo getPersistenceUnitInfo(final String name) { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "getPersistenceUnitInfo"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, name); + } + + Objects.requireNonNull(name); + final CDI cdi = CDI.current(); + assert cdi != null; + final BeanManager beanManager = cdi.getBeanManager(); + assert beanManager != null; + final Named named = NamedLiteral.of(name); + assert named != null; + Set> beans = beanManager.getBeans(PersistenceUnitInfo.class, named); + final boolean warn; + if (beans == null || beans.isEmpty()) { + beans = beanManager.getBeans(PersistenceUnitInfo.class, Any.Literal.INSTANCE); + warn = LOGGER.isLoggable(Level.WARNING) && beans != null && !beans.isEmpty(); + } else { + warn = false; + } + if (beans == null || beans.isEmpty()) { + // Let CDI blow up in whatever way it does here. + cdi.select(PersistenceUnitInfo.class, named).get(); + throw new AssertionError(); + } + Bean bean = null; + final int size = beans.size(); + assert size > 0; + switch (size) { + case 1: + // We either got the explicit one we asked for + // (e.g. "dev"), or the only one there was (we asked for + // "dev"; the only one that was there was "test"). We may + // need to revisit this; this may be *too* convenient. + bean = beans.iterator().next(); + break; + default: + bean = beanManager.resolve(beans); + break; + } + final PersistenceUnitInfo returnValue = + (PersistenceUnitInfo) beanManager.getReference(bean, + PersistenceUnitInfo.class, + beanManager.createCreationalContext(bean)); + if (warn) { + LOGGER.logp(Level.WARNING, cn, mn, + "persistenceUnitNameMismatch", + new Object[] {returnValue, returnValue.getPersistenceUnitName(), name}); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Given a {@link Map} of {@link EntityManagerFactory} instances + * indexed by their persistence unit names, an optional {@link + * PersistenceUnitInfo} and the name of a persistence unit, + * returns a suitable {@link EntityManagerFactory} for the implied + * persistence unit, creating it if necessary. + * + *

This method never returns {@code null}.

+ * + *

The contents of the supplied {@link Map} may be altered by + * this method.

+ * + * @param emfs a {@link Map} of {@link EntityManagerFactory} + * instances indexed by their persistence unit names; must not be + * {@code null} but may be {@linkplain Map#isEmpty() empty} + * + * @param info a {@link PersistenceUnitInfo}; may be {@code null} + * in which case the supplied {@code name} must not be {@code + * null} + * + * @param name the name of the persistence unit; must not be + * {@code null}; if the supplied {@link PersistenceUnitInfo} is + * not {@code null} then {@linkplain + * PersistenceUnitInfo#getPersistenceUnitName() its name} should + * be equal to this value, but is not required to be + * + * @return a non-{@code null} {@link EntityManagerFactory} + * + * @exception NullPointerException if either {@code emfs} or + * {@code name} is {@code null} + * + * @see #computeEntityManagerFactory(PersistenceUnitInfo, String, + * EntityManagerFactory) + */ + private static EntityManagerFactory computeEntityManagerFactory(final Map emfs, + final PersistenceUnitInfo info, + final String name) { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "computeEntityManagerFactory"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {emfs, info, name}); + } + + Objects.requireNonNull(emfs); + Objects.requireNonNull(name); + + final EntityManagerFactory returnValue = emfs.compute(name, (n, emf) -> computeEntityManagerFactory(info, n, emf)); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Returns the supplied {@link EntityManagerFactory}, if it is + * non-{@code null} and {@linkplain EntityManagerFactory#isOpen() + * open}, or creates a new one and returns it. + * + *

This method never returns {@code null}.

+ * + *

If creation is called for, then the supplied {@link + * PersistenceUnitInfo}'s {@linkplain + * PersistenceUnitInfo#getTransactionType() affiliated + * PersistenceUnitTransactionType} is checked to see + * if it is {@link + * javax.persistence.spi.PersistenceUnitTransactionType#RESOURCE_LOCAL + * RESOURCE_LOCAL}. If it is, then creation occurs by an + * invocation of the {@link + * Persistence#createEntityManagerFactory(String)} method. + * Otherwise, it occurs by an invocation of the {@link + * #createContainerManagedEntityManagerFactory(PersistenceUnitInfo)} + * method.

+ * + * @param info a {@link PersistenceUnitInfo} describing a + * persistence unit; may be {@code null} + * + * @param name the name of the persistence unit; must not be + * {@code null} + * + * @param existing an {@link EntityManagerFactory} that was + * already associated with the supplied {@code name}; may be + * {@code null} + * + * @return the supplied {@link EntityManagerFactory} if it is + * non-{@code null}, or a new one; never {@code null} + * + * @exception NullPointerException if {@code name} is {@code null} + * + * @exception PersistenceException if a persistence-related error + * occurs + * + * @see #createContainerManagedEntityManagerFactory(PersistenceUnitInfo) + */ + private static EntityManagerFactory computeEntityManagerFactory(final PersistenceUnitInfo info, + final String name, + final EntityManagerFactory existing) { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "computeEntityManagerFactory"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {info, name, existing}); + } + + final EntityManagerFactory returnValue; + if (existing == null) { + if (isResourceLocal(info)) { + returnValue = Persistence.createEntityManagerFactory(name); + } else { + EntityManagerFactory temp = null; + try { + temp = createContainerManagedEntityManagerFactory(info); + } catch (final ReflectiveOperationException reflectiveOperationException) { + throw new PersistenceException(reflectiveOperationException.getMessage(), reflectiveOperationException); + } finally { + returnValue = temp; + } + } + } else { + returnValue = existing; + if (LOGGER.isLoggable(Level.WARNING) && !isResourceLocal(info)) { + final Map properties = existing.getProperties(); + if (properties == null + || !Boolean.TRUE.equals(properties.get("io.helidon.integrations.cdi.jpa.weld.containerManaged"))) { + LOGGER.logp(Level.WARNING, cn, mn, + "transactionTypeMismatch", + new Object[] {name, returnValue}); + } + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Creates an {@link EntityManagerFactory} suitable for the + * supplied {@link PersistenceUnitInfo}, {@linkplain + * PersistenceProvider#createContainerEntityManagerFactory(PersistenceUnitInfo, + * Map) following the JPA 2.2 specification}. + * + *

This method returns a new {@link EntityManagerFactory} each + * time it is invoked.

+ * + *

This method never returns {@code null}.

+ * + * @param info the {@link PersistenceUnitInfo} describing the + * persistence unit; must not be {@code null}; should have an + * {@linkplain PersistenceUnitInfo#getTransactionType() affiliated + * PersistenceUnitTransactionType} equal to {@link + * javax.persistence.spi.PersistenceUnitTransactionType#JTA JTA} + * + * @return a new {@link EntityManagerFactory}; never {@code null} + * + * @exception NullPointerException if {@code info} is {@code null} + * + * @exception PersistenceException if a persistence-related error + * occurs + * + * @exception ReflectiveOperationException if a reflection-related + * error occurs + * + * @see + * PersistenceProvider#createContainerEntityManagerFactory(PersistenceUnitInfo, + * Map) + */ + private static EntityManagerFactory createContainerManagedEntityManagerFactory(final PersistenceUnitInfo info) + throws ReflectiveOperationException { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "createContainerManagedEntityManagerFactory"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, info); + } + + Objects.requireNonNull(info); + final PersistenceProvider persistenceProvider = getPersistenceProvider(info); + assert persistenceProvider != null; + final CDI cdi = CDI.current(); + assert cdi != null; + final BeanManager beanManager = cdi.getBeanManager(); + assert beanManager != null; + final Map properties = new HashMap<>(); + properties.put("io.helidon.integrations.cdi.jpa.weld.containerManaged", Boolean.TRUE); + properties.put("javax.persistence.bean.manager", beanManager); + Class validatorFactoryClass = null; + try { + validatorFactoryClass = Class.forName("javax.validation.ValidatorFactory"); + } catch (final ClassNotFoundException classNotFoundException) { + + } + if (validatorFactoryClass != null) { + final Bean vfb = getValidatorFactoryBean(beanManager, validatorFactoryClass); + if (vfb != null) { + final CreationalContext cc = beanManager.createCreationalContext(vfb); + properties.put("javax.persistence.validation.factory", beanManager.getReference(vfb, validatorFactoryClass, cc)); + } + } + final EntityManagerFactory returnValue = persistenceProvider.createContainerEntityManagerFactory(info, properties); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Returns a {@link Bean} that can {@linkplain + * Bean#create(CreationalContext) create} a {@link + * javax.validation.ValidatorFactory}, or {@code null} if no such + * {@link Bean} is available. + * + *

This method may return {@code null}.

+ * + * @param beanManager the {@link BeanManager} in effect; may be + * {@code null} in which case {@code null} will be returned + * + * @param validatorFactoryClass a {@link Class}; may be {@code + * null}; if not {@linkplain Class#getName() named} {@code + * javax.validation.ValidatorFactory} then {@code null} will be + * returned + * + * @return a {@link Bean} that can {@linkplain + * Bean#create(CreationalContext) create} a {@link + * javax.validation.ValidatorFactory}, or {@code null} + */ + private static Bean getValidatorFactoryBean(final BeanManager beanManager, + final Class validatorFactoryClass) { + Bean returnValue = null; + if (beanManager != null + && validatorFactoryClass != null + && "javax.validation.ValidatorFactory".equals(validatorFactoryClass.getName())) { + final Set> beans = beanManager.getBeans(validatorFactoryClass); + if (beans != null && !beans.isEmpty()) { + returnValue = beanManager.resolve(beans); + } + } + return returnValue; + } + + /** + * Returns {@code true} if and only if the supplied {@link + * PersistenceUnitInfo} is {@code null} or has an {@linkplain + * PersistenceUnitInfo#getTransactionType() affiliated + * PersistenceUnitTransactionType} equal to {@link + * javax.persistence.spi.PersistenceUnitTransactionType#RESOURCE_LOCAL + * RESOURCE_LOCAL}. + * + * @param persistenceUnitInfo the {@link PersistenceUnitInfo} to + * test; may be {@code null} in which case {@code true} will be + * returned + * + * @return {@code true} if and only if the supplied {@link + * PersistenceUnitInfo} is {@code null} or has an {@linkplain + * PersistenceUnitInfo#getTransactionType() affiliated + * PersistenceUnitTransactionType} equal to {@link + * javax.persistence.spi.PersistenceUnitTransactionType#RESOURCE_LOCAL + * RESOURCE_LOCAL} + */ + private static boolean isResourceLocal(final PersistenceUnitInfo persistenceUnitInfo) { + return persistenceUnitInfo == null || RESOURCE_LOCAL.equals(persistenceUnitInfo.getTransactionType()); + } + + + /* + * Inner and nested classes. + */ + + + private static final class EntityManagerFactoryResourceReference implements ResourceReference { + + private final Map emfs; + + private final String name; + + private final PersistenceUnitInfo persistenceUnitInfo; + + private EntityManagerFactoryResourceReference(final Map emfs, + final String name) { + super(); + final String cn = EntityManagerFactoryResourceReference.class.getName(); + final String mn = ""; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {emfs, name}); + } + + this.emfs = Objects.requireNonNull(emfs); + this.name = Objects.requireNonNull(name); + this.persistenceUnitInfo = getPersistenceUnitInfo(name); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + @Override + public EntityManagerFactory getInstance() { + final String cn = EntityManagerFactoryResourceReference.class.getName(); + final String mn = "getInstance"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + // See https://developer.jboss.org/message/984489#984489; + // there is no contract governing whether, for example, an + // EntityManagerFactory should be created from within + // ResourceReference#getInstance() or from within + // ResourceReferenceFactory#createResource(). The + // maintainers of Weld and CDI suggest following what + // Wildfly does, as it is most likely (!) to be correct. + // So that's what we do. + final EntityManagerFactory returnValue = computeEntityManagerFactory(emfs, this.persistenceUnitInfo, this.name); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + @Override + public void release() { + final String cn = EntityManagerFactoryResourceReference.class.getName(); + final String mn = "release"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + final EntityManagerFactory emf = this.emfs.remove(this.name); + if (emf != null && emf.isOpen()) { + emf.close(); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + } + + private final class EntityManagerResourceReference implements ResourceReference { + + // @GuardedBy("this") + private EntityManager em; + + private final Future emfFuture; + + private final Supplier emSupplier; + + private EntityManagerResourceReference(final String name, + final SynchronizationType synchronizationType) { + super(); + final String cn = EntityManagerResourceReference.class.getName(); + final String mn = ""; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {name, synchronizationType}); + } + + Objects.requireNonNull(name); + Objects.requireNonNull(synchronizationType); + + // Kick off the lengthy process of setting up an + // EntityManagerFactory in the background with the + // optimistic assumption, possibly incorrect, that someone + // will call getInstance() at some point. + final ExecutorService taskExecutorService = + ((WeldManager) CDI.current().getBeanManager()).getServices().get(ExecutorServices.class).getTaskExecutor(); + assert taskExecutorService != null; + final PersistenceUnitInfo persistenceUnitInfo = getPersistenceUnitInfo(name); + this.emfFuture = + taskExecutorService.submit(() -> computeEntityManagerFactory(emfs, persistenceUnitInfo, name)); + + if (isResourceLocal(persistenceUnitInfo)) { + this.emSupplier = () -> { + try { + return emfFuture.get().createEntityManager(); + } catch (final ExecutionException executionException) { + final Throwable cause = executionException.getCause(); + assert cause != null; + throw new PersistenceException(cause.getMessage(), cause); + } catch (final InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new RuntimeException(interruptedException.getMessage(), interruptedException); + } + }; + } else { + assert JTA.equals(persistenceUnitInfo.getTransactionType()); + this.emSupplier = () -> { + try { + final EntityManager em = emfFuture.get().createEntityManager(synchronizationType); + assert em != null; + em.setProperty(SynchronizationType.class.getName(), synchronizationType); + WeldJpaInjectionServices.this.containerManagedEntityManagers.add(em); + return em; + } catch (final ExecutionException executionException) { + final Throwable cause = executionException.getCause(); + assert cause != null; + throw new PersistenceException(cause.getMessage(), cause); + } catch (final InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new RuntimeException(interruptedException.getMessage(), interruptedException); + } + }; + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + @Override + public EntityManager getInstance() { + final String cn = EntityManagerResourceReference.class.getName(); + final String mn = "getInstance"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + final EntityManager em; + synchronized (this) { + // See + // https://developer.jboss.org/message/984489#984489; + // there is no contract governing whether, for + // example, an EntityManager should be created from + // within ResourceReference#getInstance() or from + // within ResourceReferenceFactory#createResource(). + // The maintainers of Weld and CDI suggest following + // what Wildfly does, as it is most likely (!) to be + // correct. So that's what we do. + // + // We also ensure that this + // EntityManagerResourceReference, no matter what, + // vends a non-null EntityManager whose isOpen() + // method returns true. + if (this.em == null || !this.em.isOpen()) { + this.em = this.emSupplier.get(); + } + em = this.em; + } + assert em != null; + assert em.isOpen(); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, em); + } + return em; + } + + @Override + public void release() { + final String cn = EntityManagerResourceReference.class.getName(); + final String mn = "release"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + final EntityManager em; + synchronized (this) { + em = this.em; + this.em = null; + } + + if (em != null) { + WeldJpaInjectionServices.this.containerManagedEntityManagers.remove(em); + if (em.isOpen()) { + em.close(); + } + } + if (!this.emfFuture.isDone()) { + this.emfFuture.cancel(true); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + } + +} diff --git a/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServicesExtension.java b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServicesExtension.java new file mode 100644 index 00000000000..3ec49e349c7 --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServicesExtension.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.jpa.weld; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.Extension; +import javax.inject.Singleton; + +/** + * An {@link Extension} that exists solely to make the {@link + * WeldJpaInjectionServices} class become a bean in {@link Singleton} + * scope. + * + * @see WeldJpaInjectionServices + * + * @see TransactionObserver + */ +final class WeldJpaInjectionServicesExtension implements Extension { + + + /* + * Static fields. + */ + + + /** + * The {@link Logger} for use by all instances of this class. + * + *

This field is never {@code null}.

+ */ + private static final Logger LOGGER = Logger.getLogger(WeldJpaInjectionServicesExtension.class.getName(), "messages"); + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link WeldJpaInjectionServicesExtension}. + * + *

Oddly, the fact that this constructor is {@code private} + * does not prevent Weld from loading it as a service. This is an + * unexpected bonus as nothing about this class should be {@code + * public}.

+ */ + private WeldJpaInjectionServicesExtension() { + super(); + final String cn = WeldJpaInjectionServicesExtension.class.getName(); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, ""); + LOGGER.exiting(cn, ""); + } + } + + + /* + * Instance methods. + */ + + + /** + * Creates a bean deliberately in {@link Singleton} scope to + * represent the {@link WeldJpaInjectionServices} class. + * + *

Weld often creates multiple copies of {@link + * WeldJpaInjectionServices} by virtue of the way it loads its + * bootstrap services (see + * https://issues.jboss.org/browse/WELD-2563 for details). We + * want to ensure there's just one that can be injected into + * observer methods. See the {@link TransactionObserver} class, + * which houses one such observer method.

+ * + * @param event the {@link AfterBeanDiscovery} event; may be + * {@code null} in which case no action will be taken + * + * @see WeldJpaInjectionServices + * + * @see TransactionObserver + */ + private void afterBeanDiscovery(@Observes final AfterBeanDiscovery event) { + final String cn = WeldJpaInjectionServicesExtension.class.getName(); + final String mn = "afterBeanDiscovery"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, event); + } + + if (event != null) { + event.addBean() + .addTransitiveTypeClosure(WeldJpaInjectionServices.class) + .scope(Singleton.class) + .createWith(ignored -> { + return WeldJpaInjectionServices.getInstance(); + }); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + +} diff --git a/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/package-info.java b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/package-info.java new file mode 100644 index 00000000000..b93d33e515b --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 that help to integrate JPA into Weld-based CDI 2.0 SE environments. + * + * @see + * io.helidon.integrations.cdi.jpa.weld.WeldJpaInjectionServices + */ +package io.helidon.integrations.cdi.jpa.weld; diff --git a/integrations/cdi/jpa-weld/src/main/resources/META-INF/beans.xml b/integrations/cdi/jpa-weld/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..fe2feaf624d --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/resources/META-INF/beans.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000000..aa9fb997467 --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.integrations.cdi.jpa.weld.WeldJpaInjectionServicesExtension diff --git a/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service b/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service new file mode 100644 index 00000000000..ca8a18d0a1e --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.integrations.cdi.jpa.weld.WeldJpaInjectionServices diff --git a/integrations/cdi/jpa-weld/src/main/resources/messages.properties b/integrations/cdi/jpa-weld/src/main/resources/messages.properties new file mode 100644 index 00000000000..012fe3f670a --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/resources/messages.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +transactionTypeMismatch=A resource-local EntityManagerFactory was requested for \ + the persistence unit named "{0}", but a JTA \ + EntityManagerFactory, {1}, was previously associated \ + with the persistence unit. +persistenceUnitNameMismatch=The sole PersistenceUnitInfo, {0}, representing \ + the persistence unit with name "{1}", will be used \ + instead of the requested persistence unit named \ + "{2}" +experimental=JPA support is currently experimental and not suitable for \ + production use. diff --git a/integrations/cdi/jpa-weld/src/test/java/io/helidon/integrations/cdi/jpa/weld/TestIntegration.java b/integrations/cdi/jpa-weld/src/test/java/io/helidon/integrations/cdi/jpa/weld/TestIntegration.java new file mode 100644 index 00000000000..38e58e7d87a --- /dev/null +++ b/integrations/cdi/jpa-weld/src/test/java/io/helidon/integrations/cdi/jpa/weld/TestIntegration.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.jpa.weld; + +import javax.annotation.sql.DataSourceDefinition; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.se.SeContainer; +import javax.enterprise.inject.se.SeContainerInitializer; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.Transactional; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@ApplicationScoped +@DataSourceDefinition( + name = "test", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:test", + serverName = "", + properties = { + "user=sa" + } +) +public class TestIntegration { + + private SeContainer cdiContainer; + + @Inject + private Transaction transaction; + + @PersistenceContext + private EntityManager entityManager; + + TestIntegration() { + super(); + } + + @BeforeEach + void startCdiContainer() { + shutDownCdiContainer(); + final SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .addBeanClasses(TestIntegration.class); + assertNotNull(initializer); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + this.cdiContainer = null; + } + } + + private static void onStartup(@Observes @Initialized(ApplicationScoped.class) final Object event, + final TestIntegration self) + throws SystemException { + assertNotNull(event); + assertNotNull(self); + self.doSomethingTransactional(); + self.doSomethingNonTransactional(); + } + + @Transactional(Transactional.TxType.REQUIRED) + void doSomethingTransactional() throws SystemException { + assertNotNull(this.transaction); + assertEquals(Status.STATUS_ACTIVE, this.transaction.getStatus()); + assertNotNull(this.entityManager); + assertTrue(this.entityManager.isOpen()); + assertTrue(this.entityManager.isJoinedToTransaction()); + } + + void doSomethingNonTransactional() { + assertNotNull(this.transaction); // ...but the scope won't be active + try { + this.transaction.toString(); + fail("The TransactionScoped scope was active when it should not have been"); + } catch (final ContextNotActiveException expected) { + + } + assertNotNull(this.entityManager); + assertTrue(this.entityManager.isOpen()); + + } + + @Test + void testIntegration() { + } + +} diff --git a/integrations/cdi/jpa-weld/src/test/java/logging.properties b/integrations/cdi/jpa-weld/src/test/java/logging.properties new file mode 100644 index 00000000000..fc7b373c15f --- /dev/null +++ b/integrations/cdi/jpa-weld/src/test/java/logging.properties @@ -0,0 +1,24 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +.level=WARNING +com.arjuna.level=WARNING +com.zaxxer.hikari.level=WARNING +handlers=java.util.logging.ConsoleHandler +io.helidon.integrations.cdi.jpa.level=WARNING +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.level=FINEST +org.eclipse.persistence.level=WARNING +org.jboss.weld.level=WARNING diff --git a/integrations/cdi/jpa-weld/src/test/resources/META-INF/persistence.xml b/integrations/cdi/jpa-weld/src/test/resources/META-INF/persistence.xml new file mode 100644 index 00000000000..afb0d0093f7 --- /dev/null +++ b/integrations/cdi/jpa-weld/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,48 @@ + + + + + test + + + + + + + + + + + + + + + + diff --git a/integrations/cdi/jta-cdi/README.adoc b/integrations/cdi/jta-cdi/README.adoc new file mode 100644 index 00000000000..050ef87511c --- /dev/null +++ b/integrations/cdi/jta-cdi/README.adoc @@ -0,0 +1,23 @@ += Helidon JTA CDI Integration + +The Helidon JTA CDI Integration project performs the +CDI-provider-agnostic work of integrating a JTA implementation into standalone CDI +applications (including those based on Helidon MicroProfile). It is +one of two projects that together make up overall JTA support for +standalone CDI applications. + +To function properly, this project also requires: + +* a CDI-provider-specific counterpart, such as the `jta-weld` project + found elsewhere in this git repository + +IMPORTANT: Please note that this feature is currently experimental and + not suitable for production use. + +== Installation + +Ensure that the Helidon JTA CDI Integration project and its runtime +dependencies are present on your application's runtime classpath. + +Please see the `examples/integrations/cdi/jpa` project found elsewhere +in this git repository for a working `pom.xml` file that uses this project. diff --git a/integrations/cdi/jta-cdi/pom.xml b/integrations/cdi/jta-cdi/pom.xml new file mode 100644 index 00000000000..6d7f0ebbd65 --- /dev/null +++ b/integrations/cdi/jta-cdi/pom.xml @@ -0,0 +1,109 @@ + + + + 4.0.0 + + io.helidon.integrations.cdi + helidon-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-cdi-jta + Helidon CDI Integrations JTA + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.jboss.weld.se + weld-se-core + test + + + org.slf4j + slf4j-simple + test + + + + + org.jboss + jandex + runtime + + + org.jboss + jboss-transaction-spi + runtime + + + + + javax.annotation + javax.annotation-api + provided + + + javax.transaction + javax.transaction-api + provided + + + javax.enterprise + cdi-api + provided + + + + + org.jboss.narayana.jta + cdi + compile + + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + process-classes + + + + + + diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionManager.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionManager.java new file mode 100644 index 00000000000..af1bba75d93 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionManager.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.jta.cdi; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.InvalidTransactionException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.Status; // for javadoc only +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +/** + * An {@code abstract} {@link TransactionManager} implementation that + * delegates all method invocations to another {@link + * TransactionManager}. + * + *

Design Notes

+ * + *

This class is {@code public} for convenience. It is extended by + * other non-{@code public} internal classes.

+ * + * @see TransactionManager + */ +public abstract class DelegatingTransactionManager implements TransactionManager { + + private final TransactionManager delegate; + + /** + * Creates a new {@link DelegatingTransactionManager}. + * + * @param delegate the {@link TransactionManager} to which all + * method invocations will be delegated; may be {@code null}, but + * then a {@link SystemException} will be thrown by every method + * in this class when invoked + */ + protected DelegatingTransactionManager(final TransactionManager delegate) { + super(); + this.delegate = delegate; + } + + /** + * Creates a new transaction and associates it with the current + * thread. + * + * @exception NotSupportedException if the thread is already + * associated with a transaction and this {@link + * TransactionManager} implementation does not support nested + * transactions + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public void begin() throws NotSupportedException, SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.begin(); + } + + /** + * Completes the transaction associated with the current thread. + * + *

When this method completes, the thread is no longer + * associated with a transaction.

+ * + * @exception RollbackException if the transaction has been rolled + * back rather than committed + * + * @exception HeuristicMixedException if a heuristic decision was + * made and that some relevant updates have been committed while + * others have been rolled back + * + * @exception HeuristicRollbackException if a heuristic decision + * was made and all relevant updates have been rolled back + * + * @exception SecurityException if the thread is not allowed to + * commit the transaction + * + * @exception IllegalStateException if the current thread is not + * associated with a transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.commit(); + } + + /** + * Returns the status of the transaction associated with the + * current thread. + * + * @return the transaction status expressed as the value of one of + * the {@code int} constants in the {@link Status} class; if no + * transaction is associated with the current thread, this method + * returns {@link Status#STATUS_NO_TRANSACTION} + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + * + * @see Status + */ + @Override + public int getStatus() throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + return this.delegate.getStatus(); + } + + /** + * Returns the {@link Transaction} object that represents the + * transaction context of the calling thread. + * + *

This method never returns {@code null}.

+ * + * @return the {@link Transaction} object representing the + * transaction associated with the calling thread; never {@code + * null} + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public Transaction getTransaction() throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + return this.delegate.getTransaction(); + } + + /** + * Resumes the transaction context association of the calling + * thread with the transaction represented by the supplied {@link + * Transaction} object. + * + *

When this method returns, the calling thread is associated + * with the transaction context specified.

+ * + * @param transaction the {@link Transaction} representing the + * transaction to be resumed; must not be {@code null} + * + * @exception InvalidTransactionException if {@code transaction} + * is invalid + * + * @exception IllegalStateException if the thread is already + * associated with another transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public void resume(final Transaction transaction) throws InvalidTransactionException, SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.resume(transaction); + } + + /** + * Rolls back the transaction associated with the current thread. + * + *

When this method completes, the thread is no longer + * associated with a transaction.

+ * + * @exception SecurityException if the thread is not allowed to + * roll back the transaction + * + * @exception IllegalStateException if the current thread is not + * associated with a transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public void rollback() throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.rollback(); + } + + /** + * Irrevocably modifies the transaction associated with the + * current thread such that the only possible outcome is for it to + * {@linkplain #rollback() roll back}. + * + * @exception IllegalStateException if the current thread is not + * associated with a transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public void setRollbackOnly() throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.setRollbackOnly(); + } + + /** + * Sets the timeout value that is associated with transactions + * started by the current thread with the {@link #begin()} method. + * + *

If an application has not called this method, the + * transaction service uses some default value for the transaction + * timeout.

+ * + * @param seconds the timeout in seconds; if the value is zero, + * the transaction service restores the default value; if the + * value is negative a {@link SystemException} is thrown + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition or if {@code seconds} + * is less than zero + */ + @Override + public void setTransactionTimeout(final int seconds) throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.setTransactionTimeout(seconds); + } + + /** + * Suspends the transaction currently associated with the calling + * thread and returns a {@link Transaction} that represents the + * transaction context being suspended, or {@code null} if the + * calling thread is not associated with a transaction. + * + *

This method may return {@code null}.

+ * + *

When this method returns, the calling thread is no longer + * associated with a transaction.

+ * + * @return a {@link Transaction} representing the suspended + * transaction, or {@code null} + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public Transaction suspend() throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + return this.delegate.suspend(); + } + +} diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionSynchronizationRegistry.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionSynchronizationRegistry.java new file mode 100644 index 00000000000..526bd094ed7 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionSynchronizationRegistry.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.jta.cdi; + +import java.util.Map; // for javadoc only + +import javax.transaction.Status; // for javadoc only +import javax.transaction.Synchronization; +import javax.transaction.Transaction; // for javadoc only +import javax.transaction.TransactionManager; // for javadoc only +import javax.transaction.TransactionSynchronizationRegistry; + +/** + * An {@code abstract} {@link TransactionSynchronizationRegistry} + * implementation that delegates all method invocations to another + * {@link TransactionSynchronizationRegistry}. + * + *

Design Notes

+ * + *

This class is {@code public} for convenience. It is extended by + * other non-{@code public} internal classes.

+ * + * @see TransactionSynchronizationRegistry + */ +public abstract class DelegatingTransactionSynchronizationRegistry implements TransactionSynchronizationRegistry { + + private final TransactionSynchronizationRegistry delegate; + + /** + * Creates a new {@link + * DelegatingTransactionSynchronizationRegistry}. + * + * @param delegate the {@link TransactionSynchronizationRegistry} + * to which all method invocations will be delegated; may be + * {@code null} in which case every method in this class will + * throw an {@link IllegalStateException} when invoked + * + */ + protected DelegatingTransactionSynchronizationRegistry(final TransactionSynchronizationRegistry delegate) { + super(); + this.delegate = delegate; + } + + /** + * Return an opaque object to represent the transaction bound to + * the current thread at the time this method is called. + * + *

This method may return {@code null}.

+ * + *

This object overrides {@link Object#hashCode()} and {@link + * Object#equals(Object)} to allow its use as the key in a {@link + * Map} for use by the caller. If there is no transaction + * currently active, this method will return {@code null}.

+ * + *

The {@link Object} returned will return the same hashCode + * and compare equal to all other objects returned by calling this + * method from any component executing in the same transaction + * context in the same application server.

+ * + *

The {@link Object#toString()} method returns a {@link + * String} that might be usable by a human reader to usefully + * understand the transaction context. The {@link + * Object#toString()} result is otherwise not + * defined. Specifically, there is no forward or backward + * compatibility guarantee of the results of the returned {@link + * Object}'s {@link Object#toString()} override.

+ * + *

The object is not necessarily serializable, and has no + * defined behavior outside the virtual machine whence it was + * obtained.

+ * + * @return an opaque object representing the transaction bound to + * the current thread at the time this method is called, or {@code + * null} + * + * @exception IllegalStateException if a {@code null} {@code + * delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + */ + @Override + public Object getTransactionKey() { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + return this.delegate.getTransactionKey(); + } + + /** + * Adds or replaces an object in the {@link Map} of resources + * being managed for the transaction bound to the current thread + * at the time this method is called. + * + *

The supplied key should be of an caller-defined class so as + * not to conflict with other users. The class of the key must + * guarantee that the {@link Object#hashCode() hashCode()} and + * {@link Object#equals(Object) equals(Object)} methods are + * suitable for use as keys in a {@link Map}. The key and value + * are not examined or used by the implementation. The general + * contract of this method is that of {@link Map#put(Object, + * Object)} for a {@link Map} that supports non-{@code null} keys + * and null values. For example, if there is already an value + * associated with the key, it is replaced by the {@code value} + * parameter.

+ * + * @param key the key for the {@link Map} entry; must not be + * {@code null} + * + * @param value the value for the {@link Map} entry + * + * @exception IllegalStateException if no transaction is active or + * if a {@code null} {@code delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + * + * @exception NullPointerException if the parameter {@code key} is + * {@code null} + */ + @Override + public void putResource(final Object key, final Object value) { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + this.delegate.putResource(key, value); + } + + /** + * Gets an object from the {@link Map} of resources being managed + * for the transaction bound to the current thread at the time + * this method is called. + * + *

The key should have been supplied earlier by a call to + * {@link #putResource(Object, Object)} in the same + * transaction. If the key cannot be found in the current resource + * {@link Map}, {@code null} is returned. The general contract of + * this method is that of {@link Map#get(Object)} for a {@link + * Map} that supports non-{@code null} keys and null values. For + * example, the returned value is null if there is no entry for + * the parameter {@code key} or if the value associated with the + * key is actually {@code null}.

+ * + * @param key the key for the {@link Map} entry + * + * @return the value associated with the supplied {@code key}; may + * be {@code null} + * + * @exception IllegalStateException if no transaction is active or + * if a {@code null} {@code delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + * + * @exception NullPointerException if the parameter {@code key} is + * {@code null} + */ + @Override + public Object getResource(final Object key) { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + return this.delegate.getResource(key); + } + + /** + * Registers a {@link Synchronization} instance with special + * ordering semantics. + * + *

The supplied {@link Synchronization}'s {@link + * Synchronization#beforeCompletion()} method will be called after + * all {@code SessionSynchronization#beforeCompletion()} callbacks + * and callbacks registered directly with the {@link Transaction}, + * but before the 2-phase commit process starts. Similarly, the + * {@link Synchronization#afterCompletion(int)} callback will be + * called after 2-phase commit completes but before any {@code + * SessionSynchronization} and {@link Transaction} {@code + * afterCompletion(int)} callbacks.

+ * + *

The {@link Synchronization#beforeCompletion()} callback will + * be invoked in the transaction context of the transaction bound + * to the current thread at the time this method is + * called. Allowable methods include access to resources, + * e.g. connectors. No access is allowed to "user components" + * (e.g. timer services or bean methods), as these might change + * the state of data being managed by the caller, and might change + * the state of data that has already been flushed by another + * caller of {@link + * #registerInterposedSynchronization(Synchronization)}. The + * general context is the component context of the caller of + * {@link + * #registerInterposedSynchronization(Synchronization)}.

+ * + *

The {@link Synchronization#afterCompletion(int)} callback + * will be invoked in an undefined context. No access is permitted + * to "user components" as defined above. Resources can be closed + * but no transactional work can be performed with them.

+ * + *

If this method is invoked without an active transaction + * context, an {@link IllegalStateException} is thrown.

+ * + *

If this method is invoked after the two-phase commit + * processing has started, an {@link IllegalStateException} is + * thrown.

+ * + * @param synchronization the {@link Synchronization} to register; + * must not be {@code null} + * + * @exception IllegalStateException if no transaction is active or + * two-phase commit processing has started or if a {@code null} + * {@code delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + * + * @see Synchronization + * + * @see Synchronization#beforeCompletion() + * + * @see Synchronization#afterCompletion(int) + */ + @Override + public void registerInterposedSynchronization(final Synchronization synchronization) { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + this.delegate.registerInterposedSynchronization(synchronization); + } + + /** + * Return the status of the transaction bound to the current + * thread at the time this method is called. + * + *

This is the result of executing {@link + * TransactionManager#getStatus()} in the context of the + * transaction bound to the current thread at the time this method + * is called.

+ * + * @return the status of the transaction bound to the current + * thread at the time this method is called; will be equal the + * value of one of the constants defined in the {@link Status} + * class + * + * @exception IllegalStateException if a {@code null} {@code + * delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + * + * @see TransactionManager#getStatus() + * + * @see Status + */ + @Override + public int getTransactionStatus() { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + return this.delegate.getTransactionStatus(); + } + + /** + * Sets the {@code rollbackOnly} status of the transaction bound + * to the current thread at the time this method is called. + * + * @exception IllegalStateException if no transaction is active or + * if a {@code null} {@code delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + */ + @Override + public void setRollbackOnly() { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + this.delegate.setRollbackOnly(); + } + + /** + * Get the {@code rollbackOnly} status of the transaction bound to + * the current thread at the time this method is called. + * + * @return the {@code rollbackOnly} status + * + * @exception IllegalStateException if no transaction is active or + * if a {@code null} {@code delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + */ + @Override + public boolean getRollbackOnly() { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + return this.delegate.getRollbackOnly(); + } + +} diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaExtension.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaExtension.java new file mode 100644 index 00000000000..3765aea85a9 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaExtension.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.jta.cdi; + +import java.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.CreationException; +import javax.enterprise.inject.Default; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.CDI; +import javax.enterprise.inject.spi.Extension; +import javax.inject.Singleton; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.TransactionScoped; +import javax.transaction.UserTransaction; + +import com.arjuna.ats.jta.common.JTAEnvironmentBean; +import com.arjuna.common.internal.util.propertyservice.BeanPopulator; + +import static javax.interceptor.Interceptor.Priority.LIBRARY_BEFORE; + +/** + * A CDI 2.0 + * portable extension that adapts the Narayana transaction engine to a CDI + * 2.0 SE environment. + */ +public final class NarayanaExtension implements Extension { + + + /* + * Static fields. + */ + + + /** + * The {@link Logger} for use by all instances of {@link + * NarayanaExtension}. + * + *

This field is never {@code null}.

+ */ + private static final Logger LOGGER = Logger.getLogger(NarayanaExtension.class.getName(), "messages"); + + /** + * The default {@link JTAEnvironmentBean} used throughout the + * Narayana transaction engine as configured via the {@code + * BeanPopulator} mechanism. + * + *

This field is never {@code null}.

+ */ + private static final JTAEnvironmentBean DEFAULT_JTA_ENVIRONMENT_BEAN = + BeanPopulator.getDefaultInstance(JTAEnvironmentBean.class); + + + /* + * Instance fields. + */ + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link NarayanaExtension}. + */ + public NarayanaExtension() { + super(); + } + + + /* + * Instance methods. + */ + + + /** + * Adds a synthetic bean that creates a {@link Transaction} in + * {@linkplain TransactionScoped transaction scope}. + * + * @param event the {@link AfterBeanDiscovery} event fired by the + * CDI container; may be {@code null} in which case no action will + * be taken + * + * @param beanManager the {@link BeanManager} in effect; may be + * {@code null} in which case no action will be taken + */ + private void afterBeanDiscovery(@Observes final AfterBeanDiscovery event, final BeanManager beanManager) { + final String cn = NarayanaExtension.class.getName(); + final String mn = "afterBeanDiscovery"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, beanManager}); + } + + if (event != null && beanManager != null) { + + // Weld registers a UserTransaction bean well before this + // observer method fires. OpenWebBeans does not. We need + // to handle both cases since this is not part of the CDI + // specification. + Collection> beans = beanManager.getBeans(UserTransaction.class); + if (beans == null || beans.isEmpty()) { + event.addBean() + .types(UserTransaction.class) + // OpenWebBeans does not add these qualifiers; + // Weld does automatically: + .addQualifiers(Any.Literal.INSTANCE, Default.Literal.INSTANCE) + // see + // e.g. https://docs.oracle.com/javaee/6/tutorial/doc/gmgli.html + // which reads in part: "Predefined beans are + // injected with **dependent scope** [emphasis + // mine] and the predefined default + // qualifier @Default." This scope restriction is + // not specified in the CDI specification but + // seems reasonable and widely expected. + .scope(Dependent.class) + .createWith(cc -> com.arjuna.ats.jta.UserTransaction.userTransaction()); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, cn, mn, "addedUserTransactionBean"); + } + } + + event.addBean() + .id(Transaction.class.getName()) + .types(Transaction.class) + .addQualifiers(Any.Literal.INSTANCE, Default.Literal.INSTANCE) // OpenWebBeans does not add these + .scope(TransactionScoped.class) + .createWith(cc -> { + try { + return CDI.current().select(TransactionManager.class).get().getTransaction(); + } catch (final SystemException systemException) { + throw new CreationException(systemException.getMessage(), systemException); + } + }); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, cn, mn, "addedTransactionBean"); + } + + beans = beanManager.getBeans(JTAEnvironmentBean.class); + if (beans == null || beans.isEmpty()) { + event.addBean() + .addTransitiveTypeClosure(JTAEnvironmentBean.class) + // OpenWebBeans does not add these qualifiers; + // Weld does automatically: + .addQualifiers(Any.Literal.INSTANCE, Default.Literal.INSTANCE) + .scope(Singleton.class) + .createWith(cc -> DEFAULT_JTA_ENVIRONMENT_BEAN); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, cn, mn, "addedJtaEnvironmentBeanBean"); + } + } + + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Observes the startup of the CDI container (by observing the + * {@linkplain Initialized initialization} of the {@linkplain + * ApplicationScoped application scope}) and reacts by {@linkplain + * Event#fire(Object) firing an event} consisting of the {@link + * JTAEnvironmentBean} singleton initialized by the Narayana + * transaction engine and preconfigured through its {@code + * BeanPopulator} proprietary mechanism. + * + *

This allows other portable extensions to further configure + * the default {@link JTAEnvironmentBean} in whatever manner they + * see fit.

+ * + * @param event the event representing the {@linkplain + * ApplicationScoped application scope} {@linkplain Initialized + * initialization}; may be {@code null}; ignored + * + * @param broadcaster an {@link Event} capable of {@linkplain + * Event#fire(Object) firing} a {@link JTAEnvironmentBean} + * + * @see JTAEnvironmentBean + */ + private static void onStartup(@Observes + @Initialized(ApplicationScoped.class) + @Priority(LIBRARY_BEFORE) + final Object event, + final Event broadcaster) { + final String cn = NarayanaExtension.class.getName(); + final String mn = "onStartup"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, broadcaster}); + } + if (broadcaster != null) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, cn, mn, "firingJtaEnvironmentBean", DEFAULT_JTA_ENVIRONMENT_BEAN); + } + broadcaster.fire(DEFAULT_JTA_ENVIRONMENT_BEAN); + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + +} diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionManager.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionManager.java new file mode 100644 index 00000000000..0a06e76d3b1 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionManager.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.jta.cdi; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Destroyed; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Event; +import javax.inject.Inject; +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; // for javadoc only +import javax.transaction.TransactionScoped; + +import com.arjuna.ats.jta.common.JTAEnvironmentBean; + +/** + * A {@link DelegatingTransactionManager} in {@linkplain + * ApplicationScoped application scope} that uses the return value + * that results from invoking the {@link + * JTAEnvironmentBean#getTransactionManager()} method as its backing + * implementation. + * + * @see com.arjuna.ats.jta.common.JTAEnvironmentBean#getTransactionManager() + */ +@ApplicationScoped +class NarayanaTransactionManager extends DelegatingTransactionManager { + + + /* + * Instance fields. + */ + + + /** + * An {@link Event} capable of {@linkplain Event#fire(Object) + * firing} a {@link Transaction} when {@linkplain + * TransactionScoped transaction scope} has begun. + * + *

This field may be {@code null}.

+ */ + private final Event transactionScopeInitializedBroadcaster; + + /** + * An {@link Event} capable of {@linkplain Event#fire(Object) + * firing} an {@link Object} when {@linkplain TransactionScoped + * transaction scope} has ended. + * + *

This field may be {@code null}.

+ */ + private final Event transactionScopeDestroyedBroadcaster; + + + /* + * Constructors. + */ + + + /** + * Creates a new, nonfunctional {@link + * NarayanaTransactionManager}. + * + *

This constructor exists only to conform with section + * 3.15 of the CDI specification.

+ * + * @deprecated This constructor exists only to conform with section + * 3.15 of the CDI specification. + * + * @see Section + * 3.15 of the CDI specification + */ + @Deprecated + NarayanaTransactionManager() { + this(null, null, null); + } + + /** + * Creates a new {@link NarayanaTransactionManager}. + * + * @param jtaEnvironmentBean a {@link JTAEnvironmentBean} used to + * acquire this {@link NarayanaTransactionManager}'s delegate; may + * be {@code null} but then a {@link SystemException} will be + * thrown by every method in this class when invoked + * + * @param transactionScopeInitializedBroadcaster an {@link Event} + * capable of {@linkplain Event#fire(Object) firing} {@link + * Transaction} instances; may be {@code null} + * + * @param transactionScopeDestroyedBroadcaster an {@link Event} + * capable of {@linkplain Event#fire(Object) firing} {@link + * Object} instances; may be {@code null} + * + * @see #begin() + * + * @see #commit() + * + * @see #rollback() + */ + @Inject + private NarayanaTransactionManager(final JTAEnvironmentBean jtaEnvironmentBean, + @Initialized(TransactionScoped.class) + final Event transactionScopeInitializedBroadcaster, + @Destroyed(TransactionScoped.class) + final Event transactionScopeDestroyedBroadcaster) { + super(jtaEnvironmentBean == null ? null : jtaEnvironmentBean.getTransactionManager()); + this.transactionScopeInitializedBroadcaster = transactionScopeInitializedBroadcaster; + this.transactionScopeDestroyedBroadcaster = transactionScopeDestroyedBroadcaster; + } + + + /* + * Instance methods. + */ + + + /** + * Overrides {@link DelegatingTransactionManager#begin()} to + * additionally {@linkplain Event#fire(Object) fire} an {@link + * Object} representing the {@linkplain Initialized + * initialization} of the {@linkplain TransactionScoped + * transaction scope}. + * + * @exception NotSupportedException if the thread is already + * associated with a transaction and this {@link + * TransactionManager} implementation does not support nested + * transactions + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + * + * @see DelegatingTransactionManager#begin() + * + * @see Event#fire(Object) + * + * @see Initialized + * + * @see TransactionScoped + */ + @Override + public void begin() throws NotSupportedException, SystemException { + super.begin(); + if (this.transactionScopeInitializedBroadcaster != null) { + this.transactionScopeInitializedBroadcaster.fire(this.getTransaction()); + } + } + + /** + * Overrides {@link DelegatingTransactionManager#commit()} to + * additionally {@linkplain Event#fire(Object) fire} an {@link + * Object} representing the {@linkplain Destroyed destruction} of + * the {@linkplain TransactionScoped transaction scope}. + * + * @exception RollbackException if the transaction has been rolled + * back rather than committed + * + * @exception HeuristicMixedException if a heuristic decision was + * made and that some relevant updates have been committed while + * others have been rolled back + * + * @exception HeuristicRollbackException if a heuristic decision + * was made and all relevant updates have been rolled back + * + * @exception SecurityException if the thread is not allowed to + * commit the transaction + * + * @exception IllegalStateException if the current thread is not + * associated with a transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + * + * @see DelegatingTransactionManager#commit() + * + * @see Event#fire(Object) + * + * @see Destroyed + * + * @see TransactionScoped + */ + @Override + public void commit() throws HeuristicMixedException, HeuristicRollbackException, RollbackException, SystemException { + try { + super.commit(); + } finally { + if (this.transactionScopeDestroyedBroadcaster != null) { + this.transactionScopeDestroyedBroadcaster.fire(this.toString()); + } + } + } + + /** + * Overrides {@link DelegatingTransactionManager#rollback()} to + * additionally {@linkplain Event#fire(Object) fire} an {@link + * Object} representing the {@linkplain Destroyed destruction} of + * the {@linkplain TransactionScoped transaction scope}. + * + * @exception SecurityException if the thread is not allowed to + * roll back the transaction + * + * @exception IllegalStateException if the current thread is not + * associated with a transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + * + * @see DelegatingTransactionManager#rollback() + * + * @see Event#fire(Object) + * + * @see Destroyed + * + * @see TransactionScoped + */ + @Override + public void rollback() throws SystemException { + try { + super.rollback(); + } finally { + if (this.transactionScopeDestroyedBroadcaster != null) { + this.transactionScopeDestroyedBroadcaster.fire(this.toString()); + } + } + } + +} diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionSynchronizationRegistry.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionSynchronizationRegistry.java new file mode 100644 index 00000000000..da18bd6dafc --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionSynchronizationRegistry.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.jta.cdi; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import com.arjuna.ats.jta.common.JTAEnvironmentBean; + +/** + * A {@link DelegatingTransactionSynchronizationRegistry} in + * {@linkplain ApplicationScoped application scope} that uses the + * return value that results from invoking the {@link + * JTAEnvironmentBean#getTransactionSynchronizationRegistry()} method + * as its backing implementation. + * + * @see JTAEnvironmentBean#getTransactionSynchronizationRegistry() + */ +@ApplicationScoped +class NarayanaTransactionSynchronizationRegistry extends DelegatingTransactionSynchronizationRegistry { + + /** + * Creates a new, nonfunctional {@link + * NarayanaTransactionSynchronizationRegistry}. + * + *

This constructor exists only to conform with section 3.15 of + * the CDI specification.

+ * + * @deprecated This constructor exists only to conform with + * section 3.15 of the CDI specification; please use the {@link + * #NarayanaTransactionSynchronizationRegistry(JTAEnvironmentBean)} + * constructor instead. + * + * @see + * #NarayanaTransactionSynchronizationRegistry(JTAEnvironmentBean) + * + * @see Section + * 3.15 of the CDI 2.0 specification + */ + @Deprecated + NarayanaTransactionSynchronizationRegistry() { + this(null); + } + + /** + * Creates a new {@link + * NarayanaTransactionSynchronizationRegistry}. + * + * @param jtaEnvironmentBean the {@link JTAEnvironmentBean} + * describing the environment in which transaction processing will + * take place + * + * @see JTAEnvironmentBean#getTransactionSynchronizationRegistry() + */ + @Inject + private NarayanaTransactionSynchronizationRegistry(final JTAEnvironmentBean jtaEnvironmentBean) { + super(jtaEnvironmentBean == null ? null : jtaEnvironmentBean.getTransactionSynchronizationRegistry()); + } + +} diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/package-info.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/package-info.java new file mode 100644 index 00000000000..94c152073fb --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 that integrate JTA version 1.2 + * into CDI + * version 2.0 using Narayana as the + * underlying implementation. + */ +package io.helidon.integrations.jta.cdi; diff --git a/integrations/cdi/jta-cdi/src/main/resources/META-INF/beans.xml b/integrations/cdi/jta-cdi/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..70f5bd0fe91 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/integrations/cdi/jta-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/integrations/cdi/jta-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000000..b3534bb1ff7 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.integrations.jta.cdi.NarayanaExtension diff --git a/integrations/cdi/jta-cdi/src/main/resources/default-jbossts-properties.xml b/integrations/cdi/jta-cdi/src/main/resources/default-jbossts-properties.xml new file mode 100644 index 00000000000..e9297874125 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/resources/default-jbossts-properties.xml @@ -0,0 +1,153 @@ + + + + + + + + YES + + + PutObjectStoreDirHere + + + ON + + + 1 + + + 1 + + + com.arjuna.ats.internal.jta.recovery.arjunacore.JTATransactionLogXAResourceOrphanFilter + com.arjuna.ats.internal.jta.recovery.arjunacore.JTANodeNameXAResourceOrphanFilter + com.arjuna.ats.internal.jta.recovery.arjunacore.SubordinationManagerXAResourceOrphanFilter + + + + 0 + + + + com.arjuna.ats.internal.arjuna.recovery.AtomicActionRecoveryModule + com.arjuna.ats.internal.txoj.recovery.TORecoveryModule + com.arjuna.ats.internal.jta.recovery.arjunacore.SubordinateAtomicActionRecoveryModule + com.arjuna.ats.internal.jta.recovery.arjunacore.XARecoveryModule + + + + + com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner + + + + + + 4712 + + + + + 0 + + + + + + NO + + diff --git a/integrations/cdi/jta-cdi/src/main/resources/messages.properties b/integrations/cdi/jta-cdi/src/main/resources/messages.properties new file mode 100644 index 00000000000..76dc0a59ee3 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/resources/messages.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +addedJtaEnvironmentBeanBean=Added JTAEnvironmentBean bean +addedTransactionBean=Added Transaction bean +addedUserTransactionBean=Added UserTransaction bean +firingJtaEnvironmentBean=Firing {0} diff --git a/integrations/cdi/jta-cdi/src/test/java/io/helidon/integrations/jta/cdi/TestTransactionalAnnotationSupport.java b/integrations/cdi/jta-cdi/src/test/java/io/helidon/integrations/jta/cdi/TestTransactionalAnnotationSupport.java new file mode 100644 index 00000000000..fc2a4804c60 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/test/java/io/helidon/integrations/jta/cdi/TestTransactionalAnnotationSupport.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.jta.cdi; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.se.SeContainer; +import javax.enterprise.inject.se.SeContainerInitializer; +import javax.inject.Inject; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; +import javax.transaction.Transactional; +import javax.transaction.Transaction; +import javax.transaction.TransactionScoped; +import javax.transaction.UserTransaction; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ApplicationScoped +public class TestTransactionalAnnotationSupport { + + private SeContainer cdiContainer; + + private boolean transactionScopeStarted; + + @Inject + private Transaction transaction; + + @Inject + private UserTransaction userTransaction; + + TestTransactionalAnnotationSupport() { + super(); + } + + @BeforeEach + void startCdiContainer() { + final SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .addBeanClasses(TestTransactionalAnnotationSupport.class); + assertNotNull(initializer); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + } + + private static void onStartup(@Observes @Initialized(ApplicationScoped.class) final Object event, + final TestTransactionalAnnotationSupport self) + throws SystemException { + assertNotNull(event); + assertNotNull(self); + self.doSomethingTransactional(); + } + + private void onBeginningOfTransactionScope(@Observes @Initialized(TransactionScoped.class) final Object event) { + assertTrue(event instanceof Transaction); + this.transactionScopeStarted = true; + } + + @Transactional(Transactional.TxType.REQUIRED) + void doSomethingTransactional() throws SystemException { + assertTrue(this.transactionScopeStarted); + assertNotNull(this.userTransaction); + assertEquals(Status.STATUS_ACTIVE, this.userTransaction.getStatus()); + assertNotNull(this.transaction); + assertEquals(Status.STATUS_ACTIVE, this.transaction.getStatus()); + } + + @Test + void testTransactionalAnnotationSupport() { + + } + +} diff --git a/integrations/cdi/jta-weld/README.adoc b/integrations/cdi/jta-weld/README.adoc new file mode 100644 index 00000000000..66906d522f4 --- /dev/null +++ b/integrations/cdi/jta-weld/README.adoc @@ -0,0 +1,23 @@ += Helidon JTA Weld Integration + +The Helidon JTA Weld Integration project performs the +CDI-provider-specific work of integrating a JTA implementation into +standalone CDI applications (including those based on Helidon +MicroProfile). It is one of two projects that together make up +overall JTA support for standalone CDI applications. + +To function properly, this project also requires: + +* a CDI-provider-agnostic counterpart, such as the `jta-cdi` project + found elsewhere in this git repository + +IMPORTANT: Please note that this feature is currently experimental and + not suitable for production use. + +== Installation + +Ensure that the Helidon JTA Weld Integration project and its runtime +dependencies are present on your application's runtime classpath. + +Please see the `examples/integrations/cdi/jpa` project found elsewhere +in this git repository for a working `pom.xml` file that uses this project. diff --git a/integrations/cdi/jta-weld/pom.xml b/integrations/cdi/jta-weld/pom.xml new file mode 100644 index 00000000000..a336f4cd3c5 --- /dev/null +++ b/integrations/cdi/jta-weld/pom.xml @@ -0,0 +1,104 @@ + + + + 4.0.0 + + io.helidon.integrations.cdi + helidon-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-cdi-jta-weld + Helidon CDI Integrations JTA Weld + + + + package + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.jboss.weld.se + weld-se-core + test + + + org.slf4j + slf4j-simple + test + + + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta + ${project.version} + runtime + + + + org.jboss.weld.module + weld-jta + runtime + + + + + javax.transaction + javax.transaction-api + provided + + + javax.enterprise + cdi-api + provided + + + + + org.jboss.narayana.jta + cdi + compile + + + org.jboss.weld + weld-spi + compile + + + + diff --git a/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/NarayanaTransactionServices.java b/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/NarayanaTransactionServices.java new file mode 100644 index 00000000000..eb15b2e7df7 --- /dev/null +++ b/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/NarayanaTransactionServices.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.jta.weld; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.spi.CDI; +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.Synchronization; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import com.arjuna.ats.jta.common.JTAEnvironmentBean; +import org.jboss.weld.transaction.spi.TransactionServices; + +/** + * A {@link TransactionServices} implementation that uses the Narayana transaction + * engine and does not use JNDI. + * + *

{@link TransactionServices} implementations are used by Weld for transactional observer notification as well as + * for providing the implementation backing the built-in {@code UserTransaction} CDI bean.

+ * + * @see TransactionServices + */ +final class NarayanaTransactionServices implements TransactionServices { + + + /* + * Static fields. + */ + + + /** + * The {@link Logger} used by all instances of this class. + * + *

This field is never {@code null}.

+ */ + private static final Logger LOGGER = Logger.getLogger(NarayanaTransactionServices.class.getName(), "messages"); + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link NarayanaTransactionServices}. + */ + private NarayanaTransactionServices() { + super(); + final String cn = NarayanaTransactionServices.class.getName(); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, ""); + LOGGER.exiting(cn, ""); + } + } + + + /* + * Instance methods. + */ + + + /** + * Returns the {@link UserTransaction} present in this environment + * by invoking the {@link + * com.arjuna.ats.jta.UserTransaction#userTransaction()} method + * and returning its result. + * + *

This method never returns {@code null}.

+ * + *

The return value of this method is used as the backing + * implementation of the built-in {@code UserTransaction} CDI + * bean.

+ * + * @return the non-{@code null} {@link UserTransaction} present in + * this environment + * + * @see com.arjuna.ats.jta.UserTransaction#userTransaction() + */ + @Override + public UserTransaction getUserTransaction() { + // We don't want to use, e.g., + // CDI.current().select(UserTransaction.class).get() here + // because CDI containers like Weld are obliged per the + // specification to automatically provide a bean for + // UserTransaction. Weld uses the return value of this method + // to create such a bean and we obviously need to avoid the + // infinite loop. + final String cn = NarayanaTransactionServices.class.getName(); + final String mn = "getUserTransaction"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + final Instance jtaEnvironmentBeans = CDI.current().select(JTAEnvironmentBean.class); + assert jtaEnvironmentBeans != null; + final JTAEnvironmentBean jtaEnvironmentBean; + if (jtaEnvironmentBeans.isUnsatisfied()) { + jtaEnvironmentBean = com.arjuna.ats.jta.common.jtaPropertyManager.getJTAEnvironmentBean(); + } else { + jtaEnvironmentBean = jtaEnvironmentBeans.get(); + } + assert jtaEnvironmentBean != null; + final UserTransaction returnValue = jtaEnvironmentBean.getUserTransaction(); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Returns {@code true} if the current {@link Transaction} + * {@linkplain Transaction#getStatus() has a status} indicating + * that it is active. + * + *

This method returns {@code true} if the current {@link + * Transaction} {@linkplain Transaction#getStatus() has a status} + * equal to one of the following values:

+ * + *
    + * + *
  • {@link Status#STATUS_ACTIVE}
  • + * + *
  • {@link Status#STATUS_COMMITTING}
  • + * + *
  • {@link Status#STATUS_MARKED_ROLLBACK}
  • + * + *
  • {@link Status#STATUS_PREPARED}
  • + * + *
  • {@link Status#STATUS_PREPARING}
  • + * + *
  • {@link Status#STATUS_ROLLING_BACK}
  • + * + *
+ * + * @return {@code true} if the current {@link Transaction} + * {@linkplain Transaction#getStatus() has a status} indicating + * that it is active; {@code false} otherwise + * + * @exception RuntimeException if an invocation of the {@link + * Transaction#getStatus()} method resulted in a {@link + * SystemException} + * + * @see Status + */ + @Override + public boolean isTransactionActive() { + final String cn = NarayanaTransactionServices.class.getName(); + final String mn = "isTransactionActive"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + final boolean returnValue; + final Instance transactions = CDI.current().select(Transaction.class); + assert transactions != null; + if (!transactions.isUnsatisfied()) { + final Transaction transaction = transactions.get(); + assert transaction != null; + boolean temp = false; + try { + final int status = transaction.getStatus(); + temp = + status == Status.STATUS_ACTIVE + || status == Status.STATUS_COMMITTING + || status == Status.STATUS_MARKED_ROLLBACK + || status == Status.STATUS_PREPARED + || status == Status.STATUS_PREPARING + || status == Status.STATUS_ROLLING_BACK; + } catch (final SystemException e) { + throw new RuntimeException(e.getMessage(), e); + } finally { + returnValue = temp; + } + } else { + returnValue = false; + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, Boolean.valueOf(returnValue)); + } + return returnValue; + } + + /** + * Registers the supplied {@link Synchronization} with the current + * {@link Transaction}. + * + * @exception RuntimeException if an invocation of the {@link + * TransactionManager#getTransaction()} method resulted in a + * {@link SystemException}, or if an invocation of the {@link + * Transaction#registerSynchronization(Synchronization)} method + * resulted in either a {@link SystemException} or a {@link + * RollbackException} + * + * @see Transaction#registerSynchronization(Synchronization) + */ + @Override + public void registerSynchronization(final Synchronization synchronization) { + final String cn = NarayanaTransactionServices.class.getName(); + final String mn = "registerSynchronization"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, synchronization); + } + + final CDI cdi = CDI.current(); + final Instance transactionInstance = cdi.select(Transaction.class); + Transaction transaction = null; + if (transactionInstance.isUnsatisfied()) { + Instance transactionManagerInstance = cdi.select(TransactionManager.class); + assert transactionManagerInstance != null; + final TransactionManager transactionManager; + if (transactionManagerInstance.isUnsatisfied()) { + transactionManager = com.arjuna.ats.jta.TransactionManager.transactionManager(); + } else { + transactionManager = transactionManagerInstance.get(); + } + if (transactionManager != null) { + try { + transaction = transactionManager.getTransaction(); + } catch (final SystemException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + } else { + transaction = transactionInstance.get(); + } + if (transaction != null) { + try { + transaction.registerSynchronization(synchronization); + } catch (final SystemException | RollbackException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Releases any internal resources acquired during the lifespan of + * this object. + */ + @Override + public synchronized void cleanup() { + final String cn = NarayanaTransactionServices.class.getName(); + final String mn = "cleanup"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + LOGGER.exiting(cn, mn); + } + } + +} diff --git a/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/package-info.java b/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/package-info.java new file mode 100644 index 00000000000..a7ff4eb8da5 --- /dev/null +++ b/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 that enable transactional + * observer methods in Weld-backed CDI 2.0 + * SE implementations using the Narayana engine. + */ +package io.helidon.integrations.jta.weld; diff --git a/integrations/cdi/jta-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service b/integrations/cdi/jta-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service new file mode 100644 index 00000000000..be88c7afaba --- /dev/null +++ b/integrations/cdi/jta-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.integrations.jta.weld.NarayanaTransactionServices diff --git a/integrations/cdi/jta-weld/src/main/resources/messages.properties b/integrations/cdi/jta-weld/src/main/resources/messages.properties new file mode 100644 index 00000000000..e4c231448fb --- /dev/null +++ b/integrations/cdi/jta-weld/src/main/resources/messages.properties @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + diff --git a/integrations/cdi/jta-weld/src/test/java/io/helidon/integrations/jta/weld/TestAutomaticUserTransactionInjection.java b/integrations/cdi/jta-weld/src/test/java/io/helidon/integrations/jta/weld/TestAutomaticUserTransactionInjection.java new file mode 100644 index 00000000000..451871f1b27 --- /dev/null +++ b/integrations/cdi/jta-weld/src/test/java/io/helidon/integrations/jta/weld/TestAutomaticUserTransactionInjection.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.jta.weld; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.se.SeContainer; +import javax.enterprise.inject.se.SeContainerInitializer; +import javax.enterprise.inject.spi.CDI; +import javax.inject.Inject; +import javax.transaction.NotSupportedException; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.UserTransaction; + +import com.arjuna.ats.jta.common.JTAEnvironmentBean; + +import io.helidon.integrations.jta.cdi.NarayanaExtension; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +@ApplicationScoped +public class TestAutomaticUserTransactionInjection { + + private SeContainer cdiContainer; + + TestAutomaticUserTransactionInjection() { + super(); + } + + @BeforeEach + @SuppressWarnings("unchecked") + void startCdiContainer() { + final SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses(TestAutomaticUserTransactionInjection.class) + .addExtensions(NarayanaExtension.class); + assertNotNull(initializer); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + } + + private void onJtaEnvironmentBeanLoad(@Observes final JTAEnvironmentBean instance) { + assertNotNull(instance); + } + + private void onStartup(@Observes @Initialized(ApplicationScoped.class) final Object event, + final UserTransaction userTransaction) + throws NotSupportedException, SystemException { + assertNotNull(userTransaction); + assertEquals("Transaction: unknown", userTransaction.toString()); + assertEquals(Status.STATUS_NO_TRANSACTION, userTransaction.getStatus()); + try { + userTransaction.begin(); + assertEquals(Status.STATUS_ACTIVE, userTransaction.getStatus()); + } finally { + userTransaction.rollback(); + assertEquals(Status.STATUS_NO_TRANSACTION, userTransaction.getStatus()); + } + } + + @Test + void testSpike() { + + } + +} diff --git a/integrations/cdi/pom.xml b/integrations/cdi/pom.xml index c7cb632c9b0..e7ac7d412d0 100644 --- a/integrations/cdi/pom.xml +++ b/integrations/cdi/pom.xml @@ -33,7 +33,12 @@ datasource-hikaricp + eclipselink-cdi jedis-cdi + jpa-cdi + jpa-weld + jta-cdi + jta-weld oci-objectstorage-cdi diff --git a/javadocs/pom.xml b/javadocs/pom.xml index 578f2dbcdae..ba294f519a0 100644 --- a/javadocs/pom.xml +++ b/javadocs/pom.xml @@ -356,6 +356,11 @@ helidon-integrations-cdi-datasource-hikaricp ${project.version} + + io.helidon.integrations.cdi + helidon-integrations-cdi-eclipselink + ${project.version} + io.helidon.integrations.cdi helidon-integrations-cdi-jedis @@ -366,6 +371,20 @@ helidon-integrations-cdi-oci-objectstorage ${project.version} + + javax.transaction + javax.transaction-api + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta + ${project.version} + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + ${project.version} + diff --git a/media/common/src/main/java/io/helidon/media/common/CharBuffer.java b/media/common/src/main/java/io/helidon/media/common/CharBuffer.java new file mode 100644 index 00000000000..7d258287417 --- /dev/null +++ b/media/common/src/main/java/io/helidon/media/common/CharBuffer.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.media.common; + +import java.io.Writer; +import java.lang.ref.SoftReference; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * A character buffer that acts as a {@link Writer} and uses cached {@code char[]} arrays. + *

+ * Instances of this class are not thread-safe. + */ +public class CharBuffer extends Writer { + private static final Pool POOL = new Pool(8192); + private char[] buffer; + private int count; + + /** + * Constructor. + */ + public CharBuffer() { + buffer = POOL.acquire(); + count = 0; + } + + @Override + public void write(char[] cbuf, int off, int len) { + if ((off < 0) || (off > cbuf.length) || (len < 0) || ((off + len) - cbuf.length > 0)) { + throw new IndexOutOfBoundsException(); + } + ensureCapacity(count + len); + System.arraycopy(cbuf, off, buffer, count, len); + count += len; + } + + /** + * Returns the number of characters written. + * + * @return The count. + */ + int size() { + return count; + } + + /** + * Returns the content encoded into the given character set. + * + * @param charset The character set. + * @return The encoded content. + */ + ByteBuffer encode(Charset charset) { + final ByteBuffer result = charset.encode(java.nio.CharBuffer.wrap(buffer, 0, count)); + POOL.release(buffer); + buffer = null; + return result; + } + + private void ensureCapacity(int minCapacity) { + if (minCapacity - buffer.length > 0) { + grow(minCapacity); + } + } + + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + + private void grow(int minCapacity) { + int oldCapacity = buffer.length; + int newCapacity = oldCapacity << 1; + if (newCapacity - minCapacity < 0) { + newCapacity = minCapacity; + } + if (newCapacity - MAX_ARRAY_SIZE > 0) { + newCapacity = hugeCapacity(minCapacity); + } + buffer = Arrays.copyOf(buffer, newCapacity); + } + + private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) { + throw new OutOfMemoryError(); + } + return (minCapacity > MAX_ARRAY_SIZE) + ? Integer.MAX_VALUE + : MAX_ARRAY_SIZE; + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + + private static class Pool { + private volatile SoftReference> reference; + private final int arraySize; + + /** + * Constructor. + * + * @param arraySize The size array to allocate when required. + */ + Pool(final int arraySize) { + this.arraySize = arraySize; + } + + /** + * Acquires an array from the pool if available or creates a new one. + * + * @return The array. + */ + char[] acquire() { + final char[] array = getQueue().poll(); + return array == null ? new char[arraySize] : array; + } + + /** + * Returns an array back to the pool. + * + * @param array The array to return. + */ + void release(final char[] array) { + getQueue().offer(array); + } + + private ConcurrentLinkedQueue getQueue() { + final SoftReference> reference = this.reference; + if (reference != null) { + final ConcurrentLinkedQueue queue = reference.get(); + if (queue != null) { + return queue; + } + } + final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + this.reference = new SoftReference<>(queue); + return queue; + } + } +} diff --git a/media/common/src/main/java/io/helidon/media/common/ContentTypeCharset.java b/media/common/src/main/java/io/helidon/media/common/ContentTypeCharset.java new file mode 100644 index 00000000000..2db91352688 --- /dev/null +++ b/media/common/src/main/java/io/helidon/media/common/ContentTypeCharset.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.media.common; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import io.helidon.common.http.Http; +import io.helidon.common.http.MediaType; +import io.helidon.common.http.Parameters; + +/** + * Accessor for the {@link Charset} specified by a content-type header. + */ +public class ContentTypeCharset { + + /** + * Returns the {@link Charset} specified in the content-type header, using {@link StandardCharsets#UTF_8} + * as the default. + * + * @param headers The headers. + * @return The charset. + */ + public static Charset determineCharset(Parameters headers) { + return determineCharset(headers, StandardCharsets.UTF_8); + } + + /** + * Returns the {@link Charset} specified in the content type header. If not provided or an error occurs on lookup, + * the given default is returned. + * + * @param headers The headers. + * @param defaultCharset The default. + * @return The charset. + */ + public static Charset determineCharset(Parameters headers, Charset defaultCharset) { + return headers.first(Http.Header.CONTENT_TYPE) + .map(MediaType::parse) + .flatMap(MediaType::charset) + .map(sch -> { + try { + return Charset.forName(sch); + } catch (Exception e) { + return null; + } + }) + .orElse(defaultCharset); + } + + private ContentTypeCharset() { + } +} diff --git a/media/common/src/main/java/io/helidon/media/common/ContentWriters.java b/media/common/src/main/java/io/helidon/media/common/ContentWriters.java index 25f3fb14498..d35c00a63d9 100644 --- a/media/common/src/main/java/io/helidon/media/common/ContentWriters.java +++ b/media/common/src/main/java/io/helidon/media/common/ContentWriters.java @@ -44,27 +44,29 @@ public final class ContentWriters { private static final ByteArrayWriter COPY_BYTE_ARRAY_WRITER = new ByteArrayWriter(true); private static final ByteArrayWriter BYTE_ARRAY_WRITER = new ByteArrayWriter(false); - private static final Map STRING_WRITERS = new HashMap<>(); + private static final Map CHAR_SEQUENCE_WRITERS = new HashMap<>(); + private static final Map CHAR_BUFFER_WRITERS = new HashMap<>(); static { - addWriter(StandardCharsets.UTF_8); - addWriter(StandardCharsets.UTF_16); - addWriter(StandardCharsets.ISO_8859_1); - addWriter(StandardCharsets.US_ASCII); + addWriters(StandardCharsets.UTF_8); + addWriters(StandardCharsets.UTF_16); + addWriters(StandardCharsets.ISO_8859_1); + addWriters(StandardCharsets.US_ASCII); // try to register another common charset readers - addWriter("cp1252"); - addWriter("cp1250"); - addWriter("ISO-8859-2"); + addWriters("cp1252"); + addWriters("cp1250"); + addWriters("ISO-8859-2"); } - private static void addWriter(Charset charset) { - STRING_WRITERS.put(charset, new CharSequenceWriter(charset)); + private static void addWriters(final Charset charset) { + CHAR_SEQUENCE_WRITERS.put(charset, new CharSequenceWriter(charset)); + CHAR_BUFFER_WRITERS.put(charset, new CharBufferWriter(charset)); } - private static void addWriter(String charset) { + private static void addWriters(final String charset) { try { - addWriter(Charset.forName(charset)); + addWriters(Charset.forName(charset)); } catch (Exception ignored) { // ignored } @@ -98,7 +100,20 @@ public static Function> byteArrayWriter(boolea * @throws NullPointerException if parameter {@code charset} is {@code null} */ public static Function> charSequenceWriter(Charset charset) { - return STRING_WRITERS.computeIfAbsent(charset, key -> new CharSequenceWriter(charset)); + return CHAR_SEQUENCE_WRITERS.computeIfAbsent(charset, key -> new CharSequenceWriter(charset)); + } + + /** + * Returns a writer function for {@link CharBuffer} using provided standard {@code charset}. + *

+ * An instance is by default registered in {@code ServerResponse} for all standard charsets. + * + * @param charset a standard charset to use + * @return a {@link String} writer + * @throws NullPointerException if parameter {@code charset} is {@code null} + */ + public static Function> charBufferWriter(Charset charset) { + return CHAR_BUFFER_WRITERS.computeIfAbsent(charset, key -> new CharBufferWriter(charset)); } /** @@ -185,4 +200,28 @@ public Flow.Publisher apply(CharSequence s) { } } + private static class CharBufferWriter implements Function> { + + private final Charset charset; + + /** + * Creates new instance. + * + * @param charset a charset to use + * @throws NullPointerException if parameter {@code charset} is {@code null} + */ + CharBufferWriter(Charset charset) { + Objects.requireNonNull(charset, "Parameter 'charset' is null!"); + this.charset = charset; + } + + @Override + public Flow.Publisher apply(CharBuffer buffer) { + if (buffer == null || buffer.size() == 0) { + return ReactiveStreamsAdapter.publisherToFlow(Mono.empty()); + } + final DataChunk chunk = DataChunk.create(false, buffer.encode(charset)); + return ReactiveStreamsAdapter.publisherToFlow(Mono.just(chunk)); + } + } } diff --git a/media/jackson/common/src/main/java/io/helidon/media/jackson/common/JacksonProcessing.java b/media/jackson/common/src/main/java/io/helidon/media/jackson/common/JacksonProcessing.java index d3585f04c7f..1cea26904f7 100644 --- a/media/jackson/common/src/main/java/io/helidon/media/jackson/common/JacksonProcessing.java +++ b/media/jackson/common/src/main/java/io/helidon/media/jackson/common/JacksonProcessing.java @@ -15,22 +15,24 @@ */ package io.helidon.media.jackson.common; -import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.Charset; import java.util.Objects; import java.util.function.Function; import io.helidon.common.http.DataChunk; -import io.helidon.common.http.MediaType; import io.helidon.common.http.Reader; import io.helidon.common.reactive.Flow; +import io.helidon.media.common.CharBuffer; import io.helidon.media.common.ContentReaders; import io.helidon.media.common.ContentWriters; import com.fasterxml.jackson.databind.ObjectMapper; +import static java.nio.charset.StandardCharsets.UTF_8; + /** - * Media type support for Jackson. + * Utility methods for Jackson integration. */ public final class JacksonProcessing { @@ -38,32 +40,6 @@ private JacksonProcessing() { super(); } - /** - * Check whether the type is supported by Jackson. - * - * @param type media type to check - * @return true if the media type checked is supported by Jackson - */ - public static boolean isSupported(MediaType type) { - // See https://github.com/FasterXML/jackson-jaxrs-providers/blob/jackson-jaxrs-providers-2.9.4/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JacksonJsonProvider.java#L167-L192 - final boolean returnValue; - if (type == null) { - returnValue = true; - } else { - final String subtype = type.subtype(); - if (subtype == null) { - returnValue = false; - } else { - returnValue = "json".equalsIgnoreCase(subtype) - || subtype.endsWith("+json") - || "javascript".equals(subtype) - || "x-javascript".equals(subtype) - || "x-json".equals(subtype); - } - } - return returnValue; - } - /** * Returns a {@link Reader} that converts a {@link Flow.Publisher Publisher} of {@link java.nio.ByteBuffer}s to * a Java object. @@ -93,21 +69,20 @@ public static Reader reader(final ObjectMapper objectMapper) { * of {@link DataChunk}s by using the supplied {@link ObjectMapper}. * * @param objectMapper the {@link ObjectMapper} to use; must not be {@code null} + * @param charset the charset to use; may be null * @return created function * @exception NullPointerException if {@code objectMapper} is {@code null} */ - public static Function> writer(final ObjectMapper objectMapper) { + public static Function> writer(final ObjectMapper objectMapper, final Charset charset) { Objects.requireNonNull(objectMapper); return payload -> { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CharBuffer buffer = new CharBuffer(); try { - objectMapper.writeValue(baos, payload); + objectMapper.writeValue(buffer, payload); } catch (final IOException wrapMe) { throw new JacksonRuntimeException(wrapMe.getMessage(), wrapMe); } - return ContentWriters.byteArrayWriter(false) - .apply(baos.toByteArray()); + return ContentWriters.charBufferWriter(charset == null ? UTF_8 : charset).apply(buffer); }; } - } diff --git a/media/jackson/server/pom.xml b/media/jackson/server/pom.xml index c45d26f0248..f72aec15ea7 100644 --- a/media/jackson/server/pom.xml +++ b/media/jackson/server/pom.xml @@ -37,6 +37,18 @@ com.fasterxml.jackson.core jackson-databind + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + io.helidon.webserver helidon-webserver diff --git a/media/jackson/server/src/main/java/io/helidon/media/jackson/server/JacksonSupport.java b/media/jackson/server/src/main/java/io/helidon/media/jackson/server/JacksonSupport.java index 40e64493dbb..664f15cd5a5 100644 --- a/media/jackson/server/src/main/java/io/helidon/media/jackson/server/JacksonSupport.java +++ b/media/jackson/server/src/main/java/io/helidon/media/jackson/server/JacksonSupport.java @@ -15,26 +15,28 @@ */ package io.helidon.media.jackson.server; -import java.util.Collection; import java.util.Objects; import java.util.function.BiFunction; -import io.helidon.common.http.MediaType; import io.helidon.media.jackson.common.JacksonProcessing; import io.helidon.webserver.Handler; -import io.helidon.webserver.Routing; +import io.helidon.webserver.JsonService; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +import static io.helidon.media.common.ContentTypeCharset.determineCharset; /** * A {@link Service} and a {@link Handler} that provides Jackson * support to Helidon. */ -public final class JacksonSupport implements Service, Handler { - +public final class JacksonSupport extends JsonService { private final BiFunction objectMapperProvider; /** @@ -54,19 +56,14 @@ private JacksonSupport(final BiFunction objectMapper.canDeserialize(objectMapper.constructType(cls)), JacksonProcessing.reader(objectMapper)); - response.registerWriter(payload -> objectMapper.canSerialize(payload.getClass()) && this.wantsJson(request, response), - JacksonProcessing.writer(objectMapper)); + response.registerWriter(payload -> objectMapper.canSerialize(payload.getClass()) && acceptsJson(request, response), + JacksonProcessing.writer(objectMapper, determineCharset(response.headers()))); request.next(); } @@ -76,7 +73,11 @@ public void accept(final ServerRequest request, final ServerResponse response) { * @return a new {@link JacksonSupport} */ public static JacksonSupport create() { - return create((req, res) -> new ObjectMapper()); + final ObjectMapper mapper = new ObjectMapper() + .registerModule(new ParameterNamesModule()) + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()); + return create((req, res) -> mapper); } /** @@ -96,41 +97,4 @@ public static JacksonSupport create(final BiFunction objectMapperProvider) { return new JacksonSupport(objectMapperProvider); } - - private static boolean wantsJson(final ServerRequest request, final ServerResponse response) { - final boolean returnValue; - final MediaType outgoingMediaType = response.headers().contentType().orElse(null); - if (outgoingMediaType == null) { - final MediaType preferredType; - final Collection acceptedTypes = request.headers().acceptedTypes(); - if (acceptedTypes == null || acceptedTypes.isEmpty()) { - preferredType = MediaType.APPLICATION_JSON; - } else { - preferredType = acceptedTypes - .stream() - .map(type -> { - if (type.test(MediaType.APPLICATION_JSON)) { - return MediaType.APPLICATION_JSON; - } else if (type.hasSuffix("json")) { - return MediaType.create(type.type(), type.subtype()); - } else { - return null; - } - }) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - if (preferredType == null) { - returnValue = false; - } else { - response.headers().contentType(preferredType); - returnValue = true; - } - } else { - returnValue = MediaType.JSON_PREDICATE.test(outgoingMediaType); - } - return returnValue; - } - } diff --git a/media/jackson/server/src/main/java9/module-info.java b/media/jackson/server/src/main/java9/module-info.java index bd809534806..3207cad9700 100644 --- a/media/jackson/server/src/main/java9/module-info.java +++ b/media/jackson/server/src/main/java9/module-info.java @@ -19,6 +19,9 @@ */ module io.helidon.media.jackson.server { requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.datatype.jdk8; + requires com.fasterxml.jackson.datatype.jsr310; + requires com.fasterxml.jackson.module.paramnames; requires io.helidon.media.jackson.common; requires io.helidon.webserver; diff --git a/media/jsonb/common/src/main/java/io/helidon/media/jsonb/common/JsonBinding.java b/media/jsonb/common/src/main/java/io/helidon/media/jsonb/common/JsonBinding.java index bd1800ce605..4fc7e4eb60e 100644 --- a/media/jsonb/common/src/main/java/io/helidon/media/jsonb/common/JsonBinding.java +++ b/media/jsonb/common/src/main/java/io/helidon/media/jsonb/common/JsonBinding.java @@ -16,9 +16,9 @@ package io.helidon.media.jsonb.common; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; import java.util.Objects; import java.util.function.Function; @@ -28,9 +28,12 @@ import io.helidon.common.http.DataChunk; import io.helidon.common.http.Reader; import io.helidon.common.reactive.Flow; +import io.helidon.media.common.CharBuffer; import io.helidon.media.common.ContentReaders; import io.helidon.media.common.ContentWriters; +import static java.nio.charset.StandardCharsets.UTF_8; + /** * Contains utility methods for working with JSON-B. * @@ -71,17 +74,16 @@ public static Reader reader(final Jsonb jsonb) { * of {@link DataChunk}s by using the supplied {@link Jsonb}. * * @param jsonb the {@link Jsonb} to use; must not be {@code null} + * @param charset the charset to use; may be null * @return created function * @exception NullPointerException if {@code jsonb} is {@code null} */ - public static Function> writer(final Jsonb jsonb) { + public static Function> writer(final Jsonb jsonb, final Charset charset) { Objects.requireNonNull(jsonb); return payload -> { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - jsonb.toJson(payload, baos); - return ContentWriters.byteArrayWriter(false) - .apply(baos.toByteArray()); + CharBuffer buffer = new CharBuffer(); + jsonb.toJson(payload, buffer); + return ContentWriters.charBufferWriter(charset == null ? UTF_8 : charset).apply(buffer); }; } - } diff --git a/media/jsonb/server/src/main/java/io/helidon/media/jsonb/server/JsonBindingSupport.java b/media/jsonb/server/src/main/java/io/helidon/media/jsonb/server/JsonBindingSupport.java index f6d1a570270..d53a1c1b9a3 100644 --- a/media/jsonb/server/src/main/java/io/helidon/media/jsonb/server/JsonBindingSupport.java +++ b/media/jsonb/server/src/main/java/io/helidon/media/jsonb/server/JsonBindingSupport.java @@ -16,26 +16,26 @@ package io.helidon.media.jsonb.server; -import java.util.Collection; import java.util.Objects; import java.util.function.BiFunction; import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; -import io.helidon.common.http.MediaType; import io.helidon.media.jsonb.common.JsonBinding; import io.helidon.webserver.Handler; -import io.helidon.webserver.Routing; +import io.helidon.webserver.JsonService; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; +import static io.helidon.media.common.ContentTypeCharset.determineCharset; + /** * A {@link Service} and a {@link Handler} that provides JSON-B support to Helidon. */ -public final class JsonBindingSupport implements Service, Handler { +public final class JsonBindingSupport extends JsonService { private final BiFunction jsonbProvider; @@ -46,58 +46,17 @@ private JsonBindingSupport(final BiFunction true, JsonBinding.reader(jsonb)); - response.registerWriter(payload -> wantsJson(request, response), - JsonBinding.writer(jsonb)); + response.registerWriter(payload -> acceptsJson(request, response), + JsonBinding.writer(jsonb, determineCharset(response.headers()))); request.next(); } - private static boolean wantsJson(final ServerRequest request, final ServerResponse response) { - final boolean returnValue; - final MediaType outgoingMediaType = response.headers().contentType().orElse(null); - if (outgoingMediaType == null) { - final MediaType preferredType; - final Collection acceptedTypes = request.headers().acceptedTypes(); - if (acceptedTypes == null || acceptedTypes.isEmpty()) { - preferredType = MediaType.APPLICATION_JSON; - } else { - preferredType = acceptedTypes - .stream() - .map(type -> { - if (type.test(MediaType.APPLICATION_JSON)) { - return MediaType.APPLICATION_JSON; - } else if (type.hasSuffix("json")) { - return MediaType.create(type.type(), type.subtype()); - } else { - return null; - } - }) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - if (preferredType == null) { - returnValue = false; - } else { - response.headers().contentType(preferredType); - returnValue = true; - } - } else { - returnValue = MediaType.JSON_PREDICATE.test(outgoingMediaType); - } - return returnValue; - } - /** * Creates a new {@link JsonBindingSupport}. * diff --git a/media/jsonp/common/src/main/java/io/helidon/media/jsonp/common/JsonProcessing.java b/media/jsonp/common/src/main/java/io/helidon/media/jsonp/common/JsonProcessing.java index e6aeaac8510..47d4092c8b5 100644 --- a/media/jsonp/common/src/main/java/io/helidon/media/jsonp/common/JsonProcessing.java +++ b/media/jsonp/common/src/main/java/io/helidon/media/jsonp/common/JsonProcessing.java @@ -16,12 +16,10 @@ package io.helidon.media.jsonp.common; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; -import java.util.Set; import java.util.function.Function; import javax.json.Json; @@ -31,47 +29,19 @@ import javax.json.JsonWriter; import javax.json.JsonWriterFactory; -import io.helidon.common.CollectionsHelper; import io.helidon.common.http.DataChunk; -import io.helidon.common.http.MediaType; import io.helidon.common.http.Reader; import io.helidon.common.reactive.Flow; +import io.helidon.media.common.CharBuffer; import io.helidon.media.common.ContentReaders; import io.helidon.media.common.ContentWriters; +import static java.nio.charset.StandardCharsets.UTF_8; + /** - * Media type support for json processing. + * Support for json processing integration. */ public final class JsonProcessing { - /** - * JSONP (JSON with Pending) can have this weird type. - */ - public static final MediaType APPLICATION_JAVASCRIPT = MediaType.create("application", "javascript"); - /** - * And the corresponding (obsolete, yet widely supported) text type. - */ - public static final MediaType TEXT_JAVASCRIPT = MediaType.create("text", "javascript"); - - private static final Set SUPPORTED_MEDIA_TYPES = CollectionsHelper.setOf( - MediaType.APPLICATION_JSON, - APPLICATION_JAVASCRIPT, - TEXT_JAVASCRIPT - ); - - /** - * Check whether the type is supported by JSON Processing. - * - * @param type media type to check - * @return true if the media type checked is supported by JSON Processing - */ - public static boolean isSupported(MediaType type) { - for (MediaType supportedMediaType : SUPPORTED_MEDIA_TYPES) { - if (supportedMediaType.test(type)) { - return true; - } - } - return false; - } private final JsonReaderFactory jsonReaderFactory; private final JsonWriterFactory jsonWriterFactory; @@ -129,14 +99,11 @@ public Reader reader(Charset charset) { */ public Function> writer(Charset charset) { return json -> { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonWriter writer = (charset == null) - ? jsonWriterFactory.createWriter(baos) - : jsonWriterFactory.createWriter(baos, charset); + CharBuffer buffer = new CharBuffer(); + JsonWriter writer = jsonWriterFactory.createWriter(buffer); writer.write(json); writer.close(); - return ContentWriters.byteArrayWriter(false) - .apply(baos.toByteArray()); + return ContentWriters.charBufferWriter(charset == null ? UTF_8 : charset).apply(buffer); }; } diff --git a/media/jsonp/server/src/main/java/io/helidon/media/jsonp/server/JsonSupport.java b/media/jsonp/server/src/main/java/io/helidon/media/jsonp/server/JsonSupport.java index 614e823ad13..78fdab6839f 100644 --- a/media/jsonp/server/src/main/java/io/helidon/media/jsonp/server/JsonSupport.java +++ b/media/jsonp/server/src/main/java/io/helidon/media/jsonp/server/JsonSupport.java @@ -18,8 +18,6 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.util.List; -import java.util.Objects; import java.util.function.Function; import javax.json.JsonReader; @@ -28,19 +26,18 @@ import io.helidon.common.http.Content; import io.helidon.common.http.DataChunk; -import io.helidon.common.http.Http; -import io.helidon.common.http.MediaType; -import io.helidon.common.http.Parameters; import io.helidon.common.http.Reader; import io.helidon.common.reactive.Flow; import io.helidon.media.jsonp.common.JsonProcessing; import io.helidon.webserver.Handler; +import io.helidon.webserver.JsonService; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerRequest; import io.helidon.webserver.ServerResponse; -import io.helidon.webserver.Service; import io.helidon.webserver.WebServer; +import static io.helidon.media.common.ContentTypeCharset.determineCharset; + /** * It provides contains JSON-P ({@code javax.json}) support for {@link WebServer WebServer}'s * {@link Routing}. It is intended to provide readers and writers for {@code javax.json} objects such @@ -76,7 +73,7 @@ * @see JsonReader * @see JsonWriter */ -public final class JsonSupport implements Service, Handler { +public final class JsonSupport extends JsonService { private static final JsonSupport INSTANCE = new JsonSupport(JsonProcessing.create()); private final JsonProcessing processingSupport; @@ -85,22 +82,6 @@ private JsonSupport(JsonProcessing processing) { this.processingSupport = processing; } - /** - * It registers reader and writer for {@link JsonSupport} on {@link ServerRequest}/{@link ServerResponse} for any - * {@link Http.Method HTTP method}. - *

- * This method is called from {@link Routing} during build process. The user should register whole class - * ot the routing: {@code Routing.builder().}{@link Routing.Builder#register(Service...) register}{@code (JsonSupport - * .create())}. - * - * @param routingRules a routing configuration where JSON support should be registered - * @see Routing - */ - @Override - public void update(Routing.Rules routingRules) { - routingRules.any(this); - } - /** * It registers reader and writer for {@link JsonSupport} on {@link ServerRequest}/{@link ServerResponse} on provided * routing criteria. @@ -125,7 +106,7 @@ public void accept(ServerRequest request, ServerResponse response) { return reader(charset).apply(publisher); }); // Writer - response.registerWriter(json -> (json instanceof JsonStructure) && testOrSetContentType(request, response), + response.registerWriter(json -> (json instanceof JsonStructure) && acceptsJson(request, response), json -> { Charset charset = determineCharset(response.headers()); return writer(charset).apply((JsonStructure) json); @@ -133,75 +114,6 @@ public void accept(ServerRequest request, ServerResponse response) { request.next(); } - /** - * Deals with request {@code Accept} and response {@code Content-Type} headers to determine if writer can be used. - *

- * If response has no {@code Content-Type} header then it is set to the response. - * - * @param request a server request - * @param response a server response - * @return {@code true} if JSON writer can be used - */ - private boolean testOrSetContentType(ServerRequest request, ServerResponse response) { - MediaType mt = response.headers().contentType().orElse(null); - if (mt == null) { - // Find if accepts any JSON compatible type - List acceptedTypes = request.headers().acceptedTypes(); - MediaType preferredType; - if (acceptedTypes.isEmpty()) { - preferredType = MediaType.APPLICATION_JSON; - } else { - preferredType = acceptedTypes - .stream() - .map(type -> { - if (type.test(MediaType.APPLICATION_JSON)) { - return MediaType.APPLICATION_JSON; - } else if (type.test(JsonProcessing.APPLICATION_JAVASCRIPT)) { - return JsonProcessing.APPLICATION_JAVASCRIPT; - } else if (type.test(JsonProcessing.TEXT_JAVASCRIPT)) { - return JsonProcessing.TEXT_JAVASCRIPT; - } else if (type.hasSuffix("json")) { - return MediaType.create(type.type(), type.subtype()); - } else { - return null; - } - }) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - if (preferredType == null) { - return false; - } else { - response.headers().contentType(preferredType); - return true; - } - } else { - return MediaType.JSON_PREDICATE.test(mt); - } - } - - /** - * Returns a charset from {@code Content-Type} header parameter or {@code null} if not defined. - * - * @param headers parameters representing request or response headers - * @return a charset or {@code UTF-8} as default - * @throws RuntimeException if charset is not supported - */ - private Charset determineCharset(Parameters headers) { - return headers.first(Http.Header.CONTENT_TYPE) - .map(MediaType::parse) - .flatMap(MediaType::charset) - .map(sch -> { - try { - return Charset.forName(sch); - } catch (Exception e) { - return null; // Do not need default charset. Can use JSON specification. - } - }) - .orElse(null); - } - /** * Returns a function (reader) converting {@link Flow.Publisher Publisher} of {@link ByteBuffer}s to * a JSON-P object. diff --git a/media/jsonp/server/src/test/java/io/helidon/media/jsonp/server/JsonSupportTest.java b/media/jsonp/server/src/test/java/io/helidon/media/jsonp/server/JsonSupportTest.java index a7c8296a03a..f1ede703caf 100644 --- a/media/jsonp/server/src/test/java/io/helidon/media/jsonp/server/JsonSupportTest.java +++ b/media/jsonp/server/src/test/java/io/helidon/media/jsonp/server/JsonSupportTest.java @@ -146,8 +146,7 @@ public void acceptHeaders() throws Exception { .path("/foo") .header("Accept", "application/javascript") .post(MediaPublisher.create(MediaType.APPLICATION_JSON.withCharset("UTF-8"), json.toString())); - assertThat(response.status(), is(Http.Status.OK_200)); - assertThat(response.headers().first(Http.Header.CONTENT_TYPE).orElse(null), is("application/javascript")); + assertThat(response.status(), is(Http.Status.INTERNAL_SERVER_ERROR_500)); // Without start response = TestClient.create(routing) @@ -156,4 +155,4 @@ public void acceptHeaders() throws Exception { .post(MediaPublisher.create(MediaType.APPLICATION_JSON.withCharset("UTF-8"), json.toString())); assertThat(response.status(), is(Http.Status.INTERNAL_SERVER_ERROR_500)); } -} \ No newline at end of file +} diff --git a/metrics/metrics/src/main/java/io/helidon/metrics/MetricsSupport.java b/metrics/metrics/src/main/java/io/helidon/metrics/MetricsSupport.java index 1d8a2102936..ecbe6691bc2 100644 --- a/metrics/metrics/src/main/java/io/helidon/metrics/MetricsSupport.java +++ b/metrics/metrics/src/main/java/io/helidon/metrics/MetricsSupport.java @@ -269,17 +269,29 @@ public void configureVendorMetrics(String routingName, Registry vendor = rf.getARegistry(MetricRegistry.Type.VENDOR); Counter totalCount = vendor.counter(new Metadata(metricPrefix + "count", - "Total number of requests", + "Total number of HTTP requests", "Each request (regardless of HTTP method) will increase this counter", MetricType.COUNTER, MetricUnits.NONE)); Meter totalMeter = vendor.meter(new Metadata(metricPrefix + "meter", - "Meter for overall requests", + "Meter for overall HTTP requests", "Each request will mark the meter to see overall throughput", MetricType.METERED, MetricUnits.NONE)); + vendor.counter(new Metadata("grpc.requests.count", + "Total number of gRPC requests", + "Each gRPC request (regardless of the method) will increase this counter", + MetricType.COUNTER, + MetricUnits.NONE)); + + vendor.meter(new Metadata("grpc.requests.meter", + "Meter for overall gRPC requests", + "Each gRPC request will mark the meter to see overall throughput", + MetricType.METERED, + MetricUnits.NONE)); + rules.any((req, res) -> { totalCount.inc(); totalMeter.mark(); diff --git a/microprofile/config/config/src/test/java/io/helidon/microprofile/config/MpcSourceEnvironmentVariablesTest.java b/microprofile/config/config/src/test/java/io/helidon/microprofile/config/MpcSourceEnvironmentVariablesTest.java index 3a5d58a40e1..efd6bdcf2e5 100644 --- a/microprofile/config/config/src/test/java/io/helidon/microprofile/config/MpcSourceEnvironmentVariablesTest.java +++ b/microprofile/config/config/src/test/java/io/helidon/microprofile/config/MpcSourceEnvironmentVariablesTest.java @@ -35,6 +35,7 @@ */ @ExtendWith(RestoreSystemPropertiesExt.class) public class MpcSourceEnvironmentVariablesTest { + private static final boolean WINDOWS = System.getProperty("os.name").toLowerCase().contains("win"); @Test public void testEnvironmentVariableAliases() { @@ -122,7 +123,7 @@ public void testPrecedence() { assertValue("com.ACME.size", "mapped-env-value", appAndEnv); assertValue("server.executor-service.max-pool-size", "mapped-env-value", appAndEnv); - assertNoValue("com.acme.size", appAndEnv); + assertNoValueExceptOnWindows("com.acme.size", "mapped-env-value", appAndEnv); assertValue("com/ACME/size", "mapped-env-value", appAndEnv); assertValue("server/executor-service/max-pool-size", "mapped-env-value", appAndEnv); @@ -142,7 +143,7 @@ public void testPrecedence() { assertValue("com.ACME.size", "sys-prop-value", appSysAndEnv); assertValue("server.executor-service.max-pool-size", "mapped-env-value", appSysAndEnv); - assertNoValue("com.acme.size", appAndEnv); + assertNoValueExceptOnWindows("com.acme.size", "mapped-env-value", appAndEnv); assertValue("com/ACME/size", "mapped-env-value", appSysAndEnv); assertValue("server/executor-service/max-pool-size", "mapped-env-value", appSysAndEnv); } @@ -155,6 +156,14 @@ static void assertNoValue(final String key, final MpConfig config) { assertThrows(NoSuchElementException.class, () -> config.getValue(key, String.class)); } + static void assertNoValueExceptOnWindows(final String key, final String expectedValueOnWindows, final MpConfig config) { + if (WINDOWS) { + assertValue(key, expectedValueOnWindows, config); + } else { + assertThrows(NoSuchElementException.class, () -> config.getValue(key, String.class)); + } + } + static ConfigSource toConfigSource(final Map map) { return new ConfigSource() { @Override diff --git a/microprofile/server/pom.xml b/microprofile/server/pom.xml index 15cf31b74bf..0fbe5f3ee76 100644 --- a/microprofile/server/pom.xml +++ b/microprofile/server/pom.xml @@ -85,6 +85,11 @@ helidon-webserver-jersey ${project.version} + + io.helidon.common + helidon-common-service-loader + ${project.version} + org.glassfish.jersey.ext.cdi diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java index c64daa05d81..48c10f9db0d 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.function.Supplier; @@ -37,6 +38,7 @@ import io.helidon.common.CollectionsHelper; import io.helidon.common.configurable.ThreadPoolSupplier; +import io.helidon.common.serviceloader.HelidonServiceLoader; import io.helidon.microprofile.config.MpConfig; import io.helidon.microprofile.server.spi.MpService; @@ -146,8 +148,8 @@ final class Builder { private static final Logger STARTUP_LOGGER = Logger.getLogger("io.helidon.microprofile.startup.builder"); private final List> resourceClasses = new LinkedList<>(); - private final List extensions = new LinkedList<>(); private final List applications = new LinkedList<>(); + private HelidonServiceLoader.Builder extensionBuilder; private ResourceConfig resourceConfig; private SeContainer cdiContainer; private MpConfig config; @@ -158,6 +160,7 @@ final class Builder { private Supplier defaultExecutorService; private Builder() { + extensionBuilder = HelidonServiceLoader.builder(ServiceLoader.load(MpService.class)); } private static ResourceConfig configForResourceClasses(List> resourceClasses) { @@ -333,8 +336,34 @@ public Builder basePath(String basePath) { return this; } + /** + * Configure the extension builder. + * This allows a fully customized handling of {@link io.helidon.microprofile.server.spi.MpService} instances + * to be used by the created {@link io.helidon.microprofile.server.Server}. + * + * @param loaderBuilder builder of server extensions + * @return updated builder instance + * @see io.helidon.common.serviceloader.HelidonServiceLoader.Builder#useSystemServiceLoader(boolean) + */ + public Builder extensionsService(HelidonServiceLoader.Builder loaderBuilder) { + this.extensionBuilder = loaderBuilder; + return this; + } + + /** + * Add an extension to the list of extensions. + * All {@link io.helidon.microprofile.server.spi.MpService} configured for Java Service loader are loaded + * automatically. + * This serves as a possibility to add a service that is not loaded through a service loader. + *

+ * To have a fully customized list of extensions, use + * {@link #extensionsService(io.helidon.common.serviceloader.HelidonServiceLoader.Builder)}. + * + * @param service service implementation + * @return updated builder instance + */ public Builder addExtension(MpService service) { - extensions.add(service); + extensionBuilder.addService(service); return this; } @@ -557,7 +586,7 @@ int port() { } List extensions() { - return extensions; + return extensionBuilder.build().asList(); } boolean containerCreated() { diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java index dda4bd0c9a7..b742d952216 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerImpl.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.ServiceLoader; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -33,7 +32,6 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -import javax.annotation.Priority; import javax.enterprise.inject.se.SeContainer; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Application; @@ -44,7 +42,6 @@ import io.helidon.common.http.Http; import io.helidon.config.Config; import io.helidon.microprofile.config.MpConfig; -import io.helidon.microprofile.server.spi.MpService; import io.helidon.microprofile.server.spi.MpServiceContext; import io.helidon.webserver.Routing; import io.helidon.webserver.ServerConfiguration; @@ -62,8 +59,6 @@ public class ServerImpl implements Server { private static final Logger JERSEY_LOGGER = Logger.getLogger(ServerImpl.class.getName() + ".jersey"); private static final Logger STARTUP_LOGGER = Logger.getLogger("io.helidon.microprofile.startup.server"); - private static final int DEFAULT_PRIORITY = 100; - private final SeContainer container; private final boolean containerCreated; private final String host; @@ -190,15 +185,6 @@ public Response toResponse(Exception exception) { STARTUP_LOGGER.finest("Server created"); } - private static int findPriority(Class aClass) { - Priority priorityAnnot = aClass.getAnnotation(Priority.class); - if (null != priorityAnnot) { - return priorityAnnot.value(); - } - - return DEFAULT_PRIORITY; - } - private void loadExtensions(Builder builder, MpConfig mpConfig, Config config, @@ -206,14 +192,34 @@ private void loadExtensions(Builder builder, Routing.Builder routingBuilder, Map namedRouting, ServerConfiguration.Builder serverConfigBuilder) { - // extensions - List extensions = new LinkedList<>(builder.extensions()); - ServiceLoader.load(MpService.class).forEach(extensions::add); List newApps = new LinkedList<>(); - // TODO order by Priority - MpServiceContext context = new MpServiceContext() { + MpServiceContext context = createExtensionContext(mpConfig, + config, + apps, + routingBuilder, + namedRouting, + serverConfigBuilder, + newApps); + + // extensions + builder.extensions() + .forEach(extension -> { + extension.configure(context); + apps.addAll(newApps); + newApps.clear(); + }); + } + + private MpServiceContext createExtensionContext(MpConfig mpConfig, + Config config, + List apps, + Routing.Builder routingBuilder, + Map namedRouting, + ServerConfiguration.Builder serverConfigBuilder, + List newApps) { + return new MpServiceContext() { @Override public org.eclipse.microprofile.config.Config config() { return mpConfig; @@ -267,11 +273,6 @@ public void register(Class key, U instance) { register.put(key, instance); } }; - for (MpService extension : extensions) { - extension.configure(context); - apps.addAll(newApps); - newApps.clear(); - } } @Override diff --git a/microprofile/server/src/main/java9/module-info.java b/microprofile/server/src/main/java9/module-info.java index 6b69edda6aa..82fbaa90cbd 100644 --- a/microprofile/server/src/main/java9/module-info.java +++ b/microprofile/server/src/main/java9/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ requires transitive java.activation; requires java.logging; + requires io.helidon.common.serviceloader; exports io.helidon.microprofile.server; exports io.helidon.microprofile.server.spi; diff --git a/pom.xml b/pom.xml index 4b40c8d6e13..17fdb6540e3 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ Oracle Corporation - Laird Jarrett Nelson + Laird Nelson laird.nelson@oracle.com Oracle Corporation @@ -86,6 +86,7 @@ + all UTF-8 UTF-8 @@ -111,22 +112,28 @@ 0.31.0 2.0 8.18 + 2.7.4 3.0.0 3.0.0 2.16.0 1.23.0 1.0.0-rc13 2.4.14 - 1.16.1 + 1.20.0 25.0-jre 1.4.197 1.3 2.7.8 + 1 2.9.8 0.34.0 + 2.2.2 2.0.4.Final 2.3.0 + 2.3.0.1 + 2.3.2 2.1 + 7.6.0.Final 2.9.0 2.26 4.9.0.201710071750-r @@ -144,10 +151,14 @@ 2.0 2.23.4 8.0.11 + 5.9.3.Final 4.1.34.Final + 2.0.22.Final 1.2.44 12.2.0.1 0.31.0 + 0.0.12 + 2.2 0.4.0 3.5.1 1.0.2 @@ -157,12 +168,16 @@ 2.0.8 1.7.25 1.1.1 - 1.19 + 1.24 3.1.3 6.13.1 + 1.2 1.3.3 + 2.0.1.Final 3.0.3.Final 2.6.0 + 2.12.5 + 5.0.11 1.5.18 1.1.0 1.0.3 @@ -188,11 +203,12 @@ 1.0.5 3.0.2 3.0.0 + 0.14.0 1.16 1.6.7 1.5.0.Final 3.0.0 - 0.5.0 + 0.5.1 2.7 3.0.0 3.7.1 @@ -203,6 +219,7 @@ 2.19.1 1.1 2.3 + 1.4 ${project.build.directory}/docs @@ -219,6 +236,7 @@ bundles metrics health + grpc openapi @@ -244,6 +262,7 @@ http://docs.jboss.org/cdi/api/${version.lib.cdi-api} + http://javax-inject.github.io/javax-inject/api/ https://fasterxml.github.io/jackson-annotations/javadoc/2.9/ https://fasterxml.github.io/jackson-core/javadoc/2.9/ https://fasterxml.github.io/jackson-databind/javadoc/2.9/ @@ -255,13 +274,20 @@ https://static.javadoc.io/io.opentracing/opentracing-api/${version.lib.opentracing} https://static.javadoc.io/io.prometheus/simpleclient/${version.lib.prometheus} https://static.javadoc.io/io.zipkin.reporter2/zipkin-reporter/${version.lib.zipkin} - https://static.javadoc.io/javax.json/javax.json-api/${version.lib.jsonp-api} https://static.javadoc.io/jakarta.json.bind/jakarta.json.bind-api/${version.lib.jsonb-api} + https://static.javadoc.io/jakarta.persistence/jakarta.persistence-api/${version.lib.jakarta-persistence-api} + https://static.javadoc.io/javax.json/javax.json-api/${version.lib.jsonp-api} + https://static.javadoc.io/javax.json.bind/javax.json.bind-api/${version.lib.jsonb-api} + https://static.javadoc.io/javax.persistence/javax.persistence-api/${version.lib.persistence-api} + https://static.javadoc.io/javax.transaction/javax.transaction-api/${version.lib.transaction-api} + https://static.javadoc.io/javax.xml.bind/jaxb-api/${version.lib.jaxb-api}/ https://static.javadoc.io/org.eclipse.microprofile.config/microprofile-config-api/${version.lib.microprofile-config-api} https://static.javadoc.io/org.eclipse.microprofile.health/microprofile-health-api/${version.lib.microprofile-health-api} https://static.javadoc.io/org.eclipse.microprofile.metrics/microprofile-metrics-api/${version.lib.microprofile-metrics-api} + https://docs.jboss.org/weld/javadoc/3.0/weld-spi/ + http://narayana.io/docs/api/ + https://www.eclipse.org/eclipselink/api/2.7/ - all -J-Dhttp.agent=maven-javadoc-plugin @@ -369,6 +395,17 @@ org.apache.maven.plugins maven-jar-plugin ${version.plugin.jar} + + + + true + + + https://helidon.io + ${buildNumber} + + + org.apache.maven.plugins @@ -457,6 +494,13 @@ org.xolstice.maven.plugins protobuf-maven-plugin ${version.plugin.protobuf} + + com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier} + grpc-java + + io.grpc:protoc-gen-grpc-java:${version.lib.grpc}:exe:${os.detected.classifier} + + org.codehaus.mojo @@ -497,6 +541,27 @@ ${maven.deploy.skip} + + org.jvnet.jaxb2.maven2 + maven-jaxb2-plugin + ${version.plugin.jaxb} + + + org.codehaus.mojo + buildnumber-maven-plugin + ${version.plugin.buildnumber} + + + validate + + create + + + + + true + + @@ -554,6 +619,10 @@ sitegen-maven-plugin true + + org.codehaus.mojo + buildnumber-maven-plugin + @@ -615,6 +684,16 @@ jaxb-api ${version.lib.jaxb-api} + + com.sun.xml.bind + jaxb-core + ${version.lib.jaxb-core} + + + com.sun.xml.bind + jaxb-impl + ${version.lib.jaxb-impl} + javax.ws.rs javax.ws.rs-api @@ -635,6 +714,11 @@ javax.annotation-api ${version.lib.annotation-api} + + javax.inject + javax.inject + ${version.lib.inject} + javax.json javax.json-api @@ -733,11 +817,26 @@ grpc-stub ${version.lib.grpc} + + io.grpc + grpc-services + ${version.lib.grpc} + io.grpc grpc-netty ${version.lib.grpc} + + io.netty + netty-tcnative-boringssl-static + ${version.netty.tcnative} + + + io.opentracing.contrib + opentracing-grpc + ${version.lib.opentracing.grpc} + com.google.protobuf protobuf-java @@ -760,6 +859,16 @@ zipkin-sender-urlconnection ${version.lib.zipkin} + + io.zipkin.zipkin2 + zipkin-junit + ${version.lib.zipkin.junit} + + + com.oracle.bedrock + bedrock-testing-support + ${version.lib.bedrock} + com.google.guava guava @@ -833,6 +942,13 @@ + + org.jboss.weld + weld-core-bom + ${version.lib.weld} + pom + import + org.jboss.weld.se weld-se-core @@ -980,6 +1096,26 @@ + + jakarta.persistence + jakarta.persistence-api + ${version.lib.jakarta-persistence-api} + + + javax.persistence + javax.persistence-api + ${version.lib.persistence-api} + + + javax.transaction + javax.transaction-api + ${version.lib.transaction-api} + + + javax.validation + validation-api + ${version.lib.validation-api} + com.h2database h2 @@ -1014,6 +1150,37 @@ jedis ${version.lib.jedis} + + org.eclipse.persistence + org.eclipse.persistence.jpa + ${version.lib.eclipselink} + + + org.jboss.narayana.jta + cdi + ${version.lib.narayana} + + + org.jboss.spec.javax.resource + jboss-connector-api_1.7_spec + + + sun.jdk + jconsole + + + + + org.jboss + jboss-transaction-spi + ${version.lib.jboss-transaction-spi} + + + org.jboss.spec.javax.resource + jboss-connector-api_1.7_spec + + + org.graalvm.sdk graal-sdk diff --git a/security/integration/grpc/pom.xml b/security/integration/grpc/pom.xml new file mode 100644 index 00000000000..5c8533d8194 --- /dev/null +++ b/security/integration/grpc/pom.xml @@ -0,0 +1,158 @@ + + + + + 4.0.0 + + io.helidon.security.integration + helidon-security-integration-project + 1.0.4-SNAPSHOT + + helidon-security-integration-grpc + Helidon Security Integration gRPC Server + + + + io.helidon.security + helidon-security + ${project.version} + + + io.helidon.config + helidon-config + ${project.version} + + + io.helidon.grpc + helidon-grpc-server + ${project.version} + provided + + + io.helidon.security + helidon-security-util + ${project.version} + + + + io.helidon.grpc + helidon-grpc-client + ${project.version} + test + + + + + io.helidon.security.integration + helidon-security-integration-webserver + ${project.version} + test + + + io.helidon.security.integration + helidon-security-integration-jersey + ${project.version} + test + + + io.helidon.security.providers + helidon-security-providers-http-auth + ${project.version} + test + + + io.helidon.security.providers + helidon-security-providers-abac + ${project.version} + test + + + io.helidon.config + helidon-config-encryption + ${project.version} + test + + + io.helidon.bundles + helidon-bundles-config + ${project.version} + test + + + org.glassfish.jersey.core + jersey-client + test + + + org.glassfish.jersey.inject + jersey-hk2 + test + + + io.helidon.webserver + helidon-webserver-test-support + ${project.version} + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.mockito + mockito-core + test + + + javax.activation + javax.activation-api + test + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + test-compile + test-compile-custom + + + + + + + + diff --git a/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcClientSecurity.java b/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcClientSecurity.java new file mode 100644 index 00000000000..75759cffae2 --- /dev/null +++ b/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcClientSecurity.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.integration.grpc; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +import io.helidon.common.CollectionsHelper; +import io.helidon.security.EndpointConfig; +import io.helidon.security.OutboundSecurityClientBuilder; +import io.helidon.security.OutboundSecurityResponse; +import io.helidon.security.SecurityContext; +import io.helidon.security.SecurityEnvironment; +import io.helidon.webserver.ServerRequest; + +import io.grpc.CallCredentials; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.opentracing.Span; +import io.opentracing.tag.Tags; + +import static io.helidon.security.integration.grpc.GrpcSecurity.ABAC_ATTRIBUTE_METHOD; + +/** + * A gRPC {@link CallCredentials} implementation. + *

+ * Only works as part of integration with the Helidon Security component. + */ +public class GrpcClientSecurity + extends CallCredentials { + + /** + * Property name for outbound security provider name. Set this with + * {@link GrpcClientSecurity.Builder#property(String, Object)}. + */ + public static final String PROPERTY_PROVIDER = "io.helidon.security.integration.grpc.GrpcClientSecurity.explicitProvider"; + + private final SecurityContext context; + + private final Map properties; + + private GrpcClientSecurity(SecurityContext context, Map properties) { + this.context = context; + this.properties = properties; + } + + @Override + public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) { + Span span = context.tracer() + .buildSpan("security:outbound") + .asChildOf(context.tracingSpan()) + .start(); + + String explicitProvider = (String) properties.get(PROPERTY_PROVIDER); + + try { + MethodDescriptor methodDescriptor = requestInfo.getMethodDescriptor(); + String methodName = methodDescriptor.getFullMethodName(); + SecurityEnvironment.Builder outboundEnv = context.env().derive(); + + outboundEnv.path(methodName) + .method(methodName) + .addAttribute(ABAC_ATTRIBUTE_METHOD, methodDescriptor) + .transport("grpc") + .build(); + + EndpointConfig.Builder outboundEp = context.endpointConfig().derive(); + + properties.forEach(outboundEp::addAtribute); + + OutboundSecurityClientBuilder clientBuilder = context.outboundClientBuilder() + .outboundEnvironment(outboundEnv) + .outboundEndpointConfig(outboundEp) + .explicitProvider(explicitProvider); + + OutboundSecurityResponse providerResponse = clientBuilder.buildAndGet(); + + switch (providerResponse.status()) { + case FAILURE: + case FAILURE_FINISH: + traceError(span, + providerResponse.throwable().orElse(null), + providerResponse.description().orElse(providerResponse.status().toString())); + break; + case ABSTAIN: + case SUCCESS: + case SUCCESS_FINISH: + default: + break; + } + + // TODO check response status - maybe entity was updated? + // see MIC-6785 + + Map> newHeaders = providerResponse.requestHeaders(); + + Metadata metadata = new Metadata(); + for (Map.Entry> entry : newHeaders.entrySet()) { + Metadata.Key key = Metadata.Key.of(entry.getKey(), Metadata.ASCII_STRING_MARSHALLER); + for (String value : entry.getValue()) { + metadata.put(key, value); + } + } + + applier.apply(metadata); + + span.finish(); + } catch (SecurityException e) { + traceError(span, e, null); + applier.fail(Status.UNAUTHENTICATED.withDescription("Security principal propagation error").withCause(e)); + } catch (Exception e) { + traceError(span, e, null); + applier.fail(Status.UNAUTHENTICATED.withDescription("Unknown error").withCause(e)); + } + } + + @Override + public void thisUsesUnstableApi() { + } + + /** + * Create a {@link GrpcClientSecurity} instance. + * + * @param securityContext the {@link SecurityContext} to use + * + * @return a {@link GrpcClientSecurity} builder. + */ + public static GrpcClientSecurity create(SecurityContext securityContext) { + return builder(securityContext).build(); + } + + /** + * Create a {@link GrpcClientSecurity} instance. + * + * @param req the http {@link ServerRequest} to use to obtain the {@link SecurityContext} + * + * @return a {@link GrpcClientSecurity} builder. + */ + public static GrpcClientSecurity create(ServerRequest req) { + return builder(req).build(); + } + + /** + * Obtain a {@link GrpcClientSecurity} builder. + * + * @param securityContext the {@link SecurityContext} to use + * + * @return a {@link GrpcClientSecurity} builder. + */ + public static Builder builder(SecurityContext securityContext) { + return new Builder(securityContext); + } + + /** + * Obtain a {@link GrpcClientSecurity} builder. + * + * @param req the http {@link ServerRequest} to use to obtain the {@link SecurityContext} + * + * @return a {@link GrpcClientSecurity} builder. + */ + public static Builder builder(ServerRequest req) { + return builder(getContext(req)); + } + + private static SecurityContext getContext(ServerRequest req) { + return req.context().get(SecurityContext.class) + .orElseThrow(() -> new RuntimeException("Failed to get security context from request, security not configured")); + } + + private static void traceError(Span span, Throwable throwable, String description) { + // failed + if (null != throwable) { + Tags.ERROR.set(span, true); + span.log(CollectionsHelper.mapOf("event", "error", + "error.object", throwable)); + } else { + Tags.ERROR.set(span, true); + span.log(CollectionsHelper.mapOf("event", "error", + "message", description, + "error.kind", "SecurityException")); + } + span.finish(); + } + + /** + * A builder of {@link GrpcClientSecurity} instances. + */ + public static class Builder + implements io.helidon.common.Builder { + + private final SecurityContext securityContext; + + private final Map properties; + + private Builder(SecurityContext securityContext) { + this.securityContext = securityContext; + this.properties = new HashMap<>(); + } + + /** + * Set a new property that may be used by {@link io.helidon.security.spi.SecurityProvider}s + * when creating the credentials to apply to the call. + * + * @param name property name. + * @param value (new) property value. {@code null} value removes the property + * with the given name. + * + * @return the updated builder. + */ + public Builder property(String name, Object value) { + if (value == null) { + properties.remove(name); + } else { + properties.put(name, value); + } + return this; + } + + @Override + public GrpcClientSecurity build() { + return new GrpcClientSecurity(securityContext, properties); + } + } +} diff --git a/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcSecurity.java b/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcSecurity.java new file mode 100644 index 00000000000..ce2afd88cbb --- /dev/null +++ b/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcSecurity.java @@ -0,0 +1,603 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.integration.grpc; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Priority; + +import io.helidon.config.Config; +import io.helidon.grpc.core.InterceptorPriorities; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.security.EndpointConfig; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.SecurityEnvironment; + +import io.grpc.Context; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.contrib.grpc.OpenTracingContextKey; + +/** + * Integration of security into the gRPC Server. + *

+ * Methods that start with "from" are to register GrpcSecurity with {@link io.helidon.grpc.server.GrpcServer} + * - to create {@link SecurityContext} for requests: + *

    + *
  • {@link #create(Security)}
  • + *
  • {@link #create(Config)}
  • + *
  • {@link #create(Security, Config)}
  • + *
+ *

+ * Example: + *

+ * // gRPC server routing builder - this is our integration point
+ * {@link GrpcRouting} routing = GrpcRouting.builder()
+ * // register GrpcSecurity to add the security ServerInterceptor
+ * .intercept({@link GrpcSecurity}.{@link
+ * GrpcSecurity#create(Security) create(security)})
+ * 
+ *

+ * Other methods are to create security enforcement points (gates) for specific services. + * These methods are starting points that provide an instance of {@link GrpcSecurityHandler} that has finer grained + * methods to control the gate behavior.
+ * Note that if any gate is configured, auditing will be enabled by default if you want to audit any method, invoke + * {@link #audit()} to create a gate that will always audit the route. + * If you want to create a gate and not audit it, use {@link GrpcSecurityHandler#skipAudit()} on the returned instance. + *

    + *
  • {@link #secure()} - authentication and authorization
  • + *
  • {@link #rolesAllowed(String...)} - role based access control (implies authentication and authorization)
  • + *
  • {@link #authenticate()} - authentication only
  • + *
  • {@link #authorize()} - authorization only
  • + *
  • {@link #allowAnonymous()} - authentication optional
  • + *
  • {@link #audit()} - audit all requests
  • + *
  • {@link #authenticator(String)} - use explicit authenticator (named - as configured in config or through builder)
  • + *
  • {@link #authorizer(String)} - use explicit authorizer (named - as configured in config or through builder)
  • + *
  • {@link #enforce()} - use defaults (e.g. no authentication, authorization, audit calls; this also give access to + * more fine-grained methods of {@link GrpcSecurityHandler}
  • + *
+ *

+ * Example: + *

+ * // continue from example above...
+ * // create a gate for method GET: authenticate all paths under /user and require role "user" for authorization
+ * .intercept({@link io.helidon.grpc.server.GrpcService}, GrpcSecurity.{@link GrpcSecurity#rolesAllowed(String...)
+ * rolesAllowed("user")})
+ * 
+ */ +// we need to have all fields optional and this is cleaner than checking for null +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@Priority(InterceptorPriorities.AUTHENTICATION) +public final class GrpcSecurity + implements ServerInterceptor, ServiceDescriptor.Configurer { + private static final Logger LOGGER = Logger.getLogger(GrpcSecurity.class.getName()); + + /** + * Security can accept additional headers to be added to security request. + * This will be used to obtain multi-value string map (a map of string to list of strings) from context (appropriate + * to the integration). + */ + public static final Context.Key CONTEXT_ADD_HEADERS = Context.key("security.addHeaders"); + + /** + * The SecurityContext gRPC metadata header key. + */ + public static final Context.Key SECURITY_CONTEXT = + Context.key("Helidon.SecurityContext"); + + /** + * The default security handler gRPC metadata header key. + */ + public static final Context.Key GRPC_SECURITY_HANDLER = + Context.key("Helidon.SecurityInterceptor"); + + /** + * The value used for the key of the security context environment's ABAC request remote address attribute. + */ + public static final String ABAC_ATTRIBUTE_REMOTE_ADDRESS = "userIp"; + + /** + * The value used for the key of the security context environment's ABAC request remote port attribute. + */ + public static final String ABAC_ATTRIBUTE_REMOTE_PORT = "userPort"; + + /** + * The value used for the key of the security context environment's ABAC request headers attribute. + */ + public static final String ABAC_ATTRIBUTE_HEADERS = "metadata"; + + /** + * The value used for the key of the security context environment's ABAC request method descriptor attribute. + */ + public static final String ABAC_ATTRIBUTE_METHOD = "methodDescriptor"; + + // Security configuration keys + private static final String KEY_GRPC_CONFIG = "grpc-server"; + + private static final AtomicInteger SECURITY_COUNTER = new AtomicInteger(); + + private final Security security; + private final Optional config; + private final GrpcSecurityHandler defaultHandler; + + private GrpcSecurity(Security security, Config config) { + this(security, Optional.ofNullable(config), GrpcSecurityHandler.create()); + } + + private GrpcSecurity(Security security, Optional config, GrpcSecurityHandler defaultHandler) { + this.security = security; + this.config = config.map(cfg -> cfg.get(KEY_GRPC_CONFIG)); + this.defaultHandler = this.config + .map(cfg -> GrpcSecurityHandler.create(cfg.get("defaults"), defaultHandler)) + .orElse(defaultHandler); + } + + /** + * Create a consumer of gRPC routing config to be {@link GrpcRouting.Builder#register(GrpcService)}) registered} with + * gRPC server routing to process security requests. + * This method is to be used together with other routing methods to protect gRPC service or methods programmatically. + * Example: + *
+     * .intercept(GrpcSecurity.authenticate().rolesAllowed("user"))
+     * 
+ * + * @param security initialized security + * @return routing config consumer + */ + public static GrpcSecurity create(Security security) { + return create(security, null); + } + + /** + * Create a consumer of gRPC routing config to be {@link GrpcRouting.Builder#register(GrpcService) registered} with + * gRPC server routing to process security requests. + * This method configures security and gRPC server integration from a config instance + * + * @param config Config instance to load security and gRPC server integration from configuration + * @return routing config consumer + */ + public static GrpcSecurity create(Config config) { + Security security = Security.create(config); + return create(security, config); + } + + /** + * Create a consumer of gRPC routing config to be {@link GrpcRouting.Builder#register(GrpcService) registered} with + * gRPC server routing to process security requests. + * This method expects initialized security and creates gRPC server integration from a config instance + * + * @param security Security instance to use + * @param config Config instance to load security and gRPC server integration from configuration + * @return routing config consumer + */ + public static GrpcSecurity create(Security security, Config config) { + return new GrpcSecurity(security, config); + } + + /** + * Secure access using authentication and authorization. + * Auditing is enabled by default for methods modifying content. + * When using RBAC (role based access control), just use {@link #rolesAllowed(String...)}. + * If you use a security provider, that requires additional data, use {@link GrpcSecurityHandler#customObject(Object)}. + *

+ * Behavior: + *

    + *
  • Authentication: enabled and required
  • + *
  • Authorization: enabled if provider configured
  • + *
  • Audit: not modified
  • + *
+ * + * @return {@link GrpcSecurityHandler} instance configured with authentication and authorization + */ + public static GrpcSecurityHandler secure() { + return GrpcSecurityHandler.create().authenticate().authorize(); + } + + /** + * If called, request will go through authentication process - defaults to false (even if authorize is true). + *

+ * Behavior: + *

    + *
  • Authentication: enabled and required
  • + *
  • Authorization: not modified (default: disabled)
  • + *
  • Audit: not modified
  • + *
+ * + * @return {@link GrpcSecurityHandler} instance + */ + public static GrpcSecurityHandler authenticate() { + return GrpcSecurityHandler.create().authenticate(); + } + + /** + * Whether to audit this request - defaults to false for GET and HEAD methods, true otherwise. + * Request is audited with event type "request". + *

+ * Behavior: + *

    + *
  • Authentication: not modified (default: disabled)
  • + *
  • Authorization: not modified (default: disabled)
  • + *
  • Audit: enabled for any method this gate is registered on
  • + *
+ * + * @return {@link GrpcSecurityHandler} instance + */ + public static GrpcSecurityHandler audit() { + return GrpcSecurityHandler.create().audit(); + } + + /** + * Use a named authenticator (as supported by security - if not defined, default authenticator is used). + *

+ * Behavior: + *

    + *
  • Authentication: enabled and required
  • + *
  • Authorization: not modified (default: disabled)
  • + *
  • Audit: not modified
  • + *
+ * + * @param explicitAuthenticator name of authenticator as configured in {@link Security} + * @return {@link GrpcSecurityHandler} instance + */ + public static GrpcSecurityHandler authenticator(String explicitAuthenticator) { + return GrpcSecurityHandler.create().authenticate().authenticator(explicitAuthenticator); + } + + /** + * Use a named authorizer (as supported by security - if not defined, default authorizer is used, if none defined, all is + * permitted). + *

+ * Behavior: + *

    + *
  • Authentication: enabled and required
  • + *
  • Authorization: enabled with explicit provider
  • + *
  • Audit: not modified
  • + *
+ * + * @param explicitAuthorizer name of authorizer as configured in {@link Security} + * @return {@link GrpcSecurityHandler} instance + */ + public static GrpcSecurityHandler authorizer(String explicitAuthorizer) { + return GrpcSecurityHandler.create().authenticate().authorize().authorizer(explicitAuthorizer); + } + + /** + * An array of allowed roles for this path - must have a security provider supporting roles. + *

+ * Behavior: + *

    + *
  • Authentication: enabled and required
  • + *
  • Authorization: enabled
  • + *
  • Audit: not modified
  • + *
+ * + * @param roles if subject is any of these roles, allow access + * @return {@link GrpcSecurityHandler} instance + */ + public static GrpcSecurityHandler rolesAllowed(String... roles) { + return GrpcSecurityHandler.create().rolesAllowed(roles); + } + + /** + * If called, authentication failure will not abort request and will continue as anonymous (defaults to false). + *

+ * Behavior: + *

    + *
  • Authentication: enabled and optional
  • + *
  • Authorization: not modified (default: disabled)
  • + *
  • Audit: not modified
  • + *
+ * + * @return {@link GrpcSecurityHandler} instance + */ + public static GrpcSecurityHandler allowAnonymous() { + return GrpcSecurityHandler.create().authenticate().authenticationOptional(); + } + + /** + * Enable authorization for this route. + *

+ * Behavior: + *

    + *
  • Authentication: enabled and required
  • + *
  • Authorization: enabled if provider is present
  • + *
  • Audit: not modified
  • + *
+ * + * @return {@link GrpcSecurityHandler} instance + */ + public static GrpcSecurityHandler authorize() { + return GrpcSecurityHandler.create().authorize(); + } + + /** + * Return a default instance to create a default enforcement point (or modify the result further). + *

+ * Behavior: + *

    + *
  • Authentication: not modified (default: disabled)
  • + *
  • Authorization: not modified (default: disabled)
  • + *
  • Audit: not modified
  • + *
+ * + * @return {@link GrpcSecurityHandler} instance + */ + public static GrpcSecurityHandler enforce() { + return GrpcSecurityHandler.create(); + } + + /** + * Create a new gRPC security instance using the default handler as base defaults for all handlers used. + * If handlers are loaded from config, than this is the least significant value. + * + * @param defaultHandler if a security handler is configured for a route, it will take its defaults from this handler + * @return new instance of gRPC security with the handler default + */ + public GrpcSecurity securityDefaults(GrpcSecurityHandler defaultHandler) { + Objects.requireNonNull(defaultHandler, "Default security handler must not be null"); + return new GrpcSecurity(security, config, defaultHandler); + } + + /** + * If the {@link #config} field is set then modify the {@link ServiceDescriptor.Rules} + * with any applicable security configuration. + * + * @param rules the {@link ServiceDescriptor.Rules} to modify + */ + @Override + public void configure(ServiceDescriptor.Rules rules) { + config.ifPresent(grpcConfig -> modifyServiceDescriptorConfig(rules, grpcConfig)); + } + + private void modifyServiceDescriptorConfig(ServiceDescriptor.Rules rules, Config grpcConfig) { + String serviceName = rules.name(); + + grpcConfig.get("services") + .asNodeList() + .map(list -> findServiceConfig(serviceName, list)) + .ifPresent(cfg -> configureServiceSecurity(rules, cfg)); + } + + private Config findServiceConfig(String serviceName, List list) { + return list.stream() + .filter(cfg -> cfg.get("name").asString().map(serviceName::equals).orElse(false)) + .findFirst() + .orElse(null); + } + + private void configureServiceSecurity(ServiceDescriptor.Rules rules, Config grpcServiceConfig) { + if (grpcServiceConfig.exists()) { + GrpcSecurityHandler defaults; + + if (grpcServiceConfig.get("defaults").exists()) { + defaults = GrpcSecurityHandler.create(grpcServiceConfig.get("defaults"), defaultHandler); + } else { + defaults = defaultHandler; + } + + Config methodsConfig = grpcServiceConfig.get("methods"); + if (methodsConfig.exists()) { + methodsConfig.asNodeList().ifPresent(configs -> { + for (Config methodConfig : configs) { + String name = methodConfig.get("name") + .asString() + .orElseThrow(() -> new SecurityException(methodConfig + .key() + " must contain name key with a method name to " + + "register to gRPC server security")); + + rules.intercept(name, GrpcSecurityHandler.create(methodConfig, defaults)); + } + }); + } else { + rules.intercept(defaults); + } + } + } + + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { + Context context = registerContext(call, headers); + + try { + GrpcSecurityHandler configuredHandler = GrpcSecurity.GRPC_SECURITY_HANDLER.get(context); + GrpcSecurityHandler handler = configuredHandler == null ? defaultHandler : configuredHandler; + + ServerCall.Listener listener = context.call(() -> handler.handleSecurity(call, headers, next)); + + return new ContextualizedServerCallListener<>(listener, context); + } catch (Throwable throwable) { + LOGGER.log(Level.SEVERE, "Unexpected exception during security processing", throwable); + call.close(Status.INTERNAL, new Metadata()); + return new GrpcSecurityHandler.EmptyListener<>(); + } + } + + @SuppressWarnings("unchecked") + Context registerContext(ServerCall call, Metadata headers) { + Context grpcContext; + + if (SECURITY_CONTEXT.get() == null) { + SocketAddress remoteSocket = call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + String address = null; + int port = -1; + + if (remoteSocket instanceof InetSocketAddress) { + address = ((InetSocketAddress) remoteSocket).getHostName(); + port = ((InetSocketAddress) remoteSocket).getPort(); + } else { + address = String.valueOf(remoteSocket); + } + + Map> headerMap = new HashMap<>(); + Map mapExtra = CONTEXT_ADD_HEADERS.get(); + + if (mapExtra != null) { + headerMap.putAll(mapExtra); + } + + for (String name : headers.keys()) { + Metadata.Key key = Metadata.Key.of(name, Metadata.ASCII_STRING_MARSHALLER); + Iterable iterable = headers.getAll(key); + List values = new ArrayList<>(); + + if (iterable != null) { + for (Object o : iterable) { + values.add(String.valueOf(o)); + } + } + + headerMap.put(name, values); + } + + MethodDescriptor methodDescriptor = call.getMethodDescriptor(); + String methodName = methodDescriptor.getFullMethodName(); + + SecurityEnvironment env = security.environmentBuilder() + .path(methodName) + .method(methodName) + .headers(headerMap) + .addAttribute(ABAC_ATTRIBUTE_REMOTE_ADDRESS, address) + .addAttribute(ABAC_ATTRIBUTE_REMOTE_PORT, port) + .addAttribute(ABAC_ATTRIBUTE_HEADERS, headers) + .addAttribute(ABAC_ATTRIBUTE_METHOD, methodDescriptor) + .transport("grpc") + .build(); + + EndpointConfig ec = EndpointConfig.builder().build(); + + Span span = OpenTracingContextKey.getKey().get(); + SpanContext spanContext = span == null ? null : span.context(); + SecurityContext context = security.contextBuilder(String.valueOf(SECURITY_COUNTER.incrementAndGet())) + .tracingSpan(spanContext) + .env(env) + .endpointConfig(ec) + .build(); + + grpcContext = Context.current().withValue(SECURITY_CONTEXT, context); + } else { + grpcContext = Context.current(); + } + + return grpcContext; + } + + /** + * Obtain the {@link Security} instance being used. + * + * @return the {@link Security} instance being used + */ + Security getSecurity() { + return security; + } + + /** + * Obtain the default {@link GrpcSecurityHandler}. + * + * @return the default {@link GrpcSecurityHandler} + */ + GrpcSecurityHandler getDefaultHandler() { + return defaultHandler; + } + + + /** + * Implementation of {@link io.grpc.ForwardingServerCallListener} that attaches a context before + * dispatching calls to the delegate and detaches them after the call completes. + */ + private static class ContextualizedServerCallListener extends + ForwardingServerCallListener.SimpleForwardingServerCallListener { + private final Context context; + + private ContextualizedServerCallListener(ServerCall.Listener delegate, Context context) { + super(delegate); + this.context = context; + } + + @Override + public void onMessage(ReqT message) { + Context previous = context.attach(); + try { + super.onMessage(message); + } finally { + context.detach(previous); + } + } + + @Override + public void onHalfClose() { + Context previous = context.attach(); + try { + super.onHalfClose(); + } finally { + context.detach(previous); + } + } + + @Override + public void onCancel() { + Context previous = context.attach(); + try { + super.onCancel(); + } finally { + context.detach(previous); + } + } + + @Override + public void onComplete() { + Context previous = context.attach(); + try { + super.onComplete(); + } finally { + context.detach(previous); + } + } + + @Override + public void onReady() { + Context previous = context.attach(); + try { + super.onReady(); + } finally { + context.detach(previous); + } + } + } +} diff --git a/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcSecurityHandler.java b/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcSecurityHandler.java new file mode 100644 index 00000000000..9cfdee1a788 --- /dev/null +++ b/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcSecurityHandler.java @@ -0,0 +1,1150 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.integration.grpc; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Priority; + +import io.helidon.common.CollectionsHelper; +import io.helidon.common.OptionalHelper; +import io.helidon.config.Config; +import io.helidon.grpc.core.InterceptorPriorities; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.security.AuditEvent; +import io.helidon.security.AuthenticationResponse; +import io.helidon.security.AuthorizationResponse; +import io.helidon.security.ClassToInstanceStore; +import io.helidon.security.Security; +import io.helidon.security.SecurityClientBuilder; +import io.helidon.security.SecurityContext; +import io.helidon.security.SecurityRequest; +import io.helidon.security.SecurityRequestBuilder; +import io.helidon.security.internal.SecurityAuditEvent; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.ForwardingServerCall; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; + +import static io.helidon.security.AuditEvent.AuditParam.plain; + +/** + * Handles security for the gRPC server. This handler is registered either by hand on the gRPC routing config, + * or automatically from configuration when integration is done through {@link GrpcSecurity#create(Config)} + * or {@link GrpcSecurity#create(Security)}. + *

+ * This class is an implementation of a {@link ServerInterceptor} with a priority of + * {@link InterceptorPriorities#CONTEXT} that will add itself to the call context with the key + * {@link GrpcSecurity#GRPC_SECURITY_HANDLER}. This will then cause the {@link GrpcSecurity} + * interceptor that runs later with a priority of {@link InterceptorPriorities#AUTHENTICATION} to use + * this instance of the handler. + */ +// we need to have all fields optional and this is cleaner than checking for null +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@Priority(InterceptorPriorities.CONTEXT) +public class GrpcSecurityHandler + implements ServerInterceptor, ServiceDescriptor.Configurer { + private static final Logger LOGGER = Logger.getLogger(GrpcSecurityHandler.class.getName()); + private static final String KEY_ROLES_ALLOWED = "roles-allowed"; + private static final String KEY_AUTHENTICATOR = "authenticator"; + private static final String KEY_AUTHORIZER = "authorizer"; + private static final String KEY_AUTHENTICATE = "authenticate"; + private static final String KEY_AUTHENTICATION_OPTIONAL = "authentication-optional"; + private static final String KEY_AUTHORIZE = "authorize"; + private static final String KEY_AUDIT = "audit"; + private static final String KEY_AUDIT_EVENT_TYPE = "audit-event-type"; + private static final String KEY_AUDIT_MESSAGE_FORMAT = "audit-message-format"; + private static final String DEFAULT_AUDIT_EVENT_TYPE = "grpcRequest"; + private static final String DEFAULT_AUDIT_MESSAGE_FORMAT = "%2$s %1$s %4$s %5$s requested by %3$s"; + private static final GrpcSecurityHandler DEFAULT_INSTANCE = builder().build(); + + private final Optional> rolesAllowed; + private final Optional> customObjects; + private final Optional config; + private final Optional explicitAuthenticator; + private final Optional explicitAuthorizer; + private final Optional authenticate; + private final Optional authenticationOptional; + private final Optional authorize; + private final Optional audited; + private final Optional auditEventType; + private final Optional auditMessageFormat; + private final boolean combined; + private final Map configMap = new HashMap<>(); + + // lazily initialized (as it requires a context value to first create it) + private final AtomicReference combinedHandler = new AtomicReference<>(); + + private GrpcSecurityHandler(Builder builder) { + // must copy values to be safely immutable + this.rolesAllowed = builder.rolesAllowed.flatMap(strings -> { + Set newRoles = new HashSet<>(strings); + return Optional.of(newRoles); + }); + + // must copy values to be safely immutable + this.customObjects = builder.customObjects.flatMap(store -> { + ClassToInstanceStore ctis = new ClassToInstanceStore<>(); + ctis.putAll(store); + return Optional.of(ctis); + }); + + config = builder.config; + explicitAuthenticator = builder.explicitAuthenticator; + explicitAuthorizer = builder.explicitAuthorizer; + authenticate = builder.authenticate; + authenticationOptional = builder.authenticationOptional; + audited = builder.audited; + auditEventType = builder.auditEventType; + auditMessageFormat = builder.auditMessageFormat; + authorize = builder.authorize; + combined = builder.combined; + + config.ifPresent(conf -> { + if (conf.exists() && !conf.isLeaf()) { + conf.asNodeList().get().forEach(node -> configMap.put(node.name(), node)); + } + }); + } + + /** + * Create an instance from configuration. + *

+ * The config expected (example in HOCON format): + *

+     * {
+     *   #
+     *   # these are used by {@link GrpcSecurity} when loaded from config, to register
+     *   # with the {@link io.helidon.grpc.server.GrpcServer}
+     *   #
+     *   path = "/noRoles"
+     *   methods = ["get"]
+     *
+     *   #
+     *   # these are used by this class
+     *   #
+     *   # whether to authenticate this request - defaults to false (even if authorize is true)
+     *   authenticate = true
+     *   # if set to true, authentication failure will not abort request and will continue as anonymous (defaults to false)
+     *   authentication optional
+     *   # use a named authenticator (as supported by security - if not defined, default authenticator is used)
+     *   authenticator = "basic-auth"
+     *   # an array of allowed roles for this path - must have a security provider supporting roles
+     *   roles-allowed = ["user"]
+     *   # whether to authorize this request - defaults to true (authorization is "on" by default)
+     *   authorize = true
+     *   # use a named authorizer (as supported by security - if not defined, default authorizer is used, if none defined, all is
+     *   #   permitted)
+     *   authorizer = "roles"
+     *   # whether to audit this request - defaults to false, if enabled, request is audited with event type "request"
+     *   audit = true
+     *   # override for event-type, defaults to {@value #DEFAULT_AUDIT_EVENT_TYPE}
+     *   audit-event-type = "unit_test"
+     *   # override for audit message format, defaults to {@value #DEFAULT_AUDIT_MESSAGE_FORMAT}
+     *   audit-message-format = "Unit test message format"
+     *   # override for audit severity for successful requests (1xx, 2xx and 3xx status codes),
+     *   #   defaults to {@link AuditEvent.AuditSeverity#SUCCESS}
+     *   audit-ok-severity = "AUDIT_FAILURE"
+     *   # override for audit severity for unsuccessful requests (4xx and 5xx status codes),
+     *   #   defaults to {@link AuditEvent.AuditSeverity#FAILURE}
+     *   audit-error-severity = "INFO"
+     *
+     *   #
+     *   # Any other configuration - this all gets passed to a security provider, so check your provider's documentation
+     *   #
+     *   custom-provider {
+     *      custom-key = "some value"
+     *   }
+     * }
+     * 
+ * + * @param config Config at the point of a single handler configuration + * @param defaults Default values to copy + * @return an instance configured from the config (using defaults from defaults parameter for missing values) + */ + static GrpcSecurityHandler create(Config config, GrpcSecurityHandler defaults) { + Builder builder = builder(defaults); + + config.get(KEY_ROLES_ALLOWED).asList(String.class) + .ifPresentOrElse(builder::rolesAllowed, + () -> defaults.rolesAllowed.ifPresent(builder::rolesAllowed)); + if (config.exists()) { + builder.config(config); + } + + config.get(KEY_AUTHENTICATOR).asString().or(() -> defaults.explicitAuthenticator) + .ifPresent(builder::authenticator); + config.get(KEY_AUTHORIZER).asString().or(() -> defaults.explicitAuthorizer) + .ifPresent(builder::authorizer); + config.get(KEY_AUTHENTICATE).as(Boolean.class).or(() -> defaults.authenticate) + .ifPresent(builder::authenticate); + config.get(KEY_AUTHENTICATION_OPTIONAL).as(Boolean.class) + .or(() -> defaults.authenticationOptional) + .ifPresent(builder::authenticationOptional); + config.get(KEY_AUDIT).as(Boolean.class).or(() -> defaults.audited) + .ifPresent(builder::audit); + config.get(KEY_AUTHORIZE).as(Boolean.class).or(() -> defaults.authorize) + .ifPresent(builder::authorize); + config.get(KEY_AUDIT_EVENT_TYPE).asString().or(() -> defaults.auditEventType) + .ifPresent(builder::auditEventType); + config.get(KEY_AUDIT_MESSAGE_FORMAT).asString().or(() -> defaults.auditMessageFormat) + .ifPresent(builder::auditMessageFormat); + + // now resolve implicit behavior + + // roles allowed implies atn and atz + if (config.get(KEY_ROLES_ALLOWED).exists()) { + // we have roles allowed defined + if (!config.get(KEY_AUTHENTICATE).exists()) { + builder.authenticate(true); + } + if (!config.get(KEY_AUTHORIZE).exists()) { + builder.authorize(true); + } + } + + // optional atn implies atn + config.get(KEY_AUTHENTICATION_OPTIONAL).as(Boolean.class).ifPresent(aBoolean -> { + if (aBoolean) { + if (!config.get(KEY_AUTHENTICATE).exists()) { + builder.authenticate(true); + } + } + }); + + // explicit atn provider implies atn + config.get(KEY_AUTHENTICATOR).asString().ifPresent(value -> { + if (!config.get(KEY_AUTHENTICATE).exists()) { + builder.authenticate(true); + } + }); + + // explicit atz provider implies atz + config.get(KEY_AUTHORIZER).asString().ifPresent(value -> { + if (!config.get(KEY_AUTHORIZE).exists()) { + builder.authorize(true); + } + }); + + return builder.build(); + } + + private static void configure(Config config, + String key, + Optional defaultValue, + Consumer builderMethod, + Class clazz) { + config.get(key).as(clazz).or(() -> defaultValue).ifPresent(builderMethod); + } + + static GrpcSecurityHandler create() { + // constant is OK, object is immutable + return DEFAULT_INSTANCE; + } + + private static Builder builder() { + return new Builder(); + } + + private static Builder builder(GrpcSecurityHandler toCopy) { + return new Builder().configureFrom(toCopy); + } + + static void traceError(Span span, Throwable throwable) { + // failed + + Tags.ERROR.set(span, true); + span.log(CollectionsHelper.mapOf("event", "error", + "error.object", throwable)); + + span.finish(); + } + + /** + * Modifies a {@link io.helidon.grpc.server.ServiceDescriptor.Rules} to add this {@link GrpcSecurityHandler}. + * + * @param rules the {@link io.helidon.grpc.server.ServiceDescriptor.Rules} to modify + */ + @Override + public void configure(ServiceDescriptor.Rules rules) { + rules.addContextValue(GrpcSecurity.GRPC_SECURITY_HANDLER, this); + } + + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { + + Context context = Context.current().withValue(GrpcSecurity.GRPC_SECURITY_HANDLER, this); + return Contexts.interceptCall(context, call, headers, next); + } + + /** + * Perform security checks. + * + * @param call the current gRPC call to check + * @param headers the call headers + * @param next the next handler in the chain + * @param the request type + * @param the response type + * + * @return listener for processing incoming messages for {@code call}, never {@code null} + */ + ServerCall.Listener handleSecurity(ServerCall call, + Metadata headers, + ServerCallHandler next) { + SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(); + + if (securityContext == null) { + call.close(Status.FAILED_PRECONDITION + .withDescription("Security context not present. Maybe you forgot to " + + "GrpcRouting.builder().intercept(GrpcSecurity.create" + + "(security))..."), new Metadata()); + return new EmptyListener<>(); + } + + if (combined) { + return processSecurity(securityContext, call, headers, next); + } else { + // the following condition may be met for multiple threads - and we don't really care + // as the result is exactly the same in all cases and doesn't have side effects + if (null == combinedHandler.get()) { + // we may have a default handler configured + GrpcSecurityHandler defaultHandler = GrpcSecurity.GRPC_SECURITY_HANDLER.get(); + + if (defaultHandler == null) { + defaultHandler = DEFAULT_INSTANCE; + } + + // intentional same instance comparison, as I want to prevent endless loop + //noinspection ObjectEquality + if (defaultHandler == DEFAULT_INSTANCE) { + combinedHandler.set(this); + } else { + combinedHandler.compareAndSet(null, + builder(defaultHandler).configureFrom(this).combined().build()); + } + } + + return combinedHandler.get().processSecurity(securityContext, call, headers, next); + } + } + + private ServerCall.Listener processSecurity(SecurityContext securityContext, + ServerCall call, + Metadata headers, + ServerCallHandler next) { + // authentication and authorization + Tracer tracer = securityContext.tracer(); + + Span securitySpan = tracer + .buildSpan("security") + .asChildOf(securityContext.tracingSpan()) + .start(); + + securitySpan.log(CollectionsHelper.mapOf("securityId", securityContext.id())); + + securityContext.endpointConfig(securityContext.endpointConfig() + .derive() + .configMap(configMap) + .customObjects(customObjects.orElse(new ClassToInstanceStore<>())) + .build()); + + CompletionStage stage = processAuthentication(call, headers, securityContext, tracer, securitySpan) + .thenCompose(atnResult -> { + if (atnResult.proceed) { + // authentication was OK or disabled, we should continue + return processAuthorization(securityContext, tracer, securitySpan); + } else { + // authentication told us to stop processing + return CompletableFuture.completedFuture(AtxResult.STOP); + } + }) + .thenApply(atzResult -> { + if (atzResult.proceed) { + // authorization was OK, we can continue processing + securitySpan.log("status: PROCEED"); + securitySpan.finish(); + return true; + } else { + securitySpan.log("status: DENY"); + securitySpan.finish(); + return false; + } + }); + + ServerCall.Listener listener; + CallWrapper callWrapper = new CallWrapper<>(call); + + try { + boolean proceed = stage.toCompletableFuture().get(); + + if (proceed) { + listener = next.startCall(callWrapper, headers); + } else { + callWrapper.close(Status.PERMISSION_DENIED, new Metadata()); + listener = new EmptyListener<>(); + } + } catch (Throwable throwable) { + LOGGER.log(Level.SEVERE, "Unexpected exception during security processing", throwable); + callWrapper.close(Status.INTERNAL, new Metadata()); + listener = new EmptyListener<>(); + } + + return new AuditingListener<>(listener, callWrapper, headers, securityContext); + } + + private void processAudit(ServerCall call, + Metadata headers, + SecurityContext securityContext, + Status status) { + // make sure we actually should audit + if (!audited.orElse(true)) { + // explicitly disabled + return; + } + + AuditEvent.AuditSeverity severity = status.isOk() + ? AuditEvent.AuditSeverity.SUCCESS + : AuditEvent.AuditSeverity.FAILURE; + + SecurityAuditEvent auditEvent = SecurityAuditEvent + .audit(severity, + auditEventType.orElse(DEFAULT_AUDIT_EVENT_TYPE), + auditMessageFormat.orElse(DEFAULT_AUDIT_MESSAGE_FORMAT)) + .addParam(plain("method", call.getMethodDescriptor().getFullMethodName())) + .addParam(plain("status", status.getCode())) + .addParam(plain("subject", securityContext.user().orElse(SecurityContext.ANONYMOUS))) + .addParam(plain("transport", "grpc")) + .addParam(plain("resourceType", "grpc")); + + securityContext.service().ifPresent(svc -> auditEvent.addParam(plain("service", svc.toString()))); + + securityContext.audit(auditEvent); + } + + private CompletionStage processAuthentication(ServerCall call, + Metadata headers, + SecurityContext securityContext, + Tracer tracer, + Span securitySpan) { + if (!authenticate.orElse(false)) { + return CompletableFuture.completedFuture(AtxResult.PROCEED); + } + + CompletableFuture future = new CompletableFuture<>(); + + Span atnSpan = tracer + .buildSpan("security:atn") + .asChildOf(securitySpan) + .start(); + + SecurityClientBuilder clientBuilder = securityContext.atnClientBuilder(); + + configureSecurityRequest(clientBuilder, atnSpan); + + clientBuilder.explicitProvider(explicitAuthenticator.orElse(null)).submit().thenAccept(response -> { + switch (response.status()) { + case SUCCESS: + //everything is fine, we can continue with processing + break; + case FAILURE_FINISH: + if (atnFinishFailure(future)) { + atnSpanFinish(atnSpan, response); + return; + } + break; + case SUCCESS_FINISH: + atnFinish(future); + atnSpanFinish(atnSpan, response); + return; + case ABSTAIN: + case FAILURE: + if (atnAbstainFailure(future)) { + atnSpanFinish(atnSpan, response); + return; + } + break; + default: + Exception e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); + future.completeExceptionally(e); + traceError(atnSpan, e); + return; + } + + atnSpanFinish(atnSpan, response); + future.complete(new AtxResult(clientBuilder.buildRequest())); + }).exceptionally(throwable -> { + traceError(atnSpan, throwable); + future.completeExceptionally(throwable); + return null; + }); + + return future; + } + + private void atnSpanFinish(Span atnSpan, AuthenticationResponse response) { + response.user() + .ifPresent(subject -> atnSpan + .log("security.user: " + subject.principal().getName())); + + response.service() + .ifPresent(subject -> atnSpan.log("security.service: " + subject.principal().getName())); + + atnSpan.log("status: " + response.status()); + + atnSpan.finish(); + } + + private boolean atnAbstainFailure(CompletableFuture future) { + if (authenticationOptional.orElse(false)) { + LOGGER.finest("Authentication failed, but was optional, so assuming anonymous"); + return false; + } + + future.complete(AtxResult.STOP); + return true; + } + + private boolean atnFinishFailure(CompletableFuture future) { + + if (authenticationOptional.orElse(false)) { + LOGGER.finest("Authentication failed, but was optional, so assuming anonymous"); + return false; + } else { + future.complete(AtxResult.STOP); + return true; + } + } + + private void atnFinish(CompletableFuture future) { + future.complete(AtxResult.STOP); + } + + private void configureSecurityRequest(SecurityRequestBuilder> request, + Span parentSpan) { + + request.optional(authenticationOptional.orElse(false)) + .tracingSpan(parentSpan); + } + + private CompletionStage processAuthorization( + SecurityContext context, + Tracer tracer, + Span securitySpan) { + CompletableFuture future = new CompletableFuture<>(); + + if (!authorize.orElse(false)) { + future.complete(AtxResult.PROCEED); + return future; + } + + Span atzSpan = tracer + .buildSpan("security:atz") + .asChildOf(securitySpan) + .start(); + + Set rolesSet = rolesAllowed.orElse(CollectionsHelper.setOf()); + + if (!rolesSet.isEmpty()) { + // first validate roles - RBAC is supported out of the box by security, no need to invoke provider + if (explicitAuthorizer.isPresent()) { + if (rolesSet.stream().noneMatch(role -> context.isUserInRole(role, explicitAuthorizer.get()))) { + future.complete(AtxResult.STOP); + atzSpan.finish(); + return future; + } + } else { + if (rolesSet.stream().noneMatch(context::isUserInRole)) { + future.complete(AtxResult.STOP); + atzSpan.finish(); + return future; + } + } + } + + SecurityClientBuilder client; + + client = context.atzClientBuilder(); + configureSecurityRequest(client, atzSpan); + + client.explicitProvider(explicitAuthorizer.orElse(null)).submit().thenAccept(response -> { + switch (response.status()) { + case SUCCESS: + //everything is fine, we can continue with processing + break; + case FAILURE_FINISH: + case SUCCESS_FINISH: + atzSpan.finish(); + future.complete(AtxResult.STOP); + return; + case ABSTAIN: + case FAILURE: + atzSpan.finish(); + future.complete(AtxResult.STOP); + return; + default: + SecurityException e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); + traceError(atzSpan, e); + future.completeExceptionally(e); + return; + } + + atzSpan.finish(); + // everything was OK + future.complete(AtxResult.PROCEED); + }).exceptionally(throwable -> { + traceError(atzSpan, throwable); + future.completeExceptionally(throwable); + return null; + }); + + return future; + } + + /** + * Use a named authenticator (as supported by security - if not defined, default authenticator is used). + * Will enable authentication. + * + * @param explicitAuthenticator name of authenticator as configured in {@link Security} + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler authenticator(String explicitAuthenticator) { + return builder(this).authenticator(explicitAuthenticator).build(); + } + + /** + * Use a named authorizer (as supported by security - if not defined, default authorizer is used, if none defined, all is + * permitted). + * Will enable authorization. + * + * @param explicitAuthorizer name of authorizer as configured in {@link Security} + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler authorizer(String explicitAuthorizer) { + return builder(this).authorizer(explicitAuthorizer).build(); + } + + /** + * An array of allowed roles for this path - must have a security provider supporting roles (either authentication + * or authorization provider). + * This method enables authentication and authorization (you can disable them again by calling + * {@link GrpcSecurityHandler#skipAuthorization()} + * and {@link #skipAuthentication()} if needed). + * + * @param roles if subject is any of these roles, allow access + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler rolesAllowed(String... roles) { + return builder(this).rolesAllowed(roles).authorize(true).authenticate(true).build(); + + } + + /** + * If called, authentication failure will not abort request and will continue as anonymous (authentication is not optional + * by default). + * Will enable authentication. + * + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler authenticationOptional() { + return builder(this).authenticationOptional(true).build(); + } + + /** + * If called, request will go through authentication process - (authentication is disabled by default - it may be enabled + * as a side effect of other methods, such as {@link #rolesAllowed(String...)}. + * + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler authenticate() { + return builder(this).authenticate(true).build(); + } + + /** + * If called, request will NOT go through authentication process. Use this when another method implies authentication + * (such as {@link #rolesAllowed(String...)}) and yet it is not desired (e.g. everything is handled by authorization). + * + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler skipAuthentication() { + return builder(this).authenticate(false).build(); + } + + /** + * Register a custom object for security request(s). + * This creates a hard dependency on a specific security provider, so use with care. + * + * @param object An object expected by security provider + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler customObject(Object object) { + return builder(this).customObject(object).build(); + } + + /** + * Override for event-type, defaults to {@value #DEFAULT_AUDIT_EVENT_TYPE}. + * + * @param eventType audit event type to use + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler auditEventType(String eventType) { + return builder(this).auditEventType(eventType).build(); + } + + /** + * Override for audit message format, defaults to {@value #DEFAULT_AUDIT_MESSAGE_FORMAT}. + * + * @param messageFormat audit message format to use + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler auditMessageFormat(String messageFormat) { + return builder(this).auditMessageFormat(messageFormat).build(); + } + + /** + * If called, request will go through authorization process - (authorization is disabled by default - it may be enabled + * as a side effect of other methods, such as {@link #rolesAllowed(String...)}. + * + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler authorize() { + return builder(this).authorize(true).build(); + } + + /** + * Skip authorization for this route. + * Use this when authorization is implied by another method on this class (e.g. {@link #rolesAllowed(String...)} and + * you want to explicitly forbid it. + * + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler skipAuthorization() { + return builder(this).authorize(false).build(); + } + + /** + * Audit this request for any method. Request is audited with event type {@link #DEFAULT_AUDIT_EVENT_TYPE}. + *

+ * By default audit is enabled as follows (based on HTTP methods): + *

    + *
  • GET, HEAD - not audited
  • + *
  • PUT, POST, DELETE - audited
  • + *
  • any other method (e.g. custom methods) - audited
  • + *
+ * Calling this method will override the default setting and audit any method this handler is registered for. + * + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler audit() { + return builder(this).audit(true).build(); + } + + /** + * Disable auditing of this request. Will override defaults and disable auditing for all methods this handler is registered + * for. + *

+ * By default audit is enabled as follows (based on HTTP methods): + *

    + *
  • GET, HEAD - not audited
  • + *
  • PUT, POST, DELETE - audited
  • + *
  • any other method (e.g. custom methods) - audited
  • + *
+ * + * @return new handler instance with configuration of this instance updated with this method + */ + public GrpcSecurityHandler skipAudit() { + return builder(this).audit(false).build(); + } + + /** + * Obtain the roles allowed for this {@link GrpcSecurityHandler}. + * + * @return an {@link Optional} containing the the roles allowed for + * this {@link GrpcSecurityHandler} if any have been configured + */ + Optional> getRolesAllowed() { + return rolesAllowed.map(Collections::unmodifiableSet); + } + + /** + * Obtain the explicit authenticator for this {@link GrpcSecurityHandler}. + * + * @return an {@link Optional} containing the the explicit authenticator for + * this {@link GrpcSecurityHandler} if any have been configured + */ + Optional getExplicitAuthenticator() { + return explicitAuthenticator; + } + + /** + * Obtain the explicit authorizer for this {@link GrpcSecurityHandler}. + * + * @return an {@link Optional} containing the the explicit authorizer for + * this {@link GrpcSecurityHandler} if any have been configured + */ + Optional getExplicitAuthorizer() { + return explicitAuthorizer; + } + + /** + * Obtain whether this {@link GrpcSecurityHandler} performs authentication. + * + * @return an {@link Optional} containing {@code true} if this + * {@link GrpcSecurityHandler} performs authentication + */ + Optional isAuthenticate() { + return authenticate; + } + + /** + * Obtain whether this {@link GrpcSecurityHandler} allows anonymous access. + * + * @return an {@link Optional} containing {@code true} if this + * {@link GrpcSecurityHandler} allows anonymous access + */ + Optional isAuthenticationOptional() { + return authenticationOptional; + } + + /** + * Obtain whether this {@link GrpcSecurityHandler} performs authorization. + * + * @return an {@link Optional} containing {@code true} if this + * {@link GrpcSecurityHandler} performs authorization + */ + Optional isAuthorize() { + return authorize; + } + + /** + * Obtain whether this {@link GrpcSecurityHandler} audits security operations. + * + * @return an {@link Optional} containing {@code true} if this + * {@link GrpcSecurityHandler} audits security operations + */ + Optional isAudited() { + return audited; + } + + /** + * Obtain the audit event type override. + * + * @return an {@link Optional} containing the audit event type + * override if one has been set + */ + Optional getAuditEventType() { + return auditEventType; + } + + /** + * Obtain the audit message format override. + * + * @return an {@link Optional} containing the audit message format + * override if one has been set + */ + Optional getAuditMessageFormat() { + return auditMessageFormat; + } + + private static final class AtxResult { + private static final AtxResult PROCEED = new AtxResult(true); + private static final AtxResult STOP = new AtxResult(false); + + private final boolean proceed; + + private AtxResult(boolean proceed) { + this.proceed = proceed; + } + + @SuppressWarnings("unused") + private AtxResult(SecurityRequest ignored) { + this.proceed = true; + } + } + + // WARNING: builder methods must not have side-effects, as they are used to build instance from configuration + // if you want side effects, use methods on GrpcSecurityInterceptor + private static final class Builder implements io.helidon.common.Builder { + private Optional> rolesAllowed = Optional.empty(); + private Optional> customObjects = Optional.empty(); + private Optional config = Optional.empty(); + private Optional explicitAuthenticator = Optional.empty(); + private Optional explicitAuthorizer = Optional.empty(); + private Optional authenticate = Optional.empty(); + private Optional authenticationOptional = Optional.empty(); + private Optional authorize = Optional.empty(); + private Optional audited = Optional.empty(); + private Optional auditEventType = Optional.empty(); + private Optional auditMessageFormat = Optional.empty(); + private boolean combined; + + private Builder() { + } + + @Override + public GrpcSecurityHandler build() { + return new GrpcSecurityHandler(this); + } + + private Builder combined() { + this.combined = true; + + return this; + } + + // add to this builder + private Builder configureFrom(GrpcSecurityHandler handler) { + handler.rolesAllowed.ifPresent(this::rolesAllowed); + handler.customObjects.ifPresent(this::customObjects); + handler.config.ifPresent(this::config); + handler.explicitAuthenticator.ifPresent(this::authenticator); + handler.explicitAuthorizer.ifPresent(this::authorizer); + handler.authenticate.ifPresent(this::authenticate); + handler.authenticationOptional.ifPresent(this::authenticationOptional); + handler.audited.ifPresent(this::audit); + handler.auditEventType.ifPresent(this::auditEventType); + handler.auditMessageFormat.ifPresent(this::auditMessageFormat); + handler.authorize.ifPresent(this::authorize); + + return this; + } + + private Builder customObjects(ClassToInstanceStore store) { + OptionalHelper.from(customObjects) + .ifPresentOrElse(myStore -> myStore.putAll(store), () -> { + ClassToInstanceStore ctis = new ClassToInstanceStore<>(); + ctis.putAll(store); + this.customObjects = Optional.of(ctis); + }); + + return this; + } + + /** + * Use a named authenticator (as supported by security - if not defined, default authenticator is used). + * + * @param explicitAuthenticator name of authenticator as configured in {@link Security} + * @return updated builder instance + */ + Builder authenticator(String explicitAuthenticator) { + this.explicitAuthenticator = Optional.of(explicitAuthenticator); + return this; + } + + /** + * Use a named authorizer (as supported by security - if not defined, default authorizer is used, if none defined, all is + * permitted). + * + * @param explicitAuthorizer name of authorizer as configured in {@link Security} + * @return updated builder instance + */ + Builder authorizer(String explicitAuthorizer) { + this.explicitAuthorizer = Optional.of(explicitAuthorizer); + return this; + } + + /** + * An array of allowed roles for this path - must have a security provider supporting roles. + * + * @param roles if subject is any of these roles, allow access + * @return updated builder instance + */ + Builder rolesAllowed(String... roles) { + return rolesAllowed(Arrays.asList(roles)); + } + + private Builder config(Config config) { + this.config = Optional.of(config); + return this; + } + + /** + * If called, authentication failure will not abort request and will continue as anonymous (defaults to false). + * + * @param isOptional whether authn is optional + * @return updated builder instance + */ + Builder authenticationOptional(boolean isOptional) { + this.authenticationOptional = Optional.of(isOptional); + return this; + } + + /** + * If called, request will go through authentication process - defaults to false (even if authorize is true). + * + * @param authenticate whether to authenticate or not + * @return updated builder instance + */ + Builder authenticate(boolean authenticate) { + this.authenticate = Optional.of(authenticate); + return this; + } + + /** + * Register a custom object for security request(s). + * This creates a hard dependency on a specific security provider, so use with care. + * + * @param object An object expected by security provider + * @return updated builder instance + */ + Builder customObject(Object object) { + OptionalHelper.from(customObjects) + .ifPresentOrElse(store -> store.putInstance(object), () -> { + ClassToInstanceStore ctis = new ClassToInstanceStore<>(); + ctis.putInstance(object); + customObjects = Optional.of(ctis); + }); + + return this; + } + + /** + * Override for event-type, defaults to {@value #DEFAULT_AUDIT_EVENT_TYPE}. + * + * @param eventType audit event type to use + * @return updated builder instance + */ + Builder auditEventType(String eventType) { + this.auditEventType = Optional.of(eventType); + return this; + } + + /** + * Override for audit message format, defaults to {@value #DEFAULT_AUDIT_MESSAGE_FORMAT}. + * + * @param messageFormat audit message format to use + * @return updated builder instance + */ + Builder auditMessageFormat(String messageFormat) { + this.auditMessageFormat = Optional.of(messageFormat); + return this; + } + + /** + * Enable authorization for this route. + * + * @param authorize whether to authorize + * @return updated builder instance + */ + Builder authorize(boolean authorize) { + this.authorize = Optional.of(authorize); + return this; + } + + /** + * Whether to audit this request - defaults to false, if enabled, request is audited with event type "request". + * + * @return updated builder instance + */ + Builder audit(boolean audited) { + this.audited = Optional.of(audited); + return this; + } + + Builder rolesAllowed(Collection roles) { + OptionalHelper.from(rolesAllowed).ifPresentOrElse(strings -> strings.addAll(roles), + () -> { + Set newRoles = new HashSet<>(roles); + rolesAllowed = Optional.of(newRoles); + }); + return this; + } + } + + /** + * An empty {@link ServerCall.Listener} used to terminate a call if + * authentication fails. + * + * @param the type of the call + */ + static class EmptyListener extends ServerCall.Listener { + } + + /** + * A logging {@link ServerCall.Listener}. + * + * @param the request type + */ + private class AuditingListener + extends ForwardingServerCallListener.SimpleForwardingServerCallListener { + private CallWrapper call; + private Metadata headers; + private SecurityContext securityContext; + + private AuditingListener(ServerCall.Listener delegate, + CallWrapper call, + Metadata headers, + SecurityContext securityContext) { + super(delegate); + this.call = call; + this.headers = headers; + this.securityContext = securityContext; + } + + @Override + public void onCancel() { + processAudit(call, headers, securityContext, call.getCloseStatus()); + } + + @Override + public void onComplete() { + processAudit(call, headers, securityContext, call.getCloseStatus()); + } + } + + private class CallWrapper + extends ForwardingServerCall.SimpleForwardingServerCall { + private Status closeStatus; + + private CallWrapper(ServerCall delegate) { + super(delegate); + } + + @Override + public void close(Status status, Metadata trailers) { + closeStatus = status; + super.close(status, trailers); + } + + Status getCloseStatus() { + return closeStatus; + } + } +} diff --git a/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/package-info.java b/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/package-info.java new file mode 100644 index 00000000000..be9eea4987e --- /dev/null +++ b/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Integration library for {@link io.helidon.grpc.server.GrpcServer}. + *

+ * The main security methods are duplicate - first as static methods on {@link io.helidon.security.integration.grpc.GrpcSecurity} and + * then as instance methods on {@link io.helidon.security.integration.grpc.GrpcSecurityHandler} that is returned by the static methods + * above. This is to provide a single starting point for security integration ({@link io.helidon.security.integration.grpc.GrpcSecurity}) + * and fluent API to build the "gate" to each gRPC service that is protected. + * + * @see io.helidon.security.integration.grpc.GrpcSecurity#create(io.helidon.security.Security) + * @see io.helidon.security.integration.grpc.GrpcSecurity#create(io.helidon.config.Config) + * @see io.helidon.security.integration.grpc.GrpcSecurity#create(io.helidon.security.Security, io.helidon.config.Config) + */ +package io.helidon.security.integration.grpc; diff --git a/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/GrpcSecurityTest.java b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/GrpcSecurityTest.java new file mode 100644 index 00000000000..4ef8ea0f38f --- /dev/null +++ b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/GrpcSecurityTest.java @@ -0,0 +1,495 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.integration.grpc; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.grpc.server.test.EchoServiceGrpc; +import io.helidon.security.EndpointConfig; +import io.helidon.security.Security; +import io.helidon.security.SecurityContext; +import io.helidon.security.SecurityEnvironment; + +import io.grpc.Attributes; +import io.grpc.Context; +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServiceDescriptor; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("unchecked") +public class GrpcSecurityTest { + @Test + public void shouldRegisterSecurityContext() { + MethodDescriptor descriptor = getEchoMethod(); + ServerCall call = mock(ServerCall.class); + Metadata headers = new Metadata(); + SocketAddress address = new InetSocketAddress("helidon.io", 8080); + Attributes attributes = Attributes.newBuilder() + .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, address) + .build(); + + + when(call.getAttributes()).thenReturn(attributes); + when(call.getMethodDescriptor()).thenReturn(descriptor); + + GrpcSecurity security = GrpcSecurity.create(Security.builder().build()); + Context context = security.registerContext(call, headers); + assertThat(context, is(notNullValue())); + + + SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(context); + assertThat(securityContext, is(notNullValue())); + + SecurityEnvironment environment = securityContext.env(); + assertThat(environment, is(notNullValue())); + } + + @Test + public void shouldAddAttributesToSecurityContext() { + MethodDescriptor descriptor = getEchoMethod(); + ServerCall call = mock(ServerCall.class); + Metadata headers = new Metadata(); + SocketAddress address = new InetSocketAddress("helidon.io", 8080); + Attributes attributes = Attributes.newBuilder() + .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, address) + .build(); + + + when(call.getAttributes()).thenReturn(attributes); + when(call.getMethodDescriptor()).thenReturn(descriptor); + + GrpcSecurity security = GrpcSecurity.create(Security.builder().build()); + Context context = security.registerContext(call, headers); + assertThat(context, is(notNullValue())); + + SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(context); + assertThat(securityContext, is(notNullValue())); + + SecurityEnvironment environment = securityContext.env(); + assertThat(environment, is(notNullValue())); + assertThat(environment.method(), is(descriptor.getFullMethodName())); + assertThat(environment.path().get(), is(descriptor.getFullMethodName())); + assertThat(environment.transport(), is("grpc")); + assertThat(environment.abacAttribute(GrpcSecurity.ABAC_ATTRIBUTE_REMOTE_ADDRESS).get(), is("helidon.io")); + assertThat(environment.abacAttribute(GrpcSecurity.ABAC_ATTRIBUTE_REMOTE_PORT).get(), is(8080)); + assertThat(environment.abacAttribute(GrpcSecurity.ABAC_ATTRIBUTE_HEADERS).get(), is(sameInstance(headers))); + assertThat(environment.abacAttribute(GrpcSecurity.ABAC_ATTRIBUTE_METHOD).get(), is(sameInstance(descriptor))); + + } + + @Test + public void shouldAddHeadersToSecurityContext() { + MethodDescriptor descriptor = getEchoMethod(); + ServerCall call = mock(ServerCall.class); + Metadata headers = new Metadata(); + SocketAddress address = new InetSocketAddress("helidon.io", 8080); + Attributes attributes = Attributes.newBuilder() + .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, address) + .build(); + + headers.put(Metadata.Key.of("key-1", Metadata.ASCII_STRING_MARSHALLER), "value-1.1"); + headers.put(Metadata.Key.of("key-1", Metadata.ASCII_STRING_MARSHALLER), "value-1.2"); + headers.put(Metadata.Key.of("key-2", Metadata.ASCII_STRING_MARSHALLER), "value-2"); + + when(call.getAttributes()).thenReturn(attributes); + when(call.getMethodDescriptor()).thenReturn(descriptor); + + GrpcSecurity security = GrpcSecurity.create(Security.builder().build()); + Context context = security.registerContext(call, headers); + assertThat(context, is(notNullValue())); + + SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(context); + assertThat(securityContext, is(notNullValue())); + + SecurityEnvironment environment = securityContext.env(); + assertThat(environment, is(notNullValue())); + + Map> expectedHeaders = new HashMap<>(); + Map> securityHeaders = environment.headers(); + + expectedHeaders.put("key-1", Arrays.asList("value-1.1", "value-1.2")); + expectedHeaders.put("key-2", Collections.singletonList("value-2")); + + assertThat(securityHeaders, is(notNullValue())); + assertThat(securityHeaders, is(expectedHeaders)); + } + + @Test + public void shouldAddExtraHeadersToSecurityContext() throws Exception { + MethodDescriptor descriptor = getEchoMethod(); + ServerCall call = mock(ServerCall.class); + Metadata headers = new Metadata(); + SocketAddress address = new InetSocketAddress("helidon.io", 8080); + Attributes attributes = Attributes.newBuilder() + .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, address) + .build(); + + Map extraHeaders = new HashMap(); + extraHeaders.put("key-1", Collections.singletonList("value-1")); + extraHeaders.put("key-2", Collections.singletonList("value-2")); + + when(call.getAttributes()).thenReturn(attributes); + when(call.getMethodDescriptor()).thenReturn(descriptor); + + GrpcSecurity security = GrpcSecurity.create(Security.builder().build()); + Context contextCurrent = Context.current().withValue(GrpcSecurity.CONTEXT_ADD_HEADERS, extraHeaders); + Context context = contextCurrent.call(() -> security.registerContext(call, headers)); + assertThat(context, is(notNullValue())); + + SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(context); + assertThat(securityContext, is(notNullValue())); + + SecurityEnvironment environment = securityContext.env(); + assertThat(environment, is(notNullValue())); + + Map> expectedHeaders = new HashMap<>(); + Map> securityHeaders = environment.headers(); + + expectedHeaders.put("key-1", Collections.singletonList("value-1")); + expectedHeaders.put("key-2", Collections.singletonList("value-2")); + + assertThat(securityHeaders, is(notNullValue())); + assertThat(securityHeaders, is(expectedHeaders)); + } + + @Test + public void shouldAddConfigToSecurityContext() { + MethodDescriptor descriptor = getEchoMethod(); + ServerCall call = mock(ServerCall.class); + Metadata headers = new Metadata(); + SocketAddress address = new InetSocketAddress("helidon.io", 8080); + Attributes attributes = Attributes.newBuilder() + .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, address) + .build(); + + headers.put(Metadata.Key.of("key-1", Metadata.ASCII_STRING_MARSHALLER), "value-1.1"); + headers.put(Metadata.Key.of("key-1", Metadata.ASCII_STRING_MARSHALLER), "value-1.2"); + headers.put(Metadata.Key.of("key-2", Metadata.ASCII_STRING_MARSHALLER), "value-2"); + + when(call.getAttributes()).thenReturn(attributes); + when(call.getMethodDescriptor()).thenReturn(descriptor); + + Config config = Config.builder() + .sources(ConfigSources.classpath("secure-services.conf")) + .build() + .get("security"); + + GrpcSecurity security = GrpcSecurity.create(Security.builder(config).build(), config); + Context context = security.registerContext(call, headers); + assertThat(context, is(notNullValue())); + + SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(context); + assertThat(securityContext, is(notNullValue())); + + EndpointConfig endpointConfig = securityContext.endpointConfig(); + assertThat(endpointConfig, is(notNullValue())); + + endpointConfig.config("foo"); + } + + @Test + public void shouldUseExistingSecurityContext() throws Exception { + MethodDescriptor descriptor = getEchoMethod(); + ServerCall call = mock(ServerCall.class); + Metadata headers = new Metadata(); + SocketAddress address = new InetSocketAddress("helidon.io", 8080); + Attributes attributes = Attributes.newBuilder() + .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, address) + .build(); + + Map extraHeaders = new HashMap(); + extraHeaders.put("key-1", Collections.singletonList("value-1")); + extraHeaders.put("key-2", Collections.singletonList("value-2")); + + when(call.getAttributes()).thenReturn(attributes); + when(call.getMethodDescriptor()).thenReturn(descriptor); + + Security security = Security.builder().build(); + SecurityContext securityContextCurrent = security.createContext("foo"); + GrpcSecurity grpcSecurity = GrpcSecurity.create(security); + Context contextCurrent = Context.current().withValue(GrpcSecurity.SECURITY_CONTEXT, securityContextCurrent); + Context context = contextCurrent.call(() -> grpcSecurity.registerContext(call, headers)); + + assertThat(context, is(notNullValue())); + + SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(context); + assertThat(securityContext, is(sameInstance(securityContextCurrent))); + } + + @Test + public void shouldCallDefaultHandler() { + MethodDescriptor descriptor= getEchoMethod(); + ServerCallHandler next = mock(ServerCallHandler.class); + ServerCall call = mock(ServerCall.class); + ListenerStub listener = new ListenerStub(); + Attributes attributes = Attributes.EMPTY; + Metadata headers = new Metadata(); + + GrpcSecurityHandler defaultHandler = mock(GrpcSecurityHandler.class); + + when(call.getAttributes()).thenReturn(attributes); + when(call.getMethodDescriptor()).thenReturn(descriptor); + when(defaultHandler.handleSecurity(any(ServerCall.class), any(Metadata.class), any(ServerCallHandler.class))).thenReturn(listener); + + GrpcSecurity security = GrpcSecurity.create(Security.builder().build()).securityDefaults(defaultHandler); + + ServerCall.Listener result = security.interceptCall(call, headers, next); + + verify(defaultHandler).handleSecurity(call, headers, next); + assertThat(result, is(notNullValue())); + } + + @Test + public void shouldCallSpecificHandler() throws Exception { + Metadata headers = new Metadata(); + ServerCall call = mock(ServerCall.class); + ServerCallHandler next = mock(ServerCallHandler.class); + ListenerStub listener = new ListenerStub(); + Attributes attributes = Attributes.EMPTY; + ServiceDescriptor serviceDescriptor = EchoServiceGrpc.getServiceDescriptor(); + MethodDescriptor descriptor + = (MethodDescriptor) serviceDescriptor.getMethods().stream().findAny().get(); + + GrpcSecurityHandler defaultHandler = mock(GrpcSecurityHandler.class); + GrpcSecurityHandler handler = mock(GrpcSecurityHandler.class); + + when(call.getAttributes()).thenReturn(attributes); + when(call.getMethodDescriptor()).thenReturn(descriptor); + when(handler.handleSecurity(any(ServerCall.class), any(Metadata.class), any(ServerCallHandler.class))).thenReturn(listener); + + GrpcSecurity security = GrpcSecurity.create(Security.builder().build()).securityDefaults(defaultHandler); + + Context context = Context.current().withValue(GrpcSecurity.GRPC_SECURITY_HANDLER, handler); + + ServerCall.Listener result = context.call(() -> security.interceptCall(call, headers, next)); + + verify(handler).handleSecurity(call, headers, next); + verifyNoMoreInteractions(defaultHandler); + assertThat(result, is(notNullValue())); + } + + @Test + public void shouldBuildDefaultHandler() { + GrpcSecurityHandler handler = GrpcSecurity.enforce(); + + assertThat(handler.isAuthenticate().orElse(null), is(nullValue())); + assertThat(handler.isAuthenticationOptional().orElse(null), is(nullValue())); + assertThat(handler.isAuthorize().orElse(null), is(nullValue())); + assertThat(handler.isAudited().orElse(null), is(nullValue())); + assertThat(handler.getAuditEventType().orElse(null), is(nullValue())); + assertThat(handler.getAuditMessageFormat().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthenticator().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthorizer().orElse(null), is(nullValue())); + assertThat(handler.getRolesAllowed().orElse(null), is(nullValue())); + } + + @Test + public void shouldBuildSecureHandler() { + GrpcSecurityHandler handler = GrpcSecurity.secure(); + + assertThat(handler.isAuthenticate().orElse(null), is(true)); + assertThat(handler.isAuthorize().orElse(null), is(true)); + + assertThat(handler.isAuthenticationOptional().orElse(null), is(nullValue())); + assertThat(handler.isAudited().orElse(null), is(nullValue())); + assertThat(handler.getAuditEventType().orElse(null), is(nullValue())); + assertThat(handler.getAuditMessageFormat().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthenticator().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthorizer().orElse(null), is(nullValue())); + assertThat(handler.getRolesAllowed().orElse(null), is(nullValue())); + } + + @Test + public void shouldCreateGrpcSecurityHandlerAllowAnonymous() { + GrpcSecurityHandler handler = GrpcSecurity.allowAnonymous(); + + assertThat(handler.isAuthenticate().orElse(null), is(true)); + assertThat(handler.isAuthenticationOptional().orElse(null), is(true)); + + assertThat(handler.isAuthorize().orElse(null), is(nullValue())); + assertThat(handler.isAudited().orElse(null), is(nullValue())); + assertThat(handler.getAuditEventType().orElse(null), is(nullValue())); + assertThat(handler.getAuditMessageFormat().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthenticator().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthorizer().orElse(null), is(nullValue())); + assertThat(handler.getRolesAllowed().orElse(null), is(nullValue())); + } + + @Test + public void shouldCreateGrpcSecurityHandlerWithAudit() { + GrpcSecurityHandler handler = GrpcSecurity.audit(); + + assertThat(handler.isAudited().orElse(null), is(true)); + + assertThat(handler.isAuthenticate().orElse(null), is(nullValue())); + assertThat(handler.isAuthenticationOptional().orElse(null), is(nullValue())); + assertThat(handler.isAuthorize().orElse(null), is(nullValue())); + assertThat(handler.getAuditEventType().orElse(null), is(nullValue())); + assertThat(handler.getAuditMessageFormat().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthenticator().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthorizer().orElse(null), is(nullValue())); + assertThat(handler.getRolesAllowed().orElse(null), is(nullValue())); + } + + @Test + public void shouldCreateGrpcSecurityHandlerWithAuthenticate() { + GrpcSecurityHandler handler = GrpcSecurity.authenticate(); + + assertThat(handler.isAuthenticate().orElse(null), is(true)); + + assertThat(handler.isAuthenticationOptional().orElse(null), is(nullValue())); + assertThat(handler.isAuthorize().orElse(null), is(nullValue())); + assertThat(handler.isAudited().orElse(null), is(nullValue())); + assertThat(handler.getAuditEventType().orElse(null), is(nullValue())); + assertThat(handler.getAuditMessageFormat().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthenticator().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthorizer().orElse(null), is(nullValue())); + assertThat(handler.getRolesAllowed().orElse(null), is(nullValue())); + } + + @Test + public void shouldCreateGrpcSecurityHandlerWithNamedAuthenticator() { + GrpcSecurityHandler handler = GrpcSecurity.authenticator("foo"); + + assertThat(handler.isAuthenticate().orElse(null), is(true)); + assertThat(handler.getExplicitAuthenticator().orElse(null), is("foo")); + + assertThat(handler.isAuthenticationOptional().orElse(null), is(nullValue())); + assertThat(handler.isAuthorize().orElse(null), is(nullValue())); + assertThat(handler.isAudited().orElse(null), is(nullValue())); + assertThat(handler.getAuditEventType().orElse(null), is(nullValue())); + assertThat(handler.getAuditMessageFormat().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthorizer().orElse(null), is(nullValue())); + assertThat(handler.getRolesAllowed().orElse(null), is(nullValue())); + } + + @Test + public void shouldCreateGrpcSecurityHandlerWithAuthorize() { + GrpcSecurityHandler handler = GrpcSecurity.authorize(); + + assertThat(handler.isAuthorize().orElse(null), is(true)); + + assertThat(handler.isAuthenticate().orElse(null), is(nullValue())); + assertThat(handler.isAuthenticationOptional().orElse(null), is(nullValue())); + assertThat(handler.isAudited().orElse(null), is(nullValue())); + assertThat(handler.getAuditEventType().orElse(null), is(nullValue())); + assertThat(handler.getAuditMessageFormat().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthenticator().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthorizer().orElse(null), is(nullValue())); + assertThat(handler.getRolesAllowed().orElse(null), is(nullValue())); + } + + @Test + public void shouldCreateGrpcSecurityHandlerWithNamedAuthorizer() { + GrpcSecurityHandler handler = GrpcSecurity.authorizer("foo"); + + assertThat(handler.isAuthenticate().orElse(null), is(true)); + assertThat(handler.isAuthorize().orElse(null), is(true)); + assertThat(handler.getExplicitAuthorizer().orElse(null), is("foo")); + + assertThat(handler.isAuthenticationOptional().orElse(null), is(nullValue())); + assertThat(handler.isAudited().orElse(null), is(nullValue())); + assertThat(handler.getAuditEventType().orElse(null), is(nullValue())); + assertThat(handler.getAuditMessageFormat().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthenticator().orElse(null), is(nullValue())); + assertThat(handler.getRolesAllowed().orElse(null), is(nullValue())); + } + + @Test + public void shouldCreateGrpcSecurityHandlerWithRoles() { + GrpcSecurityHandler handler = GrpcSecurity.rolesAllowed("foo", "bar"); + + assertThat(handler.getRolesAllowed().orElse(null), containsInAnyOrder("foo", "bar")); + assertThat(handler.isAuthenticate().orElse(null), is(true)); + assertThat(handler.isAuthorize().orElse(null), is(true)); + + assertThat(handler.isAuthenticationOptional().orElse(null), is(nullValue())); + assertThat(handler.isAudited().orElse(null), is(nullValue())); + assertThat(handler.getAuditEventType().orElse(null), is(nullValue())); + assertThat(handler.getAuditMessageFormat().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthenticator().orElse(null), is(nullValue())); + assertThat(handler.getExplicitAuthorizer().orElse(null), is(nullValue())); + } + + @Test + public void shouldCreateGrpcSecurity() { + Security security = Security.builder().build(); + GrpcSecurity grpcSecurity = GrpcSecurity.create(security); + + assertThat(grpcSecurity, is(notNullValue())); + assertThat(grpcSecurity.getSecurity(), is(sameInstance(security))); + assertThat(grpcSecurity.getDefaultHandler(), is(notNullValue())); + } + + @Test + public void shouldCreateGrpcSecurityWithDefaultHandler() { + Security security = Security.builder().build(); + GrpcSecurityHandler defaultHandler = GrpcSecurityHandler.create(); + GrpcSecurity grpcSecurity = GrpcSecurity.create(security) + .securityDefaults(defaultHandler); + + assertThat(grpcSecurity, is(notNullValue())); + assertThat(grpcSecurity.getSecurity(), is(sameInstance(security))); + assertThat(grpcSecurity.getDefaultHandler(), is(sameInstance(defaultHandler))); + } + + + private MethodDescriptor getEchoMethod() { + ServiceDescriptor serviceDescriptor = EchoServiceGrpc.getServiceDescriptor(); + return (MethodDescriptor) serviceDescriptor.getMethods().stream().findAny().get(); + } + + private class ListenerStub + extends ServerCall.Listener { + + private Context context; + + @Override + public void onMessage(String message) { + context = Context.current(); + } + + public Context getContext() { + return context; + } + } +} diff --git a/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/OutboundSecurityIT.java b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/OutboundSecurityIT.java new file mode 100644 index 00000000000..30c1017b361 --- /dev/null +++ b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/OutboundSecurityIT.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.integration.grpc; + +import java.util.concurrent.TimeUnit; +import java.util.logging.LogManager; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; + +import io.helidon.common.http.Http; +import io.helidon.config.Config; +import io.helidon.grpc.core.GrpcHelper; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.grpc.server.test.Echo; +import io.helidon.grpc.server.test.EchoServiceGrpc; +import io.helidon.security.Security; +import io.helidon.security.integration.webserver.WebSecurity; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.WebServer; + +import io.grpc.Channel; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessChannelBuilder; +import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import services.SecuredOutboundEchoService; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + + +public class OutboundSecurityIT { + + private static WebServer webServer; + + private static GrpcServer grpcServer; + + private static TestCallCredentials adminCreds = new TestCallCredentials("Ted", "secret"); + + private static EchoServiceGrpc.EchoServiceBlockingStub adminEchoStub; + + private static EchoServiceGrpc.EchoServiceBlockingStub noCredsEchoStub; + + private static String webServerURL; + + private static Client client; + + // ----- test lifecycle methods ----------------------------------------- + + @BeforeAll + public static void startServers() throws Exception { + LogManager.getLogManager().readConfiguration( + ServiceAndMethodLevelSecurityIT.class.getResourceAsStream("/logging.properties")); + + Config config = Config.create(); + + Security security = Security.builder() + .addProvider(HttpBasicAuthProvider.create(config.get("http-basic-auth"))) + .build(); + + + // secured web server's Routing + Routing webRouting = Routing.builder() + .register(WebSecurity.create(security).securityDefaults(WebSecurity.authenticate())) + .get("/test", WebSecurity.rolesAllowed("admin"), OutboundSecurityIT::echoWebRequest) + .get("/propagate", WebSecurity.rolesAllowed("user"), OutboundSecurityIT::propagateCredentialsWebRequest) + .get("/override", WebSecurity.rolesAllowed("user"), OutboundSecurityIT::overrideCredentialsWebRequest) + .build(); + + webServer = WebServer.create(webRouting) + .start() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + + webServerURL = "http://127.0.0.1:" + webServer.port(); + + client = ClientBuilder.newBuilder().build() + .register(HttpAuthenticationFeature.basicBuilder().build()); + + ServiceDescriptor echoService = ServiceDescriptor.builder(new SecuredOutboundEchoService(webServerURL)) + .intercept(GrpcSecurity.rolesAllowed("admin")) + .build(); + + // Add the EchoService + GrpcRouting grpcRouting = GrpcRouting.builder() + .intercept(GrpcSecurity.create(security).securityDefaults(GrpcSecurity.authenticate())) + .register(echoService) + .build(); + + // Run the server on port 0 so that it picks a free ephemeral port + GrpcServerConfiguration serverConfig = GrpcServerConfiguration.builder().port(0).build(); + + grpcServer = GrpcServer.create(serverConfig, grpcRouting) + .start() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + + Channel channel = InProcessChannelBuilder.forName(grpcServer.configuration().name()).build(); + + adminEchoStub = EchoServiceGrpc.newBlockingStub(channel).withCallCredentials(adminCreds); + noCredsEchoStub = EchoServiceGrpc.newBlockingStub(channel); + } + + @AfterAll + public static void cleanup() { + grpcServer.shutdown(); + webServer.shutdown(); + } + + // ----- test methods --------------------------------------------------- + + @Test + public void shouldMakeSecureOutboundCallFromGrpcMethod() { + Echo.EchoResponse response = adminEchoStub.echo(Echo.EchoRequest.newBuilder().setMessage("foo").build()); + assertThat(response.getMessage(), is("foo")); + } + + @Test + public void shouldPropagateCredentialsToOutboundCallFromWebMethod() { + String message = "testing..."; + String response = client.target(webServerURL) + .path("/propagate") + .queryParam("message", message) + .request() + .property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_USERNAME, "Ted") + .property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_PASSWORD, "secret") + .get(String.class); + + assertThat(response, is(message)); + } + + @Test + public void shouldPropagateInvalidCredentialsToOutboundCallFromWebMethod() { + Response response = client.target(webServerURL) + .path("/propagate") + .queryParam("message", "testing...") + .request() + .property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_USERNAME, "Bob") + .property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_PASSWORD, "password") + .get(); + + assertThat(response.getStatus(), is(Response.Status.FORBIDDEN.getStatusCode())); + } + + @Test + public void shouldOverrideCredentialsToOutboundCallFromWebMethod() { + String message = "testing..."; + String response = client.target(webServerURL) + .path("/override") + .queryParam("message", message) + .request() + .property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_USERNAME, "Bob") + .property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_PASSWORD, "password") + .get(String.class); + + assertThat(response, is(message)); + } + + // ----- helper methods ------------------------------------------------- + + private static void echoWebRequest(ServerRequest req, ServerResponse res) { + String message = req.queryParams().first("message").orElse(null); + if (message != null) { + res.send(message); + } else { + res.status(Http.ResponseStatus.create(401, "missing message query parameter")).send(); + } + } + + private static void propagateCredentialsWebRequest(ServerRequest req, ServerResponse res) { + try { + GrpcClientSecurity clientSecurity = GrpcClientSecurity.create(req); + + EchoServiceGrpc.EchoServiceBlockingStub stub = noCredsEchoStub.withCallCredentials(clientSecurity); + + String message = req.queryParams().first("message").orElse(null); + Echo.EchoResponse echoResponse = stub.echo(Echo.EchoRequest.newBuilder().setMessage(message).build()); + res.send(echoResponse.getMessage()); + } catch (StatusRuntimeException e) { + res.status(GrpcHelper.toHttpResponseStatus(e)).send(); + } catch (Throwable thrown) { + res.status(Http.ResponseStatus.create(500, thrown.getMessage())).send(); + } + } + + private static void overrideCredentialsWebRequest(ServerRequest req, ServerResponse res) { + try { + GrpcClientSecurity clientSecurity = GrpcClientSecurity.builder(req) + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_USER, "Ted") + .property(HttpBasicAuthProvider.EP_PROPERTY_OUTBOUND_PASSWORD, "secret") + .build(); + + EchoServiceGrpc.EchoServiceBlockingStub stub = noCredsEchoStub.withCallCredentials(clientSecurity); + + String message = req.queryParams().first("message").orElse(null); + Echo.EchoResponse echoResponse = stub.echo(Echo.EchoRequest.newBuilder().setMessage(message).build()); + res.send(echoResponse.getMessage()); + } catch (StatusRuntimeException e) { + res.status(GrpcHelper.toHttpResponseStatus(e)).send(); + } catch (Throwable thrown) { + res.status(Http.ResponseStatus.create(500, thrown.getMessage())).send(); + } + } +} diff --git a/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/SecurityFromConfigIT.java b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/SecurityFromConfigIT.java new file mode 100644 index 00000000000..aec18ddc367 --- /dev/null +++ b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/SecurityFromConfigIT.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.integration.grpc; + +import java.util.Iterator; +import java.util.concurrent.TimeUnit; +import java.util.logging.LogManager; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.grpc.server.test.Echo; +import io.helidon.grpc.server.test.EchoServiceGrpc; +import io.helidon.grpc.server.test.StringServiceGrpc; +import io.helidon.grpc.server.test.Strings; + +import io.grpc.Channel; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessChannelBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import services.EchoService; +import services.StringService; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +public class SecurityFromConfigIT { + private static GrpcServer grpcServer; + + private static TestCallCredentials adminCreds = new TestCallCredentials("Ted", "secret"); + + private static TestCallCredentials userCreds = new TestCallCredentials("Bob", "password"); + + private static EchoServiceGrpc.EchoServiceBlockingStub adminEchoStub; + + private static EchoServiceGrpc.EchoServiceBlockingStub userEchoStub; + + private static StringServiceGrpc.StringServiceBlockingStub adminStringStub; + + private static StringServiceGrpc.StringServiceBlockingStub userStringStub; + + private static StringServiceGrpc.StringServiceBlockingStub noCredsEchoStub; + + @BeforeAll + public static void startServer() throws Exception { + LogManager.getLogManager().readConfiguration( + ServiceAndMethodLevelSecurityIT.class.getResourceAsStream("/logging.properties")); + + // load the config containing the gRPC service security settings + Config config = Config.builder().sources(ConfigSources.classpath("secure-services.conf")).build(); + + // Create the gRPC routing configuring the GrpcSecurity interceptor from config + GrpcRouting routing = GrpcRouting.builder() + .intercept(GrpcSecurity.create(config.get("security"))) + .register(new EchoService()) + .register(new StringService()) + .build(); + + // Run the server on port 0 so that it picks a free ephemeral port + GrpcServerConfiguration serverConfig = GrpcServerConfiguration.builder().port(0).build(); + + grpcServer = GrpcServer.create(serverConfig, routing) + .start() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + + Channel channel = InProcessChannelBuilder.forName(grpcServer.configuration().name()) + .build(); + + adminEchoStub = EchoServiceGrpc.newBlockingStub(channel).withCallCredentials(adminCreds); + userEchoStub = EchoServiceGrpc.newBlockingStub(channel).withCallCredentials(userCreds); + adminStringStub = StringServiceGrpc.newBlockingStub(channel).withCallCredentials(adminCreds); + userStringStub = StringServiceGrpc.newBlockingStub(channel).withCallCredentials(userCreds); + noCredsEchoStub = StringServiceGrpc.newBlockingStub(channel); + } + + @AfterAll + public static void cleanup() { + grpcServer.shutdown(); + } + + /** + * The StringService lower method is secured at the global default level + * to allow any authenticated user so should allow access to user Bob. + */ + @Test + public void shouldBeSecuredWithGlobalSettingsAllowAccess() { + Strings.StringMessage message = userStringStub.lower(toMessage("ABCD")); + assertThat(message.getText(), is("abcd")); + } + + /** + * The StringService lower method is secured at the global default level + * to allow any authenticated user so should disallow access to access + * without credentials. + */ + @Test + public void shouldBeSecuredWithGlobalSettingsDenyAccess() { + StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> + noCredsEchoStub.lower(toMessage("FOO"))); + + assertThat(thrown.getStatus().getCode(), is(Status.PERMISSION_DENIED.getCode())); + } + + /** + * The EchoService echo method is secured roles-allowed = ["admin"] at the service level + * so should allow user Ted access. + */ + @Test + public void shouldBeSecuredWithServiceSettingsAllowAccess() { + Echo.EchoResponse response = adminEchoStub.echo(Echo.EchoRequest.newBuilder().setMessage("foo").build()); + assertThat(response.getMessage(), is("foo")); + } + + /** + * The EchoService echo method is secured roles-allowed = ["admin"] at the service level + * so should deny user Bob access. + */ + @Test + public void shouldBeSecuredWithServiceSettingsDenyAccess() { + StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> + userEchoStub.echo(Echo.EchoRequest.newBuilder().setMessage("foo").build())); + + assertThat(thrown.getStatus().getCode(), is(Status.PERMISSION_DENIED.getCode())); + } + + /** + * The StringService upper method is secured with roles-allowed = ["admin"] + * so should allow user Ted access. + */ + @Test + public void shouldBeSecuredWithMethodSettingsAllowAccess() { + Strings.StringMessage message = adminStringStub.upper(toMessage("abcd")); + assertThat(message.getText(), is("ABCD")); + } + + /** + * The StringService split method is secured with roles-allowed = ["admin"] + * so should deny user Bob access. + */ + @Test + public void shouldBeSecuredWithMethodSettingsDenyAccess() { + // StringService.split is a server streaming call so the proto generated code will + // return an Iterator even though the actual call fails with PERMISSION_DENIED + Iterator it = userStringStub.split(toMessage("a b c d")); + + // It is not until accessing methods on the Iterator that we get the exception + StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, it::hasNext); + + assertThat(thrown.getStatus().getCode(), is(Status.PERMISSION_DENIED.getCode())); + } + + private Strings.StringMessage toMessage(String text) { + return Strings.StringMessage.newBuilder().setText(text).build(); + } +} diff --git a/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/ServiceAndMethodLevelSecurityIT.java b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/ServiceAndMethodLevelSecurityIT.java new file mode 100644 index 00000000000..dc34049cd00 --- /dev/null +++ b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/ServiceAndMethodLevelSecurityIT.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.integration.grpc; + +import java.util.Iterator; +import java.util.concurrent.TimeUnit; +import java.util.logging.LogManager; + +import io.helidon.config.Config; +import io.helidon.grpc.server.GrpcRouting; +import io.helidon.grpc.server.GrpcServer; +import io.helidon.grpc.server.GrpcServerConfiguration; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.grpc.server.test.Echo; +import io.helidon.grpc.server.test.EchoServiceGrpc; +import io.helidon.grpc.server.test.StringServiceGrpc; +import io.helidon.grpc.server.test.Strings.StringMessage; +import io.helidon.security.Security; +import io.helidon.security.providers.httpauth.HttpBasicAuthProvider; + +import io.grpc.Channel; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessChannelBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import services.EchoService; +import services.StringService; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +public class ServiceAndMethodLevelSecurityIT { + + private static GrpcServer grpcServer; + + private static TestCallCredentials adminCreds = new TestCallCredentials("Ted", "secret"); + + private static TestCallCredentials userCreds = new TestCallCredentials("Bob", "password"); + + private static EchoServiceGrpc.EchoServiceBlockingStub adminEchoStub; + + private static EchoServiceGrpc.EchoServiceBlockingStub userEchoStub; + + private static StringServiceGrpc.StringServiceBlockingStub adminStringStub; + + private static StringServiceGrpc.StringServiceBlockingStub userStringStub; + + private static StringServiceGrpc.StringServiceBlockingStub noCredsEchoStub; + + @BeforeAll + public static void startServer() throws Exception { + LogManager.getLogManager().readConfiguration( + ServiceAndMethodLevelSecurityIT.class.getResourceAsStream("/logging.properties")); + + Config config = Config.create(); + + Security security = Security.builder() + .addProvider(HttpBasicAuthProvider.create(config.get("http-basic-auth"))) + .build(); + + + ServiceDescriptor echoService = ServiceDescriptor.builder(new EchoService()) + .intercept(GrpcSecurity.rolesAllowed("admin")) + .build(); + + ServiceDescriptor stringService = ServiceDescriptor.builder(new StringService()) + .intercept("Upper", GrpcSecurity.rolesAllowed("admin")) + .intercept("Split", GrpcSecurity.rolesAllowed("admin")) + .build(); + + // Add the EchoService + GrpcRouting routing = GrpcRouting.builder() + .intercept(GrpcSecurity.create(security).securityDefaults(GrpcSecurity.authenticate())) + .register(echoService) + .register(stringService) + .build(); + + // Run the server on port 0 so that it picks a free ephemeral port + GrpcServerConfiguration serverConfig = GrpcServerConfiguration.builder().port(0).build(); + + grpcServer = GrpcServer.create(serverConfig, routing) + .start() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + + Channel channel = InProcessChannelBuilder.forName(grpcServer.configuration().name()) + .build(); + + adminEchoStub = EchoServiceGrpc.newBlockingStub(channel).withCallCredentials(adminCreds); + userEchoStub = EchoServiceGrpc.newBlockingStub(channel).withCallCredentials(userCreds); + adminStringStub = StringServiceGrpc.newBlockingStub(channel).withCallCredentials(adminCreds); + userStringStub = StringServiceGrpc.newBlockingStub(channel).withCallCredentials(userCreds); + noCredsEchoStub = StringServiceGrpc.newBlockingStub(channel); + } + + @AfterAll + public static void cleanup() { + grpcServer.shutdown(); + } + + @Test + public void shouldBeSecuredWithGlobalSettingsAllowAccess() { + StringMessage message = userStringStub.lower(toMessage("ABCD")); + assertThat(message.getText(), is("abcd")); + } + + @Test + public void shouldBeSecuredWithGlobalSettingsDenyAccess() { + StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> + noCredsEchoStub.lower(toMessage("FOO"))); + + assertThat(thrown.getStatus().getCode(), is(Status.PERMISSION_DENIED.getCode())); + } + + @Test + public void shouldBeSecuredWithServiceSettingsAllowAccess() { + Echo.EchoResponse response = adminEchoStub.echo(Echo.EchoRequest.newBuilder().setMessage("foo").build()); + assertThat(response.getMessage(), is("foo")); + } + + @Test + public void shouldBeSecuredWithServiceSettingsDenyAccess() { + StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> + userEchoStub.echo(Echo.EchoRequest.newBuilder().setMessage("foo").build())); + + assertThat(thrown.getStatus().getCode(), is(Status.PERMISSION_DENIED.getCode())); + } + + @Test + public void shouldBeSecuredWithMethodSettingsAllowAccess() { + StringMessage message = adminStringStub.upper(toMessage("abcd")); + assertThat(message.getText(), is("ABCD")); + } + + @Test + public void shouldBeSecuredWithMethodSettingsDenyAccess() { + // StringService.split is a server streaming call so the proto generated code will + // return an Iterator even though the actual call fails with PERMISSION_DENIED + Iterator it = userStringStub.split(toMessage("a b c d")); + + // It is not until accessing methods on the Iterator that we get the exception + StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, it::hasNext); + + assertThat(thrown.getStatus().getCode(), is(Status.PERMISSION_DENIED.getCode())); + } + + private StringMessage toMessage(String text) { + return StringMessage.newBuilder().setText(text).build(); + } +} diff --git a/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/TestCallCredentials.java b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/TestCallCredentials.java new file mode 100644 index 00000000000..65b9b17397f --- /dev/null +++ b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/TestCallCredentials.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.integration.grpc; + +import java.util.Base64; +import java.util.concurrent.Executor; + +import io.helidon.grpc.core.ContextKeys; + +import io.grpc.CallCredentials2; +import io.grpc.Metadata; + +/** + * A {@link io.grpc.CallCredentials2} that add a basic auth + * authorization header to a request. + */ +public class TestCallCredentials + extends CallCredentials2 { + /** + * The basic auth encoded user name and password. + */ + private String basicAuth; + + /** + * Create a {@link TestCallCredentials}. + * + * @param user the user name + * @param password the password + */ + public TestCallCredentials(String user, String password) { + basicAuth = createAuth(user, password); + } + + @Override + public void applyRequestMetadata(RequestInfo requestInfo, Executor executor, MetadataApplier applier) { + Metadata metadata = new Metadata(); + + metadata.put(ContextKeys.AUTHORIZATION, "Basic " + basicAuth); + + applier.apply(metadata); + } + + @Override + public void thisUsesUnstableApi() { + } + + private static String createAuth(String user, String password) { + String basic = user + ":" + password; + return Base64.getEncoder().encodeToString(basic.getBytes()); + } +} diff --git a/security/integration/grpc/src/test/java/services/EchoService.java b/security/integration/grpc/src/test/java/services/EchoService.java new file mode 100644 index 00000000000..0d305fc4265 --- /dev/null +++ b/security/integration/grpc/src/test/java/services/EchoService.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package services; + +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.grpc.server.test.Echo; + +import io.grpc.stub.StreamObserver; + +/** + * A simple test gRPC echo service. + */ +public class EchoService + implements GrpcService { + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.proto(Echo.getDescriptor()) + .unary("Echo", this::echo); + } + + /** + * Echo the message back to the caller. + * + * @param request the echo request containing the message to echo + * @param observer the call response + */ + public void echo(Echo.EchoRequest request, StreamObserver observer) { + String message = request.getMessage(); + Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build(); + complete(observer, response); + } +} diff --git a/security/integration/grpc/src/test/java/services/SecuredOutboundEchoService.java b/security/integration/grpc/src/test/java/services/SecuredOutboundEchoService.java new file mode 100644 index 00000000000..93b27b9d3c9 --- /dev/null +++ b/security/integration/grpc/src/test/java/services/SecuredOutboundEchoService.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package services; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; + +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.grpc.server.test.Echo; +import io.helidon.security.SecurityContext; +import io.helidon.security.integration.grpc.GrpcSecurity; +import io.helidon.security.integration.jersey.ClientSecurityFeature; + +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +/** + * A simple test gRPC echo service that + * makes a secure outbound http request. + */ +public class SecuredOutboundEchoService + implements GrpcService { + + private final Client client; + + private final String url; + + public SecuredOutboundEchoService(String url) { + this.url = url; + this.client = ClientBuilder.newBuilder().build().register(new ClientSecurityFeature()); + } + + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.name("EchoService") + .proto(Echo.getDescriptor()) + .unary("Echo", this::echo); + } + + /** + * Make a web request passing this method's message parameter and send + * the web response back to the caller . + * + * @param request the echo request containing the message to echo + * @param observer the call response + */ + public void echo(Echo.EchoRequest request, StreamObserver observer) { + try { + SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get(); + String message = request.getMessage(); + + Response webResponse = client.target(url) + .path("/test") + .queryParam("message", message) + .request() + .property(ClientSecurityFeature.PROPERTY_CONTEXT, securityContext) + .get(); + + if (webResponse.getStatus() == 200) { + String value = webResponse.readEntity(String.class); + + Echo.EchoResponse echoResponse = Echo.EchoResponse.newBuilder().setMessage(value).build(); + complete(observer, echoResponse); + } else if (webResponse.getStatus() == Response.Status.FORBIDDEN.getStatusCode() + || webResponse.getStatus() == Response.Status.UNAUTHORIZED.getStatusCode()) { + + observer.onError(Status.PERMISSION_DENIED.asException()); + } else { + observer.onError(Status.UNKNOWN.withDescription("Received http response " + webResponse).asException()); + } + } catch (Throwable thrown) { + observer.onError(Status.UNKNOWN.withCause(thrown).asException()); + } + } +} diff --git a/security/integration/grpc/src/test/java/services/StringService.java b/security/integration/grpc/src/test/java/services/StringService.java new file mode 100644 index 00000000000..ac87d2d77cb --- /dev/null +++ b/security/integration/grpc/src/test/java/services/StringService.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package services; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.grpc.server.CollectingObserver; +import io.helidon.grpc.server.GrpcService; +import io.helidon.grpc.server.ServiceDescriptor; +import io.helidon.grpc.server.test.Strings; +import io.helidon.grpc.server.test.Strings.StringMessage; + +import io.grpc.stub.StreamObserver; + + +public class StringService + implements GrpcService { + @Override + public void update(ServiceDescriptor.Rules rules) { + rules.proto(Strings.getDescriptor()) + .unary("Upper", this::upper) + .unary("Lower", this::lower) + .serverStreaming("Split", this::split) + .clientStreaming("Join", this::join) + .bidirectional("Echo", this::echo); + } + + // ---- service methods ------------------------------------------------- + + private void upper(StringMessage request, StreamObserver observer) { + complete(observer, response(request.getText().toUpperCase())); + } + + private void lower(StringMessage request, StreamObserver observer) { + complete(observer, response(request.getText().toLowerCase())); + } + + private void split(StringMessage request, StreamObserver observer) { + String[] parts = request.getText().split(" "); + stream(observer, Stream.of(parts).map(this::response)); + } + + private StreamObserver join(StreamObserver observer) { + return new CollectingObserver<>( + Collectors.joining(" "), + observer, + StringMessage::getText, + this::response); + } + + private StreamObserver echo(StreamObserver observer) { + return new StreamObserver() { + public void onNext(StringMessage value) { + observer.onNext(value); + } + + public void onError(Throwable t) { + t.printStackTrace(); + } + + public void onCompleted() { + observer.onCompleted(); + } + }; + } + + // ---- helper methods -------------------------------------------------- + + private StringMessage response(String text) { + return StringMessage.newBuilder().setText(text).build(); + } + +} diff --git a/security/integration/grpc/src/test/proto/echo.proto b/security/integration/grpc/src/test/proto/echo.proto new file mode 100644 index 00000000000..1b8930c1663 --- /dev/null +++ b/security/integration/grpc/src/test/proto/echo.proto @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.grpc.server.test"; + +service EchoService { + rpc Echo (EchoRequest) returns (EchoResponse) {} +} + +message EchoRequest { + string message = 1; +} + +message EchoResponse { + string message = 1; +} diff --git a/security/integration/grpc/src/test/proto/strings.proto b/security/integration/grpc/src/test/proto/strings.proto new file mode 100644 index 00000000000..12908fce908 --- /dev/null +++ b/security/integration/grpc/src/test/proto/strings.proto @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.grpc.server.test"; + +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/security/integration/grpc/src/test/resources/application.yaml b/security/integration/grpc/src/test/resources/application.yaml new file mode 100644 index 00000000000..bfdc19d8882 --- /dev/null +++ b/security/integration/grpc/src/test/resources/application.yaml @@ -0,0 +1,34 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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" + +grpc: + name: "test.server" + port: 1408 + +webserver: + port: 8080 + +http-basic-auth: + users: + - login: "Ted" + password: "secret" + roles: ["user", "admin"] + - login: "Bob" + password: "password" + roles: ["user"] \ No newline at end of file diff --git a/security/integration/grpc/src/test/resources/logging.properties b/security/integration/grpc/src/test/resources/logging.properties new file mode 100644 index 00000000000..dcb9fcb30c9 --- /dev/null +++ b/security/integration/grpc/src/test/resources/logging.properties @@ -0,0 +1,39 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden by specific handlers and loggers +.level=INFO + +AUDIT.level=FINEST + +# Helidon Web Server has a custom log formatter that extends SimpleFormatter. +# It replaces "!thread!" with the current thread name +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +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 +#io.netty.level=INFO diff --git a/security/integration/grpc/src/test/resources/secure-services.conf b/security/integration/grpc/src/test/resources/secure-services.conf new file mode 100644 index 00000000000..15d52fac134 --- /dev/null +++ b/security/integration/grpc/src/test/resources/secure-services.conf @@ -0,0 +1,87 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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: [ + { + # must be present + name = "http-basic-auth" + } + ] + } + + providers: [ + # Security provider - basic authentication (supports roles) + { + http-basic-auth { + realm = "mic" + users: [ + { + login = "Ted" + password = "${CLEAR=secret}" + roles = ["user", "admin"] + + }, + { + login = "Bob" + password = "${CLEAR=password}" + roles = ["user"] + } + ] + } + }, + + ] + + grpc-server { + # Configuration of integration with grpc server + # The default configuration to apply to all services not explicitly configured below + defaults { + authenticate = true + } + services: [ + { + name = "EchoService" + defaults { + roles-allowed = ["admin"] + } + }, + { + name = "StringService" + methods: [ + { + name: "Upper" + roles-allowed = ["admin"] + }, + { + name: "Split" + roles-allowed = ["admin"] + } + ] + } + ] + } +} diff --git a/security/integration/pom.xml b/security/integration/pom.xml index 4d5ae575f4a..8df60ea676e 100644 --- a/security/integration/pom.xml +++ b/security/integration/pom.xml @@ -35,5 +35,6 @@ jersey webserver + grpc diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/JsonService.java b/webserver/webserver/src/main/java/io/helidon/webserver/JsonService.java new file mode 100644 index 00000000000..71ffa150f50 --- /dev/null +++ b/webserver/webserver/src/main/java/io/helidon/webserver/JsonService.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import java.util.List; + +import io.helidon.common.http.MediaType; + +/** + * A {@link Service} and abstract {@link Handler} that provides support for JSON content. + * + * @see Routing.Builder + * @see Routing.Rules + */ +public abstract class JsonService implements Service, Handler { + + /** + * Registers this handler for any HTTP method. + * + * @param routingRules a routing configuration where JSON support should be registered + * @see Routing + */ + @Override + public void update(final Routing.Rules routingRules) { + routingRules.any(this); + } + + /** + * Determines if JSON is an accepted response type, using {@code Accept} and response {@code Content-Type} headers. + *

+ * Sets the response {@code Content-Type} header if not set and JSON is accepted. + * + * @param request a server request + * @param response a server response + * @return {@code true} if JSON is accepted. + */ + protected boolean acceptsJson(ServerRequest request, ServerResponse response) { + final MediaType responseType = response.headers().contentType().orElse(null); + if (responseType == null) { + // No response type set yet. See if one of the accepted types is JSON. + final MediaType jsonResponseType = toJsonResponseType(request.headers().acceptedTypes()); + if (jsonResponseType == null) { + // Nope + return false; + } else { + // Yes, so set it as the response content-type + response.headers().contentType(jsonResponseType); + return true; + } + } else { + return MediaType.JSON_PREDICATE.test(responseType); + } + } + + private MediaType toJsonResponseType(List acceptedTypes) { + if (acceptedTypes == null || acceptedTypes.isEmpty()) { + // None provided, so go ahead and return JSON. + return MediaType.APPLICATION_JSON; + } else { + for (final MediaType type : acceptedTypes) { + final MediaType responseType = toJsonResponseType(type); + if (responseType != null) { + return responseType; + } + } + return null; + } + } + + /** + * Returns the response type for the given type if it is an accepted JSON type. + * + * @param acceptedType The accepted type. + * @return The response type or {@code null} if not an accepted JSON type. + */ + protected MediaType toJsonResponseType(MediaType acceptedType) { + if (acceptedType.test(MediaType.APPLICATION_JSON)) { + return MediaType.APPLICATION_JSON; + } else if (acceptedType.hasSuffix("json")) { + return MediaType.create(acceptedType.type(), acceptedType.subtype()); + } else { + return null; + } + } +}