diff --git a/docs/modules/ROOT/pages/reference/extensions/grpc.adoc b/docs/modules/ROOT/pages/reference/extensions/grpc.adoc index e7ef9d47290a..a96b609731af 100644 --- a/docs/modules/ROOT/pages/reference/extensions/grpc.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/grpc.adoc @@ -160,6 +160,21 @@ At present there is no support for integrating Camel Quarkus gRPC with Quarkus g | Configuration property | Type | Default +|icon:lock[title=Fixed at build time] [[quarkus.camel.grpc.service-excludes]]`link:#quarkus.camel.grpc.service-excludes[quarkus.camel.grpc.service-excludes]` + +Excludes classes from the build time scanning of gRPC service classes. +This can be useful if there are gRPC services that you want to exclude from participating in Camel gRPC route +operations. The value is a comma separated list of class name patterns. +You can specify the fully qualified class name of individual classes or use path patterns to match multiple classes. +For example to exclude all classes starting with `MyService` use: `++**++MyService++*++`. +To exclude all services from a specific package use: `com.services.++*++`. +To exclude all services from a specific package and its sub-packages, use +double wildcards: `com.services.++**++`. +And to exclude all services from two specific packages use: +`com.services.++*++,com.other.services.++*++`. +| List of `string` +| + |icon:lock[title=Fixed at build time] [[quarkus.camel.grpc.codegen.enabled]]`link:#quarkus.camel.grpc.codegen.enabled[quarkus.camel.grpc.codegen.enabled]` If `true`, Camel Quarkus gRPC code generation is run for .proto files discovered from the `proto` directory, or from diff --git a/extensions/grpc/deployment/src/main/java/org/apache/camel/quarkus/component/grpc/deployment/CamelGrpcServiceExcludesBuildItem.java b/extensions/grpc/deployment/src/main/java/org/apache/camel/quarkus/component/grpc/deployment/CamelGrpcServiceExcludesBuildItem.java new file mode 100644 index 000000000000..295299b289fc --- /dev/null +++ b/extensions/grpc/deployment/src/main/java/org/apache/camel/quarkus/component/grpc/deployment/CamelGrpcServiceExcludesBuildItem.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.grpc.deployment; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import io.quarkus.builder.item.SimpleBuildItem; +import org.apache.camel.util.AntPathMatcher; +import org.jboss.jandex.ClassInfo; + +final class CamelGrpcServiceExcludesBuildItem extends SimpleBuildItem { + private static final Set DEFAULT_SERVICE_EXCLUDES = Set.of( + // Exclude unwanted gRPC services shaded into org.apache.kafka:kafka-clients + "org.apache.kafka.shaded.**"); + + private final Set serviceExcludes = new HashSet<>(); + + CamelGrpcServiceExcludesBuildItem(Set excludes) { + this.serviceExcludes.addAll(DEFAULT_SERVICE_EXCLUDES); + if (!excludes.isEmpty()) { + this.serviceExcludes.addAll(excludes); + } + } + + Set getServiceExcludes() { + return serviceExcludes; + } + + Predicate serviceExcludesFilter() { + return classInfo -> getServiceExcludes() + .stream() + .noneMatch(exclude -> { + String className = classInfo.name().toString().replace(".", "/"); + String excludePattern = exclude.replace(".", "/"); + return AntPathMatcher.INSTANCE.match(excludePattern, className); + }); + } +} diff --git a/extensions/grpc/deployment/src/main/java/org/apache/camel/quarkus/component/grpc/deployment/GrpcProcessor.java b/extensions/grpc/deployment/src/main/java/org/apache/camel/quarkus/component/grpc/deployment/GrpcProcessor.java index 0c6b9b0c7299..0361b6b793dc 100644 --- a/extensions/grpc/deployment/src/main/java/org/apache/camel/quarkus/component/grpc/deployment/GrpcProcessor.java +++ b/extensions/grpc/deployment/src/main/java/org/apache/camel/quarkus/component/grpc/deployment/GrpcProcessor.java @@ -18,8 +18,10 @@ import java.lang.reflect.Modifier; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import com.google.api.client.json.GenericJson; import io.grpc.BindableService; @@ -44,6 +46,7 @@ import jakarta.enterprise.context.Dependent; import org.apache.camel.component.grpc.server.GrpcMethodHandler; import org.apache.camel.quarkus.grpc.runtime.CamelQuarkusBindableService; +import org.apache.camel.quarkus.grpc.runtime.GrpcBuildTimeConfig; import org.apache.camel.quarkus.grpc.runtime.QuarkusBindableServiceFactory; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -69,13 +72,22 @@ FeatureBuildItem feature() { } @BuildStep - void registerForReflection(BuildProducer reflectiveClass, - CombinedIndexBuildItem combinedIndexBuildItem) { + CamelGrpcServiceExcludesBuildItem camelGrpcServiceExcludes(GrpcBuildTimeConfig config) { + return new CamelGrpcServiceExcludesBuildItem(config.serviceExcludes.orElse(Collections.emptySet())); + } + + @BuildStep + void registerForReflection( + BuildProducer reflectiveClass, + CombinedIndexBuildItem combinedIndexBuildItem, + CamelGrpcServiceExcludesBuildItem camelGrpcServiceExcludes) { IndexView index = combinedIndexBuildItem.getIndex(); for (DotName dotName : STUB_CLASS_DOT_NAMES) { index.getAllKnownSubclasses(dotName) .stream() + .filter(camelGrpcServiceExcludes.serviceExcludesFilter()) + .peek(classInfo -> logDebugMessage("Discovered gRPC stub class %s", classInfo.name().toString())) .map(classInfo -> ReflectiveClassBuildItem.builder(classInfo.name().toString()).methods() .build()) .forEach(reflectiveClass::produce); @@ -94,10 +106,14 @@ void quarkusBindableServiceFactoryBean(BuildProducer ad void createBindableServiceBeans( BuildProducer generatedBean, BuildProducer reflectiveClass, - CombinedIndexBuildItem combinedIndexBuildItem) { + CombinedIndexBuildItem combinedIndexBuildItem, + CamelGrpcServiceExcludesBuildItem camelGrpcServiceExcludes) { IndexView index = combinedIndexBuildItem.getIndex(); - Collection bindableServiceImpls = index.getAllKnownImplementors(BINDABLE_SERVICE_DOT_NAME); + Collection bindableServiceImpls = index.getAllKnownImplementors(BINDABLE_SERVICE_DOT_NAME) + .stream() + .filter(camelGrpcServiceExcludes.serviceExcludesFilter()) + .collect(Collectors.toSet()); // Generate implementation classes from any abstract gRPC BindableService implementations included in the application archive // Override the various sync and async methods so that requests can be intercepted and delegated to Camel routing diff --git a/extensions/grpc/runtime/src/main/java/org/apache/camel/quarkus/grpc/runtime/GrpcBuildTimeConfig.java b/extensions/grpc/runtime/src/main/java/org/apache/camel/quarkus/grpc/runtime/GrpcBuildTimeConfig.java index 0f6ec948cdfa..f5ade10762b2 100644 --- a/extensions/grpc/runtime/src/main/java/org/apache/camel/quarkus/grpc/runtime/GrpcBuildTimeConfig.java +++ b/extensions/grpc/runtime/src/main/java/org/apache/camel/quarkus/grpc/runtime/GrpcBuildTimeConfig.java @@ -18,6 +18,8 @@ import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -27,6 +29,23 @@ @ConfigRoot(name = "camel.grpc", phase = ConfigPhase.BUILD_TIME) public class GrpcBuildTimeConfig { + /** + * Excludes classes from the build time scanning of gRPC service classes. + * This can be useful if there are gRPC services that you want to exclude from participating in Camel gRPC route + * operations. The value is a comma separated list of class name patterns. + * You can specify the fully qualified class name of individual classes or use path patterns to match multiple classes. + * For example to exclude all classes starting with `MyService` use: `++**++MyService++*++`. + * To exclude all services from a specific package use: `com.services.++*++`. + * To exclude all services from a specific package and its sub-packages, use + * double wildcards: `com.services.++**++`. + * And to exclude all services from two specific packages use: + * `com.services.++*++,com.other.services.++*++`. + * + * @asciidoclet + */ + @ConfigItem + public Optional> serviceExcludes; + /** * Build time configuration options for Camel Quarkus gRPC code generator. * diff --git a/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/proto-f-1.proto b/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/proto-f-1.proto new file mode 100644 index 000000000000..015711872d52 --- /dev/null +++ b/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/proto-f-1.proto @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_multiple_files = true; +option java_package = "org.acme.proto.f"; +option java_outer_classname = "PingPongProtoF"; + +package org.acme.proto.f; + +service TestF { + rpc PingSyncSync (PingRequestF) returns (PongResponseF) {} +} + +message PingRequestF { + string ping_name = 1; +} + +message PongResponseF { + string pong_name = 1; +} \ No newline at end of file diff --git a/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/sub/package/other/proto-f-4.proto b/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/sub/package/other/proto-f-4.proto new file mode 100644 index 000000000000..c1d70c6ce5fb --- /dev/null +++ b/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/sub/package/other/proto-f-4.proto @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_multiple_files = true; +option java_package = "org.acme.proto.f.sub.dir.other"; +option java_outer_classname = "PingPongProtoF"; + +package org.acme.proto.sub.dir.other.f; + +service TestF { + rpc PingSyncSync (PingRequestF) returns (PongResponseF) {} +} + +message PingRequestF { + string ping_name = 1; +} + +message PongResponseF { + string pong_name = 1; +} \ No newline at end of file diff --git a/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/sub/package/proto-f-3.proto b/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/sub/package/proto-f-3.proto new file mode 100644 index 000000000000..930ffefd6ea2 --- /dev/null +++ b/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/sub/package/proto-f-3.proto @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_multiple_files = true; +option java_package = "org.acme.proto.f.sub.dir"; +option java_outer_classname = "PingPongProtoF"; + +package org.acme.proto.sub.dir.f; + +service TestF { + rpc PingSyncSync (PingRequestF) returns (PongResponseF) {} +} + +message PingRequestF { + string ping_name = 1; +} + +message PongResponseF { + string pong_name = 1; +} \ No newline at end of file diff --git a/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/sub/proto-f-2.proto b/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/sub/proto-f-2.proto new file mode 100644 index 000000000000..1597a70cb28c --- /dev/null +++ b/integration-tests-support/grpc/src/main/resources/org/acme/proto/f/sub/proto-f-2.proto @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_multiple_files = true; +option java_package = "org.acme.proto.f.sub"; +option java_outer_classname = "PingPongProtoF"; + +package org.acme.proto.f.sub; + +service TestF { + rpc PingSyncSync (PingRequestF) returns (PongResponseF) {} +} + +message PingRequestF { + string ping_name = 1; +} + +message PongResponseF { + string pong_name = 1; +} \ No newline at end of file diff --git a/integration-tests/grpc/src/main/java/org/apache/camel/quarkus/component/grpc/it/GrpcResource.java b/integration-tests/grpc/src/main/java/org/apache/camel/quarkus/component/grpc/it/GrpcResource.java index d9aa432a6120..b680c8d0bb4a 100644 --- a/integration-tests/grpc/src/main/java/org/apache/camel/quarkus/component/grpc/it/GrpcResource.java +++ b/integration-tests/grpc/src/main/java/org/apache/camel/quarkus/component/grpc/it/GrpcResource.java @@ -19,7 +19,11 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; @@ -34,6 +38,7 @@ import org.apache.camel.component.mock.MockEndpoint; import org.apache.camel.quarkus.component.grpc.it.model.PingRequest; import org.apache.camel.quarkus.component.grpc.it.model.PongResponse; +import org.apache.camel.quarkus.grpc.runtime.CamelQuarkusBindableService; import static org.apache.camel.component.grpc.GrpcConstants.GRPC_METHOD_NAME_HEADER; @@ -184,4 +189,18 @@ public String jwtProducer(String message) { PongResponse.class); return response.getPongName(); } + + @Path("/services") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Set services() { + return Arc.container() + .listAll(CamelQuarkusBindableService.class) + .stream() + .map(InstanceHandle::get) + .map(Object::getClass) + .map(Class::getSuperclass) + .map(Class::getName) + .collect(Collectors.toSet()); + } } diff --git a/integration-tests/grpc/src/main/resources/application.properties b/integration-tests/grpc/src/main/resources/application.properties index ec91806a1eb9..7c3e77db04c7 100644 --- a/integration-tests/grpc/src/main/resources/application.properties +++ b/integration-tests/grpc/src/main/resources/application.properties @@ -22,5 +22,8 @@ quarkus.camel.grpc.codegen.scan-for-imports=com.google.protobuf:protobuf-java,or # Test codegen scanning for proto files in other dependencies quarkus.camel.grpc.codegen.scan-for-proto=org.apache.camel.quarkus:camel-quarkus-integration-tests-support-grpc -quarkus.camel.grpc.codegen.scan-for-proto-includes."org.apache.camel.quarkus\:camel-quarkus-integration-tests-support-grpc"=org/acme/proto/a/*,org/acme/proto/b/proto-b.proto,org/acme/proto/c/**,org/acme/proto/d/**,org/acme/proto/e/** +quarkus.camel.grpc.codegen.scan-for-proto-includes."org.apache.camel.quarkus\:camel-quarkus-integration-tests-support-grpc"=org/acme/proto/a/*,org/acme/proto/b/proto-b.proto,org/acme/proto/c/**,org/acme/proto/d/**,org/acme/proto/e/**,org/acme/proto/f/** quarkus.camel.grpc.codegen.scan-for-proto-excludes."org.apache.camel.quarkus\:camel-quarkus-integration-tests-support-grpc"=org/acme/proto/d/*,org/acme/proto/d/sub/proto-d-2.proto,org/acme/proto/d/sub/package** + +# Test excluding unwanted services +quarkus.camel.grpc.service-excludes=org.acme.proto.f.TestFGrpc*,org.acme.proto.f.sub.*,org.acme.proto.f.sub.** diff --git a/integration-tests/grpc/src/test/java/org/apache/camel/quarkus/component/grpc/it/GrpcTest.java b/integration-tests/grpc/src/test/java/org/apache/camel/quarkus/component/grpc/it/GrpcTest.java index ddd6fd6cfe47..33c595505807 100644 --- a/integration-tests/grpc/src/test/java/org/apache/camel/quarkus/component/grpc/it/GrpcTest.java +++ b/integration-tests/grpc/src/test/java/org/apache/camel/quarkus/component/grpc/it/GrpcTest.java @@ -65,6 +65,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -601,6 +602,21 @@ public void codeGenDependencyScan(String generatedClassPackage) { } } + @Test + public void serviceExcludes() { + JsonPath result = RestAssured.get("/grpc/services") + .then() + .statusCode(200) + .extract() + .body() + .jsonPath(); + + List services = result.getList("."); + assertFalse(services.isEmpty()); + assertTrue(services.stream().anyMatch(service -> service.startsWith("org.acme.proto"))); + assertTrue(services.stream().noneMatch(service -> service.startsWith("org.acme.proto.f"))); + } + static Stream producerMethodPorts() { return Stream.of( Arguments.of("pingSyncSync", "{{camel.grpc.test.async.server.port}}"),